Compare commits

..
52 Commits
Author SHA1 Message Date
dni ⚡andGitHub 033a57cad4 chore: update to 0.12.3 (#2344) 2024-03-22 13:03:00 +01:00
dni ⚡andGitHub 299228b7b5 feat: reduce initial requests on wallet page (#2335)
* feat: reduce initial request on wallet page
- refactor allowed_currencies into function to use in generic and api.
- remove currencies request in frontend move it to generic
- dont request balance on first payments fetch
2024-03-22 12:59:49 +01:00
dni ⚡andGitHub 5b022e2ef3 feat: make route hints configurable for lndrest (#2304)
* feat: make route hints configurable for lndrest
boolean if route hints for private channels will be included from `LndRest` inside bolt11 invoice.
2024-03-22 12:45:39 +01:00
Pavol RusnakandGitHub c8818f5774 remove CACHE_VERSION from service-worker.js (#2342)
use server_startup_time as the version
(the same trick we use for static files)
2024-03-22 12:33:42 +01:00
dni ⚡andGitHub 1398857688 refactor: move registering of tasks into app.py (#2290)
* refactor: move registering of tasks into `app.py`

let the app have the responsibility to create the tasks not a helper function inside tasks.py.
makes it way easier to follow on how those background tasks are ran.
2024-03-22 12:29:05 +01:00
Gonçalo ValérioandGitHub 2a1505bc0d bug: Fix regression. Admin-users and allowed-users can now be removed through the web interface (#2331)
* Fix regression. Admin-users and allowed-users can now be removed through the web interface
* remove raw usage from user tab on server admin
2024-03-22 07:26:25 +01:00
dni ⚡andGitHub 4bba6ecc82 hotfix: qrcode broke wallet.html (#2340)
introduced by 43a79744
2024-03-21 17:39:12 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
522c4df44c chore(deps): bump follow-redirects from 1.15.5 to 1.15.6 (#2326)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-21 16:43:48 +01:00
dni ⚡andGitHub c03b81d2ea refactor: tiny url to use require decorators and wallet.id (#2338)
- also use `wallet.wallet.id` as key instead of `wallet.wallet.inkey`
2024-03-21 13:32:55 +01:00
arbadacarbaandPavol Rusnak 43a797444a recover user for export-to-phone 2024-03-21 13:14:28 +01:00
dni ⚡andPavol Rusnak d0cb961b49 bug: wallet page fetchbalance ran twice
there is a listener on payments which executes fetchbalance automatically if payments change. line 822
2024-03-21 13:13:33 +01:00
ArcandGitHub d208e68885 feat: added home button on error page (#2330)
* Added redirect on error page if user id not present as users keep getting stuck
2024-03-21 12:24:38 +01:00
dni ⚡andGitHub 8dcb53aea0 chore: keychecker todo remove type ignores (#2337)
fixes a comment `TODO`
2024-03-21 11:36:01 +01:00
dni ⚡andGitHub 241b286e21 chore: update ruff and black in precommit config (#2336)
ran `poetry lock --no-update`
2024-03-21 11:05:01 +01:00
dni ⚡andGitHub 17839f6a25 feat: add database debug env var (#2333)
* feat: add database debug env var
makes it visible what db actions happen
2024-03-21 09:59:17 +01:00
dni ⚡andGitHub e4d3faefa0 feat: create_unique_task helper (#2292)
this is especially useful for extension.

with this an extension can only call `create_permanent_unique_task("my-ext", task)` and it will restarted when it exists and cancels when lnbits is stopped
2024-03-18 13:02:04 +00:00
116ca7f011 Remove all {% raw %} usage (#2318)
* Remove raw from templates 
* Remove v-text from q-tooltip
* extension buttons
* Move more to spans
* Reformatted

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2024-03-15 11:10:52 +01:00
Vlad StanandGitHub d44339b018 fix: pending property for PaymentStatus (#2324)
* fix: pending property for PaymentStatus

* fix: invoice status

* fix: check pending status from the payment details

* refactor: make condition more explicit
2024-03-14 14:38:10 +02:00
Vlad StanandGitHub 352fd23c0b refactor: dev-friendly payment status name (#2322)
* refactor: dev-friendly payment status name
* refactor: change `PaymentStatus(True, ...)` to `PaymentSuccessStatus(...)`
2024-03-13 16:17:33 +01:00
Vlad StanandGitHub 65b8868c36 fix: enforce order of payments (#2313)
* fix: enforce order of payments

* fix: do not return wallet by key if the wallet is deleted
2024-03-12 15:31:40 +02:00
dni ⚡andGitHub 54dec171f9 fix: raise failed request to not run mark_webhook_sent (#2289)
* fix: webhook sent raise a failed request

is not raised even failed webhook would be marked is sent

* add warn log

* fix error

* improve exceptions

* fixup!

* ConnectError is already captured by RequestError

https://www.python-httpx.org/exceptions/

* log exception
2024-03-12 15:12:24 +02:00
Vlad StanandGitHub 5b4398911a fix: check if key present in settings before updating (#2306) 2024-03-12 13:03:11 +00:00
4c0bd132b1 bug: cln paymentstate check on error (#2151)
* CLN: paymentstate check on error

---------

Co-authored-by: dni  <office@dnilabs.com>
2024-03-12 13:56:36 +01:00
7ce4eddb0e feat: add group_by to fetch_page (#2140)
---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-03-12 13:55:38 +01:00
callebtcandGitHub 14519135d8 Fix: LNbits backend payent state (#2187) 2024-03-12 13:43:41 +01:00
dni ⚡andGitHub 60839481ad feat: add tool preimage generator (#2321)
`poetry run python tools/preimage.py`
2024-03-12 11:46:46 +01:00
dni ⚡andGitHub 3ef1941fc0 test: fix TestModel warning (#2317)
PytestCollectionWarning: cannot collect test class 'TestModel' because it has a __init__ constructor (from: tests/core/test_helpers_query.py)
2024-03-12 11:32:10 +01:00
dni ⚡andGitHub 16cb1a8026 fix: lnbits funding source (#2319)
fix a bug where payments are deleted
2024-03-12 11:28:42 +01:00
dni ⚡andGitHub e3b9bd6a70 fix: update pre-commit ruff (#2316)
wasnt working after latest ruff update
2024-03-12 08:47:13 +01:00
Vlad StanandGitHub a1ea04acf8 Jmeter take 3 (#2284)
* chore: add tests

* chore: add reference to jmter job

* chore: add jmeter job

* chore: touch tests

* chorE: disable other workflows

* chore: do not set `LNBITS_ADMIN_UI: true`

* chore: extra log file

* chore: rollback

* chore: testing stuff

* feat: top-up wallet

* chore: clean-up usr

* chore: clean-up user query param

* chore: remove hardcoded usr

* chore: clean-up

* chore: clean-up

* chore: different improvements

* chore: clean-up

* chore: remove hardcoded values

* refactor: extract `init-server` fragment

* chore: move fragment

* feat: use test frgment

* chore: clean-up hardcoded values

* chore: clean-up

* fix: get lnurl working

* chore: re-arange elements

* chore: re-arange elements

* fix: add `lnurlp, withdraw` extensions

* chore: rename file

* fix: login with user id

* refactor: extract init account

* fix: tips wallet

* fix: assertion

* refactor: use fragments; group loops

* chore: re-enable the other CI jobs

* fix: separate wallet for receive

* feat: check receive wallet balance

* feat: increase wait time
2024-03-08 16:40:23 +02:00
3e341a3555 update black to 24.2.0 and ruff to 0.3.0 (#2307)
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2024-03-07 09:55:37 +00:00
Vlad StanandGitHub cb5c9b03bf Fix login logout on safari (#2309)
* fix: relax security level for public cookies

* fix: corner case for logout
2024-03-07 09:22:42 +00:00
dni ⚡andGitHub 884a1b9d6f test: lint tests and tools aswell (#2296)
* test: lint `tests` and `tools` aswell

more linting :)

* fix linting issues in tests and tools

* fixup!

* how is this working?
2024-02-27 13:30:52 +00:00
Vlad StanandGitHub e8aa498683 feat: add get endpoint for user wallets (#2299)
* feat: add get endpoint for user wallets

* feat: only expose relevant fields

* refactor: extract `BaseWallet`

* doc: add open api doc
2024-02-27 10:08:10 +02:00
Pavol Rusnak 1b7efd854a chore: make bundle 2024-02-23 16:11:27 -03:00
Pavol Rusnak 8b32c3dcb6 remove webhook_listener
following commit e419c74ebb
2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 0821b28eac rename self.auth to headers 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 4c71d5ac42 format file 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak a8e0b5a5ac use bolt11.decoder from lnbits instead of zbd remote 2024-02-23 16:11:27 -03:00
BitkarrotandPavol Rusnak 9b9014a042 Update zbd.py
set timeout on pay_invoice to 40
2024-02-23 16:11:27 -03:00
Pavol Rusnak 96b42b1784 add ZBD to homepage 2024-02-23 16:11:27 -03:00
BloggingBitcoinandPavol Rusnak 26d854ebe8 added ZBD logos 2024-02-23 16:11:27 -03:00
Pavol Rusnak ed36d01b40 fix typing issues 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 646556a457 add zbd to translation files 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak d20a35eddc add zbd to settings
update api methods

add zbd as funding source to settings and js

fix statuses

cast str into int for status method

fix outbound payment hash issue

restore create_invoice

remove print stmts
2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak d69946db8a initial scaffolding for zbd wallet 2024-02-23 16:11:27 -03:00
dni ⚡andPavol Rusnak ddab4075d2 fix: raise proper exception in services pay_invoice
also values where checked twice
2024-02-23 00:39:00 -03:00
d6c8ad1d0d Paid extensions (#2229)
* fix: download archive file `async`

* feat: add `pay_link` property

* feat: basic install using internal wallet for payment

* fix: pop-up issues

* chore: refactor

* feat: detect paid extensions

* fix: payment check

* feat: small stuff

* feat: show external invoice

* fix: regression for extension install

* feat: store previos successful payments

* refactor: simplify, almost works

* chore: gugu gaga

* fix: pay and install

* fix: do not pay invoice on the back-end

* chore: code clean-up

* feat: basic websocker listener

* feat: use websocket to watch for invoice payment

* feat: remember hanging invoices

* refactor: extract `localStorage` methods

* chore: code format

* chore: code clean-up after test

* feat: remember previous payment_hashes

* chore: code format

* refactor: rename `ExtensionPaymentInfo` to `ReleasePaymentInfo`

* refactor: method rename

* fix: release version matters now

* chore: code format

* refactor: method rename

* refactor: extract method `_restore_payment_info`

* refactor: extract method

* chore: rollback `CACHE_VERSION`

* chore: code format

* feat: i18n

* chore: update bundle

* refactor: public method name

* chore: code format

* fix: websocket connection

* Update installation.md (#2259)

* Update installation.md (#2260)

* fix: try to fix `openapi` error

* chore: bundle

* chore:bundle

---------

Co-authored-by: benarc <ben@arc.wales>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2024-02-22 13:16:41 +00:00
dni ⚡andPavol Rusnak 54c6faa4b6 refactor: move mark_webhook_sent into crud.py
database functions should be in crud
2024-02-21 11:14:04 -03:00
dni ⚡andPavol Rusnak c88f05c5c0 refactor: tasks should be run with internal wrapper
listeners used did not use the helper function
2024-02-21 10:56:31 -03:00
dni ⚡andPavol Rusnak e419c74ebb refactor: remove unused webhook handler for wallets
it was only used in lnpay, opennode and alby which all had it disabled anayways with a pretty old TODO, so i guess the feature is disabled for a y ear now and nobody used it, so i just removed it
2024-02-21 10:55:57 -03:00
dni ⚡andGitHub b6dc66b070 chore: remove dead code send_push_promise (#2285)
dead code removal :)
2024-02-21 14:24:06 +02:00
96 changed files with 12296 additions and 1005 deletions
+6 -1
View File
@@ -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/
+10
View File
@@ -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 }}
+98
View File
@@ -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/
+2 -2
View File
@@ -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 ]
+2 -8
View File
@@ -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"
+1
View File
@@ -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.
+8
View File
@@ -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
+488
View File
@@ -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">{&quot;name&quot;:&quot;a1&quot;}</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(&quot;userId&quot;, resp.user || &quot;no-user-id&quot;);
vars.put(&quot;walletId&quot;, resp.id || &quot;no-wallet-id&quot;);
vars.put(&quot;inkey&quot;, resp.inkey || &apos;no-inkey&apos;);
vars.put(&quot;adminkey&quot;, resp.adminkey || &apos;no-adminkey&apos;);
</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">{&quot;usr&quot;:&quot;${userId}&quot;}</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">[&quot;AED&quot;,&quot;AFN&quot;,&quot;ALL&quot;,&quot;AMD&quot;,&quot;ANG&quot;,&quot;AOA&quot;,&quot;ARS&quot;,&quot;AUD&quot;,&quot;AWG&quot;,&quot;AZN&quot;,&quot;BAM&quot;,&quot;BBD&quot;,&quot;BDT&quot;,&quot;BGN&quot;,&quot;BHD&quot;,&quot;BIF&quot;,&quot;BMD&quot;,&quot;BND&quot;,&quot;BOB&quot;,&quot;BRL&quot;,&quot;BSD&quot;,&quot;BTN&quot;,&quot;BWP&quot;,&quot;BYN&quot;,&quot;BYR&quot;,&quot;BZD&quot;,&quot;CAD&quot;,&quot;CDF&quot;,&quot;CHF&quot;,&quot;CLF&quot;,&quot;CLP&quot;,&quot;CNH&quot;,&quot;CNY&quot;,&quot;COP&quot;,&quot;CRC&quot;,&quot;CUC&quot;,&quot;CVE&quot;,&quot;CZK&quot;,&quot;DJF&quot;,&quot;DKK&quot;,&quot;DOP&quot;,&quot;DZD&quot;,&quot;EGP&quot;,&quot;ERN&quot;,&quot;ETB&quot;,&quot;EUR&quot;,&quot;FJD&quot;,&quot;FKP&quot;,&quot;GBP&quot;,&quot;GEL&quot;,&quot;GGP&quot;,&quot;GHS&quot;,&quot;GIP&quot;,&quot;GMD&quot;,&quot;GNF&quot;,&quot;GTQ&quot;,&quot;GYD&quot;,&quot;HKD&quot;,&quot;HNL&quot;,&quot;HRK&quot;,&quot;HTG&quot;,&quot;HUF&quot;,&quot;IDR&quot;,&quot;ILS&quot;,&quot;IMP&quot;,&quot;INR&quot;,&quot;IQD&quot;,&quot;IRT&quot;,&quot;ISK&quot;,&quot;JEP&quot;,&quot;JMD&quot;,&quot;JOD&quot;,&quot;JPY&quot;,&quot;KES&quot;,&quot;KGS&quot;,&quot;KHR&quot;,&quot;KMF&quot;,&quot;KRW&quot;,&quot;KWD&quot;,&quot;KYD&quot;,&quot;KZT&quot;,&quot;LAK&quot;,&quot;LBP&quot;,&quot;LKR&quot;,&quot;LRD&quot;,&quot;LSL&quot;,&quot;LYD&quot;,&quot;MAD&quot;,&quot;MDL&quot;,&quot;MGA&quot;,&quot;MKD&quot;,&quot;MMK&quot;,&quot;MNT&quot;,&quot;MOP&quot;,&quot;MRO&quot;,&quot;MUR&quot;,&quot;MVR&quot;,&quot;MWK&quot;,&quot;MXN&quot;,&quot;MYR&quot;,&quot;MZN&quot;,&quot;NAD&quot;,&quot;NGN&quot;,&quot;NIO&quot;,&quot;NOK&quot;,&quot;NPR&quot;,&quot;NZD&quot;,&quot;OMR&quot;,&quot;PAB&quot;,&quot;PEN&quot;,&quot;PGK&quot;,&quot;PHP&quot;,&quot;PKR&quot;,&quot;PLN&quot;,&quot;PYG&quot;,&quot;QAR&quot;,&quot;RON&quot;,&quot;RSD&quot;,&quot;RUB&quot;,&quot;RWF&quot;,&quot;SAR&quot;,&quot;SBD&quot;,&quot;SCR&quot;,&quot;SEK&quot;,&quot;SGD&quot;,&quot;SHP&quot;,&quot;SLL&quot;,&quot;SOS&quot;,&quot;SRD&quot;,&quot;SSP&quot;,&quot;STD&quot;,&quot;SVC&quot;,&quot;SZL&quot;,&quot;THB&quot;,&quot;TJS&quot;,&quot;TMT&quot;,&quot;TND&quot;,&quot;TOP&quot;,&quot;TRY&quot;,&quot;TTD&quot;,&quot;TWD&quot;,&quot;TZS&quot;,&quot;UAH&quot;,&quot;UGX&quot;,&quot;USD&quot;,&quot;UYU&quot;,&quot;UZS&quot;,&quot;VEF&quot;,&quot;VES&quot;,&quot;VND&quot;,&quot;VUV&quot;,&quot;WST&quot;,&quot;XAF&quot;,&quot;XAG&quot;,&quot;XAU&quot;,&quot;XCD&quot;,&quot;XDR&quot;,&quot;XOF&quot;,&quot;XPD&quot;,&quot;XPF&quot;,&quot;XPT&quot;,&quot;YER&quot;,&quot;ZAR&quot;,&quot;ZMW&quot;,&quot;ZWL&quot;]</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(&quot;Balance is not zero, but: &quot;+ 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>
+487
View File
@@ -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(&apos;is_first_install&apos;).contains(&quot;first_install&quot;),)}</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">{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin_password&quot;,&quot;password_repeat&quot;:&quot;admin_password&quot;}</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(&apos;is_first_install&apos;).contains(&quot;first_install&quot;),)}</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">{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin_password&quot;}</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(&quot;adminWalletId&quot;, resp.id || &quot;no-admin-wallet-id&quot;);
vars.put(&quot;adminWalletKey&quot;, resp.adminkey || &apos;no-adminkey&apos;);
</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">{&quot;amount&quot;:&quot;1000000&quot;,&quot;id&quot;:&quot;${adminWalletId}&quot;}</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?&amp;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
+3214
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+27 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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
# -------------
+8 -5
View File
@@ -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
View File
@@ -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"])
+41 -58
View File
@@ -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,26 +17,16 @@ from lnbits.core.services import (
switch_to_voidwallet,
)
from lnbits.settings import get_wallet_class, settings
from lnbits.tasks import (
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] = {}
def register_killswitch():
async def killswitch_task():
"""
Registers a killswitch which will check lnbits-status repository for a signal from
killswitch 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)
async def killswitch_task():
while True:
WALLET = get_wallet_class()
if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet":
@@ -51,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}"
@@ -59,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":
@@ -84,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:
@@ -121,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)
@@ -134,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)
@@ -148,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):
+15 -15
View File
@@ -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>
+5 -11
View File
@@ -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 %}
+10 -13
View File
@@ -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>
+2 -2
View File
@@ -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 />
+12 -8
View File
@@ -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>
+369 -97
View File
@@ -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>&nbsp;</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 (
+8 -1
View File
@@ -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>
+1 -1
View File
@@ -370,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>
+15 -19
View File
@@ -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 = 117
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)
}
})
+72 -20
View File
@@ -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)
@@ -838,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()
@@ -903,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
@@ -915,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)],
+4 -6
View File
@@ -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
+13 -7
View File
@@ -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")
+5 -3
View File
@@ -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 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]:
+14 -10
View File
@@ -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
View File
@@ -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"],
)
+9
View File
@@ -182,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)
@@ -206,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)
@@ -245,6 +251,7 @@ class FundingSourcesSettings(
LndGrpcFundingSource,
LnPayFundingSource,
AlbyFundingSource,
ZBDFundingSource,
OpenNodeFundingSource,
SparkFundingSource,
LnTipsFundingSource,
@@ -352,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)
@@ -399,6 +407,7 @@ class SuperUserSettings(LNbitsSettings):
"LnTipsWallet",
"LNPayWallet",
"AlbyWallet",
"ZBDWallet",
"LNbitsWallet",
"OpenNodeWallet",
]
+9 -9
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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。您可以使用手机扫描的方式打开您的钱包。',
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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.',
+9 -2
View File
@@ -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.',
@@ -239,5 +239,12 @@ This service is in BETA. LNbits holds no responsibility for loss of access to fu
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'
}
+1 -1
View File
@@ -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í.',
+1 -1
View File
@@ -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.",
+1 -1
View File
@@ -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ì.",
+1 -1
View File
@@ -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:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
+1 -1
View File
@@ -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을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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!',
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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ľ.',
+1 -1
View File
@@ -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.',
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -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',
+6 -16
View File
@@ -360,7 +360,6 @@ new Vue({
},
onPaymentReceived: function (paymentHash) {
this.fetchPayments()
this.fetchBalance()
if (this.receive.paymentHash === paymentHash) {
this.receive.show = false
@@ -589,7 +588,6 @@ new Vue({
clearInterval(this.parse.paymentChecker)
dismissPaymentMsg()
this.fetchPayments()
this.fetchBalance()
}
})
}, 2000)
@@ -630,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) {
@@ -822,8 +818,10 @@ new Vue({
}
},
watch: {
payments: function () {
this.fetchBalance()
payments: function (_, oldVal) {
if (oldVal && oldVal.length !== 0) {
this.fetchBalance()
}
},
'paymentsChart.group': function () {
this.showChart()
@@ -840,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
+25 -19
View File
@@ -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,10 +75,6 @@ async def catch_everything_and_restart(func):
await catch_everything_and_restart(func)
async def send_push_promise(a, b) -> None:
pass
invoice_listeners: Dict[str, asyncio.Queue] = {}
@@ -79,17 +96,6 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = No
invoice_listeners[name] = send_chan
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)
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
@@ -103,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():
@@ -116,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():
+24 -11
View File
@@ -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 = '/'
}
}
})
+3
View File
@@ -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 %}
+10
View File
@@ -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
+1
View File
@@ -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):
+2 -5
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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(
+18 -15
View File
@@ -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:
+3 -5
View File
@@ -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:
+3 -2
View File
@@ -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:
+9 -3
View File
@@ -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:
+24 -8
View File
@@ -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
View File
@@ -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:
+15 -8
View File
@@ -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
View File
@@ -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)
+3 -2
View File
@@ -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
+3 -22
View File
@@ -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
View File
@@ -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]:
+3 -2
View File
@@ -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 ""
+159
View File
@@ -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
+634 -277
View File
File diff suppressed because it is too large Load Diff
Generated
+113 -41
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lnbits"
version = "0.12.2"
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
+1 -2
View File
@@ -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
+74
View File
@@ -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;"],
)
+4 -10
View File
@@ -1,27 +1,21 @@
import pytest
from pydantic import BaseModel
from lnbits.helpers import (
insert_query,
update_query,
)
from tests.helpers import DbTestModel
class TestModel(BaseModel):
id: int
name: str
test = TestModel(id=1, name="test")
test = DbTestModel(id=1, name="test", value="yes")
@pytest.mark.asyncio
async def test_helpers_insert_query():
q = insert_query("test_helpers_query", test)
assert q == "INSERT INTO test_helpers_query (id, name) VALUES (?, ?)"
assert q == "INSERT INTO test_helpers_query (id, name, value) VALUES (?, ?, ?)"
@pytest.mark.asyncio
async def test_helpers_update_query():
q = update_query("test_helpers_query", test)
assert q == "UPDATE test_helpers_query SET id = ?, name = ? WHERE id = ?"
assert q == "UPDATE test_helpers_query SET id = ?, name = ?, value = ? WHERE id = ?"
+1 -1
View File
@@ -763,7 +763,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
# status should still be available and be False
status = await payment_db.check_status()
assert status.paid is False
assert not status.paid
# now the payment should be gone after the status check
# payment_db_after_status_check = await get_standalone_payment(
+1
View File
@@ -109,6 +109,7 @@ async def test_channel_management(node_client):
[channel for channel in data if channel.state == ChannelState.ACTIVE]
)
assert close, "No active channel found"
assert close.point, "No channel point found"
response = await node_client.delete(
"/node/api/v1/channels",
+13 -14
View File
@@ -5,17 +5,23 @@ import random
import string
import time
from subprocess import PIPE, Popen, TimeoutExpired
from typing import Tuple
from typing import Optional, Tuple
import psycopg2
from loguru import logger
from sqlalchemy.engine.url import make_url
from psycopg2 import connect
from psycopg2.errors import InvalidCatalogName
from lnbits import core
from lnbits.db import DB_TYPE, POSTGRES
from lnbits.db import DB_TYPE, POSTGRES, FromRowModel
from lnbits.wallets import get_wallet_class, set_wallet_class
class DbTestModel(FromRowModel):
id: int
name: str
value: Optional[str] = None
def get_random_string(N: int = 10):
return "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
@@ -158,24 +164,17 @@ def pay_onchain(address: str, sats: int) -> str:
def clean_database(settings):
if DB_TYPE == POSTGRES:
db_url = make_url(settings.lnbits_database_url)
conn = psycopg2.connect(settings.lnbits_database_url)
conn = connect(settings.lnbits_database_url)
conn.autocommit = True
with conn.cursor() as cur:
try:
cur.execute("DROP DATABASE lnbits_test")
except psycopg2.errors.InvalidCatalogName:
except InvalidCatalogName:
pass
cur.execute("CREATE DATABASE lnbits_test")
db_url.database = "lnbits_test"
settings.lnbits_database_url = str(db_url)
core.db.__init__("database")
conn.close()
else:
# FIXME: do this once mock data is removed from test data folder
# TODO: do this once mock data is removed from test data folder
# os.remove(settings.lnbits_data_folder + "/database.sqlite3")
pass
+8 -8
View File
@@ -48,7 +48,7 @@ def check_db_versions(sqdb):
postgres = get_postgres_cursor()
postgres.execute("SELECT * FROM public.dbversions;")
dbpost = dict(postgres.fetchall())
dbpost = dict(postgres.fetchall()) # type: ignore
for key, value in dblite.items():
if key in dblite and key in dbpost:
@@ -104,7 +104,7 @@ def insert_to_pg(query, data):
connection.close()
def migrate_core(file: str, exclude_tables: List[str] = None):
def migrate_core(file: str, exclude_tables: List[str] = []):
print(f"Migrating core: {file}")
migrate_db(file, "public", exclude_tables)
print("✅ Migrated core")
@@ -118,12 +118,12 @@ def migrate_ext(file: str):
print(f"✅ Migrated ext: {schema}")
def migrate_db(file: str, schema: str, exclude_tables: List[str] = None):
def migrate_db(file: str, schema: str, exclude_tables: List[str] = []):
# first we check if this file exists:
assert os.path.isfile(file), f"{file} does not exist!"
sq = get_sqlite_cursor(file)
tables = sq.execute(
cursor = get_sqlite_cursor(file)
tables = cursor.execute(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name not like 'sqlite?_%' escape '?'
@@ -139,16 +139,16 @@ def migrate_db(file: str, schema: str, exclude_tables: List[str] = None):
if exclude_tables and tableName in exclude_tables:
continue
columns = sq.execute(f"PRAGMA table_info({tableName})").fetchall()
columns = cursor.execute(f"PRAGMA table_info({tableName})").fetchall()
q = build_insert_query(schema, tableName, columns)
data = sq.execute(f"SELECT * FROM {tableName};").fetchall()
data = cursor.execute(f"SELECT * FROM {tableName};").fetchall()
if len(data) == 0:
print(f"🛑 You sneaky dev! Table {tableName} is empty!")
insert_to_pg(q, data)
sq.close()
cursor.close()
def build_insert_query(schema, tableName, columns):
+9 -5
View File
@@ -13,16 +13,20 @@ if len(sys.argv) < 2:
sys.exit(1)
lang = sys.argv[1]
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY env var not set"
def load_language(lang):
def load_language(lang: str) -> dict:
s = open(f"lnbits/static/i18n/{lang}.js", "rt").read()
prefix = "window.localisation.%s = {\n" % lang
assert s.startswith(prefix)
s = s[len(prefix) - 2 :]
return json5.loads(s)
json = json5.loads(s)
assert isinstance(json, dict)
return json
def save_language(lang, data):
def save_language(lang: str, data) -> None:
with open(f"lnbits/static/i18n/{lang}.js", "wt") as f:
f.write("window.localisation.%s = {\n" % lang)
row = 0
@@ -40,7 +44,7 @@ def save_language(lang, data):
f.write("}\n")
def string_variables_match(str1, str2):
def string_variables_match(str1: str, str2: str) -> bool:
pat = re.compile(r"%\{[a-z0-9_]*\}")
m1 = re.findall(pat, str1)
m2 = re.findall(pat, str2)
@@ -66,7 +70,6 @@ def translate_string(lang_from, lang_to, text):
"kr": "Korean",
"fi": "Finnish",
}[lang_to]
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY env var not set"
client = OpenAI()
try:
chat_completion = client.chat.completions.create(
@@ -82,6 +85,7 @@ def translate_string(lang_from, lang_to, text):
],
model="gpt-4-1106-preview", # aka GPT-4 Turbo
)
assert chat_completion.choices[0].message.content, "No response from GPT-4"
translated = chat_completion.choices[0].message.content.strip()
# return translated string only if variables were not broken
if string_variables_match(text, translated):
+8
View File
@@ -0,0 +1,8 @@
import hashlib
import os
preimage = os.urandom(32)
preimage_hash = hashlib.sha256(preimage).hexdigest()
print(f"preimage hash: {preimage_hash}")
print(f"preimage: {preimage.hex()}")