Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
033a57cad4 | ||
|
|
299228b7b5 | ||
|
|
5b022e2ef3 | ||
|
|
c8818f5774 | ||
|
|
1398857688 | ||
|
|
2a1505bc0d | ||
|
|
4bba6ecc82 | ||
|
|
522c4df44c | ||
|
|
c03b81d2ea | ||
|
|
43a797444a | ||
|
|
d0cb961b49 | ||
|
|
d208e68885 | ||
|
|
8dcb53aea0 | ||
|
|
241b286e21 | ||
|
|
17839f6a25 | ||
|
|
e4d3faefa0 | ||
|
|
116ca7f011 | ||
|
|
d44339b018 | ||
|
|
352fd23c0b | ||
|
|
65b8868c36 | ||
|
|
54dec171f9 | ||
|
|
5b4398911a | ||
|
|
4c0bd132b1 | ||
|
|
7ce4eddb0e | ||
|
|
14519135d8 | ||
|
|
60839481ad | ||
|
|
3ef1941fc0 | ||
|
|
16cb1a8026 | ||
|
|
e3b9bd6a70 | ||
|
|
a1ea04acf8 | ||
|
|
3e341a3555 | ||
|
|
cb5c9b03bf | ||
|
|
884a1b9d6f | ||
|
|
e8aa498683 | ||
|
|
1b7efd854a | ||
|
|
8b32c3dcb6 | ||
|
|
0821b28eac | ||
|
|
4c71d5ac42 | ||
|
|
a8e0b5a5ac | ||
|
|
9b9014a042 | ||
|
|
96b42b1784 | ||
|
|
26d854ebe8 | ||
|
|
ed36d01b40 | ||
|
|
646556a457 | ||
|
|
d20a35eddc | ||
|
|
d69946db8a | ||
|
|
ddab4075d2 | ||
|
|
d6c8ad1d0d | ||
|
|
54c6faa4b6 | ||
|
|
c88f05c5c0 | ||
|
|
e419c74ebb | ||
|
|
b6dc66b070 | ||
|
|
7b4d9bf5db | ||
|
|
c51e7351e8 | ||
|
|
fe12eccc42 | ||
|
|
17b7826753 | ||
|
|
14ae6d8b1a | ||
|
|
7b559991c7 | ||
|
|
decd4cdf0a | ||
|
|
dc7a6551c5 |
+6
-1
@@ -28,7 +28,7 @@ PORT=5000
|
||||
######################################
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, OpenNodeWallet"
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, OpenNodeWallet"
|
||||
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
@@ -84,6 +84,10 @@ LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
|
||||
ALBY_API_ENDPOINT=https://api.getalby.com/
|
||||
ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
||||
|
||||
# ZBDWallet
|
||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||
|
||||
# OpenNodeWallet
|
||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
||||
@@ -222,6 +226,7 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
######################################
|
||||
|
||||
DEBUG=false
|
||||
DEBUG_DATABASE=false
|
||||
BUNDLE_ASSETS=true
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
|
||||
@@ -48,3 +48,13 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||
|
||||
jmeter:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.5.1"]
|
||||
uses: ./.github/workflows/jmeter.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: JMeter Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
type: string
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
required: true
|
||||
default: "1.5.1"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
action_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry ${{ inputs.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ inputs.poetry-version }}
|
||||
|
||||
- name: create logs and reports dir
|
||||
run: |
|
||||
mkdir logs
|
||||
mkdir reports
|
||||
|
||||
- name: install packages
|
||||
run: poetry install
|
||||
|
||||
- name: run LNbits
|
||||
env:
|
||||
LNBITS_ADMIN_UI: true
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
poetry run lnbits > logs/lnbits.log &
|
||||
sleep 5
|
||||
|
||||
|
||||
- name: install jmeter
|
||||
run: |
|
||||
java -version
|
||||
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.2.zip
|
||||
unzip apache-jmeter-5.6.2.zip
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -v
|
||||
|
||||
- name: start mirror server
|
||||
run: |
|
||||
libs_dir=$GITHUB_WORKSPACE/apache-jmeter-5.6.2/lib/
|
||||
echo "Fix bad Jmeter lib names. Lib dir: $libs_dir"
|
||||
|
||||
mv $libs_dir/slf4j-api-1.7.36.jar $libs_dir/slf4j-api-1.7.25.jar
|
||||
mv $libs_dir/log4j-slf4j-impl-2.20.0.jar $libs_dir/log4j-slf4j-impl-2.11.0.jar
|
||||
mv $libs_dir/log4j-api-2.20.0.jar $libs_dir/log4j-api-2.11.1.jar
|
||||
mv $libs_dir/log4j-core-2.20.0.jar $libs_dir/log4j-core-2.11.1.jar
|
||||
mv $libs_dir/log4j-1.2-api-2.20.0.jar $libs_dir/og4j-1.2-api-2.11.1.jar
|
||||
|
||||
echo "Starting the mirror server on dfault port 8081."
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/mirror-server &
|
||||
|
||||
- name: run jmx scripts
|
||||
run: |
|
||||
for file in $( ls $GITHUB_WORKSPACE/integration/*.jmx); do
|
||||
echo "Running test with $file"
|
||||
filename=$(basename "$file" ".jmx")
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -n -t $file -l logs/$filename.log -e -o reports ;
|
||||
error_count=$(cat jmeter.log | grep "summary =" | awk '{print $19}')
|
||||
echo "Error count: $error_count"
|
||||
if [[ "$error_count" == "0" ]]; then
|
||||
echo "Test $filename OK."
|
||||
rm -r reports/*
|
||||
else
|
||||
echo "Test $filename failed. Error count: $error_count."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: jmeter-test-results
|
||||
path: |
|
||||
reports/
|
||||
logs/
|
||||
|
||||
@@ -14,11 +14,11 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
rev: 24.2.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.283
|
||||
rev: v0.3.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ FROM python:3.10-slim-bullseye
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl pkg-config build-essential
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
@@ -86,7 +86,7 @@ bak:
|
||||
sass:
|
||||
npm run sass
|
||||
|
||||
bundle_no_bump:
|
||||
bundle:
|
||||
npm install
|
||||
npm run sass
|
||||
npm run vendor_copy
|
||||
@@ -97,16 +97,10 @@ bundle_no_bump:
|
||||
npm run vendor_bundle_js
|
||||
npm run vendor_minify_js
|
||||
|
||||
bundle:
|
||||
make bundle_no_bump
|
||||
# increment serviceworker version
|
||||
awk '/CACHE_VERSION =/ {sub(/[0-9]+$$/, $$NF+1)} 1' lnbits/static/js/service-worker.js > lnbits/static/js/service-worker.js.new
|
||||
mv lnbits/static/js/service-worker.js.new lnbits/static/js/service-worker.js
|
||||
|
||||
checkbundle:
|
||||
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
||||
cp lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old
|
||||
make bundle_no_bump
|
||||
make bundle
|
||||
diff -q lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old || exit 1
|
||||
diff -q lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old || exit 1
|
||||
@echo "Bundle is OK"
|
||||
|
||||
@@ -29,6 +29,7 @@ LNbits can run on top of any Lightning funding source. It currently supports the
|
||||
- LNbits
|
||||
- OpenNode
|
||||
- Alby
|
||||
- ZBD
|
||||
- LightningTipBot
|
||||
|
||||
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
|
||||
|
||||
@@ -87,6 +87,14 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
|
||||
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
|
||||
|
||||
### ZBD
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an ZBD API Key here: https://zbd.dev/docs/dashboard/projects/api
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
|
||||
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
|
||||
- `ZBD_API_KEY`: ZBDApiKey
|
||||
|
||||
### Cliche Wallet
|
||||
|
||||
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Account" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get main page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Create new account" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"name":"a1"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/account</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract user and wallet" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
vars.put("userId", resp.user || "no-user-id");
|
||||
vars.put("walletId", resp.id || "no-wallet-id");
|
||||
vars.put("inkey", resp.inkey || 'no-inkey');
|
||||
vars.put("adminkey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Login with user id" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"usr":"${userId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/usr</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Go to wallet page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="wal" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.name">wal</stringProp>
|
||||
<stringProp name="Argument.value">${walletId}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get currencies" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/currencies</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">undefined</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Currency List" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="1438956759">["AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP","BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNH","CNY","COP","CRC","CUC","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD","FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRT","ISK","JEP","JMD","JOD","JPY","KES","KGS","KHR","KMF","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LYD","MAD","MDL","MGA","MKD","MMK","MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SEK","SGD","SHP","SLL","SOS","SRD","SSP","STD","SVC","SZL","THB","TJS","TMT","TND","TOP","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VES","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPD","XPF","XPT","YER","ZAR","ZMW","ZWL"]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get wallet balance" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Check Balance is Zero" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
if (resp.balance !== 0) {
|
||||
AssertionResult.setFailureMessage("Balance is not zero, but: "+ resp.balance);
|
||||
AssertionResult.setFailure(true)
|
||||
}
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223Assertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get payments" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/payments</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check empty payment list" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="2914">[]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
@@ -0,0 +1,487 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Server" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to home page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
<stringProp name="TestPlan.comments">Detected the start of a redirect chain</stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
<stringProp name="50554">307</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">40</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract `is_first_install` var" enabled="true">
|
||||
<stringProp name="RegexExtractor.useHeaders">true</stringProp>
|
||||
<stringProp name="RegexExtractor.refname">is_first_install</stringProp>
|
||||
<stringProp name="RegexExtractor.regex">location: (.*)</stringProp>
|
||||
<stringProp name="RegexExtractor.template">$1$</stringProp>
|
||||
<stringProp name="RegexExtractor.default">not-first-install</stringProp>
|
||||
<stringProp name="RegexExtractor.match_number">0</stringProp>
|
||||
<stringProp name="Sample.scope">all</stringProp>
|
||||
</RegexExtractor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="If First Install" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to first_install" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Init super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password","password_repeat":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/first_install</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="Else (not first install)" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(!vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Login super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Get all admin wallets" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract admin wallet and keys" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())[0]
|
||||
|
||||
vars.put("adminWalletId", resp.id || "no-admin-wallet-id");
|
||||
vars.put("adminWalletKey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Top up wallet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"amount":"1000000","id":"${adminWalletId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/admin/api/v1/topup/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?&wal=c1daa33fc4014ef69cd1505f14f322c2</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Logout super user" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/logout</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+27
-18
@@ -26,12 +26,16 @@ from starlette.responses import JSONResponse
|
||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog,
|
||||
register_killswitch,
|
||||
register_task_listeners,
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
killswitch_task,
|
||||
wait_for_paid_invoices,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import cancel_all_tasks, create_permanent_task
|
||||
from lnbits.tasks import (
|
||||
cancel_all_tasks,
|
||||
create_permanent_task,
|
||||
register_invoice_listener,
|
||||
)
|
||||
from lnbits.utils.cache import cache
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
|
||||
@@ -61,7 +65,6 @@ from .tasks import (
|
||||
check_pending_payments,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
webhook_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -195,7 +198,7 @@ async def check_installed_extensions(app: FastAPI):
|
||||
|
||||
for ext in installed_extensions:
|
||||
try:
|
||||
installed = check_installed_extension_files(ext)
|
||||
installed = await check_installed_extension_files(ext)
|
||||
if not installed:
|
||||
await restore_installed_extension(app, ext)
|
||||
logger.info(
|
||||
@@ -253,14 +256,14 @@ async def build_all_installed_extensions_list(
|
||||
]
|
||||
|
||||
|
||||
def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
if ext.has_installed_version:
|
||||
return True
|
||||
|
||||
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
|
||||
|
||||
if f"./{str(ext.zip_path)}" not in zip_files:
|
||||
ext.download_archive()
|
||||
await ext.download_archive()
|
||||
ext.extract_archive()
|
||||
|
||||
return False
|
||||
@@ -465,19 +468,21 @@ def get_db_vendor_name():
|
||||
|
||||
|
||||
def register_async_tasks(app):
|
||||
@app.route("/wallet/webhook")
|
||||
async def webhook_listener():
|
||||
return await webhook_handler()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def listeners():
|
||||
create_permanent_task(check_pending_payments)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
register_task_listeners()
|
||||
register_killswitch()
|
||||
# await run_deferred_async() # calle: doesn't do anyting?
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue = asyncio.Queue(5)
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
# TODO: implement watchdog properly
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
@@ -540,9 +545,7 @@ def register_exception_handlers(app: FastAPI):
|
||||
response = RedirectResponse("/")
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.set_cookie(
|
||||
"is_access_token_expired", "true", samesite="none", secure=True
|
||||
)
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
@@ -586,6 +589,12 @@ def configure_logger() -> None:
|
||||
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.error").propagate = False
|
||||
|
||||
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").propagate = False
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
|
||||
+8
-2
@@ -397,7 +397,10 @@ async def install_extension(
|
||||
return False, "No release selected"
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension, archive=release.archive, source_repo=release.source_repo
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
click.echo(f"Extension '{extension}' ({release.version}) installed.")
|
||||
@@ -445,7 +448,10 @@ async def update_extension(
|
||||
click.echo(f"Updating '{extension}' extension to version: {release.version }")
|
||||
|
||||
data = CreateExtension(
|
||||
ext_id=extension, archive=release.archive, source_repo=release.source_repo
|
||||
ext_id=extension,
|
||||
archive=release.archive,
|
||||
source_repo=release.source_repo,
|
||||
version=release.version,
|
||||
)
|
||||
|
||||
await _call_install_extension(data, url, admin_user)
|
||||
|
||||
+14
-1
@@ -322,6 +322,7 @@ async def add_installed_extension(
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"dependencies": ext.dependencies,
|
||||
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
|
||||
}
|
||||
|
||||
version = ext.installed_release.version if ext.installed_release else ""
|
||||
@@ -553,7 +554,8 @@ async def get_wallet_for_key(
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
|
||||
AS balance_msat FROM wallets WHERE adminkey = ? OR inkey = ?
|
||||
AS balance_msat FROM wallets
|
||||
WHERE (adminkey = ? OR inkey = ?) AND deleted = false
|
||||
""",
|
||||
(key, key),
|
||||
)
|
||||
@@ -601,6 +603,7 @@ async def get_standalone_payment(
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE {clause}
|
||||
ORDER BY amount
|
||||
LIMIT 1
|
||||
""",
|
||||
tuple(values),
|
||||
@@ -1012,6 +1015,16 @@ async def check_internal_pending(
|
||||
return row["pending"]
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(status, payment_hash),
|
||||
)
|
||||
|
||||
|
||||
# balance_check
|
||||
# -------------
|
||||
|
||||
|
||||
+42
-1
@@ -53,8 +53,45 @@ async def stop_extension_background_work(
|
||||
):
|
||||
"""
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extensions SHOULD expose a DELETE enpoint at the root level of their API.
|
||||
Extensions SHOULD expose a `api_stop()` function and/or a DELETE enpoint
|
||||
at the root level of their API.
|
||||
"""
|
||||
stopped = await _stop_extension_background_work(ext_id)
|
||||
|
||||
if not stopped:
|
||||
# fallback to REST API call
|
||||
await _stop_extension_background_work_via_api(ext_id, user, access_token)
|
||||
|
||||
|
||||
async def _stop_extension_background_work(ext_id) -> bool:
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id) or ""
|
||||
ext = Extension(ext_id, True, False, upgrade_hash=upgrade_hash)
|
||||
|
||||
try:
|
||||
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
||||
old_module = importlib.import_module(ext.module_name)
|
||||
|
||||
# Extensions must expose an `{ext_id}_stop()` function at the module level
|
||||
# The `api_stop()` function is for backwards compatibility (will be deprecated)
|
||||
stop_fns = [f"{ext_id}_stop", "api_stop"]
|
||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
||||
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
|
||||
|
||||
await getattr(old_module, stop_fn_name)()
|
||||
|
||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
||||
logger.warning(ex)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _stop_extension_background_work_via_api(ext_id, user, access_token):
|
||||
logger.info(
|
||||
f"Stopping background work for extension '{ext_id}' using the REST API."
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
|
||||
@@ -63,7 +100,11 @@ async def stop_extension_background_work(
|
||||
)
|
||||
resp = await client.delete(url=url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
logger.info(f"Stopped background work for extension '{ext_id}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(
|
||||
f"Failed to stop background work for '{ext_id}' using the REST API."
|
||||
)
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
|
||||
@@ -18,17 +18,20 @@ from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
class BaseWallet(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
currency: Optional[str]
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class Wallet(BaseWallet):
|
||||
user: str
|
||||
currency: Optional[str]
|
||||
deleted: bool
|
||||
created_at: Optional[int] = None
|
||||
updated_at: Optional[int] = None
|
||||
@@ -255,7 +258,7 @@ class Payment(FromRowModel):
|
||||
conn: Optional[Connection] = None,
|
||||
) -> PaymentStatus:
|
||||
if self.is_uncheckable:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
logger.debug(
|
||||
f"Checking {'outgoing' if self.is_out else 'incoming'} "
|
||||
|
||||
+22
-16
@@ -31,7 +31,12 @@ from lnbits.settings import (
|
||||
)
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||
from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class
|
||||
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
||||
from lnbits.wallets.base import (
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
)
|
||||
|
||||
from .crud import (
|
||||
check_internal,
|
||||
@@ -189,12 +194,15 @@ async def pay_invoice(
|
||||
If the payment is still in flight, we hope that some other process
|
||||
will regularly check for the payment.
|
||||
"""
|
||||
invoice = bolt11_decode(payment_request)
|
||||
try:
|
||||
invoice = bolt11_decode(payment_request)
|
||||
except Exception:
|
||||
raise InvoiceFailure("Bolt11 decoding failed.")
|
||||
|
||||
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
raise InvoiceFailure("Amountless invoices not supported.")
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
raise InvoiceFailure("Amount in invoice is too high.")
|
||||
|
||||
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
|
||||
|
||||
@@ -202,11 +210,6 @@ async def pay_invoice(
|
||||
temp_id = invoice.payment_hash
|
||||
internal_id = f"internal_{invoice.payment_hash}"
|
||||
|
||||
if invoice.amount_msat == 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
_, extra = await calculate_fiat_amounts(
|
||||
invoice.amount_msat / 1000, wallet_id, extra=extra, conn=conn
|
||||
)
|
||||
@@ -579,10 +582,10 @@ async def check_transaction_status(
|
||||
wallet_id, payment_hash, conn=conn
|
||||
)
|
||||
if not payment:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if not payment.pending:
|
||||
# note: before, we still checked the status of the payment again
|
||||
return PaymentStatus(True, fee_msat=payment.fee)
|
||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||
|
||||
status: PaymentStatus = await payment.check_status()
|
||||
return status
|
||||
@@ -711,11 +714,14 @@ async def check_webpush_settings():
|
||||
|
||||
def update_cached_settings(sets_dict: dict):
|
||||
for key, value in sets_dict.items():
|
||||
if key not in readonly_variables:
|
||||
try:
|
||||
setattr(settings, key, value)
|
||||
except Exception:
|
||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
||||
if key in readonly_variables:
|
||||
continue
|
||||
if key not in settings.dict().keys():
|
||||
continue
|
||||
try:
|
||||
setattr(settings, key, value)
|
||||
except Exception:
|
||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
||||
if "super_user" in sets_dict:
|
||||
setattr(settings, "super_user", sets_dict["super_user"])
|
||||
|
||||
|
||||
+44
-64
@@ -8,8 +8,8 @@ from lnbits.core.crud import (
|
||||
get_balance_notify,
|
||||
get_wallet,
|
||||
get_webpush_subscriptions_for_user,
|
||||
mark_webhook_sent,
|
||||
)
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import (
|
||||
get_balance_delta,
|
||||
@@ -17,29 +17,16 @@ from lnbits.core.services import (
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.tasks import (
|
||||
SseListenersDict,
|
||||
create_permanent_task,
|
||||
create_task,
|
||||
register_invoice_listener,
|
||||
send_push_notification,
|
||||
)
|
||||
from lnbits.tasks import send_push_notification
|
||||
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
|
||||
"api_invoice_listeners"
|
||||
)
|
||||
|
||||
|
||||
def register_killswitch():
|
||||
"""
|
||||
Registers a killswitch which will check lnbits-status repository for a signal from
|
||||
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
logger.debug("Starting killswitch task")
|
||||
create_permanent_task(killswitch_task)
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
||||
|
||||
|
||||
async def killswitch_task():
|
||||
"""
|
||||
killswitch will check lnbits-status repository for a signal from
|
||||
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet":
|
||||
@@ -54,7 +41,7 @@ async def killswitch_task():
|
||||
"Switching to VoidWallet. Killswitch triggered."
|
||||
)
|
||||
await switch_to_voidwallet()
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
except (httpx.RequestError, httpx.HTTPStatusError):
|
||||
logger.error(
|
||||
"Cannot fetch lnbits status manifest."
|
||||
f" {settings.lnbits_status_manifest}"
|
||||
@@ -62,17 +49,11 @@ async def killswitch_task():
|
||||
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||
|
||||
|
||||
async def register_watchdog():
|
||||
async def watchdog_task():
|
||||
"""
|
||||
Registers a watchdog which will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
# TODO: implement watchdog properly
|
||||
# logger.debug("Starting watchdog task")
|
||||
# create_permanent_task(watchdog_task)
|
||||
|
||||
|
||||
async def watchdog_task():
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_watchdog and WALLET.__class__.__name__ != "VoidWallet":
|
||||
@@ -87,36 +68,23 @@ async def watchdog_task():
|
||||
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
|
||||
|
||||
|
||||
def register_task_listeners():
|
||||
"""
|
||||
Registers an invoice listener queue for the core tasks. Incoming payments in this
|
||||
queue will eventually trigger the signals sent to all other extensions
|
||||
and fulfill other core tasks such as dispatching webhooks.
|
||||
"""
|
||||
invoice_paid_queue = asyncio.Queue(5)
|
||||
# we register invoice_paid_queue to receive all incoming invoices
|
||||
register_invoice_listener(invoice_paid_queue, "core/tasks.py")
|
||||
# register a worker that will react to invoices
|
||||
create_task(wait_for_paid_invoices(invoice_paid_queue))
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
"""
|
||||
This worker dispatches events to all extensions,
|
||||
dispatches webhooks and balance notifys.
|
||||
This task dispatches events to all api_invoice_listeners,
|
||||
webhooks, push notifications and balance notifications.
|
||||
"""
|
||||
while True:
|
||||
payment = await invoice_paid_queue.get()
|
||||
logger.trace("received invoice paid event")
|
||||
# send information to sse channel
|
||||
# dispatch api_invoice_listeners
|
||||
await dispatch_api_invoice_listeners(payment)
|
||||
# payment notification
|
||||
wallet = await get_wallet(payment.wallet_id)
|
||||
if wallet:
|
||||
await send_payment_notification(wallet, payment)
|
||||
# dispatch webhook
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
|
||||
# dispatch balance_notify
|
||||
url = await get_balance_notify(payment.wallet_id)
|
||||
if url:
|
||||
@@ -124,10 +92,22 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
r = await client.post(url, timeout=4)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
pass
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
status_code = exc.response.status_code
|
||||
await mark_webhook_sent(payment.payment_hash, status_code)
|
||||
logger.warning(
|
||||
f"balance_notify returned a bad status_code: {status_code} "
|
||||
f"while requesting {exc.request.url!r}."
|
||||
)
|
||||
logger.warning(exc)
|
||||
except httpx.RequestError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, -1)
|
||||
logger.warning(f"Could not send balance_notify to {url}")
|
||||
logger.warning(exc)
|
||||
|
||||
# dispatch push notification
|
||||
await send_payment_push_notification(payment)
|
||||
|
||||
|
||||
@@ -137,10 +117,12 @@ async def dispatch_api_invoice_listeners(payment: Payment):
|
||||
"""
|
||||
for chan_name, send_channel in api_invoice_listeners.items():
|
||||
try:
|
||||
logger.debug(f"sending invoice paid event to {chan_name}")
|
||||
logger.debug(f"api invoice listener: sending paid event to {chan_name}")
|
||||
send_channel.put_nowait(payment)
|
||||
except asyncio.QueueFull:
|
||||
logger.error(f"removing sse listener {send_channel}:{chan_name}")
|
||||
logger.error(
|
||||
f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}"
|
||||
)
|
||||
api_invoice_listeners.pop(chan_name)
|
||||
|
||||
|
||||
@@ -151,26 +133,24 @@ async def dispatch_webhook(payment: Payment):
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
|
||||
if not payment.webhook:
|
||||
return await mark_webhook_sent(payment, -1)
|
||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(status, payment.payment_hash),
|
||||
)
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
||||
logger.warning(
|
||||
f"webhook returned a bad status_code: {exc.response.status_code} "
|
||||
f"while requesting {exc.request.url!r}."
|
||||
)
|
||||
except httpx.RequestError:
|
||||
await mark_webhook_sent(payment.payment_hash, -1)
|
||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||
|
||||
|
||||
async def send_payment_push_notification(payment: Payment):
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
<div class="col">
|
||||
<p>Funding Source Info</p>
|
||||
<ul>
|
||||
{%raw%}
|
||||
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
|
||||
<li>
|
||||
Node Balance: {{(auditData.node_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
LNbits Balance: {{(auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
Reserve Percent: {{(auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2)}} %
|
||||
</li>
|
||||
{%endraw%}
|
||||
<li
|
||||
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Node Balance: ' + (auditData.node_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
></li>
|
||||
<li
|
||||
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString() + ' sats'"
|
||||
></li>
|
||||
<li
|
||||
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
|
||||
></li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isSuperUser" id="funding-sources">
|
||||
<div v-if="isSuperUser">
|
||||
<lnbits-funding-sources
|
||||
:form-data="formData"
|
||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
style="padding: 10px; color: #fafafa; height: 320px"
|
||||
>
|
||||
<small v-for="log in logs"
|
||||
>{% raw %}{{ log }}{% endraw %}<br
|
||||
><span v-text="log"></span><br
|
||||
/></small>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
@@ -166,7 +166,6 @@
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="blocked_ip in formData.lnbits_blocked_ips"
|
||||
:key="blocked_ip"
|
||||
@@ -174,10 +173,8 @@
|
||||
@remove="removeBlockedIPs(blocked_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ blocked_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="blocked_ip"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
@@ -198,7 +195,6 @@
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="allowed_ip in formData.lnbits_allowed_ips"
|
||||
:key="allowed_ip"
|
||||
@@ -206,10 +202,8 @@
|
||||
@remove="removeAllowedIPs(allowed_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ allowed_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="allowed_ip"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{% raw %}
|
||||
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||
<q-icon size="28px" name="update"></q-icon>
|
||||
|
||||
@@ -30,9 +29,12 @@
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width> </q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props"
|
||||
>{{ col.label }}</q-th
|
||||
>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
@@ -51,12 +53,16 @@
|
||||
color="red"
|
||||
></q-icon>
|
||||
</q-td>
|
||||
<q-td auto-width key="date" :props="props">
|
||||
{{ formatDate(props.row.date) }}
|
||||
<q-td
|
||||
auto-width
|
||||
key="date"
|
||||
:props="props"
|
||||
v-text="formatDate(props.row.date)"
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="message" :props="props"
|
||||
>{{ props.row.message }}
|
||||
<a
|
||||
><span v-text="props.row.message"></span
|
||||
><a
|
||||
v-if="props.row.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -69,4 +75,3 @@
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endraw %}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<div class="col">
|
||||
<p>Server Info</p>
|
||||
<ul>
|
||||
{%raw%}
|
||||
<li v-if="settings.lnbits_data_folder">
|
||||
SQlite: {{settings.lnbits_data_folder}}
|
||||
</li>
|
||||
<li v-if="settings.lnbits_database_url">
|
||||
Postgres: {{settings.lnbits_database_url}}
|
||||
</li>
|
||||
{%endraw%}
|
||||
<li
|
||||
v-if="settings.lnbits_data_folder"
|
||||
v-text="'SQlite: ' + settings.lnbits_data_folder"
|
||||
></li>
|
||||
<li
|
||||
v-if="settings.lnbits_database_url"
|
||||
v-text="'Postgres: ' + settings.lnbits_database_url"
|
||||
></li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
@@ -154,7 +154,6 @@
|
||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
||||
:key="manifestUrl"
|
||||
@@ -162,10 +161,8 @@
|
||||
@remove="removeExtensionsManifest(manifestUrl)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ manifestUrl }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
v-text="manifestUrl"
|
||||
></q-chip>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
@remove="removeAdminUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
<span v-text="user" />
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
@@ -49,8 +49,8 @@
|
||||
@remove="removeAllowedUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
<span v-text="user" />
|
||||
</q-chip>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
@click="updateSettings"
|
||||
:disabled="!checkChanges"
|
||||
>
|
||||
<q-tooltip v-if="checkChanges"
|
||||
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
|
||||
>
|
||||
<q-tooltip v-if="checkChanges">
|
||||
<span v-text="$t('save_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="checkChanges"
|
||||
@@ -27,9 +27,9 @@
|
||||
color="primary"
|
||||
@click="restartServer"
|
||||
>
|
||||
<q-tooltip v-if="needsRestart"
|
||||
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
|
||||
>
|
||||
<q-tooltip v-if="needsRestart">
|
||||
<span v-text="$t('restart_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="needsRestart"
|
||||
@@ -46,7 +46,9 @@
|
||||
color="primary"
|
||||
@click="topUpDialog.show = true"
|
||||
>
|
||||
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('add_funds_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
|
||||
@@ -59,7 +61,9 @@
|
||||
@click="deleteSettings"
|
||||
class="float-right"
|
||||
>
|
||||
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('reset_defaults_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,10 +169,4 @@
|
||||
</q-dialog>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||
<style>
|
||||
.introjs-tooltip-custom .introjs-tooltiptext,
|
||||
.introjs-tooltip-header {
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card-section>
|
||||
<strong>Node URL: </strong><em v-text="origin"></em><br />
|
||||
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
|
||||
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br />
|
||||
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<p class="text-h4 gt-sm">{%raw%}{{ $t('extensions') }}{%endraw%}</p>
|
||||
<p class="text-h4 gt-sm" v-text="$t('extensions')"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-xs-12 q-ml-auto">
|
||||
@@ -43,9 +43,10 @@
|
||||
:label="$t('featured')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
<i v-if="!g.user.admin && tab != 'installed'"
|
||||
>{%raw%}{{ $t('only_admins_can_install') }}{%endraw%}</i
|
||||
>
|
||||
<i
|
||||
v-if="!g.user.admin && tab != 'installed'"
|
||||
v-text="$t('only_admins_can_install')"
|
||||
></i>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,45 +90,42 @@
|
||||
color="green"
|
||||
class="float-right"
|
||||
>
|
||||
<small>{%raw%}{{ $t('new_version') }}{%endraw%}</small>
|
||||
<small v-text="$t('new_version')"></small>
|
||||
<q-tooltip
|
||||
><span v-text="extension.latestRelease.version"></span
|
||||
></q-tooltip>
|
||||
</q-badge>
|
||||
{% raw %}
|
||||
<div class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm"
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-h5 gt-sm q-mt-sm q-mb-xs lt-md"
|
||||
style="min-height: 60px"
|
||||
>
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle2 gt-sm"
|
||||
style="font-size: 11px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription ||
|
||||
extension.installedRelease?.description }}
|
||||
</div>
|
||||
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
v-text="extension.shortDescription || extension.installedRelease?.description"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle1 lt-md q-mt-sm q-mb-xs"
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div
|
||||
class="text-subtitle2 lt-md"
|
||||
style="font-size: 9px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription }}
|
||||
</div>
|
||||
{% endraw %}
|
||||
v-text="extension.shortDescription"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-pt-sm">
|
||||
<div class="col">
|
||||
<small v-if="extension.dependencies?.length"
|
||||
>{%raw%}{{ $t('extension_depends_on') }}{%endraw%}</small
|
||||
>
|
||||
<small
|
||||
v-if="extension.dependencies?.length"
|
||||
v-text="$t('extension_depends_on')"
|
||||
></small>
|
||||
<small v-else> </small>
|
||||
<q-badge
|
||||
v-for="dep in extension.dependencies"
|
||||
@@ -148,20 +146,18 @@
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
|
||||
></q-rating
|
||||
>
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
<q-rating
|
||||
v-model="maxStars"
|
||||
class="lt-md"
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
|
||||
></q-rating
|
||||
>
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
<q-toggle
|
||||
v-if="extension.isAvailable && extension.isInstalled && g.user.admin"
|
||||
:label="extension.isActive ? $t('activated'): $t('deactivated') "
|
||||
@@ -169,11 +165,11 @@
|
||||
style="max-height: 21px"
|
||||
v-model="extension.isActive"
|
||||
@input="toggleExtension(extension)"
|
||||
><q-tooltip
|
||||
>{%raw%}{{ $t('activate_extension_details')
|
||||
}}{%endraw%}</q-tooltip
|
||||
></q-toggle
|
||||
>
|
||||
><q-tooltip>
|
||||
<span
|
||||
v-text="$t('activate_extension_details')"
|
||||
></span> </q-tooltip
|
||||
></q-toggle>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
@@ -186,8 +182,8 @@
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="extension.id + '/'"
|
||||
>{%raw%}{{ $t('open') }}{%endraw%}</q-btn
|
||||
>
|
||||
:label="$t('open')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
|
||||
flat
|
||||
@@ -196,11 +192,12 @@
|
||||
:href="['{{
|
||||
url_for('install.extensions')
|
||||
}}', '?disable=', extension.id].join('')"
|
||||
:label="$t('disable')"
|
||||
></q-btn>
|
||||
<q-badge
|
||||
v-if="extension.isAdminOnly && !user.admin"
|
||||
v-text="$t('admin_only')"
|
||||
>
|
||||
{%raw%}{{ $t('disable') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-badge v-if="extension.isAdminOnly && !user.admin">
|
||||
{%raw%}{{ $t('admin_only') }}{%endraw%}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
|
||||
@@ -210,8 +207,8 @@
|
||||
:href="['{{
|
||||
url_for('install.extensions')
|
||||
}}', '?enable=', extension.id].join('')"
|
||||
:label="$t('enable')"
|
||||
>
|
||||
{%raw%}{{ $t('enable') }}{%endraw%}
|
||||
<q-tooltip>
|
||||
<span v-text="$t('enable_extension_details')">
|
||||
</span> </q-tooltip
|
||||
@@ -222,12 +219,11 @@
|
||||
flat
|
||||
color="primary"
|
||||
v-if="g.user.admin"
|
||||
>
|
||||
{%raw%}{{ $t('manage') }}{%endraw%}<q-tooltip
|
||||
>{%raw%}{{ $t('manage_extension_details')
|
||||
}}{%endraw%}</q-tooltip
|
||||
></q-btn
|
||||
>
|
||||
:label="$t('manage')"
|
||||
><q-tooltip
|
||||
><span v-text="$t('manage_extension_details')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
@@ -240,7 +236,7 @@
|
||||
class="float-right"
|
||||
>
|
||||
<q-badge>
|
||||
{% raw %}{{ extension.installedRelease.version }}{% endraw %}
|
||||
<span v-text="extension.installedRelease.version"></span>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('extension_installed_version')"></span>
|
||||
</q-tooltip>
|
||||
@@ -252,10 +248,10 @@
|
||||
</div>
|
||||
<q-dialog v-model="showUninstallDialog">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p>
|
||||
{%raw%}{{ $t('extension_uninstall_warning') }}{%endraw%} <br />
|
||||
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
|
||||
<span v-text="$t('extension_uninstall_warning')"></span><br />
|
||||
<span v-text="$t('confirm_continue')"></span>
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
@@ -264,32 +260,39 @@
|
||||
value="false"
|
||||
label="Cleanup database tables"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
{%raw%}{{ $t('extension_db_drop_info') }}{%endraw%}
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
><span v-text="$t('extension_db_drop_info')"></span>
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="uninstallExtension()"
|
||||
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="uninstallExtension()"
|
||||
v-text="$t('uninstall_confirm')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showDropDbDialog">
|
||||
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||
<p>{%raw%}{{ $t('extension_db_drop_warning') }}{%endraw%} <br /></p>
|
||||
<h6 class="q-my-md text-primary" v-text="$t('warning')"></h6>
|
||||
<p><span v-text="$t('extension_db_drop_warning')"></span><br /></p>
|
||||
<q-input
|
||||
v-model="dropDbExtensionId"
|
||||
:label="selectedExtension.id"
|
||||
></q-input>
|
||||
<br />
|
||||
<p>{%raw%}{{ $t('confirm_continue') }}{%endraw%}</p>
|
||||
<p v-text="$t('confirm_continue')"></p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@@ -297,17 +300,58 @@
|
||||
outline
|
||||
color="red"
|
||||
@click="dropExtensionDb()"
|
||||
>{%raw%}{{ $t('confirm') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||
>
|
||||
v-text="$t('confirm')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showUpgradeDialog">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<a :href="'lightning:' + selectedRelease.paymentRequest">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-spinner color="primary" size="2.55em"></q-spinner>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
v-if="selectedRelease.paymentRequest"
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(selectedRelease.paymentRequest)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="float-right q-ml-lg"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-text="selectedExtension?.name"></div>
|
||||
</q-card-section>
|
||||
@@ -337,7 +381,7 @@
|
||||
<q-item-section>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
{%raw%}{{ $t('repository') }}{%endraw%}
|
||||
<span v-text="$t('repository')"></span>
|
||||
<br />
|
||||
<small v-text="repoName"></small>
|
||||
<q-tooltip
|
||||
@@ -376,19 +420,104 @@
|
||||
color="pink"
|
||||
size="70px"
|
||||
></q-icon>
|
||||
Cannot get the release details.
|
||||
<span v-text="$t('release_details_error')"></span>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
<q-card-section v-if="release.is_version_compatible">
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
>{%raw%}{{ $t('install') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-else @click="showUninstall()" flat color="red">
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
<span
|
||||
v-if="release.requiresPayment && !release.paid_sats"
|
||||
v-text="$t('extension_cost', {cost: release.cost_sats})"
|
||||
class="q-mb-lg"
|
||||
></span>
|
||||
<span
|
||||
v-if="release.requiresPayment && release.paid_sats"
|
||||
class="q-mb-lg"
|
||||
v-text="$t('extension_paid_sats', {paid_sats: release.paid_sats})"
|
||||
></span>
|
||||
<div
|
||||
v-if="!release.requiresPayment || (release.requiresPayment && release.paid_sats)"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!release.isInstalled"
|
||||
@click="installExtension(release)"
|
||||
color="primary unelevated mt-lg pt-lg"
|
||||
:label="$t('install')"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="release.requiresPayment && !release.paid_sats">
|
||||
<div v-if="!release.payment_hash">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="release.paidAmount"
|
||||
type="number"
|
||||
:min="release.cost_sats"
|
||||
suffix="sat"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="release.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
</q-select>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="payAndInstall(release)"
|
||||
:disabled="!release.wallet"
|
||||
class="q-mt-sm"
|
||||
:label="$t('pay_from_wallet')"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="showQRCode(release)"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('show_qr')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<br />
|
||||
<span
|
||||
class="q-mb-lg q-mt-lg"
|
||||
v-text="'There is a previous pending invoice for this release.'"
|
||||
></span>
|
||||
<br />
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="installExtension(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm"
|
||||
:label="$t('retry_install')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
@click="clearHangingInvoice(release)"
|
||||
color="primary"
|
||||
class="q-mt-sm float-right"
|
||||
:label="$t('new_payment')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="release.isInstalled"
|
||||
@click="showUninstall()"
|
||||
:label="$t('uninstall')"
|
||||
flat
|
||||
color="red"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="release.html_url"
|
||||
class="text-secondary float-right"
|
||||
@@ -396,11 +525,11 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: inherit"
|
||||
>{%raw%}{{ $t('release_notes') }}{%endraw%}</a
|
||||
>
|
||||
v-text="$t('release_notes')"
|
||||
></a>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
{%raw%}{{ $t('extension_min_lnbits_version') }}{%endraw%}
|
||||
<span v-text="$t('extension_min_lnbits_version')"></span>
|
||||
<strong>
|
||||
<span v-text="release.min_lnbits_version"></span>
|
||||
</strong>
|
||||
@@ -408,8 +537,11 @@
|
||||
<q-card v-if="release.warning">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<q-badge color="yellow" text-color="black">
|
||||
{%raw%}{{ $t('warning') }}{%endraw%}
|
||||
<q-badge
|
||||
color="yellow"
|
||||
text-color="black"
|
||||
v-text="$t('warning')"
|
||||
>
|
||||
</q-badge>
|
||||
</div>
|
||||
<div class="text-subtitle2">
|
||||
@@ -432,9 +564,8 @@
|
||||
@click="showUninstall()"
|
||||
flat
|
||||
color="red"
|
||||
>
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
>
|
||||
v-text="$t('uninstall')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else-if="selectedExtension?.hasDatabaseTables"
|
||||
@click="showDropDb()"
|
||||
@@ -442,9 +573,13 @@
|
||||
color="red"
|
||||
:label="$t('drop_db')"
|
||||
></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -464,8 +599,10 @@
|
||||
dropDbExtensionId: '',
|
||||
selectedExtension: null,
|
||||
selectedExtensionRepos: null,
|
||||
selectedRelease: null,
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
@@ -504,10 +641,18 @@
|
||||
.filter(extensionNameContains(term))
|
||||
this.tab = tab
|
||||
},
|
||||
|
||||
installExtension: async function (release) {
|
||||
// no longer required to check if the invoice was paid
|
||||
// the install logic has been triggered one way or another
|
||||
this.unsubscribeFromPaylinkWs()
|
||||
|
||||
const extension = this.selectedExtension
|
||||
extension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
release.payment_hash =
|
||||
release.payment_hash || this.getPaylinkHash(release.pay_link)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
@@ -516,7 +661,9 @@
|
||||
{
|
||||
ext_id: extension.id,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo
|
||||
source_repo: release.source_repo,
|
||||
payment_hash: release.payment_hash,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
@@ -530,8 +677,9 @@
|
||||
this.tab = 'installed'
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
console.warn(err)
|
||||
extension.inProgress = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
uninstallExtension: async function () {
|
||||
@@ -623,8 +771,10 @@
|
||||
|
||||
showUpgrade: async function (extension) {
|
||||
this.selectedExtension = extension
|
||||
this.showUpgradeDialog = true
|
||||
this.selectedRelease = null
|
||||
this.selectedExtensionRepos = null
|
||||
this.showUpgradeDialog = true
|
||||
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
@@ -648,6 +798,12 @@
|
||||
if (release.isInstalled) {
|
||||
repos[release.source_repo].isInstalled = true
|
||||
}
|
||||
if (release.pay_link) {
|
||||
release.requiresPayment = true
|
||||
release.paidAmount = release.cost_sats
|
||||
release.payment_hash = this.getPaylinkHash(release.pay_link)
|
||||
}
|
||||
|
||||
repos[release.source_repo].releases.push(release)
|
||||
return repos
|
||||
}, {})
|
||||
@@ -656,6 +812,122 @@
|
||||
extension.inProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
async payAndInstall(release) {
|
||||
try {
|
||||
this.selectedExtension.inProgress = true
|
||||
this.showUpgradeDialog = false
|
||||
const paymentInfo = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
|
||||
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
|
||||
const {data} = await LNbits.api.payInvoice(
|
||||
wallet,
|
||||
paymentInfo.payment_request
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
|
||||
await this.installExtension(release)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
} finally {
|
||||
this.selectedExtension.inProgress = false
|
||||
}
|
||||
},
|
||||
async showQRCode(release) {
|
||||
this.selectedRelease = release
|
||||
|
||||
try {
|
||||
const data = await this.requestPayment(
|
||||
this.selectedExtension.id,
|
||||
release
|
||||
)
|
||||
|
||||
this.selectedRelease.paymentRequest = data.payment_request
|
||||
this.selectedRelease.payment_hash = data.payment_hash
|
||||
this.selectedRelease = _.clone(this.selectedRelease)
|
||||
this.rememberPaylinkHash(
|
||||
this.selectedRelease.pay_link,
|
||||
this.selectedRelease.payment_hash
|
||||
)
|
||||
|
||||
this.subscribeToPaylinkWs(
|
||||
this.selectedRelease.pay_link,
|
||||
data.payment_hash
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
|
||||
async requestPayment(extId, release) {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/invoice`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: extId,
|
||||
archive: release.archive,
|
||||
source_repo: release.source_repo,
|
||||
cost_sats: release.paidAmount,
|
||||
version: release.version
|
||||
}
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
||||
clearHangingInvoice(release) {
|
||||
this.forgetPaylinkHash(release.pay_link)
|
||||
release.payment_hash = null
|
||||
},
|
||||
|
||||
rememberPaylinkHash(pay_link, payment_hash) {
|
||||
this.$q.localStorage.set(
|
||||
`lnbits.extensions.paylink.${pay_link}`,
|
||||
payment_hash
|
||||
)
|
||||
},
|
||||
getPaylinkHash(pay_link) {
|
||||
return this.$q.localStorage.getItem(
|
||||
`lnbits.extensions.paylink.${pay_link}`
|
||||
)
|
||||
},
|
||||
forgetPaylinkHash(pay_link) {
|
||||
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
|
||||
},
|
||||
subscribeToPaylinkWs(pay_link, payment_hash) {
|
||||
const url = new URL(`${pay_link}/${payment_hash}`)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||
this.paylinkWebsocket = new WebSocket(url)
|
||||
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
|
||||
const resp = JSON.parse(data)
|
||||
if (resp.paid) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Invoice Paid!'
|
||||
})
|
||||
this.installExtension(this.selectedRelease)
|
||||
} else {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Invoice tracking lost!'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
unsubscribeFromPaylinkWs() {
|
||||
try {
|
||||
this.paylinkWebsocket && this.paylinkWebsocket.close()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
|
||||
hasNewVersion: function (extension) {
|
||||
if (extension.installedRelease && extension.latestRelease) {
|
||||
return (
|
||||
|
||||
@@ -490,7 +490,14 @@
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md"></div>
|
||||
<div class="col q-pl-md">
|
||||
<a href="https://zbd.gg" target="_blank" rel="noopener noreferrer">
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
|
||||
<style>
|
||||
.introjs-tooltip-custom .introjs-tooltiptext,
|
||||
.introjs-tooltip-header {
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
<!---->
|
||||
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
||||
@@ -40,7 +34,7 @@
|
||||
} : ''"
|
||||
>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none text-no-wrap" id="wallet-balance">
|
||||
<h3 class="q-my-none text-no-wrap">
|
||||
<strong v-text="formattedBalance"></strong>
|
||||
<small>{{LNBITS_DENOMINATION}}</small>
|
||||
<lnbits-update-balance
|
||||
@@ -74,10 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<div
|
||||
class="row q-pb-md q-px-md q-col-gutter-md gt-sm"
|
||||
id="wallet-buttons"
|
||||
>
|
||||
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -174,7 +165,6 @@
|
||||
:hide-header="mobileSimple"
|
||||
:hide-bottom="mobileSimple"
|
||||
@request="fetchPayments"
|
||||
id="wallet-table"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
@@ -380,7 +370,7 @@
|
||||
<q-card-section class="text-center">
|
||||
<p v-text="$t('export_to_phone_desc')"></p>
|
||||
<qrcode
|
||||
:value="'{{request.base_url}}' +'wallet?wal={{wallet.id}}'"
|
||||
:value="'{{request.base_url}}wallet?usr={{user.id}}&wal={{wallet.id}}'"
|
||||
:options="{width:240}"
|
||||
></qrcode>
|
||||
</q-card-section>
|
||||
@@ -905,7 +895,6 @@
|
||||
</q-dialog>
|
||||
|
||||
<q-tabs
|
||||
id="mobile-wallet-buttons"
|
||||
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
||||
active-class="px-0"
|
||||
indicator-color="transparent"
|
||||
@@ -933,8 +922,11 @@
|
||||
|
||||
<q-dialog v-model="disclaimerDialog.show" position="top">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">Warning</h6>
|
||||
<p v-text="$t('disclaimer_dialog')"></p>
|
||||
<h6
|
||||
class="q-my-md text-primary"
|
||||
v-text="$t('disclaimer_dialog_title')"
|
||||
></h6>
|
||||
<p class="whitespace-pre-line" v-text="$t('disclaimer_dialog')"></p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
Open channel
|
||||
</q-btn>
|
||||
</div>
|
||||
{% raw %}
|
||||
<div>
|
||||
<div class="text-subtitle1 col-grow">Total</div>
|
||||
<lnbits-channel-balance
|
||||
@@ -172,11 +171,12 @@
|
||||
<q-tr :props="props">
|
||||
<div class="q-pb-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div class="text-subtitle1 col-grow">
|
||||
{{props.row.name}}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle1 col-grow"
|
||||
v-text="props.row.name"
|
||||
></div>
|
||||
<div class="text-caption" v-if="props.row.short_id">
|
||||
{{ props.row.short_id }}
|
||||
<span v-text="props.row.short_id"></span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
@@ -188,9 +188,8 @@
|
||||
<q-badge
|
||||
rounded
|
||||
:color="states.find(s => s.value == props.row.state)?.color"
|
||||
v-text="states.find(s => s.value == props.row.state)?.label"
|
||||
>
|
||||
{{ states.find(s => s.value == props.row.state)?.label
|
||||
}}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
:disable='props.row.state !== "active"'
|
||||
@@ -210,15 +209,12 @@
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="column q-gutter-y-sm">
|
||||
{% raw %}
|
||||
<div
|
||||
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
|
||||
>
|
||||
@@ -254,19 +250,21 @@
|
||||
<q-tr :props="props">
|
||||
<div class="row no-wrap items-center q-gutter-sm">
|
||||
<div class="q-my-sm col-grow">
|
||||
<div class="text-subtitle1 text-bold">
|
||||
{{ props.row.alias }}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle1 text-bold"
|
||||
v-text="props.row.alias"
|
||||
></div>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.color}`"
|
||||
class="text-bold"
|
||||
v-text="'#'+props.row.color"
|
||||
>
|
||||
#{{ props.row.color }}
|
||||
</q-badge>
|
||||
<div class="text-bold">
|
||||
{{ shortenNodeId(props.row.id) }}
|
||||
</div>
|
||||
<div
|
||||
class="text-bold"
|
||||
v-text="shortenNodeId(props.row.id)"
|
||||
></div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
@@ -302,8 +300,6 @@
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<q-tab-panel name="dashboard">
|
||||
<q-card-section class="q-pa-none">
|
||||
{% raw %}
|
||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
||||
<div class="row q-col-gutter-lg q-mt-sm">
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
@@ -65,6 +64,5 @@
|
||||
></lnbits-channel-stats>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<q-dialog v-model="transactionDetailsDialog.show">
|
||||
<q-card class="my-card">
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<div class="text-center q-mb-lg">
|
||||
<div
|
||||
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
|
||||
@@ -18,7 +17,9 @@
|
||||
<div class="row q-my-md">
|
||||
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ transactionDetailsDialog.data.payment_hash }}
|
||||
<span
|
||||
v-text="transactionDetailsDialog.data.payment_hash"
|
||||
></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.payment_hash)"
|
||||
@@ -33,7 +34,7 @@
|
||||
>
|
||||
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ transactionDetailsDialog.data.preimage }}
|
||||
<span v-text="transactionDetailsDialog.data.preimage"></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.preimage)"
|
||||
@@ -66,7 +67,6 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -102,7 +102,6 @@
|
||||
:filter="paymentsTable.filter"
|
||||
@request="getPayments"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
@@ -211,9 +210,8 @@
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.destination?.color}`"
|
||||
class="text-bold"
|
||||
>
|
||||
{{ props.row.destination?.alias }}
|
||||
</q-badge>
|
||||
v-text="props.row.destination?.alias"
|
||||
></q-badge>
|
||||
<div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
@@ -233,7 +231,6 @@
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -266,7 +263,6 @@
|
||||
:filter="invoiceTable.filter"
|
||||
@request="getInvoices"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
@@ -305,8 +301,6 @@
|
||||
></lnbits-date>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 115
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
const CURRENT_CACHE = 'lnbits-{{ cache_version }}-'
|
||||
|
||||
const getApiKey = request => {
|
||||
let api_key = request.headers.get('X-Api-Key')
|
||||
@@ -17,8 +16,7 @@ self.addEventListener('activate', evt =>
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
|
||||
if (currentCacheVersion !== CACHE_VERSION) {
|
||||
if (!cacheName.startsWith(CURRENT_CACHE)) {
|
||||
return caches.delete(cacheName)
|
||||
}
|
||||
})
|
||||
+75
-22
@@ -32,6 +32,7 @@ from lnbits.core.helpers import (
|
||||
stop_extension_background_work,
|
||||
)
|
||||
from lnbits.core.models import (
|
||||
BaseWallet,
|
||||
ConversionData,
|
||||
CreateInvoice,
|
||||
CreateLnurl,
|
||||
@@ -51,6 +52,7 @@ from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_access_token,
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
get_key_type,
|
||||
parse_filters,
|
||||
require_admin_key,
|
||||
@@ -62,13 +64,14 @@ from lnbits.extension_manager import (
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
fetch_github_release_config,
|
||||
fetch_release_payment_info,
|
||||
get_valid_extensions,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi, url_for
|
||||
from lnbits.lnurl import decode as lnurl_decode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
allowed_currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
@@ -83,6 +86,7 @@ from ..crud import (
|
||||
delete_wallet,
|
||||
drop_extension_db,
|
||||
get_dbversions,
|
||||
get_installed_extension,
|
||||
get_payments,
|
||||
get_payments_history,
|
||||
get_payments_paginated,
|
||||
@@ -125,6 +129,15 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/wallets",
|
||||
name="Wallets",
|
||||
description="Get basic info for all of user's wallets.",
|
||||
)
|
||||
async def api_wallets(user: User = Depends(check_user_exists)) -> List[BaseWallet]:
|
||||
return [BaseWallet(**w.dict()) for w in user.wallets]
|
||||
|
||||
|
||||
@api_router.put("/api/v1/wallet/{new_name}")
|
||||
async def api_update_wallet_name(
|
||||
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
@@ -526,10 +539,7 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
# We use X_Api_Key here because we want this call to work with and without keys
|
||||
# If a valid key is given, we also return the field "details", otherwise not
|
||||
wallet = await get_wallet_for_key(X_Api_Key) if isinstance(X_Api_Key, str) else None
|
||||
wallet = wallet if wallet and not wallet.deleted else None
|
||||
# we have to specify the wallet id here, because postgres and sqlite return
|
||||
# internal payments in different order and get_standalone_payment otherwise
|
||||
# just fetches the first one, causing unpredictable results
|
||||
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, wallet_id=wallet.id if wallet else None
|
||||
)
|
||||
@@ -712,14 +722,8 @@ async def api_perform_lnurlauth(
|
||||
|
||||
|
||||
@api_router.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
if len(settings.lnbits_allowed_currencies) > 0:
|
||||
return [
|
||||
item
|
||||
for item in currencies.keys()
|
||||
if item.upper() in settings.lnbits_allowed_currencies
|
||||
]
|
||||
return list(currencies.keys())
|
||||
async def api_list_currencies_available() -> List[str]:
|
||||
return allowed_currencies()
|
||||
|
||||
|
||||
@api_router.post("/api/v1/conversion")
|
||||
@@ -796,7 +800,7 @@ async def api_install_extension(
|
||||
access_token: Optional[str] = Depends(check_access_token),
|
||||
):
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
if not release:
|
||||
raise HTTPException(
|
||||
@@ -808,13 +812,17 @@ async def api_install_extension(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
|
||||
)
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
ext_info = InstallableExtension(
|
||||
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
|
||||
)
|
||||
|
||||
ext_info.download_archive()
|
||||
|
||||
try:
|
||||
installed_ext = await get_installed_extension(data.ext_id)
|
||||
ext_info.payments = installed_ext.payments if installed_ext else []
|
||||
|
||||
await ext_info.download_archive()
|
||||
|
||||
ext_info.extract_archive()
|
||||
|
||||
extension = Extension.from_installable_ext(ext_info)
|
||||
@@ -824,8 +832,9 @@ async def api_install_extension(
|
||||
|
||||
await add_installed_extension(ext_info)
|
||||
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(data.ext_id, user.id, access_token)
|
||||
if extension.is_upgrade_extension:
|
||||
# call stop while the old routes are still active
|
||||
await stop_extension_background_work(data.ext_id, user.id, access_token)
|
||||
|
||||
if data.ext_id not in settings.lnbits_deactivated_extensions:
|
||||
settings.lnbits_deactivated_extensions += [data.ext_id]
|
||||
@@ -837,7 +846,8 @@ async def api_install_extension(
|
||||
ext_info.nofiy_upgrade()
|
||||
|
||||
return extension
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
ext_info.clean_extension_files()
|
||||
@@ -902,9 +912,18 @@ async def api_uninstall_extension(
|
||||
)
|
||||
async def get_extension_releases(ext_id: str):
|
||||
try:
|
||||
extension_releases: List[
|
||||
ExtensionRelease
|
||||
] = await InstallableExtension.get_extension_releases(ext_id)
|
||||
extension_releases: List[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
)
|
||||
|
||||
installed_ext = await get_installed_extension(ext_id)
|
||||
if not installed_ext:
|
||||
return extension_releases
|
||||
|
||||
for release in extension_releases:
|
||||
payment_info = installed_ext.find_existing_payment(release.pay_link)
|
||||
if payment_info:
|
||||
release.paid_sats = payment_info.amount
|
||||
|
||||
return extension_releases
|
||||
|
||||
@@ -914,6 +933,40 @@ async def get_extension_releases(ext_id: str):
|
||||
)
|
||||
|
||||
|
||||
@api_router.put("/api/v1/extension/invoice", dependencies=[Depends(check_admin)])
|
||||
async def get_extension_invoice(data: CreateExtension):
|
||||
try:
|
||||
assert data.cost_sats, "A non-zero amount must be specified"
|
||||
release = await InstallableExtension.get_extension_release(
|
||||
data.ext_id, data.source_repo, data.archive, data.version
|
||||
)
|
||||
assert release, "Release not found"
|
||||
assert release.pay_link, "Pay link not found for release"
|
||||
|
||||
payment_info = await fetch_release_payment_info(
|
||||
release.pay_link, data.cost_sats
|
||||
)
|
||||
assert payment_info and payment_info.payment_request, "Cannot request invoice"
|
||||
invoice = bolt11.decode(payment_info.payment_request)
|
||||
|
||||
assert invoice.amount_msat is not None, "Invoic amount is missing"
|
||||
invoice_amount = int(invoice.amount_msat / 1000)
|
||||
assert (
|
||||
invoice_amount == data.cost_sats
|
||||
), f"Wrong invoice amount: {invoice_amount}."
|
||||
assert (
|
||||
payment_info.payment_hash == invoice.payment_hash
|
||||
), "Wroong invoice payment hash"
|
||||
|
||||
return payment_info
|
||||
|
||||
except AssertionError as e:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(e))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice")
|
||||
|
||||
|
||||
@api_router.get(
|
||||
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
|
||||
dependencies=[Depends(check_admin)],
|
||||
|
||||
@@ -142,6 +142,8 @@ async def logout() -> JSONResponse:
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
response.delete_cookie("lnbits_last_active_wallet")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -286,9 +288,7 @@ def _auth_success_response(
|
||||
)
|
||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie(
|
||||
"is_lnbits_user_authorized", "true", samesite="none", secure=True
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
|
||||
return response
|
||||
@@ -298,9 +298,7 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
access_token = create_access_token(data={"sub": "" or "", "email": email})
|
||||
response = RedirectResponse(path)
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie(
|
||||
"is_lnbits_user_authorized", "true", samesite="none", secure=True
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
return response
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
from ...extension_manager import InstallableExtension, get_valid_extensions
|
||||
from ...utils.exchange_rates import currencies
|
||||
from ...utils.exchange_rates import allowed_currencies, currencies
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_wallet,
|
||||
@@ -216,14 +216,13 @@ async def wallet(
|
||||
"request": request,
|
||||
"user": user.dict(),
|
||||
"wallet": user_wallet.dict(),
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
},
|
||||
)
|
||||
resp.set_cookie(
|
||||
"lnbits_last_active_wallet", wallet_id, samesite="none", secure=True
|
||||
)
|
||||
resp.set_cookie("lnbits_last_active_wallet", wallet_id)
|
||||
return resp
|
||||
|
||||
|
||||
@@ -354,9 +353,16 @@ async def lnurlwallet(request: Request):
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/service-worker.js", response_class=FileResponse)
|
||||
async def service_worker():
|
||||
return FileResponse(Path("lnbits", "static", "js", "service-worker.js"))
|
||||
@generic_router.get("/service-worker.js")
|
||||
async def service_worker(request: Request):
|
||||
return template_renderer().TemplateResponse(
|
||||
"service-worker.js",
|
||||
{
|
||||
"request": request,
|
||||
"cache_version": settings.server_startup_time,
|
||||
},
|
||||
media_type="text/javascript",
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/manifest/{usr}.webmanifest")
|
||||
|
||||
@@ -108,9 +108,11 @@ async def api_delete_channel(
|
||||
) -> Optional[List[NodeChannel]]:
|
||||
return await node.close_channel(
|
||||
short_id,
|
||||
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
|
||||
if funding_txid is not None and output_index is not None
|
||||
else None,
|
||||
(
|
||||
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
|
||||
if funding_txid is not None and output_index is not None
|
||||
else None
|
||||
),
|
||||
force,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ from starlette.responses import RedirectResponse
|
||||
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
|
||||
from ..crud import (
|
||||
@@ -28,15 +29,15 @@ tinyurl_router = APIRouter()
|
||||
description="creates a tinyurl",
|
||||
)
|
||||
async def api_create_tinyurl(
|
||||
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
tinyurls = await get_tinyurl_by_url(url)
|
||||
try:
|
||||
for tinyurl in tinyurls:
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
return tinyurl
|
||||
return await create_tinyurl(url, endless, wallet.wallet.inkey)
|
||||
return await create_tinyurl(url, endless, wallet.wallet.id)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
|
||||
@@ -49,12 +50,12 @@ async def api_create_tinyurl(
|
||||
description="get a tinyurl by id",
|
||||
)
|
||||
async def api_get_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
return tinyurl
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
@@ -71,12 +72,12 @@ async def api_get_tinyurl(
|
||||
description="delete a tinyurl by id",
|
||||
)
|
||||
async def api_delete_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
if tinyurl.wallet == wallet.wallet.id:
|
||||
await delete_tinyurl(tinyurl_id)
|
||||
return {"deleted": True}
|
||||
raise HTTPException(
|
||||
|
||||
+17
-2
@@ -179,16 +179,27 @@ class Connection(Compat):
|
||||
values: Optional[List[str]] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[Type[TRowModel]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
) -> Page[TRowModel]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
clause = filters.where(where)
|
||||
parsed_values = filters.values(values)
|
||||
|
||||
group_by_string = ""
|
||||
if group_by:
|
||||
for field in group_by:
|
||||
if not re.fullmatch(
|
||||
r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", field
|
||||
):
|
||||
raise ValueError("Value for GROUP BY is invalid")
|
||||
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
||||
|
||||
rows = await self.fetchall(
|
||||
f"""
|
||||
{query}
|
||||
{clause}
|
||||
{group_by_string}
|
||||
{filters.order_by()}
|
||||
{filters.pagination()}
|
||||
""",
|
||||
@@ -202,6 +213,7 @@ class Connection(Compat):
|
||||
SELECT COUNT(*) FROM (
|
||||
{query}
|
||||
{clause}
|
||||
{group_by_string}
|
||||
) as count
|
||||
""",
|
||||
parsed_values,
|
||||
@@ -242,7 +254,9 @@ class Database(Compat):
|
||||
else:
|
||||
self.schema = None
|
||||
|
||||
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
|
||||
self.engine = create_engine(
|
||||
database_uri, strategy=ASYNCIO_STRATEGY, echo=settings.debug_database
|
||||
)
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
logger.trace(f"database {self.type} added for {self.name}")
|
||||
@@ -288,9 +302,10 @@ class Database(Compat):
|
||||
values: Optional[List[str]] = None,
|
||||
filters: Optional[Filters] = None,
|
||||
model: Optional[Type[TRowModel]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
) -> Page[TRowModel]:
|
||||
async with self.connect() as conn:
|
||||
return await conn.fetch_page(query, where, values, filters, model)
|
||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||
|
||||
async def execute(self, query: str, values: tuple = ()):
|
||||
async with self.connect() as conn:
|
||||
|
||||
+14
-13
@@ -3,7 +3,7 @@ from typing import Annotated, Literal, Optional, Type, Union
|
||||
|
||||
from fastapi import Cookie, Depends, Query, Request, Security
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn, SecuritySchemeType
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
||||
from fastapi.security.base import SecurityBase
|
||||
from jose import ExpiredSignatureError, JWTError, jwt
|
||||
@@ -17,14 +17,13 @@ from lnbits.core.crud import (
|
||||
get_user,
|
||||
get_wallet_for_key,
|
||||
)
|
||||
from lnbits.core.models import User, WalletType, WalletTypeInfo
|
||||
from lnbits.core.models import User, Wallet, WalletType, WalletTypeInfo
|
||||
from lnbits.db import Filter, Filters, TFilterModel
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth", auto_error=False)
|
||||
|
||||
|
||||
# TODO: fix type ignores
|
||||
class KeyChecker(SecurityBase):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,23 +32,25 @@ class KeyChecker(SecurityBase):
|
||||
api_key: Optional[str] = None,
|
||||
):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
self._key_type = WalletType.invoice
|
||||
self.auto_error: bool = auto_error
|
||||
self._key_type: WalletType = WalletType.invoice
|
||||
self._api_key = api_key
|
||||
if api_key:
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.query}, # type: ignore
|
||||
openapi_model = APIKey(
|
||||
**{"in": APIKeyIn.query},
|
||||
type=SecuritySchemeType.apiKey,
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - QUERY",
|
||||
)
|
||||
else:
|
||||
key = APIKey(
|
||||
**{"in": APIKeyIn.header}, # type: ignore
|
||||
openapi_model = APIKey(
|
||||
**{"in": APIKeyIn.header},
|
||||
type=SecuritySchemeType.apiKey,
|
||||
name="X-API-KEY",
|
||||
description="Wallet API Key - HEADER",
|
||||
)
|
||||
self.wallet = None
|
||||
self.model: APIKey = key
|
||||
self.wallet: Optional[Wallet] = None
|
||||
self.model: APIKey = openapi_model
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
try:
|
||||
@@ -62,12 +63,12 @@ class KeyChecker(SecurityBase):
|
||||
# avoided here. Also, we should not return the wallet here - thats
|
||||
# silly. Possibly store it in a Redis DB
|
||||
wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||
if not wallet or wallet.deleted:
|
||||
if not wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Invalid key or wallet.",
|
||||
)
|
||||
self.wallet = wallet # type: ignore
|
||||
self.wallet = wallet
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
|
||||
|
||||
+121
-28
@@ -1,16 +1,15 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Any, List, NamedTuple, Optional, Tuple
|
||||
from urllib import request
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
from pydantic import BaseModel
|
||||
@@ -33,6 +32,7 @@ class ExplicitRelease(BaseModel):
|
||||
warning: Optional[str]
|
||||
info_notification: Optional[str]
|
||||
critical_notification: Optional[str]
|
||||
pay_link: Optional[str]
|
||||
|
||||
def is_version_compatible(self):
|
||||
if not self.min_lnbits_version:
|
||||
@@ -78,8 +78,15 @@ class ExtensionConfig(BaseModel):
|
||||
return version_parse(self.min_lnbits_version) <= version_parse(settings.version)
|
||||
|
||||
|
||||
class ReleasePaymentInfo(BaseModel):
|
||||
amount: Optional[int] = None
|
||||
pay_link: Optional[str] = None
|
||||
payment_hash: Optional[str] = None
|
||||
payment_request: Optional[str] = None
|
||||
|
||||
|
||||
def download_url(url, save_path):
|
||||
with request.urlopen(url) as dl_file:
|
||||
with request.urlopen(url, timeout=60) as dl_file:
|
||||
with open(save_path, "wb") as out_file:
|
||||
out_file.write(dl_file.read())
|
||||
|
||||
@@ -155,6 +162,21 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def fetch_release_payment_info(
|
||||
url: str, amount: Optional[int] = None
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if amount:
|
||||
url = f"{url}?amount={amount}"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
return ReleasePaymentInfo(**resp.json())
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return None
|
||||
|
||||
|
||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
@@ -260,6 +282,26 @@ class ExtensionRelease(BaseModel):
|
||||
repo: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
pay_link: Optional[str] = None
|
||||
cost_sats: Optional[int] = None
|
||||
paid_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
@property
|
||||
def archive_url(self) -> str:
|
||||
if not self.pay_link:
|
||||
return self.archive
|
||||
return (
|
||||
f"{self.archive}?version=v{self.version}&payment_hash={self.payment_hash}"
|
||||
)
|
||||
|
||||
async def check_payment_requirements(self):
|
||||
if not self.pay_link:
|
||||
return
|
||||
|
||||
payment_info = await fetch_release_payment_info(self.pay_link)
|
||||
self.cost_sats = payment_info.amount if payment_info else None
|
||||
|
||||
@classmethod
|
||||
def from_github_release(
|
||||
cls, source_repo: str, r: "GitHubRepoRelease"
|
||||
@@ -290,12 +332,13 @@ class ExtensionRelease(BaseModel):
|
||||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
pay_link=e.pay_link,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def all_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
async def get_github_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
|
||||
try:
|
||||
github_releases = await fetch_github_releases(org, repo)
|
||||
return [
|
||||
@@ -318,6 +361,7 @@ class InstallableExtension(BaseModel):
|
||||
featured = False
|
||||
latest_release: Optional[ExtensionRelease] = None
|
||||
installed_release: Optional[ExtensionRelease] = None
|
||||
payments: List[ReleasePaymentInfo] = []
|
||||
archive: Optional[str] = None
|
||||
|
||||
@property
|
||||
@@ -368,30 +412,32 @@ class InstallableExtension(BaseModel):
|
||||
return self.installed_release.version
|
||||
return ""
|
||||
|
||||
def download_archive(self):
|
||||
async def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
try:
|
||||
assert self.installed_release, "installed_release is none."
|
||||
download_url(self.installed_release.archive, ext_zip_file)
|
||||
|
||||
self._restore_payment_info()
|
||||
|
||||
await asyncio.to_thread(
|
||||
download_url, self.installed_release.archive_url, ext_zip_file
|
||||
)
|
||||
|
||||
self._remember_payment_info()
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Cannot fetch extension archive file",
|
||||
)
|
||||
raise AssertionError("Cannot fetch extension archive file")
|
||||
|
||||
archive_hash = file_hash(ext_zip_file)
|
||||
if self.installed_release.hash and self.installed_release.hash != archive_hash:
|
||||
# remove downloaded archive
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="File hash missmatch. Will not install.",
|
||||
)
|
||||
raise AssertionError("File hash missmatch. Will not install.")
|
||||
|
||||
def extract_archive(self):
|
||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||
@@ -468,14 +514,54 @@ class InstallableExtension(BaseModel):
|
||||
if version_parse(self.latest_release.version) < version_parse(release.version):
|
||||
self.latest_release = release
|
||||
|
||||
def find_existing_payment(
|
||||
self, pay_link: Optional[str]
|
||||
) -> Optional[ReleasePaymentInfo]:
|
||||
if not pay_link:
|
||||
return None
|
||||
return next(
|
||||
(p for p in self.payments if p.pay_link == pay_link),
|
||||
None,
|
||||
)
|
||||
|
||||
def _restore_payment_info(self):
|
||||
if not self.installed_release:
|
||||
return
|
||||
if not self.installed_release.pay_link:
|
||||
return
|
||||
if self.installed_release.payment_hash:
|
||||
return
|
||||
payment_info = self.find_existing_payment(self.installed_release.pay_link)
|
||||
if payment_info:
|
||||
self.installed_release.payment_hash = payment_info.payment_hash
|
||||
|
||||
def _remember_payment_info(self):
|
||||
if not self.installed_release or not self.installed_release.pay_link:
|
||||
return
|
||||
payment_info = ReleasePaymentInfo(
|
||||
amount=self.installed_release.cost_sats,
|
||||
pay_link=self.installed_release.pay_link,
|
||||
payment_hash=self.installed_release.payment_hash,
|
||||
)
|
||||
self.payments = [
|
||||
p for p in self.payments if p.pay_link != payment_info.pay_link
|
||||
]
|
||||
self.payments.append(payment_info)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, data: dict) -> "InstallableExtension":
|
||||
meta = json.loads(data["meta"])
|
||||
ext = InstallableExtension(**data)
|
||||
if "installed_release" in meta:
|
||||
ext.installed_release = ExtensionRelease(**meta["installed_release"])
|
||||
if meta.get("payments"):
|
||||
ext.payments = [ReleasePaymentInfo(**p) for p in meta["payments"]]
|
||||
return ext
|
||||
|
||||
@classmethod
|
||||
def from_rows(cls, rows: List[Any] = []) -> List["InstallableExtension"]:
|
||||
return [InstallableExtension.from_row(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def from_github_release(
|
||||
cls, github_release: GitHubRelease
|
||||
@@ -565,17 +651,19 @@ class InstallableExtension(BaseModel):
|
||||
try:
|
||||
manifest = await fetch_manifest(url)
|
||||
for r in manifest.repos:
|
||||
if r.id == ext_id:
|
||||
repo_releases = await ExtensionRelease.all_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
if r.id != ext_id:
|
||||
continue
|
||||
repo_releases = await ExtensionRelease.get_github_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
|
||||
for e in manifest.extensions:
|
||||
if e.id == ext_id:
|
||||
extension_releases += [
|
||||
ExtensionRelease.from_explicit_release(url, e)
|
||||
]
|
||||
if e.id != ext_id:
|
||||
continue
|
||||
explicit_release = ExtensionRelease.from_explicit_release(url, e)
|
||||
await explicit_release.check_payment_requirements()
|
||||
extension_releases.append(explicit_release)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Manifest {url} failed with '{str(e)}'")
|
||||
@@ -584,15 +672,17 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@classmethod
|
||||
async def get_extension_release(
|
||||
cls, ext_id: str, source_repo: str, archive: str
|
||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||
) -> Optional["ExtensionRelease"]:
|
||||
all_releases: List[
|
||||
ExtensionRelease
|
||||
] = await InstallableExtension.get_extension_releases(ext_id)
|
||||
all_releases: List[ExtensionRelease] = (
|
||||
await InstallableExtension.get_extension_releases(ext_id)
|
||||
)
|
||||
selected_release = [
|
||||
r
|
||||
for r in all_releases
|
||||
if r.archive == archive and r.source_repo == source_repo
|
||||
if r.archive == archive
|
||||
and r.source_repo == source_repo
|
||||
and r.version == version
|
||||
]
|
||||
|
||||
return selected_release[0] if len(selected_release) != 0 else None
|
||||
@@ -602,6 +692,9 @@ class CreateExtension(BaseModel):
|
||||
ext_id: str
|
||||
archive: str
|
||||
source_repo: str
|
||||
version: str
|
||||
cost_sats: Optional[int] = 0
|
||||
payment_hash: Optional[str] = None
|
||||
|
||||
|
||||
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
|
||||
|
||||
@@ -92,6 +92,10 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
||||
|
||||
def get_current_extension_name() -> str:
|
||||
"""
|
||||
DEPRECATED: Use the repo name instead, will be removed in the future
|
||||
before: `register_invoice_listener(invoice_queue, get_current_extension_name())`
|
||||
after: `register_invoice_listener(invoice_queue, "my-extension")`
|
||||
|
||||
Returns the name of the extension that calls this method.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
+14
-10
@@ -217,17 +217,21 @@ class CoreLightningNode(Node):
|
||||
state=(
|
||||
ChannelState.ACTIVE
|
||||
if ch["state"] == "CHANNELD_NORMAL"
|
||||
else ChannelState.PENDING
|
||||
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
|
||||
else ChannelState.CLOSED
|
||||
if ch["state"]
|
||||
in (
|
||||
"CHANNELD_CLOSING",
|
||||
"CLOSINGD_COMPLETE",
|
||||
"CLOSINGD_SIGEXCHANGE",
|
||||
"ONCHAIN",
|
||||
else (
|
||||
ChannelState.PENDING
|
||||
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
|
||||
else (
|
||||
ChannelState.CLOSED
|
||||
if ch["state"]
|
||||
in (
|
||||
"CHANNELD_CLOSING",
|
||||
"CLOSINGD_COMPLETE",
|
||||
"CLOSINGD_SIGEXCHANGE",
|
||||
"ONCHAIN",
|
||||
)
|
||||
else ChannelState.INACTIVE
|
||||
)
|
||||
)
|
||||
else ChannelState.INACTIVE
|
||||
),
|
||||
)
|
||||
for ch in funds["channels"]
|
||||
|
||||
+12
-8
@@ -237,9 +237,11 @@ class LndRestNode(Node):
|
||||
remote_msat=msat(channel["remote_balance"]),
|
||||
total_msat=msat(channel["capacity"]),
|
||||
),
|
||||
state=ChannelState.ACTIVE
|
||||
if channel["active"]
|
||||
else ChannelState.INACTIVE,
|
||||
state=(
|
||||
ChannelState.ACTIVE
|
||||
if channel["active"]
|
||||
else ChannelState.INACTIVE
|
||||
),
|
||||
# name=channel['peer_alias'],
|
||||
name=info.alias,
|
||||
color=info.color,
|
||||
@@ -318,11 +320,13 @@ class LndRestNode(Node):
|
||||
amount=payment["value_msat"],
|
||||
fee=payment["fee_msat"],
|
||||
time=payment["creation_date"],
|
||||
destination=await self.get_peer_info(
|
||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
||||
)
|
||||
if payment["htlcs"]
|
||||
else None,
|
||||
destination=(
|
||||
await self.get_peer_info(
|
||||
payment["htlcs"][0]["route"]["hops"][-1]["pub_key"]
|
||||
)
|
||||
if payment["htlcs"]
|
||||
else None
|
||||
),
|
||||
bolt11=payment["payment_request"],
|
||||
preimage=payment["payment_preimage"],
|
||||
)
|
||||
|
||||
@@ -68,6 +68,16 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||
# list of redirects that extensions want to perform
|
||||
lnbits_extensions_redirects: List[Any] = Field(default=[])
|
||||
|
||||
def extension_upgrade_path(self, ext_id: str) -> Optional[str]:
|
||||
return next(
|
||||
(e for e in self.lnbits_upgraded_extensions if e.endswith(f"/{ext_id}")),
|
||||
None,
|
||||
)
|
||||
|
||||
def extension_upgrade_hash(self, ext_id: str) -> Optional[str]:
|
||||
path = settings.extension_upgrade_path(ext_id)
|
||||
return path.split("/")[0] if path else None
|
||||
|
||||
|
||||
class ThemesSettings(LNbitsSettings):
|
||||
lnbits_site_title: str = Field(default="LNbits")
|
||||
@@ -172,6 +182,7 @@ class LndRestFundingSource(LNbitsSettings):
|
||||
lnd_rest_cert: Optional[str] = Field(default=None)
|
||||
lnd_rest_macaroon: Optional[str] = Field(default=None)
|
||||
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
|
||||
lnd_rest_route_hints: bool = Field(default=True)
|
||||
lnd_cert: Optional[str] = Field(default=None)
|
||||
lnd_admin_macaroon: Optional[str] = Field(default=None)
|
||||
lnd_invoice_macaroon: Optional[str] = Field(default=None)
|
||||
@@ -196,6 +207,11 @@ class LnPayFundingSource(LNbitsSettings):
|
||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class ZBDFundingSource(LNbitsSettings):
|
||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class AlbyFundingSource(LNbitsSettings):
|
||||
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
|
||||
alby_access_token: Optional[str] = Field(default=None)
|
||||
@@ -235,6 +251,7 @@ class FundingSourcesSettings(
|
||||
LndGrpcFundingSource,
|
||||
LnPayFundingSource,
|
||||
AlbyFundingSource,
|
||||
ZBDFundingSource,
|
||||
OpenNodeFundingSource,
|
||||
SparkFundingSource,
|
||||
LnTipsFundingSource,
|
||||
@@ -342,6 +359,7 @@ class UpdateSettings(EditableSettings):
|
||||
|
||||
class EnvSettings(LNbitsSettings):
|
||||
debug: bool = Field(default=False)
|
||||
debug_database: bool = Field(default=False)
|
||||
bundle_assets: bool = Field(default=True)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=5000)
|
||||
@@ -389,6 +407,7 @@ class SuperUserSettings(LNbitsSettings):
|
||||
"LnTipsWallet",
|
||||
"LNPayWallet",
|
||||
"AlbyWallet",
|
||||
"ZBDWallet",
|
||||
"LNbitsWallet",
|
||||
"OpenNodeWallet",
|
||||
]
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -550,3 +550,7 @@ video {
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ window.localisation.br = {
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, ZBD, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
@@ -61,9 +61,10 @@ window.localisation.br = {
|
||||
service_fee_tooltip:
|
||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
payment_reactions: 'Reações de Pagamento',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
api_keys_api_docs: 'Chaves de API e documentação da API',
|
||||
api_keys_api_docs: 'URL do Node, chaves da API e documentação da API',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
@@ -190,6 +191,12 @@ window.localisation.br = {
|
||||
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
|
||||
enter_ip: 'Digite o IP e pressione enter',
|
||||
rate_limiter: 'Limitador de Taxa',
|
||||
wallet_limiter: 'Limitador de Carteira',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Retirada máxima diária da carteira em sats (0 para desativar)',
|
||||
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutos e segundos entre transações por carteira (0 para desativar)',
|
||||
number_of_requests: 'Número de solicitações',
|
||||
time_unit: 'Unidade de tempo',
|
||||
minute: 'minuto',
|
||||
@@ -208,6 +215,7 @@ window.localisation.br = {
|
||||
account_settings: 'Configurações da Conta',
|
||||
signin_with_google: 'Entrar com o Google',
|
||||
signin_with_github: 'Entrar com GitHub',
|
||||
signin_with_keycloak: 'Entrar com Keycloak',
|
||||
username_or_email: 'Nome de usuário ou E-mail',
|
||||
password: 'Senha',
|
||||
password_config: 'Configuração de Senha',
|
||||
@@ -230,5 +238,8 @@ window.localisation.br = {
|
||||
auth_provider: 'Provedor de Autenticação',
|
||||
my_account: 'Minha Conta',
|
||||
back: 'Voltar',
|
||||
logout: 'Sair'
|
||||
logout: 'Sair',
|
||||
look_and_feel: 'Aparência',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de Cores'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.cn = {
|
||||
name_your_wallet: '给你的 %{name}钱包起个名字',
|
||||
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
|
||||
lnbits_description:
|
||||
'LNbits 设置简单、轻量级,可以运行在任何闪电网络的版本上,目前支持 LND、Core Lightning、OpenNode、Alby, LNPay,甚至 LNbits 本身!您可以为自己运行 LNbits,或者为他人轻松提供资金托管。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
'LNbits 设置简单、轻量级,可以运行在任何闪电网络的版本上,目前支持 LND、Core Lightning、OpenNode、Alby, ZBD, LNPay,甚至 LNbits 本身!您可以为自己运行 LNbits,或者为他人轻松提供资金托管。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
export_to_phone: '通过二维码导出到手机',
|
||||
export_to_phone_desc:
|
||||
'这个二维码包含您钱包的URL。您可以使用手机扫描的方式打开您的钱包。',
|
||||
@@ -57,9 +57,10 @@ window.localisation.cn = {
|
||||
service_fee_max: '服务费:%{amount}% 每笔交易(最高 %{max} sats)',
|
||||
service_fee_tooltip: 'LNbits服务器管理员每笔外发交易收取的服务费',
|
||||
toggle_darkmode: '切换暗黑模式',
|
||||
payment_reactions: '支付反应',
|
||||
view_swagger_docs: '查看 LNbits Swagger API 文档',
|
||||
api_docs: 'API文档',
|
||||
api_keys_api_docs: 'API密钥和API文档',
|
||||
api_keys_api_docs: '节点URL、API密钥和API文档',
|
||||
lnbits_version: 'LNbits版本',
|
||||
runs_on: '可运行在',
|
||||
credit_hint: '按 Enter 键充值账户',
|
||||
@@ -179,6 +180,11 @@ window.localisation.cn = {
|
||||
allow_access_hint: '允许通过IP访问(将覆盖被屏蔽的IP)',
|
||||
enter_ip: '输入IP地址并按回车键',
|
||||
rate_limiter: '速率限制器',
|
||||
wallet_limiter: '钱包限制器',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'每日钱包最大提现额度(单位:sats)(设为0则禁用)',
|
||||
wallet_max_ballance: '钱包最大余额(以sats计)(设为0则禁用)',
|
||||
wallet_limit_secs_between_trans: '每个钱包交易间最少秒数(设为0则禁用)',
|
||||
number_of_requests: '请求次数',
|
||||
time_unit: '时间单位',
|
||||
minute: '分钟',
|
||||
@@ -196,7 +202,8 @@ window.localisation.cn = {
|
||||
create_account: '创建账户',
|
||||
account_settings: '账户设置',
|
||||
signin_with_google: '使用谷歌账号登录',
|
||||
signin_with_github: '使用 GitHub 登录',
|
||||
signin_with_github: '使用GitHub登录',
|
||||
signin_with_keycloak: '使用Keycloak登录',
|
||||
username_or_email: '用户名或电子邮箱',
|
||||
password: '密码',
|
||||
password_config: '密码配置',
|
||||
@@ -219,5 +226,8 @@ window.localisation.cn = {
|
||||
auth_provider: '认证提供者',
|
||||
my_account: '我的账户',
|
||||
back: '返回',
|
||||
logout: '注销'
|
||||
logout: '注销',
|
||||
look_and_feel: '外观和感觉',
|
||||
language: '语言',
|
||||
color_scheme: '配色方案'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.cs = {
|
||||
name_your_wallet: 'Pojmenujte svou %{name} peněženku',
|
||||
paste_invoice_label: 'Vložte fakturu, platební požadavek nebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Snadno nastavitelný a lehkotonážní, LNbits může běžet na jakémkoliv zdroji financování lightning-network, v současné době podporuje LND, Core Lightning, OpenNode, Alby, LNPay a dokonce LNbits samotné! LNbits můžete provozovat pro sebe, nebo snadno nabízet správu peněženek pro ostatní. Každá peněženka má své vlastní API klíče a není omezen počet peněženek, které můžete vytvořit. Možnost rozdělení prostředků dělá z LNbits užitečný nástroj pro správu peněz a jako vývojový nástroj. Rozšíření přidávají extra funkčnost k LNbits, takže můžete experimentovat s řadou špičkových technologií na lightning network. Vývoj rozšíření jsme učinili co nejjednodušší a jako svobodný a open-source projekt podporujeme lidi ve vývoji a zasílání vlastních rozšíření.',
|
||||
'Snadno nastavitelný a lehkotonážní, LNbits může běžet na jakémkoliv zdroji financování lightning-network, v současné době podporuje LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a dokonce LNbits samotné! LNbits můžete provozovat pro sebe, nebo snadno nabízet správu peněženek pro ostatní. Každá peněženka má své vlastní API klíče a není omezen počet peněženek, které můžete vytvořit. Možnost rozdělení prostředků dělá z LNbits užitečný nástroj pro správu peněz a jako vývojový nástroj. Rozšíření přidávají extra funkčnost k LNbits, takže můžete experimentovat s řadou špičkových technologií na lightning network. Vývoj rozšíření jsme učinili co nejjednodušší a jako svobodný a open-source projekt podporujeme lidi ve vývoji a zasílání vlastních rozšíření.',
|
||||
export_to_phone: 'Exportovat do telefonu pomocí QR kódu',
|
||||
export_to_phone_desc:
|
||||
'Tento QR kód obsahuje URL vaší peněženky s plným přístupem. Můžete jej naskenovat z telefonu a otevřít peněženku odtamtud.',
|
||||
@@ -61,9 +61,10 @@ window.localisation.cs = {
|
||||
service_fee_tooltip:
|
||||
'Servisní poplatek účtovaný správcem LNbits serveru za odchozí transakci',
|
||||
toggle_darkmode: 'Přepnout tmavý režim',
|
||||
payment_reactions: 'Reakce na platby',
|
||||
view_swagger_docs: 'Zobrazit LNbits Swagger API dokumentaci',
|
||||
api_docs: 'API dokumentace',
|
||||
api_keys_api_docs: 'API klíče a API dokumentace',
|
||||
api_keys_api_docs: 'Adresa uzlu, API klíče a API dokumentace',
|
||||
lnbits_version: 'Verze LNbits',
|
||||
runs_on: 'Běží na',
|
||||
credit_hint: 'Stiskněte Enter pro připsání na účet',
|
||||
@@ -187,6 +188,12 @@ window.localisation.cs = {
|
||||
allow_access_hint: 'Povolit přístup podle IP (přepíše blokované IP)',
|
||||
enter_ip: 'Zadejte IP a stiskněte enter',
|
||||
rate_limiter: 'Omezovač počtu požadavků',
|
||||
wallet_limiter: 'Omezení peněženky',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximální denní limit pro výběr z peněženky v sats (0 pro deaktivaci)',
|
||||
wallet_max_ballance: 'Maximální zůstatek v peněžence v sats (0 pro zakázání)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minimální počet sekund mezi transakcemi na peněženku (0 pro vypnutí)',
|
||||
number_of_requests: 'Počet požadavků',
|
||||
time_unit: 'Časová jednotka',
|
||||
minute: 'minuta',
|
||||
@@ -205,6 +212,7 @@ window.localisation.cs = {
|
||||
account_settings: 'Nastavení účtu',
|
||||
signin_with_google: 'Přihlásit se přes Google',
|
||||
signin_with_github: 'Přihlásit se přes GitHub',
|
||||
signin_with_keycloak: 'Přihlásit se přes Keycloak',
|
||||
username_or_email: 'Uživatelské jméno nebo Email',
|
||||
password: 'Heslo',
|
||||
password_config: 'Konfigurace hesla',
|
||||
@@ -227,5 +235,8 @@ window.localisation.cs = {
|
||||
auth_provider: 'Poskytovatel ověření',
|
||||
my_account: 'Můj účet',
|
||||
back: 'Zpět',
|
||||
logout: 'Odhlásit se'
|
||||
logout: 'Odhlásit se',
|
||||
look_and_feel: 'Vzhled a chování',
|
||||
language: 'Jazyk',
|
||||
color_scheme: 'Barevné schéma'
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ window.localisation.de = {
|
||||
paste_invoice_label:
|
||||
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
|
||||
lnbits_description:
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, Alby, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, Alby, ZBD, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
export_to_phone: 'Auf dem Telefon öffnen',
|
||||
export_to_phone_desc:
|
||||
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
|
||||
@@ -63,9 +63,10 @@ window.localisation.de = {
|
||||
service_fee_tooltip:
|
||||
'Bearbeitungsgebühr, die vom LNbits Server-Administrator pro ausgehender Transaktion berechnet wird',
|
||||
toggle_darkmode: 'Auf Dark Mode umschalten',
|
||||
payment_reactions: 'Zahlungsreaktionen',
|
||||
view_swagger_docs: 'LNbits Swagger API-Dokumentation',
|
||||
api_docs: 'API-Dokumentation',
|
||||
api_keys_api_docs: 'API-Schlüssel und API-Dokumentation',
|
||||
api_keys_api_docs: 'Knoten-URL, API-Schlüssel und API-Dokumentation',
|
||||
lnbits_version: 'LNbits-Version',
|
||||
runs_on: 'Läuft auf',
|
||||
credit_hint: 'Klicke Enter, um das Konto zu belasten',
|
||||
@@ -192,6 +193,13 @@ window.localisation.de = {
|
||||
allow_access_hint: 'Zugriff durch IP erlauben (überschreibt blockierte IPs)',
|
||||
enter_ip: 'Geben Sie die IP ein und drücken Sie die Eingabetaste',
|
||||
rate_limiter: 'Ratenbegrenzer',
|
||||
wallet_limiter: 'Geldbeutel-Limiter',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximales tägliches Wallet-Auszahlungslimit in Sats (0 zum Deaktivieren)',
|
||||
wallet_max_ballance:
|
||||
'Maximales Guthaben der Wallet in Sats (0 zum Deaktivieren)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Mindestsekunden zwischen Transaktionen pro Wallet (0 zum Deaktivieren)',
|
||||
number_of_requests: 'Anzahl der Anfragen',
|
||||
time_unit: 'Zeiteinheit',
|
||||
minute: 'Minute',
|
||||
@@ -211,6 +219,7 @@ window.localisation.de = {
|
||||
account_settings: 'Kontoeinstellungen',
|
||||
signin_with_google: 'Mit Google anmelden',
|
||||
signin_with_github: 'Anmelden mit GitHub',
|
||||
signin_with_keycloak: 'Mit Keycloak anmelden',
|
||||
username_or_email: 'Benutzername oder E-Mail',
|
||||
password: 'Passwort',
|
||||
password_config: 'Passwortkonfiguration',
|
||||
@@ -233,5 +242,8 @@ window.localisation.de = {
|
||||
auth_provider: 'Anbieter für Authentifizierung',
|
||||
my_account: 'Mein Konto',
|
||||
back: 'Zurück',
|
||||
logout: 'Abmelden'
|
||||
logout: 'Abmelden',
|
||||
look_and_feel: 'Aussehen und Verhalten',
|
||||
language: 'Sprache',
|
||||
color_scheme: 'Farbschema'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.en = {
|
||||
name_your_wallet: 'Name your %{name} wallet',
|
||||
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
||||
lnbits_description:
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, ZBD, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||
@@ -62,7 +62,7 @@ window.localisation.en = {
|
||||
payment_reactions: 'Payment Reactions',
|
||||
view_swagger_docs: 'View LNbits Swagger API docs',
|
||||
api_docs: 'API docs',
|
||||
api_keys_api_docs: 'API keys and API docs',
|
||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||
lnbits_version: 'LNbits version',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: 'Press Enter to credit account',
|
||||
@@ -97,8 +97,12 @@ window.localisation.en = {
|
||||
'This is an LNURL-withdraw QR code for slurping everything from this wallet. Do not share with anyone. It is compatible with balanceCheck and balanceNotify so your wallet may keep pulling the funds continuously from here after the first withdraw.',
|
||||
i_understand: 'I understand',
|
||||
copy_wallet_url: 'Copy wallet URL',
|
||||
disclaimer_dialog:
|
||||
'To ensure continuous access to your wallets, please remember to securely store your login credentials! Please visit the "My Account" page. This service is in BETA, and we hold no responsibility for people losing access to funds.',
|
||||
disclaimer_dialog_title: 'Important!',
|
||||
disclaimer_dialog: `You *must* save your login credentials to be able to access your wallet again.If you lose them, you will lose access to your wallet and funds.
|
||||
|
||||
Find your login credentials on your account settings page.
|
||||
|
||||
This service is in BETA. LNbits holds no responsibility for loss of access to funds.`,
|
||||
no_transactions: 'No transactions made yet',
|
||||
manage: 'Manage',
|
||||
extensions: 'Extensions',
|
||||
@@ -235,5 +239,12 @@ window.localisation.en = {
|
||||
logout: 'Logout',
|
||||
look_and_feel: 'Look and Feel',
|
||||
language: 'Language',
|
||||
color_scheme: 'Color Scheme'
|
||||
color_scheme: 'Color Scheme',
|
||||
extension_cost: 'This release requires a payment of minimum %{cost} sats.',
|
||||
extension_paid_sats: 'You have already paid %{paid_sats} sats.',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
pay_from_wallet: 'Pay from Wallet',
|
||||
show_qr: 'Show QR',
|
||||
retry_install: 'Retry Install',
|
||||
new_payment: 'Make New Payment'
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ window.localisation.es = {
|
||||
name_your_wallet: 'Nombre de su billetera %{name}',
|
||||
paste_invoice_label: 'Pegue la factura aquí',
|
||||
lnbits_description:
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, Alby, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, Alby, ZBD, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
export_to_phone: 'Exportar a teléfono con código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
|
||||
@@ -61,9 +61,10 @@ window.localisation.es = {
|
||||
service_fee_tooltip:
|
||||
'Comisión de servicio cobrada por el administrador del servidor LNbits por cada transacción saliente',
|
||||
toggle_darkmode: 'Cambiar modo oscuro',
|
||||
payment_reactions: 'Reacciones de Pago',
|
||||
view_swagger_docs: 'Ver documentación de API de LNbits Swagger',
|
||||
api_docs: 'Documentación de API',
|
||||
api_keys_api_docs: 'Claves de API y documentación de API',
|
||||
api_keys_api_docs: 'URL del nodo, claves de API y documentación de API',
|
||||
lnbits_version: 'Versión de LNbits',
|
||||
runs_on: 'Corre en',
|
||||
credit_hint: 'Presione Enter para cargar la cuenta',
|
||||
@@ -190,6 +191,13 @@ window.localisation.es = {
|
||||
allow_access_hint: 'Permitir acceso por IP (anulará las IPs bloqueadas)',
|
||||
enter_ip: 'Ingrese la IP y presione enter',
|
||||
rate_limiter: 'Limitador de tasa',
|
||||
wallet_limiter: 'Limitador de Cartera',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Límite diario de retiro de la cartera en sats (0 para deshabilitar)',
|
||||
wallet_max_ballance:
|
||||
'Saldo máximo de la billetera en sats (0 para desactivar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Mín. segs entre transacciones por cartera (0 para desactivar)',
|
||||
number_of_requests: 'Número de solicitudes',
|
||||
time_unit: 'Unidad de tiempo',
|
||||
minute: 'minuto',
|
||||
@@ -209,6 +217,7 @@ window.localisation.es = {
|
||||
account_settings: 'Configuración de la cuenta',
|
||||
signin_with_google: 'Inicia sesión con Google',
|
||||
signin_with_github: 'Inicia sesión con GitHub',
|
||||
signin_with_keycloak: 'Iniciar sesión con Keycloak',
|
||||
username_or_email: 'Nombre de usuario o correo electrónico',
|
||||
password: 'Contraseña',
|
||||
password_config: 'Configuración de Contraseña',
|
||||
@@ -231,5 +240,8 @@ window.localisation.es = {
|
||||
auth_provider: 'Proveedor de Autenticación',
|
||||
my_account: 'Mi cuenta',
|
||||
back: 'Atrás',
|
||||
logout: 'Cerrar sesión'
|
||||
logout: 'Cerrar sesión',
|
||||
look_and_feel: 'Apariencia',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de colores'
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ window.localisation.fi = {
|
||||
toggle_reactions: 'Käytä tapahtuma efektejä',
|
||||
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
|
||||
api_docs: 'API-dokumentaatio',
|
||||
api_keys_api_docs: 'API-avaimet ja -dokumentaatio',
|
||||
api_keys_api_docs: 'Solmun URL, API-avaimet ja -dokumentaatio',
|
||||
lnbits_version: 'LNbits versio',
|
||||
runs_on: 'Mukana menossa',
|
||||
credit_hint: 'Hyväksy painamalla Enter',
|
||||
@@ -191,6 +191,12 @@ window.localisation.fi = {
|
||||
allow_access_hint: 'Salli pääsy IP-osoitteen perusteella (ohittaa estot)',
|
||||
enter_ip: 'Anna IP ja paina +',
|
||||
rate_limiter: 'Toiston rajoitin',
|
||||
wallet_limiter: 'Lompakon Rajoitin',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maksimi päivittäinen lompakon nosto sateissa (0 poistaa käytöstä)',
|
||||
wallet_max_ballance: 'Lompakon maksimisaldo satosheina (0 poistaa käytöstä)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min sekuntia transaktioiden välillä lompakkoa kohden (0 poistaa käytöstä)',
|
||||
number_of_requests: 'Pyyntöjen lukumäärä',
|
||||
time_unit: 'aikayksikkö',
|
||||
minute: 'minuutti',
|
||||
@@ -209,6 +215,7 @@ window.localisation.fi = {
|
||||
account_settings: 'Tilin asetukset',
|
||||
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
||||
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
||||
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
|
||||
username_or_email: 'Käyttäjänimi tai sähköposti',
|
||||
password: 'Anna uusi salasana',
|
||||
password_config: 'Salasanan määritys',
|
||||
|
||||
@@ -39,7 +39,7 @@ window.localisation.fr = {
|
||||
paste_invoice_label:
|
||||
'Coller une facture, une demande de paiement ou un code lnurl *',
|
||||
lnbits_description:
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, Alby, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, Alby, ZBD, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
export_to_phone: 'Exporter vers le téléphone avec un code QR',
|
||||
export_to_phone_desc:
|
||||
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
|
||||
@@ -65,9 +65,10 @@ window.localisation.fr = {
|
||||
service_fee_tooltip:
|
||||
"Frais de service facturés par l'administrateur du serveur LNbits pour chaque transaction sortante",
|
||||
toggle_darkmode: 'Basculer le mode sombre',
|
||||
payment_reactions: 'Réactions de paiement',
|
||||
view_swagger_docs: "Voir les documentation de l'API Swagger de LNbits",
|
||||
api_docs: "Documentation de l'API",
|
||||
api_keys_api_docs: "Clés API et documentation de l'API",
|
||||
api_keys_api_docs: 'URL du nœud, clés API et documentation API',
|
||||
lnbits_version: 'Version de LNbits',
|
||||
runs_on: 'Fonctionne sur',
|
||||
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
|
||||
@@ -195,6 +196,13 @@ window.localisation.fr = {
|
||||
"Autoriser l'accès par IP (cela passera outre les IP bloquées)",
|
||||
enter_ip: "Entrez l'adresse IP et appuyez sur Entrée",
|
||||
rate_limiter: 'Limiteur de débit',
|
||||
wallet_limiter: 'Limiteur de portefeuille',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Retrait quotidien maximum du portefeuille en sats (0 pour désactiver)',
|
||||
wallet_max_ballance:
|
||||
'Solde maximum du portefeuille en sats (0 pour désactiver)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutes et secondes entre les transactions par portefeuille (0 pour désactiver)',
|
||||
number_of_requests: 'Nombre de requêtes',
|
||||
time_unit: 'Unité de temps',
|
||||
minute: 'minute',
|
||||
@@ -213,6 +221,7 @@ window.localisation.fr = {
|
||||
account_settings: 'Paramètres du compte',
|
||||
signin_with_google: 'Connectez-vous avec Google',
|
||||
signin_with_github: 'Connectez-vous avec GitHub',
|
||||
signin_with_keycloak: 'Connectez-vous avec Keycloak',
|
||||
username_or_email: "Nom d'utilisateur ou e-mail",
|
||||
password: 'Mot de passe',
|
||||
password_config: 'Configuration du mot de passe',
|
||||
@@ -235,5 +244,8 @@ window.localisation.fr = {
|
||||
auth_provider: "Fournisseur d'authentification",
|
||||
my_account: 'Mon compte',
|
||||
back: 'Retour',
|
||||
logout: 'Déconnexion'
|
||||
logout: 'Déconnexion',
|
||||
look_and_feel: 'Apparence',
|
||||
language: 'Langue',
|
||||
color_scheme: 'Schéma de couleurs'
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ window.localisation.it = {
|
||||
paste_invoice_label:
|
||||
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
|
||||
lnbits_description:
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, Alby, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, Alby, ZBD, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
export_to_phone: 'Esportazione su telefono con codice QR',
|
||||
export_to_phone_desc:
|
||||
"Questo codice QR contiene l'URL del portafoglio con accesso da amministratore. È possibile scansionarlo dal telefono per aprire il portafoglio da lì.",
|
||||
@@ -62,9 +62,10 @@ window.localisation.it = {
|
||||
service_fee_tooltip:
|
||||
"Commissione di servizio addebitata dall'amministratore del server LNbits per ogni transazione in uscita",
|
||||
toggle_darkmode: 'Attiva la modalità notturna',
|
||||
payment_reactions: 'Reazioni al Pagamento',
|
||||
view_swagger_docs: "Visualizza i documentazione dell'API Swagger di LNbits",
|
||||
api_docs: "Documentazione dell'API",
|
||||
api_keys_api_docs: "Chiavi API e documentazione dell'API",
|
||||
api_keys_api_docs: 'URL del nodo, chiavi API e documentazione API',
|
||||
lnbits_version: 'Versione di LNbits',
|
||||
runs_on: 'Esegue su',
|
||||
credit_hint: 'Premere Invio per accreditare i fondi',
|
||||
@@ -191,6 +192,13 @@ window.localisation.it = {
|
||||
"Consenti l'accesso per IP (sovrascriverà gli IP bloccati)",
|
||||
enter_ip: "Inserisci l'IP e premi invio",
|
||||
rate_limiter: 'Limitatore di frequenza',
|
||||
wallet_limiter: 'Limitatore del Portafoglio',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Prelievo massimo giornaliero dal portafoglio in sats (0 per disabilitare)',
|
||||
wallet_max_ballance:
|
||||
'Saldo massimo del portafoglio in sats (0 per disabilitare)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minuti e secondi tra transazioni per portafoglio (0 per disabilitare)',
|
||||
number_of_requests: 'Numero di richieste',
|
||||
time_unit: 'Unità di tempo',
|
||||
minute: 'minuto',
|
||||
@@ -210,6 +218,7 @@ window.localisation.it = {
|
||||
account_settings: "Impostazioni dell'account",
|
||||
signin_with_google: 'Accedi con Google',
|
||||
signin_with_github: 'Accedi con GitHub',
|
||||
signin_with_keycloak: 'Accedi con Keycloak',
|
||||
username_or_email: 'Nome utente o Email',
|
||||
password: 'Password',
|
||||
password_config: 'Configurazione della password',
|
||||
@@ -232,5 +241,8 @@ window.localisation.it = {
|
||||
auth_provider: 'Provider di Autenticazione',
|
||||
my_account: 'Il mio account',
|
||||
back: 'Indietro',
|
||||
logout: 'Esci'
|
||||
logout: 'Esci',
|
||||
look_and_feel: 'Aspetto e Comportamento',
|
||||
language: 'Lingua',
|
||||
color_scheme: 'Schema dei colori'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.jp = {
|
||||
name_your_wallet: 'あなたのウォレットの名前 %{name}',
|
||||
paste_invoice_label: '請求書を貼り付けてください',
|
||||
lnbits_description:
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、Alby, LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、Alby, ZBD, LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
export_to_phone: '電話にエクスポート',
|
||||
export_to_phone_desc:
|
||||
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
|
||||
@@ -59,9 +59,10 @@ window.localisation.jp = {
|
||||
service_fee_max: '取引手数料:%{amount}%(最大%{max}サトシ)',
|
||||
service_fee_tooltip: 'LNbitsサーバー管理者が発生する送金ごとの手数料',
|
||||
toggle_darkmode: 'ダークモードを切り替える',
|
||||
payment_reactions: '支払いの反応',
|
||||
view_swagger_docs: 'Swaggerドキュメントを表示',
|
||||
api_docs: 'APIドキュメント',
|
||||
api_keys_api_docs: 'APIキーとAPIドキュメント',
|
||||
api_keys_api_docs: 'ノードURL、APIキー、APIドキュメント',
|
||||
lnbits_version: 'LNbits バージョン',
|
||||
runs_on: 'で実行',
|
||||
credit_hint:
|
||||
@@ -188,6 +189,12 @@ window.localisation.jp = {
|
||||
'IPによるアクセスを許可する(ブロックされたIPを上書きします)',
|
||||
enter_ip: 'IPを入力してエンターキーを押してください',
|
||||
rate_limiter: 'レートリミッター',
|
||||
wallet_limiter: 'ウォレットリミッター',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'1日あたりの最大ウォレット出金額をsatsで入力してください(0 で無効)。',
|
||||
wallet_max_ballance: 'ウォレットの最大残高(sats)(0は無効)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'トランザクション間の最小秒数(ウォレットごと)(0は無効)',
|
||||
number_of_requests: 'リクエストの数',
|
||||
time_unit: '時間単位',
|
||||
minute: '分',
|
||||
@@ -207,6 +214,7 @@ window.localisation.jp = {
|
||||
account_settings: 'アカウント設定',
|
||||
signin_with_google: 'Googleでサインイン',
|
||||
signin_with_github: 'GitHubでサインイン',
|
||||
signin_with_keycloak: 'Keycloakでサインイン',
|
||||
username_or_email: 'ユーザー名またはメールアドレス',
|
||||
password: 'パスワード',
|
||||
password_config: 'パスワード設定',
|
||||
@@ -229,5 +237,8 @@ window.localisation.jp = {
|
||||
auth_provider: '認証プロバイダ',
|
||||
my_account: 'マイアカウント',
|
||||
back: '戻る',
|
||||
logout: 'ログアウト'
|
||||
logout: 'ログアウト',
|
||||
look_and_feel: 'ルック・アンド・フィール',
|
||||
language: '言語',
|
||||
color_scheme: 'カラースキーム'
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ window.localisation.kr = {
|
||||
name_your_wallet: '사용할 %{name}지갑의 이름을 정하세요',
|
||||
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
|
||||
lnbits_description:
|
||||
'설정이 쉽고 가벼운 LNBits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다. 현재 지원하는 예산 자원의 형태는 LND, Core Lightning, OpenNode, Alby, LNPay, 그리고 다른 LNBits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNBits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNBits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNBits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNBits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
'설정이 쉽고 가벼운 LNBits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다. 현재 지원하는 예산 자원의 형태는 LND, Core Lightning, OpenNode, Alby, ZBD, LNPay, 그리고 다른 LNBits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNBits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNBits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNBits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNBits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
|
||||
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
|
||||
export_to_phone_desc:
|
||||
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
|
||||
@@ -60,9 +60,10 @@ window.localisation.kr = {
|
||||
service_fee_tooltip:
|
||||
'지불 결제 시마다 LNBits 서버 관리자에게 납부되는 서비스 수수료',
|
||||
toggle_darkmode: '다크 모드 전환',
|
||||
payment_reactions: '결제 반응',
|
||||
view_swagger_docs: 'LNbits Swagger API 문서를 봅니다',
|
||||
api_docs: 'API 문서',
|
||||
api_keys_api_docs: 'API 키와 API 문서',
|
||||
api_keys_api_docs: '노드 URL, API 키와 API 문서',
|
||||
lnbits_version: 'LNbits 버전',
|
||||
runs_on: 'Runs on',
|
||||
credit_hint: '계정에 자금을 넣으려면 Enter를 눌러주세요',
|
||||
@@ -187,6 +188,11 @@ window.localisation.kr = {
|
||||
allow_access_hint: 'IP 기준으로 접속 허용하기 (차단한 IP들을 무시합니다)',
|
||||
enter_ip: 'IP 주소를 입력하고 Enter를 눌러주세요',
|
||||
rate_limiter: '횟수로 제한하기',
|
||||
wallet_limiter: '지갑 제한기',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'일일 최대 지갑 출금액(sats) (0은 비활성화)',
|
||||
wallet_max_ballance: '지갑 최대 잔액(sats) (0은 비활성화)',
|
||||
wallet_limit_secs_between_trans: '지갑 당 거래 사이 최소 초 (0은 비활성화)',
|
||||
number_of_requests: '요청 횟수',
|
||||
time_unit: '시간 단위',
|
||||
minute: '분',
|
||||
@@ -205,6 +211,7 @@ window.localisation.kr = {
|
||||
account_settings: '계정 설정',
|
||||
signin_with_google: 'Google으로 로그인',
|
||||
signin_with_github: 'GitHub으로 로그인',
|
||||
signin_with_keycloak: 'Keycloak으로 로그인',
|
||||
username_or_email: '사용자 이름 또는 이메일',
|
||||
password: '비밀번호',
|
||||
password_config: '비밀번호 설정',
|
||||
@@ -227,5 +234,8 @@ window.localisation.kr = {
|
||||
auth_provider: '인증 제공자',
|
||||
my_account: '내 계정',
|
||||
back: '뒤로',
|
||||
logout: '로그아웃'
|
||||
logout: '로그아웃',
|
||||
look_and_feel: '외관과 느낌',
|
||||
language: '언어',
|
||||
color_scheme: '색상 구성'
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ window.localisation.nl = {
|
||||
name_your_wallet: 'Geef je %{name} portemonnee een naam',
|
||||
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
|
||||
lnbits_description:
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, Alby, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, Alby, ZBD, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
export_to_phone: 'Exporteren naar telefoon met QR-code',
|
||||
export_to_phone_desc:
|
||||
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
|
||||
@@ -63,9 +63,10 @@ window.localisation.nl = {
|
||||
service_fee_tooltip:
|
||||
'Transactiekosten in rekening gebracht door de LNbits serverbeheerder per uitgaande transactie',
|
||||
toggle_darkmode: 'Donkere modus aan/uit',
|
||||
payment_reactions: 'Betalingsreacties',
|
||||
view_swagger_docs: 'Bekijk LNbits Swagger API-documentatie',
|
||||
api_docs: 'API-documentatie',
|
||||
api_keys_api_docs: 'API-sleutels en API-documentatie',
|
||||
api_keys_api_docs: 'Node URL, API-sleutels en API-documentatie',
|
||||
lnbits_version: 'LNbits-versie',
|
||||
runs_on: 'Draait op',
|
||||
credit_hint: 'Druk op Enter om de rekening te crediteren',
|
||||
@@ -191,6 +192,13 @@ window.localisation.nl = {
|
||||
"Toegang verlenen op basis van IP (zal geblokkeerde IP's overschrijven)",
|
||||
enter_ip: 'Voer IP in en druk op enter',
|
||||
rate_limiter: 'Snelheidsbegrenzer',
|
||||
wallet_limiter: 'Portemonnee Limietsteller',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximale dagelijkse opname van wallet in sats (0 om uit te schakelen)',
|
||||
wallet_max_ballance:
|
||||
'Maximale portefeuillesaldo in sats (0 om uit te schakelen)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min seconden tussen transacties per portemonnee (0 om uit te schakelen)',
|
||||
number_of_requests: 'Aantal verzoeken',
|
||||
time_unit: 'Tijdeenheid',
|
||||
minute: 'minuut',
|
||||
@@ -209,6 +217,7 @@ window.localisation.nl = {
|
||||
account_settings: 'Accountinstellingen',
|
||||
signin_with_google: 'Inloggen met Google',
|
||||
signin_with_github: 'Inloggen met GitHub',
|
||||
signin_with_keycloak: 'Inloggen met Keycloak',
|
||||
username_or_email: 'Gebruikersnaam of e-mail',
|
||||
password: 'Wachtwoord',
|
||||
password_config: 'Wachtwoordconfiguratie',
|
||||
@@ -231,5 +240,8 @@ window.localisation.nl = {
|
||||
auth_provider: 'Auth Provider',
|
||||
my_account: 'Mijn Account',
|
||||
back: 'Terug',
|
||||
logout: 'Afmelden'
|
||||
logout: 'Afmelden',
|
||||
look_and_feel: 'Uiterlijk en gedrag',
|
||||
language: 'Taal',
|
||||
color_scheme: 'Kleurenschema'
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ window.localisation.pi = {
|
||||
name_your_wallet: 'Name yer %{name} treasure chest',
|
||||
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
|
||||
lnbits_description:
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, Alby, ZBD, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code, me hearties',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains yer chest URL with full access. Ye can scan it from yer phone to open yer chest from there, arr!',
|
||||
@@ -61,9 +61,10 @@ window.localisation.pi = {
|
||||
service_fee_tooltip:
|
||||
"Service fee charged by the LNbits server admin per goin' transaction",
|
||||
toggle_darkmode: 'Toggle Dark Mode, arr!',
|
||||
payment_reactions: 'Payment Reactions',
|
||||
view_swagger_docs: 'View LNbits Swagger API docs and learn the secrets',
|
||||
api_docs: 'API docs for the scallywags',
|
||||
api_keys_api_docs: 'API keys and API docs',
|
||||
api_keys_api_docs: 'Node URL, API keys and API docs',
|
||||
lnbits_version: 'LNbits version, arr!',
|
||||
runs_on: 'Runs on, matey',
|
||||
credit_hint: 'Press Enter to credit account and make it richer',
|
||||
@@ -189,6 +190,12 @@ window.localisation.pi = {
|
||||
allow_access_hint: 'Grant permission by IP (will override barred IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
wallet_limiter: 'Pouch Limitar',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Max daily wallet withdrawal in sats (0 ter disable)',
|
||||
wallet_max_ballance: 'Purse max heaviness in sats (0 fer scuttle)',
|
||||
wallet_limit_secs_between_trans:
|
||||
"Min secs 'tween transactions per wallet (0 to disable)",
|
||||
number_of_requests: "Number o' requests",
|
||||
time_unit: "time bein'",
|
||||
minute: 'minnit',
|
||||
@@ -207,6 +214,7 @@ window.localisation.pi = {
|
||||
account_settings: "Account Settin's",
|
||||
signin_with_google: "Sign in wit' Google",
|
||||
signin_with_github: "Sign in wit' GitHub",
|
||||
signin_with_keycloak: "Sign in wit' Keycloak",
|
||||
username_or_email: 'Usarrrname or Email',
|
||||
password: 'Passwarrd',
|
||||
password_config: 'Passwarrd Config',
|
||||
@@ -229,5 +237,8 @@ window.localisation.pi = {
|
||||
auth_provider: 'Auth Provider becometh Auth Provider, ye see?',
|
||||
my_account: 'Me Arrrccount',
|
||||
back: 'Return',
|
||||
logout: 'Log out yer session'
|
||||
logout: 'Log out yer session',
|
||||
look_and_feel: 'Look and Feel',
|
||||
language: 'Langwidge',
|
||||
color_scheme: 'Colour Scheme'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.pl = {
|
||||
name_your_wallet: 'Nazwij swój portfel %{name}',
|
||||
paste_invoice_label: 'Wklej fakturę, żądanie zapłaty lub kod lnurl *',
|
||||
lnbits_description:
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, Alby, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, Alby, ZBD, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
export_to_phone: 'Eksport kodu QR na telefon',
|
||||
export_to_phone_desc:
|
||||
'Ten kod QR zawiera adres URL Twojego portfela z pełnym dostępem do niego. Możesz go zeskanować na swoim telefonie aby otworzyć na nim ten portfel.',
|
||||
@@ -60,9 +60,10 @@ window.localisation.pl = {
|
||||
service_fee_tooltip:
|
||||
'Opłata serwisowa pobierana przez administratora serwera LNbits za każdą wychodzącą transakcję',
|
||||
toggle_darkmode: 'Tryb nocny',
|
||||
payment_reactions: 'Reakcje na płatność',
|
||||
view_swagger_docs: 'Dokumentacja Swagger API',
|
||||
api_docs: 'Dokumentacja API',
|
||||
api_keys_api_docs: 'Klucze API i dokumentacja API',
|
||||
api_keys_api_docs: 'Adres URL węzła, klucze API i dokumentacja API',
|
||||
lnbits_version: 'Wersja LNbits',
|
||||
runs_on: 'Działa na',
|
||||
credit_hint: 'Naciśnij Enter aby doładować konto',
|
||||
@@ -188,6 +189,12 @@ window.localisation.pl = {
|
||||
'Zezwól na dostęp przez IP (zignoruje zablokowane adresy IP)',
|
||||
enter_ip: 'Wpisz adres IP i naciśnij enter',
|
||||
rate_limiter: 'Ogranicznik Częstotliwości',
|
||||
wallet_limiter: 'Ogranicznik Portfela',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maksymalna dzienna wypłata z portfela w satoshi (0 aby wyłączyć)',
|
||||
wallet_max_ballance: 'Maksymalny stan portfela w satoshi (0 aby wyłączyć)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min sekund pomiędzy transakcjami na portfel (0 aby wyłączyć)',
|
||||
number_of_requests: 'Liczba żądań',
|
||||
time_unit: 'Jednostka czasu',
|
||||
minute: 'minuta',
|
||||
@@ -206,6 +213,7 @@ window.localisation.pl = {
|
||||
account_settings: 'Ustawienia konta',
|
||||
signin_with_google: 'Zaloguj się przez Google',
|
||||
signin_with_github: 'Zaloguj się przez GitHub',
|
||||
signin_with_keycloak: 'Zaloguj się przez Keycloak',
|
||||
username_or_email: 'Nazwa użytkownika lub Email',
|
||||
password: 'Hasło',
|
||||
password_config: 'Konfiguracja Hasła',
|
||||
@@ -228,5 +236,8 @@ window.localisation.pl = {
|
||||
auth_provider: 'Dostawca uwierzytelniania',
|
||||
my_account: 'Moje Konto',
|
||||
back: 'Wstecz',
|
||||
logout: 'Wyloguj'
|
||||
logout: 'Wyloguj',
|
||||
look_and_feel: 'Wygląd i zachowanie',
|
||||
language: 'Język',
|
||||
color_scheme: 'Schemat kolorów'
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ window.localisation.pt = {
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, Alby, ZBD, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
@@ -61,9 +61,10 @@ window.localisation.pt = {
|
||||
service_fee_tooltip:
|
||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
payment_reactions: 'Reações de Pagamento',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
api_keys_api_docs: 'Chaves de API e documentação de API',
|
||||
api_keys_api_docs: 'URL do Nó, chaves de API e documentação de API',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
@@ -189,6 +190,12 @@ window.localisation.pt = {
|
||||
allow_access_hint: 'Permitir acesso por IP (substituirá IPs bloqueados)',
|
||||
enter_ip: 'Digite o IP e pressione enter.',
|
||||
rate_limiter: 'Limitador de Taxa',
|
||||
wallet_limiter: 'Limitador de Carteira',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Limite diário máximo de saque da carteira em sats (0 para desativar)',
|
||||
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minutos seg. entre transações por carteira (0 para desativar)',
|
||||
number_of_requests: 'Número de solicitações',
|
||||
time_unit: 'Unidade de tempo',
|
||||
minute: 'minuto',
|
||||
@@ -207,6 +214,7 @@ window.localisation.pt = {
|
||||
account_settings: 'Configurações da Conta',
|
||||
signin_with_google: 'Entrar com o Google',
|
||||
signin_with_github: 'Entrar com o GitHub',
|
||||
signin_with_keycloak: 'Entrar com o Keycloak',
|
||||
username_or_email: 'Nome de usuário ou Email',
|
||||
password: 'Senha',
|
||||
password_config: 'Configuração de Senha',
|
||||
@@ -229,5 +237,8 @@ window.localisation.pt = {
|
||||
auth_provider: 'Provedor de Autenticação',
|
||||
my_account: 'Minha Conta',
|
||||
back: 'Voltar',
|
||||
logout: 'Sair'
|
||||
logout: 'Sair',
|
||||
look_and_feel: 'Aparência e Sensação',
|
||||
language: 'Idioma',
|
||||
color_scheme: 'Esquema de Cores'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.sk = {
|
||||
name_your_wallet: 'Pomenujte vašu %{name} peňaženku',
|
||||
paste_invoice_label: 'Vložte faktúru, platobnú požiadavku alebo lnurl kód *',
|
||||
lnbits_description:
|
||||
'Ľahko nastaviteľný a ľahkotonážny, LNbits môže bežať na akomkoľvek zdroji financovania lightning-network, momentálne podporuje LND, Core Lightning, OpenNode, Alby, LNPay a dokonca LNbits samotný! LNbits môžete používať pre seba, alebo ľahko ponúknuť správcovské riešenie pre iných. Každá peňaženka má svoje vlastné API kľúče a nie je limit na počet peňaženiek, ktoré môžete vytvoriť. Schopnosť rozdeľovať finančné prostriedky robí z LNbits užitočný nástroj pre správu peňazí a ako vývojový nástroj. Rozšírenia pridávajú extra funkčnosť do LNbits, takže môžete experimentovať s radou najnovších technológií na lightning sieti. Vývoj rozšírení sme urobili čo najjednoduchší a ako voľný a open-source projekt, podporujeme ľudí vývoj a odovzdávanie vlastných rozšírení.',
|
||||
'Ľahko nastaviteľný a ľahkotonážny, LNbits môže bežať na akomkoľvek zdroji financovania lightning-network, momentálne podporuje LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a dokonca LNbits samotný! LNbits môžete používať pre seba, alebo ľahko ponúknuť správcovské riešenie pre iných. Každá peňaženka má svoje vlastné API kľúče a nie je limit na počet peňaženiek, ktoré môžete vytvoriť. Schopnosť rozdeľovať finančné prostriedky robí z LNbits užitočný nástroj pre správu peňazí a ako vývojový nástroj. Rozšírenia pridávajú extra funkčnosť do LNbits, takže môžete experimentovať s radou najnovších technológií na lightning sieti. Vývoj rozšírení sme urobili čo najjednoduchší a ako voľný a open-source projekt, podporujeme ľudí vývoj a odovzdávanie vlastných rozšírení.',
|
||||
export_to_phone: 'Exportovať do telefónu s QR kódom',
|
||||
export_to_phone_desc:
|
||||
'Tento QR kód obsahuje URL vašej peňaženky s plným prístupom. Môžete ho naskenovať z vášho telefónu a otvoriť vašu peňaženku odtiaľ.',
|
||||
@@ -60,9 +60,10 @@ window.localisation.sk = {
|
||||
service_fee_tooltip:
|
||||
'Servisný poplatok účtovaný správcom LNbits servera za odchádzajúcu transakciu',
|
||||
toggle_darkmode: 'Prepnúť Tmavý režim',
|
||||
payment_reactions: 'Reakcie na platbu',
|
||||
view_swagger_docs: 'Zobraziť LNbits Swagger API dokumentáciu',
|
||||
api_docs: 'API dokumentácia',
|
||||
api_keys_api_docs: 'API kľúče a API dokumentácia',
|
||||
api_keys_api_docs: 'Adresa uzla, API kľúče a API dokumentácia',
|
||||
lnbits_version: 'Verzia LNbits',
|
||||
runs_on: 'Beží na',
|
||||
credit_hint: 'Stlačte Enter pre pripísanie na účet',
|
||||
@@ -187,6 +188,13 @@ window.localisation.sk = {
|
||||
allow_access_hint: 'Povoliť prístup podľa IP (prebije blokované IP)',
|
||||
enter_ip: 'Zadajte IP a stlačte enter',
|
||||
rate_limiter: 'Obmedzovač počtu požiadaviek',
|
||||
wallet_limiter: 'Obmedzovač peňaženky',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Maximálny denný výber z peňaženky v satošiach (0 pre zrušenie)',
|
||||
wallet_max_ballance:
|
||||
'Maximálny zostatok v peňaženke v satošiach (0 pre deaktiváciu)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Minimálny počet sekúnd medzi transakciami na peňaženku (0 na deaktiváciu)',
|
||||
number_of_requests: 'Počet požiadaviek',
|
||||
time_unit: 'Časová jednotka',
|
||||
minute: 'minúta',
|
||||
@@ -203,8 +211,9 @@ window.localisation.sk = {
|
||||
login_to_account: 'Prihláste sa do vášho účtu',
|
||||
create_account: 'Vytvoriť účet',
|
||||
account_settings: 'Nastavenia účtu',
|
||||
signin_with_google: 'Prihlásiť sa cez Google',
|
||||
signin_with_github: 'Prihláste sa pomocou GitHub',
|
||||
signin_with_google: 'Prihlásiť sa pomocou Google',
|
||||
signin_with_github: 'Prihlásiť sa pomocou GitHub',
|
||||
signin_with_keycloak: 'Prihlásiť sa pomocou Keycloak',
|
||||
username_or_email: 'Používateľské meno alebo email',
|
||||
password: 'Heslo',
|
||||
password_config: 'Konfigurácia hesla',
|
||||
@@ -227,5 +236,8 @@ window.localisation.sk = {
|
||||
auth_provider: 'Poskytovateľ autentifikácie',
|
||||
my_account: 'Môj účet',
|
||||
back: 'Späť',
|
||||
logout: 'Odhlásiť sa'
|
||||
logout: 'Odhlásiť sa',
|
||||
look_and_feel: 'Vzhľad a dojem',
|
||||
language: 'Jazyk',
|
||||
color_scheme: 'Farebná schéma'
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ window.localisation.we = {
|
||||
name_your_wallet: 'Enwch eich waled %{name}',
|
||||
paste_invoice_label: 'Gludwch anfoneb, cais am daliad neu god lnurl *',
|
||||
lnbits_description:
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, Alby, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, Alby, ZBD, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
export_to_phone: 'Allforio i Ffôn gyda chod QR',
|
||||
export_to_phone_desc:
|
||||
'Mae`r cod QR hwn yn cynnwys URL eich waled gyda mynediad llawn. Gallwch ei sganio o`ch ffôn i agor eich waled oddi yno.',
|
||||
@@ -61,9 +61,10 @@ window.localisation.we = {
|
||||
service_fee_tooltip:
|
||||
"Ffi gwasanaeth a godir gan weinyddwr gweinydd LNbits ym mhob trafodiad sy'n mynd allan",
|
||||
toggle_darkmode: 'Toglo Modd Tywyll',
|
||||
payment_reactions: 'Adweithiau Talu',
|
||||
view_swagger_docs: 'Gweld dogfennau API LNbits Swagger',
|
||||
api_docs: 'Dogfennau API',
|
||||
api_keys_api_docs: 'Allweddi API a dogfennau API',
|
||||
api_keys_api_docs: 'URL y nod, allweddi API a dogfennau API',
|
||||
lnbits_version: 'Fersiwn LNbits',
|
||||
runs_on: 'Yn rhedeg ymlaen',
|
||||
credit_hint: 'Pwyswch Enter i gyfrif credyd',
|
||||
@@ -188,6 +189,12 @@ window.localisation.we = {
|
||||
"Caniatáu mynediad gan IP (bydd yn diystyru IPs sydd wedi'u blocio)",
|
||||
enter_ip: 'Rhowch IP a gwasgwch enter',
|
||||
rate_limiter: 'Cyfyngydd Cyfradd',
|
||||
wallet_limiter: 'Cyfyngwr Waled',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Uchafswm tynnu’n ôl waled dyddiol mewn sats (0 i analluogi)',
|
||||
wallet_max_ballance: 'Uchafswm balans y waled mewn sats (0 i analluogi)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Eiliadau lleiaf rhwng trafodion fesul waled (0 i analluogi)',
|
||||
number_of_requests: 'Nifer y ceisiadau',
|
||||
time_unit: 'Uned amser',
|
||||
minute: 'munud',
|
||||
@@ -206,6 +213,7 @@ window.localisation.we = {
|
||||
account_settings: 'Gosodiadau Cyfrif',
|
||||
signin_with_google: 'Mewngofnodi gyda Google',
|
||||
signin_with_github: 'Mewngofnodi gyda GitHub',
|
||||
signin_with_keycloak: 'Mewngofnodi gyda Keycloak',
|
||||
username_or_email: 'Defnyddiwr neu E-bost',
|
||||
password: 'Cyfrinair',
|
||||
password_config: 'Ffurfweddiad Cyfrinair',
|
||||
@@ -228,5 +236,8 @@ window.localisation.we = {
|
||||
auth_provider: 'Darparwr Dilysiad',
|
||||
my_account: 'Fy Nghyfrif',
|
||||
back: 'Yn ôl',
|
||||
logout: 'Allgofnodi'
|
||||
logout: 'Allgofnodi',
|
||||
look_and_feel: 'Edrych a Theimlo',
|
||||
language: 'Iaith',
|
||||
color_scheme: 'Cynllun Lliw'
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -48,8 +48,7 @@ new Vue({
|
||||
show: false
|
||||
},
|
||||
tab: 'funding',
|
||||
needsRestart: false,
|
||||
introJs: null
|
||||
needsRestart: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -57,32 +56,6 @@ new Vue({
|
||||
this.getAudit()
|
||||
this.balance = +'{{ balance|safe }}'
|
||||
},
|
||||
mounted() {
|
||||
const url = window.location.href
|
||||
const queryString = url.split('?')[1]
|
||||
const queryObject = {}
|
||||
|
||||
if (queryString) {
|
||||
for (const param of queryString.split('&')) {
|
||||
const [key, value] = param.split('=')
|
||||
queryObject[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (queryObject.hasOwnProperty('first_use')) {
|
||||
const scriptTag = document.createElement('script')
|
||||
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
|
||||
const linkTag = document.createElement('link')
|
||||
linkTag.rel = 'stylesheet'
|
||||
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
|
||||
|
||||
document.body.appendChild(scriptTag)
|
||||
document.head.appendChild(linkTag)
|
||||
scriptTag.onload = () => {
|
||||
this.runOnboarding()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lnbitsVersion() {
|
||||
return LNBITS_VERSION
|
||||
@@ -95,60 +68,6 @@ new Vue({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
runOnboarding() {
|
||||
introJs()
|
||||
.onbeforeexit(() => {
|
||||
return window.history.replaceState(
|
||||
null,
|
||||
null,
|
||||
window.location.pathname
|
||||
)
|
||||
})
|
||||
.setOptions({
|
||||
disableInteraction: true,
|
||||
tooltipClass: 'introjs-tooltip-custom',
|
||||
dontShowAgain: true,
|
||||
steps: [
|
||||
{
|
||||
title: 'Welcome to LNbits',
|
||||
intro: 'Start your tour!'
|
||||
},
|
||||
{
|
||||
title: 'Add a funding source',
|
||||
element: document.getElementById('funding-sources'),
|
||||
intro:
|
||||
'Select a Lightning Network funding source to add funds to your LNbits.'
|
||||
},
|
||||
{
|
||||
title: 'Access your wallet',
|
||||
element: document.getElementById('wallet-list'),
|
||||
position: 'right',
|
||||
intro:
|
||||
'Wallets are the core of LNbits. They are the place where you can store your funds.'
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Manage LNbits',
|
||||
element: document.getElementById('admin-manage'),
|
||||
position: 'right',
|
||||
intro:
|
||||
'This is the place where you can manage your LNbits. You can configure settings, install extensions, themes, view logs, manage a node, and more.'
|
||||
},
|
||||
{
|
||||
title: 'Access extensions',
|
||||
element: document.getElementById('extension-list'),
|
||||
position: 'right',
|
||||
intro:
|
||||
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
|
||||
},
|
||||
{
|
||||
title: 'Farewell!',
|
||||
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
|
||||
}
|
||||
]
|
||||
})
|
||||
.start()
|
||||
},
|
||||
addAdminUser() {
|
||||
let addUser = this.formAddAdmin
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
|
||||
@@ -50,7 +50,8 @@ Vue.component('lnbits-funding-sources', {
|
||||
lnd_rest_endpoint: 'Endpoint',
|
||||
lnd_rest_cert: 'Certificate',
|
||||
lnd_rest_macaroon: 'Macaroon',
|
||||
lnd_rest_macaroon_encrypted: 'Encrypted Macaroon'
|
||||
lnd_rest_macaroon_encrypted: 'Encrypted Macaroon',
|
||||
lnd_rest_route_hints: 'Enable Route Hints'
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -105,6 +106,14 @@ Vue.component('lnbits-funding-sources', {
|
||||
alby_access_token: 'Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'ZBDWallet',
|
||||
'ZBD',
|
||||
{
|
||||
zbd_api_endpoint: 'Endpoint',
|
||||
zbd_api_key: 'Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'OpenNodeWallet',
|
||||
'OpenNode',
|
||||
|
||||
@@ -50,7 +50,7 @@ new Vue({
|
||||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
window.location.href = '/wallet?first_use'
|
||||
window.location.href = '/wallet'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
||||
+7
-116
@@ -90,6 +90,7 @@ new Vue({
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
origin: window.location.origin,
|
||||
user: LNbits.map.user(window.user),
|
||||
receive: {
|
||||
show: false,
|
||||
@@ -359,7 +360,6 @@ new Vue({
|
||||
},
|
||||
onPaymentReceived: function (paymentHash) {
|
||||
this.fetchPayments()
|
||||
this.fetchBalance()
|
||||
|
||||
if (this.receive.paymentHash === paymentHash) {
|
||||
this.receive.show = false
|
||||
@@ -588,7 +588,6 @@ new Vue({
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
dismissPaymentMsg()
|
||||
this.fetchPayments()
|
||||
this.fetchBalance()
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
@@ -629,8 +628,6 @@ new Vue({
|
||||
dismissPaymentMsg()
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
this.fetchPayments()
|
||||
this.fetchBalance()
|
||||
|
||||
// show lnurlpay success action
|
||||
if (response.data.success_action) {
|
||||
switch (response.data.success_action.tag) {
|
||||
@@ -818,110 +815,13 @@ new Vue({
|
||||
navigator.clipboard.readText().then(text => {
|
||||
this.$refs.textArea.value = text
|
||||
})
|
||||
},
|
||||
checkFirstUse() {
|
||||
const url = window.location.href
|
||||
const queryString = url.split('?')[1]
|
||||
const queryObject = {}
|
||||
|
||||
if (queryString) {
|
||||
for (const param of queryString.split('&')) {
|
||||
const [key, value] = param.split('=')
|
||||
queryObject[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (queryObject.hasOwnProperty('first_use')) {
|
||||
const scriptTag = document.createElement('script')
|
||||
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
|
||||
const linkTag = document.createElement('link')
|
||||
linkTag.rel = 'stylesheet'
|
||||
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
|
||||
|
||||
document.body.appendChild(scriptTag)
|
||||
document.head.appendChild(linkTag)
|
||||
scriptTag.onload = () => {
|
||||
if (!this.g.visibleDrawer) {
|
||||
this.g.visibleDrawer = true
|
||||
}
|
||||
this.runOnboarding()
|
||||
}
|
||||
}
|
||||
},
|
||||
runOnboarding() {
|
||||
introJs()
|
||||
.onbeforechange(step => {
|
||||
if (!step.id || !this.mobileSimple) return
|
||||
if (step.id === 'wallet-list' || step.id === 'admin-manage') {
|
||||
this.g.visibleDrawer = true
|
||||
} else {
|
||||
this.g.visibleDrawer = false
|
||||
}
|
||||
})
|
||||
// .onbeforeexit(() => {
|
||||
// return window.history.replaceState(
|
||||
// null,
|
||||
// null,
|
||||
// window.location.pathname
|
||||
// )
|
||||
// })
|
||||
.setOptions({
|
||||
disableInteraction: true,
|
||||
tooltipClass: 'introjs-tooltip-custom',
|
||||
dontShowAgain: true,
|
||||
steps: [
|
||||
{
|
||||
title: 'Welcome to LNbits',
|
||||
intro: 'Start your tour!'
|
||||
},
|
||||
{
|
||||
title: 'Access your wallet',
|
||||
element: document.getElementById('wallet-list'),
|
||||
position: 'right',
|
||||
intro:
|
||||
'Wallets are the core of LNbits. They are the place where you can store your funds.'
|
||||
},
|
||||
{
|
||||
title: 'Access extensions',
|
||||
element: document.getElementById('admin-manage'),
|
||||
position: 'right',
|
||||
intro:
|
||||
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
|
||||
},
|
||||
{
|
||||
title: 'Wallet balance',
|
||||
element: document.getElementById('wallet-balance'),
|
||||
position: 'right',
|
||||
intro: 'Your wallet balance is displayed here.'
|
||||
},
|
||||
{
|
||||
title: 'Send and receive payments',
|
||||
element: document.getElementById(
|
||||
this.mobileSimple ? 'mobile-wallet-buttons' : 'wallet-buttons'
|
||||
),
|
||||
position: 'bottom',
|
||||
intro:
|
||||
'Use these buttons to send and receive payments or scan a QR code.'
|
||||
},
|
||||
{
|
||||
title: 'Transaction details',
|
||||
element: document.getElementById('wallet-table'),
|
||||
position: 'bottom',
|
||||
intro:
|
||||
'Here you can see all the transactions made in your wallet. You can also export them as a CSV file.'
|
||||
},
|
||||
{
|
||||
title: 'Farewell!',
|
||||
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
|
||||
}
|
||||
]
|
||||
})
|
||||
.start()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
payments: function () {
|
||||
this.fetchBalance()
|
||||
payments: function (_, oldVal) {
|
||||
if (oldVal && oldVal.length !== 0) {
|
||||
this.fetchBalance()
|
||||
}
|
||||
},
|
||||
'paymentsChart.group': function () {
|
||||
this.showChart()
|
||||
@@ -938,20 +838,12 @@ new Vue({
|
||||
if (this.$q.screen.lt.md) {
|
||||
this.mobileSimple = true
|
||||
}
|
||||
this.fetchBalance()
|
||||
this.fetchPayments()
|
||||
this.balance = Math.floor(window.wallet.balance_msat / 1000)
|
||||
|
||||
this.update.name = this.g.wallet.name
|
||||
this.update.currency = this.g.wallet.currency
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/currencies')
|
||||
.then(response => {
|
||||
this.receive.units = ['sat', ...response.data]
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
this.receive.units = ['sat', ...window.currencies]
|
||||
},
|
||||
mounted: function () {
|
||||
// show disclaimer
|
||||
@@ -964,7 +856,6 @@ new Vue({
|
||||
this.onPaymentReceived(payment.payment_hash)
|
||||
)
|
||||
eventReactionWebocket(wallet.id)
|
||||
this.checkFirstUse()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -227,3 +227,7 @@ video {
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.whitespace-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
+40
-49
@@ -4,9 +4,8 @@ import time
|
||||
import traceback
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Coroutine, Dict, List, Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
from py_vapid import Vapid
|
||||
from pywebpush import WebPushException, webpush
|
||||
@@ -21,6 +20,7 @@ from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
tasks: List[asyncio.Task] = []
|
||||
unique_tasks: Dict[str, asyncio.Task] = {}
|
||||
|
||||
|
||||
def create_task(coro):
|
||||
@@ -33,12 +33,33 @@ def create_permanent_task(func):
|
||||
return create_task(catch_everything_and_restart(func))
|
||||
|
||||
|
||||
def create_unique_task(name: str, coro: Coroutine):
|
||||
if unique_tasks.get(name):
|
||||
logger.warning(f"task `{name}` already exists, cancelling it")
|
||||
try:
|
||||
unique_tasks[name].cancel()
|
||||
except Exception as exc:
|
||||
logger.warning(f"error while cancelling task `{name}`: {str(exc)}")
|
||||
task = asyncio.create_task(coro)
|
||||
unique_tasks[name] = task
|
||||
return task
|
||||
|
||||
|
||||
def create_permanent_unique_task(name: str, coro: Coroutine):
|
||||
return create_unique_task(name, catch_everything_and_restart(coro))
|
||||
|
||||
|
||||
def cancel_all_tasks():
|
||||
for task in tasks:
|
||||
try:
|
||||
task.cancel()
|
||||
except Exception as exc:
|
||||
logger.warning(f"error while cancelling task: {str(exc)}")
|
||||
for name, task in unique_tasks.items():
|
||||
try:
|
||||
task.cancel()
|
||||
except Exception as exc:
|
||||
logger.warning(f"error while cancelling task `{name}`: {str(exc)}")
|
||||
|
||||
|
||||
async def catch_everything_and_restart(func):
|
||||
@@ -54,57 +75,25 @@ async def catch_everything_and_restart(func):
|
||||
await catch_everything_and_restart(func)
|
||||
|
||||
|
||||
async def send_push_promise(a, b) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class SseListenersDict(dict):
|
||||
"""
|
||||
A dict of sse listeners.
|
||||
"""
|
||||
|
||||
def __init__(self, name: Optional[str] = None):
|
||||
self.name = name or f"sse_listener_{str(uuid.uuid4())[:8]}"
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
assert isinstance(key, str), f"{key} is not a string"
|
||||
assert isinstance(value, asyncio.Queue), f"{value} is not an asyncio.Queue"
|
||||
logger.trace(f"sse: adding listener {key} to {self.name}. len = {len(self)+1}")
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
|
||||
return super().__delitem__(key)
|
||||
|
||||
_RaiseKeyError = object() # singleton for no-default behavior
|
||||
|
||||
def pop(self, key, v=_RaiseKeyError) -> None:
|
||||
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
|
||||
return super().pop(key)
|
||||
|
||||
|
||||
invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
|
||||
invoice_listeners: Dict[str, asyncio.Queue] = {}
|
||||
|
||||
|
||||
# TODO: name should not be optional
|
||||
# some extensions still dont use a name, but they should
|
||||
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
|
||||
"""
|
||||
A method intended for extensions (and core/tasks.py) to call when they want to be
|
||||
notified about new invoice payments incoming. Will emit all incoming payments.
|
||||
"""
|
||||
name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
|
||||
logger.trace(f"sse: registering invoice listener {name_unique}")
|
||||
invoice_listeners[name_unique] = send_chan
|
||||
if not name:
|
||||
# fallback to a random name if extension didn't provide one
|
||||
name = f"no_name_{str(uuid.uuid4())[:8]}"
|
||||
|
||||
if invoice_listeners.get(name):
|
||||
logger.warning(f"invoice listener `{name}` already exists, replacing it")
|
||||
|
||||
async def webhook_handler():
|
||||
"""
|
||||
Returns the webhook_handler for the selected wallet if present. Used by API.
|
||||
"""
|
||||
WALLET = get_wallet_class()
|
||||
handler = getattr(WALLET, "webhook_listener", None)
|
||||
if handler:
|
||||
return await handler()
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
logger.trace(f"registering invoice listener `{name}`")
|
||||
invoice_listeners[name] = send_chan
|
||||
|
||||
|
||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||
@@ -120,7 +109,7 @@ async def internal_invoice_listener():
|
||||
while True:
|
||||
checking_id = await internal_invoice_queue.get()
|
||||
logger.info("> got internal payment notification", checking_id)
|
||||
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||
create_task(invoice_callback_dispatcher(checking_id))
|
||||
|
||||
|
||||
async def invoice_listener():
|
||||
@@ -133,7 +122,7 @@ async def invoice_listener():
|
||||
WALLET = get_wallet_class()
|
||||
async for checking_id in WALLET.paid_invoices_stream():
|
||||
logger.info("> got a payment notification", checking_id)
|
||||
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||
create_task(invoice_callback_dispatcher(checking_id))
|
||||
|
||||
|
||||
async def check_pending_payments():
|
||||
@@ -191,10 +180,12 @@ async def invoice_callback_dispatcher(checking_id: str):
|
||||
"""
|
||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||
if payment and payment.is_in:
|
||||
logger.trace(f"sse sending invoice callback for payment {checking_id}")
|
||||
logger.trace(
|
||||
f"invoice listeners: sending invoice callback for payment {checking_id}"
|
||||
)
|
||||
await payment.set_pending(False)
|
||||
for chan_name, send_chan in invoice_listeners.items():
|
||||
logger.trace(f"sse sending to chan: {chan_name}")
|
||||
for name, send_chan in invoice_listeners.items():
|
||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||
await send_chan.put(payment)
|
||||
|
||||
|
||||
|
||||
@@ -168,17 +168,13 @@
|
||||
show-if-above
|
||||
:elevated="$q.screen.lt.md"
|
||||
>
|
||||
<lnbits-wallet-list id="wallet-list"></lnbits-wallet-list>
|
||||
<lnbits-wallet-list></lnbits-wallet-list>
|
||||
|
||||
<lnbits-manage
|
||||
id="admin-manage"
|
||||
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||
:show-node="'{{LNBITS_NODE_UI}}' == 'True'"
|
||||
></lnbits-manage>
|
||||
<lnbits-extension-list
|
||||
id="extension-list"
|
||||
class="q-pb-xl"
|
||||
></lnbits-extension-list>
|
||||
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list>
|
||||
</q-drawer>
|
||||
{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
|
||||
+24
-11
@@ -13,18 +13,28 @@
|
||||
></q-icon>
|
||||
|
||||
<h5 class="q-my-none">{{ err }}</h5>
|
||||
<h4>
|
||||
If you believe this shouldn't be an error please bring it up on
|
||||
https://t.me/lnbits
|
||||
</h4>
|
||||
|
||||
<br />
|
||||
<q-btn
|
||||
@click="goBack"
|
||||
color="grey"
|
||||
outline
|
||||
label="Back"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
@click="goBack"
|
||||
color="grey"
|
||||
outline
|
||||
label="Back"
|
||||
style="width: 100%"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
@click="goHome"
|
||||
color="grey"
|
||||
outline
|
||||
label="Home"
|
||||
style="width: 100%"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -42,6 +52,9 @@
|
||||
methods: {
|
||||
goBack: function () {
|
||||
window.history.back()
|
||||
},
|
||||
goHome: function () {
|
||||
window.location.href = '/'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{% macro window_vars(user, wallet) -%}
|
||||
<script>
|
||||
window.extensions = {{ EXTENSIONS | tojson | safe }};
|
||||
{% if currencies %}
|
||||
window.currencies = {{ currencies | tojson | safe }};
|
||||
{% endif %}
|
||||
{% if user %}
|
||||
window.user = {{ user | tojson | safe }};
|
||||
{% endif %}
|
||||
|
||||
@@ -176,6 +176,16 @@ currencies = {
|
||||
}
|
||||
|
||||
|
||||
def allowed_currencies():
|
||||
if len(settings.lnbits_allowed_currencies) > 0:
|
||||
return [
|
||||
item
|
||||
for item in currencies.keys()
|
||||
if item.upper() in settings.lnbits_allowed_currencies
|
||||
]
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
class Provider(NamedTuple):
|
||||
name: str
|
||||
domain: str
|
||||
|
||||
@@ -25,6 +25,7 @@ from .lntips import LnTipsWallet
|
||||
from .opennode import OpenNodeWallet
|
||||
from .spark import SparkWallet
|
||||
from .void import VoidWallet
|
||||
from .zbd import ZBDWallet
|
||||
|
||||
|
||||
def set_wallet_class(class_name: Optional[str] = None):
|
||||
|
||||
@@ -9,6 +9,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -111,7 +112,7 @@ class AlbyWallet(Wallet):
|
||||
r = await self.client.get(f"/invoices/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()
|
||||
|
||||
@@ -126,7 +127,3 @@ class AlbyWallet(Wallet):
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
||||
async def webhook_listener(self):
|
||||
logger.error("Alby webhook listener disabled")
|
||||
return
|
||||
|
||||
+13
-1
@@ -35,7 +35,7 @@ class PaymentStatus(NamedTuple):
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self.paid is not True
|
||||
return self.paid is None
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
@@ -52,6 +52,18 @@ class PaymentStatus(NamedTuple):
|
||||
return "unknown (should never happen)"
|
||||
|
||||
|
||||
class PaymentSuccessStatus(PaymentStatus):
|
||||
paid = True
|
||||
|
||||
|
||||
class PaymentFailedStatus(PaymentStatus):
|
||||
paid = False
|
||||
|
||||
|
||||
class PaymentPendingStatus(PaymentStatus):
|
||||
paid = None
|
||||
|
||||
|
||||
class Wallet(ABC):
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
@@ -10,6 +10,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -139,7 +140,7 @@ class ClicheWallet(Wallet):
|
||||
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
statuses = {"pending": None, "complete": True, "failed": False}
|
||||
return PaymentStatus(statuses[data["result"]["status"]])
|
||||
@@ -152,7 +153,7 @@ class ClicheWallet(Wallet):
|
||||
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
payment = data["result"]
|
||||
statuses = {"pending": None, "complete": True, "failed": False}
|
||||
return PaymentStatus(
|
||||
|
||||
@@ -12,8 +12,11 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Unsupported,
|
||||
Wallet,
|
||||
@@ -139,8 +142,6 @@ class CoreLightningWallet(Wallet):
|
||||
f" '{exc.error.get('message') or exc.error}'." # type: ignore
|
||||
)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
except Exception as exc:
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"])
|
||||
return PaymentResponse(
|
||||
@@ -151,33 +152,33 @@ class CoreLightningWallet(Wallet):
|
||||
try:
|
||||
r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore
|
||||
except RpcError:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if not r["invoices"]:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
invoice_resp = r["invoices"][-1]
|
||||
|
||||
if invoice_resp["payment_hash"] == checking_id:
|
||||
if invoice_resp["status"] == "paid":
|
||||
return PaymentStatus(True)
|
||||
return PaymentSuccessStatus()
|
||||
elif invoice_resp["status"] == "unpaid":
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
elif invoice_resp["status"] == "expired":
|
||||
return PaymentStatus(False)
|
||||
return PaymentFailedStatus()
|
||||
else:
|
||||
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r: dict = self.ln.listpays(payment_hash=checking_id) # type: ignore
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if "pays" not in r:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if not r["pays"]:
|
||||
# no payment with this payment_hash is found
|
||||
return PaymentStatus(False)
|
||||
return PaymentFailedStatus()
|
||||
|
||||
payment_resp = r["pays"][-1]
|
||||
|
||||
@@ -188,14 +189,16 @@ class CoreLightningWallet(Wallet):
|
||||
payment_resp["amount_sent_msat"] - payment_resp["amount_msat"]
|
||||
)
|
||||
|
||||
return PaymentStatus(True, fee_msat, payment_resp["preimage"])
|
||||
return PaymentSuccessStatus(
|
||||
fee_msat=fee_msat, preimage=payment_resp["preimage"]
|
||||
)
|
||||
elif status == "failed":
|
||||
return PaymentStatus(False)
|
||||
return PaymentFailedStatus()
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
else:
|
||||
logger.warning(f"supplied an invalid checking_id: {checking_id}")
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -12,6 +12,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -162,9 +163,6 @@ class CoreLightningRestWallet(Wallet):
|
||||
|
||||
data = r.json()
|
||||
|
||||
if data["status"] != "complete":
|
||||
return PaymentResponse(False, None, None, None, "payment failed")
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["msatoshi_sent"] - data["msatoshi"]
|
||||
@@ -187,7 +185,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentStatus(self.statuses.get(data["invoices"][0]["status"]))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice status: {e}")
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(
|
||||
@@ -214,7 +212,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
return PaymentStatus(self.statuses.get(pay["status"]), fee_msat, preimage)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {e}")
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -13,6 +13,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -180,7 +181,7 @@ class EclairWallet(Wallet):
|
||||
}
|
||||
return PaymentStatus(statuses.get(data["status"]["type"]))
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
@@ -211,7 +212,7 @@ class EclairWallet(Wallet):
|
||||
statuses.get(data["status"]["type"]), fee_msat, preimage
|
||||
)
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -19,8 +19,11 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
@@ -117,11 +120,14 @@ class FakeWallet(Wallet):
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
paid = checking_id in self.paid_invoices
|
||||
return PaymentStatus(paid)
|
||||
if checking_id in self.paid_invoices:
|
||||
return PaymentSuccessStatus()
|
||||
if checking_id in list(self.payment_secrets.keys()):
|
||||
return PaymentPendingStatus()
|
||||
return PaymentFailedStatus()
|
||||
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -9,8 +9,11 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
@@ -119,22 +122,35 @@ class LNbitsWallet(Wallet):
|
||||
r = await self.client.get(
|
||||
url=f"/api/v1/payments/{checking_id}",
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
details = data.get("details", None)
|
||||
|
||||
if details and details.get("pending", False) is True:
|
||||
return PaymentPendingStatus()
|
||||
if data.get("paid", False) is True:
|
||||
return PaymentSuccessStatus()
|
||||
return PaymentFailedStatus()
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(url=f"/api/v1/payments/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(False)
|
||||
return PaymentPendingStatus()
|
||||
data = r.json()
|
||||
if "paid" not in data and "details" not in data:
|
||||
return PaymentStatus(None)
|
||||
|
||||
return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"])
|
||||
if "paid" not in data or not data["paid"]:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if "details" not in data:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
return PaymentSuccessStatus(
|
||||
fee_msat=data["details"]["fee"], preimage=data["preimage"]
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"{self.endpoint}/api/v1/payments/sse"
|
||||
|
||||
+12
-11
@@ -16,8 +16,10 @@ from lnbits.utils.crypto import AESCipher
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
@@ -203,15 +205,15 @@ class LndWallet(Wallet):
|
||||
except ValueError:
|
||||
# this may happen if we switch between backend wallets
|
||||
# that use different checking_id formats
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
try:
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||
except grpc.RpcError:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
if resp.settled:
|
||||
return PaymentStatus(True)
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
@@ -224,7 +226,7 @@ class LndWallet(Wallet):
|
||||
except ValueError:
|
||||
# this may happen if we switch between backend wallets
|
||||
# that use different checking_id formats
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
resp = self.routerpc.TrackPaymentV2(
|
||||
router.TrackPaymentRequest(payment_hash=r_hash)
|
||||
@@ -247,16 +249,15 @@ class LndWallet(Wallet):
|
||||
try:
|
||||
async for payment in resp:
|
||||
if len(payment.htlcs) and statuses[payment.status]:
|
||||
return PaymentStatus(
|
||||
True,
|
||||
-payment.htlcs[-1].route.total_fees_msat,
|
||||
bytes_to_hex(payment.htlcs[-1].preimage),
|
||||
return PaymentSuccessStatus(
|
||||
fee_msat=-payment.htlcs[-1].route.total_fees_msat,
|
||||
preimage=bytes_to_hex(payment.htlcs[-1].preimage),
|
||||
)
|
||||
return PaymentStatus(statuses[payment.status])
|
||||
except Exception: # most likely the payment wasn't found
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
@@ -13,8 +13,11 @@ from lnbits.utils.crypto import AESCipher
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
@@ -104,7 +107,11 @@ class LndRestWallet(Wallet):
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: Dict = {"value": amount, "private": True, "memo": memo or ""}
|
||||
data: Dict = {
|
||||
"value": amount,
|
||||
"private": settings.lnd_rest_route_hints,
|
||||
"memo": memo or "",
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
data["expiry"] = kwargs["expiry"]
|
||||
if description_hash:
|
||||
@@ -168,9 +175,9 @@ class LndRestWallet(Wallet):
|
||||
if r.is_error or not r.json().get("settled"):
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
# it will return an error and no "settled" attribute on the object
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
return PaymentStatus(True)
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
@@ -182,7 +189,7 @@ class LndRestWallet(Wallet):
|
||||
"ascii"
|
||||
)
|
||||
except ValueError:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
url = f"/v2/router/track/{checking_id}"
|
||||
|
||||
@@ -210,8 +217,8 @@ class LndRestWallet(Wallet):
|
||||
and line["error"].get("message")
|
||||
== "payment isn't initiated"
|
||||
):
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
payment = line.get("result")
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
@@ -220,11 +227,11 @@ class LndRestWallet(Wallet):
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
|
||||
+2
-26
@@ -9,6 +9,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -134,7 +135,7 @@ class LNPayWallet(Wallet):
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()
|
||||
preimage = data["payment_preimage"]
|
||||
@@ -147,28 +148,3 @@ class LNPayWallet(Wallet):
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
||||
async def webhook_listener(self):
|
||||
logger.error("LNPay webhook listener disabled.")
|
||||
return
|
||||
# TODO: request.get_data is undefined, was it something with Flask or quart?
|
||||
# probably issue introduced when refactoring?
|
||||
# text: str = await request.get_data()
|
||||
# try:
|
||||
# data = json.loads(text)
|
||||
# except json.decoder.JSONDecodeError:
|
||||
# logger.error(f"error on lnpay webhook endpoint: {text[:200]}")
|
||||
# data = None
|
||||
# if (
|
||||
# type(data) is not dict
|
||||
# or "event" not in data
|
||||
# or data["event"].get("name") != "wallet_receive"
|
||||
# ):
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
# lntx_id = data["data"]["wtx"]["lnTx"]["id"]
|
||||
# r = await self.client.get(f"/lntx/{lntx_id}?fields=settled")
|
||||
# data = r.json()
|
||||
# if data["settled"]:
|
||||
# await self.queue.put(lntx_id)
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
@@ -11,6 +11,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -132,7 +133,7 @@ class LnTipsWallet(Wallet):
|
||||
data = r.json()
|
||||
return PaymentStatus(data["paid"])
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
@@ -147,7 +148,7 @@ class LnTipsWallet(Wallet):
|
||||
paid_to_status = {False: None, True: True}
|
||||
return PaymentStatus(paid_to_status[data.get("paid")])
|
||||
except Exception:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
last_connected = None
|
||||
|
||||
@@ -8,6 +8,7 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -80,7 +81,6 @@ class OpenNodeWallet(Wallet):
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
@@ -117,7 +117,7 @@ class OpenNodeWallet(Wallet):
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
data = r.json()["data"]
|
||||
statuses = {"processing": None, "paid": True, "unpaid": None}
|
||||
return PaymentStatus(statuses[data.get("status")])
|
||||
@@ -126,7 +126,7 @@ class OpenNodeWallet(Wallet):
|
||||
r = await self.client.get(f"/v1/withdrawal/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()["data"]
|
||||
statuses = {
|
||||
@@ -144,22 +144,3 @@ class OpenNodeWallet(Wallet):
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
|
||||
async def webhook_listener(self):
|
||||
logger.error("webhook listener for opennode is disabled.")
|
||||
return
|
||||
# TODO: request.form is undefined, was it something with Flask or quart?
|
||||
# probably issue introduced when refactoring?
|
||||
# data = await request.form # type: ignore
|
||||
# if "status" not in data or data["status"] != "paid":
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
# charge_id = data["id"]
|
||||
# x = hmac.new(self.key.encode("ascii"), digestmod="sha256")
|
||||
# x.update(charge_id.encode("ascii"))
|
||||
# if x.hexdigest() != data["hashed_order"]:
|
||||
# logger.error("invalid webhook, not from opennode")
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
# await self.queue.put(charge_id)
|
||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
+16
-11
@@ -11,8 +11,11 @@ from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
@@ -196,33 +199,33 @@ class SparkWallet(Wallet):
|
||||
try:
|
||||
r = await self.listinvoices(label=checking_id)
|
||||
except (SparkError, UnknownError):
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if not r or not r.get("invoices"):
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r["invoices"][0]["status"] == "paid":
|
||||
return PaymentStatus(True)
|
||||
return PaymentSuccessStatus()
|
||||
else:
|
||||
return PaymentStatus(False)
|
||||
return PaymentFailedStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
# check if it's 32 bytes hex
|
||||
if len(checking_id) != 64:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
try:
|
||||
int(checking_id, 16)
|
||||
except ValueError:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
# ask sparko
|
||||
try:
|
||||
r = await self.listpays(payment_hash=checking_id)
|
||||
except (SparkError, UnknownError):
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if not r["pays"]:
|
||||
return PaymentStatus(False)
|
||||
return PaymentFailedStatus()
|
||||
if r["pays"][0]["payment_hash"] == checking_id:
|
||||
status = r["pays"][0]["status"]
|
||||
if status == "complete":
|
||||
@@ -230,10 +233,12 @@ class SparkWallet(Wallet):
|
||||
int(r["pays"][0]["amount_sent_msat"][0:-4])
|
||||
- int(r["pays"][0]["amount_msat"][0:-4])
|
||||
)
|
||||
return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"])
|
||||
return PaymentSuccessStatus(
|
||||
fee_msat=fee_msat, preimage=r["pays"][0]["preimage"]
|
||||
)
|
||||
if status == "failed":
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
return PaymentFailedStatus()
|
||||
return PaymentPendingStatus()
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
|
||||
@@ -4,6 +4,7 @@ from loguru import logger
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
@@ -31,10 +32,10 @@ class VoidWallet(Wallet):
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, *_, **__) -> PaymentStatus:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def get_payment_status(self, *_, **__) -> PaymentStatus:
|
||||
return PaymentStatus(None)
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
yield ""
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Unsupported,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
|
||||
class ZBDWallet(Wallet):
|
||||
"""https://zbd.dev/api-reference/"""
|
||||
|
||||
def __init__(self):
|
||||
if not settings.zbd_api_endpoint:
|
||||
raise ValueError("cannot initialize ZBDWallet: missing zbd_api_endpoint")
|
||||
if not settings.zbd_api_key:
|
||||
raise ValueError("cannot initialize ZBDWallet: missing zbd_api_key")
|
||||
|
||||
self.endpoint = self.normalize_endpoint(settings.zbd_api_endpoint)
|
||||
headers = {
|
||||
"apikey": settings.zbd_api_key,
|
||||
"User-Agent": settings.user_agent,
|
||||
}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers)
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
await self.client.aclose()
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Error closing wallet connection: {e}")
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
r = await self.client.get("wallet", timeout=10)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return StatusResponse(error_message, 0)
|
||||
|
||||
data = int(r.json()["data"]["balance"])
|
||||
# ZBD returns everything as a str not int
|
||||
# balance is returned in msats already in ZBD
|
||||
return StatusResponse(None, data)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://api.zebedee.io/v0/charges
|
||||
if description_hash or unhashed_description:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
msats_amount = amount * 1000
|
||||
data: Dict = {
|
||||
"amount": f"{msats_amount}",
|
||||
"description": memo,
|
||||
"expiresIn": 3600,
|
||||
"callbackUrl": "",
|
||||
"internalId": "",
|
||||
}
|
||||
|
||||
r = await self.client.post(
|
||||
"charges",
|
||||
json=data,
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"] # this is a zbd id
|
||||
payment_request = data["invoice"]["request"]
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(
|
||||
self, bolt11_invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# https://api.zebedee.io/v0/payments
|
||||
r = await self.client.post(
|
||||
"payments",
|
||||
json={
|
||||
"invoice": bolt11_invoice,
|
||||
"description": "",
|
||||
"amount": "",
|
||||
"internalId": "",
|
||||
"callbackUrl": "",
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
data = r.json()
|
||||
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
fee_msat = -int(data["data"]["fee"])
|
||||
preimage = data["data"]["preimage"]
|
||||
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"charges/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentPendingStatus()
|
||||
data = r.json()["data"]
|
||||
|
||||
statuses = {
|
||||
"pending": None,
|
||||
"paid": True,
|
||||
"unpaid": None,
|
||||
"expired": False,
|
||||
"completed": True,
|
||||
}
|
||||
return PaymentStatus(statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"payments/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
data = r.json()["data"]
|
||||
|
||||
statuses = {
|
||||
"initial": None,
|
||||
"pending": None,
|
||||
"completed": True,
|
||||
"error": None,
|
||||
"expired": False,
|
||||
"failed": False,
|
||||
}
|
||||
|
||||
return PaymentStatus(statuses[data.get("status")], fee_msat=None, preimage=None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
while True:
|
||||
value = await self.queue.get()
|
||||
yield value
|
||||
Generated
+634
-277
File diff suppressed because it is too large
Load Diff
Generated
+113
-41
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
@@ -252,29 +252,33 @@ bitarray = ">=2.8.0,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "23.11.0"
|
||||
version = "24.2.0"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
|
||||
{file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
|
||||
{file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
|
||||
{file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
|
||||
{file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
|
||||
{file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
|
||||
{file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
|
||||
{file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
|
||||
{file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
|
||||
{file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
|
||||
{file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
|
||||
{file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
|
||||
{file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
|
||||
{file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
|
||||
{file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
|
||||
{file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
|
||||
{file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
|
||||
{file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
|
||||
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
|
||||
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
|
||||
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
|
||||
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
|
||||
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
|
||||
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
|
||||
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
|
||||
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
|
||||
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
|
||||
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
|
||||
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
|
||||
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
|
||||
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
|
||||
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
|
||||
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
|
||||
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
|
||||
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -288,7 +292,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
@@ -716,6 +720,17 @@ files = [
|
||||
{file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
description = "Distro - an OS platform information API"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
|
||||
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.4.2"
|
||||
@@ -1072,6 +1087,20 @@ MarkupSafe = ">=2.0"
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.9.17"
|
||||
description = "A Python implementation of the JSON5 data format."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "json5-0.9.17-py2.py3-none-any.whl", hash = "sha256:f8ec1ecf985951d70f780f6f877c4baca6a47b6e61e02c4cd190138d10a7805a"},
|
||||
{file = "json5-0.9.17.tar.gz", hash = "sha256:717d99d657fa71b7094877b1d921b1cce40ab444389f6d770302563bb7dfd9ae"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["hypothesis"]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.20.0"
|
||||
@@ -1399,6 +1428,29 @@ rsa = ["cryptography (>=3.0.0)"]
|
||||
signals = ["blinker (>=1.4.0)"]
|
||||
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.12.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.7.1"
|
||||
files = [
|
||||
{file = "openai-1.12.0-py3-none-any.whl", hash = "sha256:a54002c814e05222e413664f651b5916714e4700d041d5cf5724d3ae1a3e3481"},
|
||||
{file = "openai-1.12.0.tar.gz", hash = "sha256:99c5d257d09ea6533d689d1cc77caa0ac679fa21efef8893d8b0832a86877f1b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.5.0,<5"
|
||||
distro = ">=1.7.0,<2"
|
||||
httpx = ">=0.23.0,<1"
|
||||
pydantic = ">=1.9.0,<3"
|
||||
sniffio = "*"
|
||||
tqdm = ">4"
|
||||
typing-extensions = ">=4.7,<5"
|
||||
|
||||
[package.extras]
|
||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-schema-validator"
|
||||
version = "0.6.2"
|
||||
@@ -2214,28 +2266,28 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.291"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
version = "0.3.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.0.291-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b97d0d7c136a85badbc7fd8397fdbb336e9409b01c07027622f28dcd7db366f2"},
|
||||
{file = "ruff-0.0.291-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ab44ea607967171e18aa5c80335237be12f3a1523375fa0cede83c5cf77feb4"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04b384f2d36f00d5fb55313d52a7d66236531195ef08157a09c4728090f2ef0"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b727c219b43f903875b7503a76c86237a00d1a39579bb3e21ce027eec9534051"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87671e33175ae949702774071b35ed4937da06f11851af75cd087e1b5a488ac4"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b75f5801547f79b7541d72a211949754c21dc0705c70eddf7f21c88a64de8b97"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b09b94efdcd162fe32b472b2dd5bf1c969fcc15b8ff52f478b048f41d4590e09"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d5b56bc3a2f83a7a1d7f4447c54d8d3db52021f726fdd55d549ca87bca5d747"},
|
||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6"},
|
||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b3eeee1b1a45a247758ecdc3ab26c307336d157aafc61edb98b825cadb153df3"},
|
||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c06006350c3bb689765d71f810128c9cdf4a1121fd01afc655c87bab4fb4f83"},
|
||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629"},
|
||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5383ba67ad360caf6060d09012f1fb2ab8bd605ab766d10ca4427a28ab106e0b"},
|
||||
{file = "ruff-0.0.291-py3-none-win32.whl", hash = "sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3"},
|
||||
{file = "ruff-0.0.291-py3-none-win_amd64.whl", hash = "sha256:8a69bfbde72db8ca1c43ee3570f59daad155196c3fbe357047cd9b77de65f15b"},
|
||||
{file = "ruff-0.0.291-py3-none-win_arm64.whl", hash = "sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6"},
|
||||
{file = "ruff-0.0.291.tar.gz", hash = "sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed"},
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"},
|
||||
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"},
|
||||
{file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"},
|
||||
{file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"},
|
||||
{file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"},
|
||||
{file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"},
|
||||
{file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"},
|
||||
{file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2458,6 +2510,26 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.2"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
|
||||
{file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
|
||||
notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "types-passlib"
|
||||
version = "1.7.7.13"
|
||||
@@ -2862,4 +2934,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "b2e21e0075047150888581a401d46fbe8efd6226e85a85189c3f3db51f825a48"
|
||||
content-hash = "b18e0159abb15b2d0f93770db2d1c37e723d08781cd7cc91dce6ce76e4b8d0c6"
|
||||
|
||||
+28
-41
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "0.12.1"
|
||||
version = "0.12.3"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
@@ -55,7 +55,7 @@ wallycore = {version = "^1.0.0", optional = true}
|
||||
liquid = ["wallycore"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.7.0"
|
||||
black = "^24.2.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest = "^7.3.2"
|
||||
pytest-cov = "^4.1.0"
|
||||
@@ -63,12 +63,14 @@ mypy = "^1.5.1"
|
||||
types-protobuf = "^4.24.0.2"
|
||||
pre-commit = "^3.2.2"
|
||||
openapi-spec-validator = "^0.6.0"
|
||||
ruff = "^0.0.291"
|
||||
ruff = "^0.3.2"
|
||||
# not our dependency but needed indirectly by openapi-spec-validator
|
||||
# we want to use 0.10.3 because newer versions are broken on nix
|
||||
rpds-py = "0.10.3"
|
||||
types-passlib = "^1.7.7.13"
|
||||
types-python-jose = "^3.3.4.8"
|
||||
openai = "^1.12.0"
|
||||
json5 = "^0.9.17"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@@ -80,7 +82,9 @@ lnbits-cli = "lnbits.commands:main"
|
||||
|
||||
[tool.pyright]
|
||||
include = [
|
||||
"lnbits"
|
||||
"lnbits",
|
||||
"tests",
|
||||
"tools",
|
||||
]
|
||||
exclude = [
|
||||
"lnbits/wallets/lnd_grpc_files",
|
||||
@@ -89,7 +93,11 @@ exclude = [
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
files = "lnbits"
|
||||
files = [
|
||||
"lnbits",
|
||||
"tests",
|
||||
"tools",
|
||||
]
|
||||
exclude = [
|
||||
"^lnbits/wallets/lnd_grpc_files",
|
||||
"^lnbits/extensions",
|
||||
@@ -118,6 +126,7 @@ module = [
|
||||
"py_vapid.*",
|
||||
"pywebpush.*",
|
||||
"fastapi_sso.sso.*",
|
||||
"json5.*",
|
||||
]
|
||||
ignore_missing_imports = "True"
|
||||
|
||||
@@ -143,53 +152,31 @@ extend-exclude = """(
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
|
||||
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
|
||||
# (`I`) is for `isort`.
|
||||
select = ["E", "F", "I"]
|
||||
# Exclude generated files.
|
||||
extend-exclude = [
|
||||
"lnbits/wallets/lnd_grpc_files"
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable:
|
||||
# F - pyflakes
|
||||
# E - pycodestyle errors
|
||||
# W - pycodestyle warnings
|
||||
# I - isort
|
||||
select = ["F", "E", "W", "I"]
|
||||
ignore = []
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
"lnbits/static",
|
||||
"lnbits/extensions",
|
||||
"lnbits/wallets/lnd_grpc_files",
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# Assume Python
|
||||
# target-version = "py39"
|
||||
|
||||
# Ignore unused imports in __init__.py files.
|
||||
[tool.ruff.extend-per-file-ignores]
|
||||
[tool.ruff.lint.extend-per-file-ignores]
|
||||
"__init__.py" = ["F401", "F403"]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
[tool.ruff.lint.mccabe]
|
||||
# Unlike Flake8, default to a complexity level of 10.
|
||||
max-complexity = 10
|
||||
|
||||
@@ -34,5 +34,4 @@ async def test_create_wallet_and_delete_wallet(app, to_user):
|
||||
assert del_wallet.deleted is True
|
||||
|
||||
del_wallet = await get_wallet_for_key(wallet.inkey)
|
||||
assert del_wallet is not None
|
||||
assert del_wallet.deleted is True
|
||||
assert del_wallet is None
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from tests.helpers import DbTestModel
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def fetch_page(db):
|
||||
await db.execute("DROP TABLE IF EXISTS test_db_fetch_page")
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE test_db_fetch_page (
|
||||
id TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO test_db_fetch_page (id, name, value) VALUES
|
||||
('1', 'Alice', 'foo'),
|
||||
('2', 'Bob', 'bar'),
|
||||
('3', 'Carol', 'bar'),
|
||||
('4', 'Dave', 'bar'),
|
||||
('5', 'Dave', 'foo')
|
||||
"""
|
||||
)
|
||||
yield
|
||||
await db.execute("DROP TABLE test_db_fetch_page")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_fetch_page_simple(fetch_page, db):
|
||||
row = await db.fetch_page(
|
||||
query="select * from test_db_fetch_page",
|
||||
model=DbTestModel,
|
||||
)
|
||||
|
||||
assert row
|
||||
assert row.total == 5
|
||||
assert len(row.data) == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_fetch_page_group_by(fetch_page, db):
|
||||
row = await db.fetch_page(
|
||||
query="select max(id) as id, name from test_db_fetch_page",
|
||||
model=DbTestModel,
|
||||
group_by=["name"],
|
||||
)
|
||||
assert row
|
||||
assert row.total == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_fetch_page_group_by_multiple(fetch_page, db):
|
||||
row = await db.fetch_page(
|
||||
query="select max(id) as id, name, value from test_db_fetch_page",
|
||||
model=DbTestModel,
|
||||
group_by=["value", "name"],
|
||||
)
|
||||
assert row
|
||||
assert row.total == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_fetch_page_group_by_evil(fetch_page, db):
|
||||
with pytest.raises(ValueError, match="Value for GROUP BY is invalid"):
|
||||
await db.fetch_page(
|
||||
query="select * from test_db_fetch_page",
|
||||
model=DbTestModel,
|
||||
group_by=["name;"],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user