├── .github
└── workflows
│ ├── check.yml
│ └── static.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── Makefile
├── README.md
├── build.sh
├── check.py
├── extensions.json
├── index.html
├── integration
├── 001_extensions-enable-all.jmx
├── 002_lnurl.jmx
├── 003_tpos.jmx
├── 004_tpos-pay-to-enable.jmx
├── 005_lnurlw-race.jmx
├── 006_watchonly-satspay-tipjar.jmx
├── 007_lndhub.jmx
├── 008_lnaddress-conflict.jmx
├── 009_example_upgrade.jmx
├── 010_nostrnip5.jmx
├── 011_auction_house.jmx
├── fragments
│ ├── init-account.jmx
│ └── init-server.jmx
└── scripts
│ ├── check_admin_extension_status_code.js
│ ├── check_price_in_sats_vs_millisats.js
│ ├── extract_extension_data.js
│ ├── extract_extension_release_by_version.js
│ ├── extract_extensions_ids.js
│ ├── extract_latest_extension_release.js
│ ├── lndhub_check_balance.js
│ ├── lndhub_check_balance_after_payments.js
│ ├── lndhub_check_payments.js
│ ├── lndhub_check_payments_after_payment.js
│ ├── lndhub_check_transactions.js
│ ├── lndhub_check_transactions_after_payment.js
│ ├── lnurl_check_balance.js
│ ├── lnurl_check_balance_after_withdraw.js
│ ├── lnurl_check_created_pay_link.js
│ ├── lnurl_check_created_withdraw_link.js
│ ├── lnurl_check_receive_wallet_balance.js
│ ├── lnurl_check_webhook_response.js
│ ├── lnurl_extract_pay_link_id.js
│ ├── lnurlw_check_executed_withdraws.js
│ ├── nip5_check_referer_balance.js
│ ├── satspay_check_charge_details.js
│ ├── satspay_check_charge_webhook_call.js
│ ├── satspay_check_charges_count.js
│ ├── satspay_check_payments_status.js
│ ├── satspay_check_wallet_balance.js
│ ├── tipjar_check_tips_for_user.js
│ ├── tipjar_check_user_balance.js
│ ├── tipjar_extract_charge_url_for_tip.js
│ ├── tops_check_updated.js
│ ├── tpos_check_admin_balance.js
│ ├── tpos_check_atm_created.js
│ ├── tpos_check_created.js
│ ├── tpos_check_tip_wallet_created.js
│ ├── tpos_check_user_balance.js
│ └── tpos_extract_tip_wallet_data.js
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── logo.svg
├── pyproject.toml
├── src
├── index.jsx
└── index.scss
├── test.sh
├── toc.md
├── update_version.py
├── util.sh
└── vite.config.js
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: LNbits Extension CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | jobs:
9 | validate-json:
10 | runs-on: ubuntu-24.04
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: json-syntax-check
14 | uses: limitusus/json-syntax-check@v1
15 | with:
16 | pattern: "\\.json$"
17 |
18 | check-json:
19 | runs-on: ubuntu-24.04
20 | steps:
21 | - uses: actions/checkout@v3
22 | - uses: actions/setup-python@v4
23 | with:
24 | python-version: "3.10"
25 | - name: Install dependencies
26 | run: pip install Pillow requests
27 | - name: Run check.py
28 | run: python check.py
29 |
30 | jmeter:
31 | needs: [validate-json, check-json]
32 | runs-on: ubuntu-24.04
33 | steps:
34 |
35 | - uses: actions/checkout@v4
36 |
37 | - name: Set up Python 3.10
38 | uses: actions/setup-python@v5
39 | with:
40 | python-version: "3.10"
41 | cache: "pip"
42 |
43 | - name: Set up Poetry 1.5.1
44 | uses: abatilo/actions-poetry@v2
45 | with:
46 | poetry-version: "1.5.1"
47 |
48 | - name: create logs and reports dir
49 | run: |
50 | mkdir logs
51 | mkdir reports
52 |
53 | - name: setup java version
54 | run: |
55 | update-java-alternatives --list
56 | sudo update-java-alternatives --set /usr/lib/jvm/temurin-8-jdk-amd64
57 | java -version
58 |
59 | - name: install jmeter
60 | env:
61 | JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
62 | run: |
63 | make install-jmeter
64 |
65 | - name: start mirror server
66 | env:
67 | JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
68 | run: |
69 | make start-mirror-server
70 |
71 | - name: Setup LNbits and run
72 | env:
73 | LNBITS_ADMIN_UI: true
74 | DEBUG: true
75 | LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
76 | LNBITS_EXTENSIONS_MANIFESTS: "https://raw.githubusercontent.com/lnbits/lnbits-extensions/${{ github.head_ref || github.ref_name }}/extensions.json"
77 | LNBITS_BACKEND_WALLET_CLASS: FakeWallet
78 | run: |
79 | git clone https://github.com/lnbits/lnbits.git
80 | cd lnbits
81 | git checkout dev
82 | poetry env use python3.10
83 | poetry install
84 | poetry run lnbits &
85 | sleep 10
86 |
87 | - name: run jmx scripts
88 | env:
89 | EXTENSIONS_MANIFEST_PATH: "/lnbits/lnbits-extensions/${{ github.head_ref || github.ref_name }}/extensions.json"
90 | JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
91 | run: |
92 | make test
93 |
94 | - name: log results
95 | if: ${{ always() }}
96 | run: |
97 | # catch up time for lnbits
98 | sleep 1
99 | cat lnbits/data/logs/debug.log
100 |
101 | - uses: actions/upload-artifact@v4
102 | if: ${{ always() }}
103 | with:
104 | name: jmeter-test-results
105 | path: |
106 | reports/
107 | logs/
108 | lnbits/data/logs/
109 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v3
34 | - name: Use Node.js
35 | uses: actions/setup-node@v3
36 | with:
37 | node-version: "18.x"
38 | - name: Install dependencies
39 | run: npm i
40 | - name: install build deps
41 | run: sudo apt-get install -y jq curl
42 | - name: Prepare extension configs
43 | run: bash build.sh
44 | - name: Build
45 | run: npm run build
46 | - name: Setup Pages
47 | uses: actions/configure-pages@v3
48 | - name: Upload artifact
49 | uses: actions/upload-pages-artifact@v3
50 | with:
51 | # Upload entire repository
52 | path: "dist"
53 | - name: Deploy to GitHub Pages
54 | id: deployment
55 | uses: actions/deploy-pages@v4
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 | .mypy_cache
3 | extensions
4 | node_modules
5 | dist
6 | apache-jmeter*
7 | logs
8 | reports
9 | jmeter.log
10 | upgrades
11 | metadata.json
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | extensions
2 | .mypy_cache
3 | package-lock.json
4 | dist
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "bracketSameLine": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Vlad Stan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install-jmeter:
2 | wget --quiet https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.3.zip
3 | unzip -qq apache-jmeter-5.6.3.zip
4 | ./apache-jmeter-5.6.3/bin/jmeter -v
5 |
6 | start-mirror-server:
7 | echo "Fix bad Jmeter lib names."
8 | cp ./apache-jmeter-5.6.3/lib/slf4j-api-1.7.36.jar ./apache-jmeter-5.6.3/lib/slf4j-api-1.7.25.jar
9 | cp ./apache-jmeter-5.6.3/lib/log4j-slf4j-impl-2.22.1.jar ./apache-jmeter-5.6.3/lib/log4j-slf4j-impl-2.11.0.jar
10 | cp ./apache-jmeter-5.6.3/lib/log4j-api-2.22.1.jar ./apache-jmeter-5.6.3/lib/log4j-api-2.11.1.jar
11 | cp ./apache-jmeter-5.6.3/lib/log4j-core-2.22.1.jar ./apache-jmeter-5.6.3/lib/log4j-core-2.11.1.jar
12 | cp ./apache-jmeter-5.6.3/lib/log4j-1.2-api-2.22.1.jar ./apache-jmeter-5.6.3/lib/log4j-1.2-api-2.11.1.jar
13 | echo "Starting the mirror server on port 8500."
14 |
15 | ./apache-jmeter-5.6.3/bin/mirror-server --port 8500 &
16 |
17 | test:
18 | sh ./test.sh
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LNbits Vetted Extensions
2 |
3 | Official registry for vetted LNbits extensions
4 |
5 | To submit an extension to this registry add your manifest into the [`extensions.json`](extensions.json) file in this repository.
6 | ### Important
7 |
8 | Only submit fully working extensions, the review process is not intended to improve the extension code.
9 |
10 | Do not add dependencies, LNbits has plenty of dependencies you can use.
11 |
12 | The easier an extension is to review, the quicker the review process will be.
13 |
14 | ### Manifest format
15 |
16 | The file MUST use the `extensions` format:
17 |
18 | ```json
19 | {
20 | "id": "gerty",
21 | "repo": "https://github.com/lnbits/gerty",
22 | "name": "Gerty",
23 | "version": "0.1.2",
24 | "short_description": "Your bitcoin assistant",
25 | "icon": "https://raw.githubusercontent.com/lnbits/gerty/main/static/gerty.png",
26 | "archive": "https://github.com/lnbits/gerty/archive/refs/tags/0.1.2.zip",
27 | "hash": "baff0b6162ffb65cc0b4c721a4aa40a7d3d48acd55a3e344cba3eb1d35cf2074"
28 | },
29 | ```
30 |
31 | For an exensions local [`manifest.json`](https://github.com/lnbits/gerty/blob/main/manifest.json) use the `repos` format:
32 |
33 | ```json
34 | {
35 | "repos": [
36 | {
37 | "id": "gerty",
38 | "organisation": "lnbits",
39 | "repository": "gerty"
40 | }
41 | ]
42 | }
43 | ```
44 |
45 | ### Paid extensions
46 | It is possible for developers to require a payment for their extensions. In order to do so an extension release must have this field:
47 | ```json
48 | "pay_link": "# payment URL"
49 | ```
50 |
51 | **Example**:
52 | ```json
53 | {
54 | "id": "testext",
55 | "repo": "https://github.com/lnbits/testext",
56 | "name": "Test Extension",
57 | "version": "0.5",
58 | "short_description": "Private Test Extension",
59 | "icon": "https://raw.githubusercontent.com/lnbits/example/main/static/bitcoin-extension.png",
60 | "archive": "https://demo.lnbits.com/paywall/download/m2FVCFktJzMcGKXTaHbyhi",
61 | "pay_link": "https://demo.lnbits.com/paywall/api/v1/paywalls/invoice/m2FVCFktJzMcGKXTaHbyhi",
62 | "hash": "455527407fcfdc5e8aba93f16802d1083d36dcdfdde829f919cee07420791d61"
63 | }
64 | ```
65 |
66 | The [Paywall LNbits Extension](https://github.com/lnbits/paywall/blob/main/README.md#file-paywall) can be used to serve the extension `zip` file.
67 |
68 | If you do not want to use the [Paywall LNbits Extension](https://github.com/lnbits/paywall/) to server your extension, but instead you want to use your own paywall, then the `pay_link` endpoint must follow these specifications:
69 |
70 |
71 |
72 | HTTP Request |
73 | HTTP Response |
74 | Description |
75 |
76 |
77 |
78 |
79 | ```HTTP
80 | GET pay_link
81 | ```
82 | |
83 |
84 |
85 | ```json
86 | {
87 | "amount": 5
88 | }
89 | ````
90 |
91 | |
92 | Get the amount in `sats` required by this extension release. |
93 |
94 |
95 |
96 |
97 | ```HTTP
98 | GET pay_link?amount=5
99 | ```
100 |
101 | |
102 |
103 |
104 | ```json
105 | {
106 | "payment_hash": "04c33f37d01aff...fd7c407a",
107 | "payment_request": "lnbc50n1pju...n7h8gucqn2cgau"
108 | }
109 | ```
110 |
111 | |
112 | Request an invoice for the specified amount (or higher). |
113 |
114 |
115 |
116 |
117 | ```HTTP
118 | WS pay_link/{payment_hash}
119 |
120 | ```
121 |
122 | |
123 |
124 |
125 | ```json
126 | {"paid": true|false}
127 | ```
128 |
129 | |
130 | Open a websocket to be notified when the invoice has been paid. |
131 |
132 |
133 |
134 | In order to download the file one must add the `payment_hash` and an `version` (optional) query parameters to the `archive` URL. Eg:
135 |
136 | ```HTTP
137 | GET https://demo.lnbits.com/paywall/download/m2FVCFktJzMcGKXTaHbyhi?payment_hash=3bf...7ec&version=v0.1
138 | ```
139 |
140 |
141 | ### Getting sha256 checksum for a release
142 |
143 | ```console
144 | $ wget -O - https://github.com/lnbits/withdraw/archive/refs/tags/0.1.1.zip 2> /dev/null | sha256sum | cut -d" " -f 1
145 | baff0b6162ffb65cc0b4c721a4aa40a7d3d48acd55a3e344cba3eb1d35cf2074
146 | ```
147 |
148 | ### Lighter ZIP archive
149 |
150 | - documentation, tests and other type of files should not be included in the zip archive generated when a GitHub release is created
151 | - keep the `README.md` and `LICENSE` files in the zip as these are required!
152 | - in order to exclude these files one must:
153 | - create a `.gitattributes` file (on the top level of the repo)
154 | - add a line for the ignored files/dirs: `tests/ export-ignore`
155 |
156 | ### Checking the changes before sending a pull request
157 |
158 | - after editing the `manifest.json` file in this repo you should run `python3 check.py` as a sanity check
159 | - you can run `python3 check.py foo bar` only to run sanity checks on extensions named `foo` and `bar`
160 |
161 | ### util for cloning and pulling all extensions
162 |
163 | cloning all extensions into `extensions` dir. requires `jq` to be installed.
164 |
165 | ```sh
166 | sh util.sh clone
167 | ```
168 |
169 | pulling all extensions from `extensions` dir
170 |
171 | ```sh
172 | sh util.sh pull
173 | ```
174 |
175 | get LNbits env variables for all extensions
176 |
177 | ```sh
178 | sh util.sh env
179 | ```
180 |
181 | update a extension in extensions.json with id and version
182 |
183 | ```sh
184 | sh util.sh update_extension example v0.4.2
185 | ```
186 |
187 | ### Example video on how to release a extension into this repo
188 |
189 | this uses a github workflow like this: https://github.com/lnbits/example/blob/main/.github/workflows/release.yml
190 |
191 | https://github.com/lnbits/lnbits-extensions/assets/1743657/0d0a6626-655b-4528-9547-9fdc348cf9a6
192 |
193 |
194 | # Integration Tests
195 | ## setup
196 | ```sh
197 | make install-jmeter
198 | ```
199 | ## configure
200 | make sure LNbits is running and start the mirror server
201 | ```sh
202 | make start-mirror-server
203 | ```
204 | ## run
205 | ```sh
206 | make test
207 | ```
208 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | CONFS=$(cat extensions.json \
4 | | jq -r '[.extensions[].repo] | unique | .[]' \
5 | | sed -e 's/https:\/\/github.com\//https:\/\/raw.githubusercontent.com\//' -e 's/$/\/main\/config.json/')
6 |
7 | for conf in $CONFS; do
8 | name=$(echo $conf | cut -d'/' -f5)
9 | echo "Downloading $conf"
10 | curl -s $conf | jq --arg name $name '. + {id: $name}' >> metadata.tmp.json
11 | done
12 |
13 | cat metadata.tmp.json | jq -s . > metadata.json
14 | rm metadata.tmp.json
15 |
--------------------------------------------------------------------------------
/check.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | from hashlib import sha256
4 | from io import BytesIO
5 | from os.path import basename
6 | from zipfile import ZipFile
7 |
8 | import requests
9 | from PIL import Image
10 |
11 | EXTENSIONS_FILE = "extensions.json"
12 | CHECK_ZIP_CONTENTS = True
13 | UNPACK_ZIP_CONTENTS = False
14 |
15 | MANDATORY_FILES = [
16 | "LICENSE",
17 | "README.md",
18 | "__init__.py",
19 | "config.json",
20 | "manifest.json",
21 | ]
22 |
23 | try:
24 | # use requests_cache if installed
25 | # useful for subsequent runs of the check
26 | # so the files are not downloaded again and again
27 | import requests_cache
28 |
29 | requests_cache.install_cache("lnbits-extensions-download-cache")
30 | except ImportError:
31 | pass
32 |
33 |
34 | def get_remote_file(url):
35 | r = requests.get(url)
36 | return r.content
37 |
38 |
39 | def get_remote_zip(url):
40 | d = get_remote_file(url)
41 | z = ZipFile(BytesIO(d))
42 | return sha256(d).hexdigest(), z
43 |
44 |
45 | class Extension:
46 | def __init__(self, ext):
47 | self.id = ext["id"]
48 | self.repo = ext["repo"]
49 | self.name = ext["name"]
50 | self.version = ext["version"]
51 | self.short_description = ext["short_description"]
52 | self.icon = ext["icon"]
53 | self.archive = ext["archive"]
54 | self.hash = ext["hash"]
55 | self.min_lnbits_version = ext.get("min_lnbits_version")
56 | self.max_lnbits_version = ext.get("max_lnbits_version")
57 |
58 | def validate(self) -> tuple[bool, str]:
59 | print(f"Checking '{self.name}' extension ({self.id} {self.version})")
60 |
61 | # sanity checks
62 | if not self.name[0].isupper():
63 | return False, "name does not start with uppercase letter"
64 | if not self.short_description[0].isupper():
65 | return False, "short_description does not start with uppercase letter"
66 | if self.short_description.endswith("."):
67 | return False, "short_description should not end with '.'"
68 | if not self.id == self.id.lower():
69 | return False, "id has mixed casing"
70 | if not self.archive.startswith(self.repo):
71 | return False, "archive URL does not start with repo URL"
72 | if not self.archive.endswith(f"{self.version}.zip"):
73 | bn = basename(self.archive)
74 | return False, f"archive name '{bn}' doesn't end with {self.version}.zip"
75 |
76 | # print archive info from json
77 | archive_hash, archive_zip = get_remote_zip(self.archive)
78 | print(f"- url : {self.archive}")
79 | print(f"- hash : {self.hash} (expected)")
80 | print(f"- hash : {archive_hash} (real)")
81 | if self.hash != archive_hash:
82 | return False, f"hash mismatch {self.hash} != {archive_hash}"
83 |
84 | # check downloaded zip
85 | bad_file = archive_zip.testzip()
86 | if bad_file is not None:
87 | return False, f"archive check for file {bad_file} failed"
88 |
89 | if UNPACK_ZIP_CONTENTS:
90 | archive_zip.extractall("extensions")
91 |
92 | if CHECK_ZIP_CONTENTS:
93 | filelist = archive_zip.namelist()
94 | prefix = f"{basename(self.repo)}-{self.version}"
95 |
96 | mandatory_files = MANDATORY_FILES.copy()
97 | # these are known to not include the LICENSE file in their latest release
98 | # this is fixed in master, but we are waiting for the upcoming release
99 | if self.id in [
100 | "bleskomat",
101 | "deezy",
102 | "discordbot",
103 | "livestream",
104 | "market",
105 | "paywall",
106 | "smtp",
107 | "streamalerts",
108 | "subdomains",
109 | ]:
110 | mandatory_files.remove("LICENSE")
111 |
112 | # these are known to not include the manifest.json file in their latest release
113 | # this is fixed in master, but we are waiting for the upcoming release
114 | if self.id in [
115 | "bleskomat",
116 | "smtp",
117 | "subdomains",
118 | ]:
119 | mandatory_files.remove("manifest.json")
120 |
121 | for f in mandatory_files:
122 | fn = f"{prefix}/{f}"
123 | if fn not in filelist:
124 | return False, f"file {fn} not contained in the archive"
125 |
126 | # check config
127 | config = json.load(archive_zip.open(f"{prefix}/config.json"))
128 |
129 | # don't check name for now, they mismatch quite often :(
130 | """
131 | name = config.get("name")
132 | if name != self.name:
133 | return False, f"name mismatch: {name} != {self.name}"
134 | """
135 |
136 | # don't check short_description for now, they mismatch quite often :(
137 | """
138 | short_description = config.get("short_description")
139 | if short_description != self.short_description:
140 | return False, f"short_description mismatch: {short_description} != {self.short_description}"
141 | """
142 |
143 | min_lnbits_version = config.get("min_lnbits_version")
144 | print(f"- min_lnbits_version : {min_lnbits_version}")
145 | if min_lnbits_version != self.min_lnbits_version:
146 | return (
147 | False,
148 | f"min_lnbits_version mismatch: {min_lnbits_version} != {self.min_lnbits_version}",
149 | )
150 |
151 | # check max_lnbits_version only if it's present in the config.json
152 | max_lnbits_version = config.get("max_lnbits_version")
153 | if max_lnbits_version:
154 | print(f"- max_lnbits_version : {max_lnbits_version}")
155 | if max_lnbits_version != self.max_lnbits_version:
156 | return (
157 | False,
158 | f"max_lnbits_version mismatch: {max_lnbits_version} != {self.max_lnbits_version}",
159 | )
160 |
161 | # check icon
162 | try:
163 | icon = get_remote_file(self.icon)
164 | img = Image.open(BytesIO(icon))
165 | print(f"- icon : OK {img.size[0]}x{img.size[1]} @ {img.mode} ({self.icon})")
166 | except Exception:
167 | print(f"- icon : broken ({self.icon})")
168 | return False, f"broken icon ({self.icon})"
169 |
170 | return True, ""
171 |
172 |
173 | def main(args):
174 | # load stuff from json
175 | try:
176 | print(f"Loading {EXTENSIONS_FILE}")
177 | j = json.load(open(EXTENSIONS_FILE, "rt"))
178 | extensions = j["extensions"]
179 | featured = j["featured"]
180 | print("OK")
181 | except Exception as ex:
182 | print(f"FAILED: {ex}")
183 | return False
184 | print()
185 |
186 | # check whether extensions listed in featured exist
187 | try:
188 | print("Checking whether featured extensions exist")
189 | extensions_ids = [e["id"] for e in extensions]
190 | for f in featured:
191 | assert f in extensions_ids
192 | print("OK")
193 | except Exception as ex:
194 | print(f"FAILED: {ex}")
195 | return False
196 | print()
197 |
198 | failed = {}
199 | for ext in extensions:
200 | # skip extension if args are provided and extension mentioned
201 | if args and ext["id"] not in args:
202 | continue
203 | e = Extension(ext)
204 | ok, err = e.validate()
205 | if ok:
206 | print("OK")
207 | else:
208 | print(f"FAILED: {err}")
209 | failed[f"{ext['id']} {ext['version']}"] = err
210 | print()
211 |
212 | print("Failed extensions:")
213 | if not failed:
214 | print("NONE, ALL GOOD!")
215 | else:
216 | for k, v in failed.items():
217 | print(f"- {k}: {v}")
218 |
219 | return not failed
220 |
221 |
222 | if __name__ == "__main__":
223 | if not main(sys.argv[1:]):
224 | sys.exit(1)
225 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
12 |
13 | LNbits Extension Repository
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/integration/009_example_upgrade.jmx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | host
18 | ${__property(host,,127.0.0.1)}
19 | =
20 |
21 |
22 | scheme
23 | ${__property(scheme,,http)}
24 | =
25 |
26 |
27 | port
28 | ${__property(port,,5000)}
29 | =
30 |
31 |
32 | thinkTime
33 | 3000
34 | =
35 |
36 |
37 | config_path
38 | ${__BeanShell(import org.apache.jmeter.services.FileServer; FileServer.getFileServer().getBaseDir();)}
39 | =
40 |
41 |
42 | extension
43 | example
44 | =
45 |
46 |
47 | ext_version_1
48 | 1.0.1
49 | =
50 |
51 |
52 | ext_version_2
53 | 1.0.6
54 | =
55 |
56 |
57 | test_version
58 | 0
59 | =
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | ${host}
69 | ${port}
70 | ${scheme}
71 |
72 |
73 | 6
74 | Java
75 | 20000
76 | 30000
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | stoptest
85 |
86 | false
87 | 1
88 |
89 | 1
90 | 1
91 | 1370726934000
92 | 1370726934000
93 | false
94 |
95 |
96 | false
97 |
98 |
99 |
100 |
101 | false
102 | false
103 |
104 |
105 |
106 | false
107 | true
108 |
109 |
110 |
111 | fragments/init-server.jmx
112 |
113 |
114 |
115 |
116 | false
117 | true
118 |
119 |
120 |
121 | fragments/init-account.jmx
122 |
123 |
124 |
125 |
126 | true
127 |
128 |
129 |
130 | false
131 | {"username":"admin","password":"secret1234"}
132 | =
133 |
134 |
135 |
136 | ${host}
137 | ${port}
138 | ${scheme}
139 | UTF-8
140 | /api/v1/auth
141 | POST
142 | true
143 | false
144 | true
145 | false
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | Referer
155 | ${scheme}://${host}:${port}/
156 |
157 |
158 | Accept-Language
159 | en-US,en;q=0.5
160 |
161 |
162 | Origin
163 | ${scheme}://${host}:${port}
164 |
165 |
166 | Content-Type
167 | application/json
168 |
169 |
170 | Accept-Encoding
171 | gzip, deflate
172 |
173 |
174 | User-Agent
175 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0
176 |
177 |
178 | Accept
179 | application/json, text/plain, */*
180 |
181 |
182 |
183 |
184 |
185 |
186 | 200
187 |
188 |
189 | Assertion.response_code
190 | false
191 | 8
192 |
193 |
194 |
195 |
196 | false
197 | true
198 |
199 |
200 |
201 | true
202 |
203 |
204 | vars.put("test_version", "1")
205 | vars.put("ext_version", vars.get("ext_version_1"));
206 |
207 | javascript
208 |
209 |
210 |
211 |
212 | Test Plan
213 | Test Plan
214 | Reusable Fragments
215 | [${extension}] Install and Check version ${test_version}
216 |
217 |
218 |
219 |
220 |
221 | false
222 | true
223 |
224 |
225 |
226 | true
227 |
228 |
229 | vars.put("test_version", "2")
230 | vars.put("ext_version", vars.get("ext_version_2"));
231 |
232 | javascript
233 |
234 |
235 |
236 |
237 | Test Plan
238 | Test Plan
239 | Reusable Fragments
240 | [${extension}] Install and Check version ${test_version}
241 |
242 |
243 |
244 |
245 |
246 | false
247 | true
248 |
249 |
250 |
251 | true
252 |
253 |
254 | vars.put("test_version", "1")
255 | vars.put("ext_version", vars.get("ext_version_1"));
256 |
257 | javascript
258 |
259 |
260 |
261 |
262 | Test Plan
263 | Test Plan
264 | Reusable Fragments
265 | [${extension}] Install and Check version ${test_version}
266 |
267 |
268 |
269 |
270 |
271 | false
272 | true
273 |
274 |
275 |
276 | true
277 |
278 |
279 | vars.put("test_version", "2")
280 | vars.put("ext_version", vars.get("ext_version_2"));
281 |
282 | javascript
283 |
284 |
285 |
286 |
287 | Test Plan
288 | Test Plan
289 | Reusable Fragments
290 | [${extension}] Install and Check version ${test_version}
291 |
292 |
293 |
294 |
295 |
296 | false
297 | true
298 |
299 |
300 |
301 | true
302 |
303 |
304 | vars.put("test_version", "1")
305 | vars.put("ext_version", vars.get("ext_version_1"));
306 |
307 | javascript
308 |
309 |
310 |
311 |
312 | Test Plan
313 | Test Plan
314 | Reusable Fragments
315 | [${extension}] Install and Check version ${test_version}
316 |
317 |
318 |
319 |
320 |
321 | false
322 | true
323 | false
324 |
325 |
326 |
327 | false
328 |
329 | saveConfig
330 |
331 |
332 | true
333 | true
334 | true
335 |
336 | true
337 | true
338 | true
339 | true
340 | false
341 | true
342 | true
343 | false
344 | false
345 | false
346 | true
347 | false
348 | false
349 | false
350 | true
351 | 0
352 | true
353 | true
354 | true
355 | true
356 | true
357 | true
358 |
359 |
360 |
361 |
362 |
363 |
364 | false
365 |
366 | saveConfig
367 |
368 |
369 | true
370 | true
371 | true
372 |
373 | true
374 | true
375 | true
376 | true
377 | false
378 | true
379 | true
380 | false
381 | false
382 | false
383 | true
384 | false
385 | false
386 | false
387 | true
388 | 0
389 | true
390 | true
391 | true
392 | true
393 | true
394 | true
395 |
396 |
397 |
398 |
399 |
400 |
401 | false
402 |
403 | saveConfig
404 |
405 |
406 | true
407 | true
408 | true
409 |
410 | true
411 | true
412 | true
413 | true
414 | false
415 | true
416 | true
417 | false
418 | false
419 | false
420 | true
421 | false
422 | false
423 | false
424 | true
425 | 0
426 | true
427 | true
428 | true
429 | true
430 | true
431 | true
432 |
433 |
434 |
435 |
436 |
437 |
438 | false
439 |
440 | saveConfig
441 |
442 |
443 | true
444 | true
445 | true
446 |
447 | true
448 | true
449 | true
450 | true
451 | false
452 | true
453 | true
454 | false
455 | false
456 | false
457 | true
458 | false
459 | false
460 | false
461 | true
462 | 0
463 | true
464 | true
465 | true
466 | true
467 | true
468 | true
469 |
470 |
471 |
472 | 100
473 | true
474 | (wallet){1}
475 |
476 |
477 |
478 | false
479 |
480 | saveConfig
481 |
482 |
483 | true
484 | true
485 | true
486 |
487 | true
488 | true
489 | true
490 | true
491 | false
492 | true
493 | true
494 | false
495 | false
496 | false
497 | true
498 | false
499 | false
500 | false
501 | true
502 | 0
503 | true
504 | true
505 | true
506 | true
507 | true
508 | true
509 |
510 |
511 |
512 |
513 |
514 |
515 | false
516 |
517 | saveConfig
518 |
519 |
520 | true
521 | true
522 | true
523 |
524 | true
525 | true
526 | true
527 | true
528 | false
529 | true
530 | true
531 | false
532 | false
533 | false
534 | true
535 | false
536 | false
537 | false
538 | true
539 | 0
540 | true
541 | true
542 | true
543 | true
544 | true
545 | true
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 | 8888
554 |
555 | windowsupdate\.microsoft\.com.*
556 | (?i).*\.(bmp|css|js|gif|ico|jpe?g|png|swf|eot|otf|ttf|mp4|woff|woff2)
557 | .*msg\.yahoo\.com.*
558 | www\.download\.windowsupdate\.com.*
559 | toolbarqueries\.google\..*
560 | http?://self-repair\.mozilla\.org.*
561 | tiles.*\.mozilla\.com.*
562 | .*detectportal\.firefox\.com.*
563 | us\.update\.toolbar\.yahoo\.com.*
564 | .*\.google\.com.*/safebrowsing/.*
565 | api\.bing\.com.*
566 | toolbar\.google\.com.*
567 | .*yimg\.com.*
568 | toolbar\.msn\.com.*
569 | (?i).*\.(bmp|css|js|gif|ico|jpe?g|png|swf|eot|otf|ttf|mp4|woff|woff2)[\?;].*
570 | toolbar\.avg\.com/.*
571 | www\.google-analytics\.com.*
572 | pgq\.yahoo\.com.*
573 | safebrowsing.*\.google\.com.*
574 | sqm\.microsoft\.com.*
575 | g\.msn.*
576 | clients.*\.google.*
577 | .*toolbar\.yahoo\.com.*
578 | geo\.yahoo\.com.*
579 |
580 |
581 | true
582 | 4
583 | false
584 |
585 | false
586 | true
587 | true
588 | false
589 | true
590 |
591 |
592 | false
593 |
594 | 0
595 |
596 | true
597 | UTF-8
598 |
599 |
600 |
601 | 8081
602 | 0
603 | 25
604 |
605 |
606 |
607 | continue
608 |
609 | false
610 | 1
611 |
612 | 1
613 | 1
614 | false
615 |
616 |
617 | true
618 |
619 |
620 |
621 |
622 |
623 | false
624 | true
625 |
626 |
627 |
628 | false
629 | true
630 | by admin
631 |
632 |
633 |
634 |
635 |
636 |
637 | ${host}
638 | ${port}
639 | ${scheme}
640 | utf-8
641 | /api/v1/extension/${extension}/releases
642 | GET
643 | true
644 | false
645 | true
646 | false
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 | Sec-Fetch-Mode
656 | same-origin
657 |
658 |
659 | Sec-Fetch-Site
660 | same-origin
661 |
662 |
663 | Accept-Language
664 | en-US,en;q=0.
665 |
666 |
667 | Upgrade-Insecure-Requests
668 | 1
669 |
670 |
671 | Accept-Encoding
672 | gzip, deflate, br
673 |
674 |
675 | User-Agent
676 | Mozilla.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0
677 |
678 |
679 | Accept
680 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
681 |
682 |
683 | Sec-Fetch-Dest
684 | empty
685 |
686 |
687 | X-Api-Key
688 | ${adminWalletKey}
689 |
690 |
691 |
692 |
693 |
694 |
695 | 200
696 |
697 |
698 | Assertion.response_code
699 | false
700 | 8
701 |
702 |
703 |
704 | javascript
705 |
706 | ${config_path}/scripts/extract_extension_release_by_version.js
707 | true
708 |
709 |
710 |
711 |
712 |
713 | true
714 |
715 |
716 |
717 | false
718 | {
719 | "ext_id": "${ext_id}",
720 | "archive": "${archive}",
721 | "source_repo": "${source_repo}",
722 | "version": "${version}"
723 | }
724 | =
725 |
726 |
727 |
728 | ${host}
729 | ${port}
730 | ${scheme}
731 | UTF-8
732 | /api/v1/extension
733 | POST
734 | true
735 | false
736 | true
737 | false
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 | Sec-Fetch-Mode
747 | cors
748 |
749 |
750 | Sec-Fetch-Site
751 | same-origin
752 |
753 |
754 | Accept-Language
755 | en-US,en;q=0.${lnurlpCount}
756 |
757 |
758 | Origin
759 | ${scheme}://${host}:${port}
760 |
761 |
762 | Accept
763 | application/json, text/plain, */*
764 |
765 |
766 | X-Api-Key
767 | ${adminkey}
768 |
769 |
770 | Content-Type
771 | application/json
772 |
773 |
774 | Accept-Encoding
775 | gzip, deflate, br
776 |
777 |
778 | User-Agent
779 | Mozilla/${lnurlpCount}.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0
780 |
781 |
782 | Sec-Fetch-Dest
783 | empty
784 |
785 |
786 |
787 |
788 |
789 |
790 | 200
791 |
792 |
793 | Assertion.response_code
794 | false
795 | 8
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 | false
805 | enable
806 | ${extension}
807 | =
808 | true
809 |
810 |
811 |
812 | ${host}
813 | ${port}
814 | ${scheme}
815 | utf-8
816 | /api/v1/extension/${extension}/enable
817 | PUT
818 | true
819 | false
820 | true
821 | false
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 | Sec-Fetch-Mode
831 | same-origin
832 |
833 |
834 | Sec-Fetch-Site
835 | same-origin
836 |
837 |
838 | Accept-Language
839 | en-US,en;q=0.${paidChargeCount}
840 |
841 |
842 | Upgrade-Insecure-Requests
843 | 1
844 |
845 |
846 | Accept-Encoding
847 | gzip, deflate, br
848 |
849 |
850 | User-Agent
851 | Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0
852 |
853 |
854 | Accept
855 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
856 |
857 |
858 | Sec-Fetch-Dest
859 | empty
860 |
861 |
862 |
863 |
864 |
865 |
866 | 200
867 |
868 |
869 | Assertion.response_code
870 | false
871 | 40
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 | ${host}
880 | ${port}
881 | ${scheme}
882 | UTF-8
883 | /${extension}
884 | GET
885 | true
886 | false
887 | true
888 | false
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 | Sec-Fetch-Mode
898 | same-origin
899 |
900 |
901 | Referer
902 | ${scheme}://${host}:${port}/tpos/
903 |
904 |
905 | Sec-Fetch-Site
906 | same-origin
907 |
908 |
909 | Accept-Language
910 | en-US,en;q=0.${lnurlpCount}
911 |
912 |
913 | Upgrade-Insecure-Requests
914 | 1
915 |
916 |
917 | Accept-Encoding
918 | gzip, deflate, br
919 |
920 |
921 | User-Agent
922 | Mozilla/${lnurlpCount}.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0
923 |
924 |
925 | Accept
926 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
927 |
928 |
929 | Sec-Fetch-Dest
930 | empty
931 |
932 |
933 |
934 |
935 |
936 |
937 | 200
938 |
939 |
940 | Assertion.response_code
941 | false
942 | 40
943 |
944 |
945 |
946 |
947 | Do not remove. Test install extension version: ${test_version}
948 |
949 |
950 | Assertion.response_data
951 | false
952 | 2
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 | ${host}
961 | ${port}
962 | ${scheme}
963 | UTF-8
964 | /${extension}/api/v1/test/00000000
965 | GET
966 | true
967 | false
968 | true
969 | false
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 | Sec-Fetch-Mode
979 | same-origin
980 |
981 |
982 | Referer
983 | ${scheme}://${host}:${port}/tpos/
984 |
985 |
986 | Sec-Fetch-Site
987 | same-origin
988 |
989 |
990 | Accept-Language
991 | en-US,en;q=0.${lnurlpCount}
992 |
993 |
994 | Upgrade-Insecure-Requests
995 | 1
996 |
997 |
998 | Accept-Encoding
999 | gzip, deflate, br
1000 |
1001 |
1002 | User-Agent
1003 | Mozilla/${lnurlpCount}.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0
1004 |
1005 |
1006 | Accept
1007 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
1008 |
1009 |
1010 | Sec-Fetch-Dest
1011 | empty
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 | 200
1019 |
1020 |
1021 | Assertion.response_code
1022 | false
1023 | 40
1024 |
1025 |
1026 |
1027 | id
1028 | "${test_version}"
1029 | true
1030 | false
1031 | false
1032 | false
1033 |
1034 |
1035 |
1036 | wallet
1037 | 00000000
1038 | true
1039 | false
1040 | false
1041 | true
1042 |
1043 |
1044 |
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
--------------------------------------------------------------------------------
/integration/fragments/init-account.jmx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ${host}
13 | ${port}
14 | ${scheme}
15 | UTF-8
16 | /
17 | GET
18 | true
19 | false
20 | true
21 | false
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Accept-Language
31 | en-US,en;q=0.5
32 |
33 |
34 | Upgrade-Insecure-Requests
35 | 1
36 |
37 |
38 | Accept-Encoding
39 | gzip, deflate
40 |
41 |
42 | User-Agent
43 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0
44 |
45 |
46 | Accept
47 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
48 |
49 |
50 |
51 |
52 |
53 |
54 | 200
55 |
56 |
57 | Assertion.response_code
58 | false
59 | 8
60 |
61 |
62 |
63 |
64 | true
65 |
66 |
67 |
68 | false
69 | {"name":"a1"}
70 | =
71 |
72 |
73 |
74 | ${host}
75 | ${port}
76 | ${scheme}
77 | UTF-8
78 | /api/v1/account
79 | POST
80 | true
81 | false
82 | true
83 | false
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Referer
93 | ${scheme}://${host}:${port}/
94 |
95 |
96 | Accept-Language
97 | en-US,en;q=0.${paidChargeCount}
98 |
99 |
100 | Origin
101 | ${scheme}://${host}:${port}
102 |
103 |
104 | Content-Type
105 | application/json
106 |
107 |
108 | Accept-Encoding
109 | gzip, deflate
110 |
111 |
112 | User-Agent
113 | Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
114 |
115 |
116 | Accept
117 | application/json, text/plain, */*
118 |
119 |
120 |
121 |
122 |
123 |
124 | 200
125 |
126 |
127 | Assertion.response_code
128 | false
129 | 8
130 |
131 |
132 |
133 | userId
134 | user
135 |
136 |
137 |
138 |
139 | walletId
140 | id
141 |
142 |
143 |
144 |
145 | inkey
146 | inkey
147 |
148 | no-inkey
149 |
150 |
151 |
152 | adminkey
153 | adminkey
154 |
155 | no-adminkey
156 |
157 |
158 |
159 |
160 | true
161 |
162 |
163 |
164 | false
165 | {"usr":"${userId}"}
166 | =
167 |
168 |
169 |
170 | ${host}
171 | ${port}
172 | ${scheme}
173 | UTF-8
174 | /api/v1/auth/usr
175 | POST
176 | true
177 | false
178 | true
179 | false
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | Referer
189 | ${scheme}://${host}:${port}/
190 |
191 |
192 | Accept-Language
193 | en-US,en;q=0.${paidChargeCount}
194 |
195 |
196 | Origin
197 | ${scheme}://${host}:${port}
198 |
199 |
200 | Content-Type
201 | application/json
202 |
203 |
204 | Accept-Encoding
205 | gzip, deflate
206 |
207 |
208 | User-Agent
209 | Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
210 |
211 |
212 | Accept
213 | application/json, text/plain, */*
214 |
215 |
216 |
217 |
218 |
219 |
220 | 200
221 |
222 |
223 | Assertion.response_code
224 | false
225 | 8
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | false
234 | wal
235 | ${walletId}
236 | =
237 | true
238 |
239 |
240 |
241 | ${host}
242 | ${port}
243 | ${scheme}
244 | UTF-8
245 | /wallet
246 | GET
247 | true
248 | false
249 | true
250 | false
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | Referer
260 | ${scheme}://${host}:${port}/
261 |
262 |
263 | Accept-Language
264 | en-US,en;q=0.5
265 |
266 |
267 | Upgrade-Insecure-Requests
268 | 1
269 |
270 |
271 | Accept-Encoding
272 | gzip, deflate
273 |
274 |
275 | User-Agent
276 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0
277 |
278 |
279 | Accept
280 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
281 |
282 |
283 |
284 |
285 |
286 |
287 | 200
288 |
289 |
290 | Assertion.response_code
291 | false
292 | 8
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 | ${host}
301 | ${port}
302 | ${scheme}
303 | UTF-8
304 | /api/v1/currencies
305 | GET
306 | true
307 | false
308 | true
309 | false
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 | Referer
319 | ${scheme}://${host}:${port}/wallet?wal=${walletId}
320 |
321 |
322 | Accept-Language
323 | en-US,en;q=0.5
324 |
325 |
326 | X-Api-Key
327 | undefined
328 |
329 |
330 | Accept-Encoding
331 | gzip, deflate
332 |
333 |
334 | User-Agent
335 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0
336 |
337 |
338 | Accept
339 | application/json, text/plain, */*
340 |
341 |
342 |
343 |
344 |
345 |
346 | 200
347 |
348 |
349 | Assertion.response_code
350 | false
351 | 8
352 |
353 |
354 |
355 |
356 | ["AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP","BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNH","CNY","COP","CRC","CUC","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD","FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRT","ISK","JEP","JMD","JOD","JPY","KES","KGS","KHR","KMF","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LYD","MAD","MDL","MGA","MKD","MMK","MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SEK","SGD","SHP","SLL","SOS","SRD","SSP","STD","SVC","SZL","THB","TJS","TMT","TND","TOP","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VES","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPD","XPF","XPT","YER","ZAR","ZMW","ZWL"]
357 |
358 |
359 | Assertion.response_data
360 | false
361 | 8
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 | ${host}
370 | ${port}
371 | ${scheme}
372 | UTF-8
373 | /api/v1/wallet
374 | GET
375 | true
376 | false
377 | true
378 | false
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 | Referer
388 | ${scheme}://${host}:${port}/wallet?wal=${walletId}
389 |
390 |
391 | Accept-Language
392 | en-US,en;q=0.5
393 |
394 |
395 | X-Api-Key
396 | ${inkey}
397 |
398 |
399 | Accept-Encoding
400 | gzip, deflate
401 |
402 |
403 | User-Agent
404 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0
405 |
406 |
407 | Accept
408 | application/json, text/plain, */*
409 |
410 |
411 |
412 |
413 |
414 |
415 | 200
416 |
417 |
418 | Assertion.response_code
419 | false
420 | 8
421 |
422 |
423 |
424 | balance
425 | 0
426 | true
427 | false
428 | false
429 | false
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 | ${host}
438 | ${port}
439 | ${scheme}
440 | UTF-8
441 | /api/v1/payments
442 | GET
443 | true
444 | false
445 | true
446 | false
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 | Referer
456 | ${scheme}://${host}:${port}/wallet?wal=${walletId}
457 |
458 |
459 | Accept-Language
460 | en-US,en;q=0.5
461 |
462 |
463 | X-Api-Key
464 | ${inkey}
465 |
466 |
467 | Accept-Encoding
468 | gzip, deflate
469 |
470 |
471 | User-Agent
472 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0
473 |
474 |
475 | Accept
476 | application/json, text/plain, */*
477 |
478 |
479 |
480 |
481 |
482 |
483 | []
484 |
485 |
486 | Assertion.response_data
487 | false
488 | 8
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
--------------------------------------------------------------------------------
/integration/fragments/init-server.jmx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | false
7 | true
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ${host}
24 | ${port}
25 | ${scheme}
26 | UTF-8
27 | /
28 | GET
29 | false
30 | false
31 | true
32 | false
33 |
34 |
35 |
36 | Detected the start of a redirect chain
37 |
38 |
39 |
40 |
41 |
42 | Accept-Language
43 | en-US,en;q=0.5
44 |
45 |
46 | Upgrade-Insecure-Requests
47 | 1
48 |
49 |
50 | Accept-Encoding
51 | gzip, deflate
52 |
53 |
54 | User-Agent
55 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
56 |
57 |
58 | Accept
59 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
60 |
61 |
62 |
63 |
64 |
65 |
66 | 200
67 | 307
68 |
69 |
70 | Assertion.response_code
71 | false
72 | 40
73 |
74 |
75 |
76 | true
77 | is_first_install
78 | location: (.*)
79 | $1$
80 | not-first-install
81 | 0
82 | all
83 |
84 |
85 |
86 |
87 | ${__groovy(vars.get('is_first_install').contains("first_install"),)}
88 | false
89 | true
90 |
91 |
92 |
93 |
94 |
95 |
96 | ${host}
97 | ${port}
98 | ${scheme}
99 | UTF-8
100 | /first_install
101 | GET
102 | true
103 | false
104 | true
105 | false
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Accept-Language
115 | en-US,en;q=0.5
116 |
117 |
118 | Upgrade-Insecure-Requests
119 | 1
120 |
121 |
122 | Accept-Encoding
123 | gzip, deflate
124 |
125 |
126 | User-Agent
127 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
128 |
129 |
130 | Accept
131 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
132 |
133 |
134 |
135 |
136 |
137 |
138 | 200
139 |
140 |
141 | Assertion.response_code
142 | false
143 | 8
144 |
145 |
146 |
147 |
148 | true
149 |
150 |
151 |
152 | false
153 | {"username":"admin","password":"secret1234","password_repeat":"secret1234"}
154 | =
155 |
156 |
157 |
158 | ${host}
159 | ${port}
160 | ${scheme}
161 | UTF-8
162 | /api/v1/auth/first_install
163 | PUT
164 | true
165 | false
166 | true
167 | false
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | Referer
177 | ${scheme}://${host}:${port}/first_install
178 |
179 |
180 | Accept-Language
181 | en-US,en;q=0.5
182 |
183 |
184 | Origin
185 | ${scheme}://${host}:${port}
186 |
187 |
188 | Content-Type
189 | application/json
190 |
191 |
192 | Accept-Encoding
193 | gzip, deflate
194 |
195 |
196 | User-Agent
197 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
198 |
199 |
200 | Accept
201 | application/json, text/plain, */*
202 |
203 |
204 |
205 |
206 |
207 |
208 | 200
209 |
210 |
211 | Assertion.response_code
212 | false
213 | 8
214 |
215 |
216 |
217 |
218 |
219 | ${__groovy(!vars.get('is_first_install').contains("first_install"),)}
220 | false
221 | true
222 |
223 |
224 |
225 | true
226 |
227 |
228 |
229 | false
230 | {"username":"admin","password":"secret1234"}
231 | =
232 |
233 |
234 |
235 | ${host}
236 | ${port}
237 | ${scheme}
238 | UTF-8
239 | /api/v1/auth
240 | POST
241 | true
242 | false
243 | true
244 | false
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | Referer
254 | ${scheme}://${host}:${port}/
255 |
256 |
257 | Accept-Language
258 | en-US,en;q=0.5
259 |
260 |
261 | Origin
262 | ${scheme}://${host}:${port}
263 |
264 |
265 | Content-Type
266 | application/json
267 |
268 |
269 | Accept-Encoding
270 | gzip, deflate
271 |
272 |
273 | User-Agent
274 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0
275 |
276 |
277 | Accept
278 | application/json, text/plain, */*
279 |
280 |
281 |
282 |
283 |
284 |
285 | 200
286 |
287 |
288 | Assertion.response_code
289 | false
290 | 8
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 | ${host}
300 | ${port}
301 | ${scheme}
302 | UTF-8
303 | /api/v1/wallets
304 | GET
305 | true
306 | false
307 | true
308 | false
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | Accept-Language
318 | en-US,en;q=0.5
319 |
320 |
321 | Upgrade-Insecure-Requests
322 | 1
323 |
324 |
325 | Accept-Encoding
326 | gzip, deflate
327 |
328 |
329 | User-Agent
330 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
331 |
332 |
333 | Accept
334 | text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
335 |
336 |
337 |
338 |
339 |
340 |
341 | 200
342 |
343 |
344 | Assertion.response_code
345 | false
346 | 8
347 |
348 |
349 |
350 | adminWalletId
351 | $[0].id
352 |
353 | no-admin-wallet-id
354 |
355 |
356 |
357 | adminWalletKey
358 | $[0].adminkey
359 |
360 | no-admin-wallet-key
361 |
362 |
363 |
364 |
365 | true
366 |
367 |
368 |
369 | false
370 | {
371 | "lnbits_callback_url_rules": []
372 | }
373 | =
374 |
375 |
376 |
377 | ${host}
378 | ${port}
379 | ${scheme}
380 | UTF-8
381 | /admin/api/v1/settings
382 | PUT
383 | true
384 | false
385 | true
386 | false
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 | Referer
396 | ${scheme}://${host}:${port}/wallet?&wal=c1daa33fc4014ef69cd1505f14f322c2
397 |
398 |
399 | Accept-Language
400 | en-US,en;q=0.5
401 |
402 |
403 | Origin
404 | ${scheme}://${host}:${port}
405 |
406 |
407 | Content-Type
408 | application/json
409 |
410 |
411 | Accept-Encoding
412 | gzip, deflate
413 |
414 |
415 | User-Agent
416 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
417 |
418 |
419 | Accept
420 | application/json, text/plain, */*
421 |
422 |
423 |
424 |
425 |
426 |
427 | 200
428 |
429 |
430 | Assertion.response_code
431 | false
432 | 8
433 |
434 |
435 |
436 |
437 | true
438 |
439 |
440 |
441 | false
442 | {"amount":"1000000","id":"${adminWalletId}"}
443 | =
444 |
445 |
446 |
447 | ${host}
448 | ${port}
449 | ${scheme}
450 | UTF-8
451 | /users/api/v1/balance
452 | PUT
453 | true
454 | false
455 | true
456 | false
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 | Referer
466 | ${scheme}://${host}:${port}/wallet?&wal=c1daa33fc4014ef69cd1505f14f322c2
467 |
468 |
469 | Accept-Language
470 | en-US,en;q=0.5
471 |
472 |
473 | Origin
474 | ${scheme}://${host}:${port}
475 |
476 |
477 | Content-Type
478 | application/json
479 |
480 |
481 | Accept-Encoding
482 | gzip, deflate
483 |
484 |
485 | User-Agent
486 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
487 |
488 |
489 | Accept
490 | application/json, text/plain, */*
491 |
492 |
493 |
494 |
495 |
496 |
497 | 200
498 |
499 |
500 | Assertion.response_code
501 | false
502 | 8
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 | ${host}
511 | ${port}
512 | ${scheme}
513 | UTF-8
514 | /api/v1/auth/logout
515 | POST
516 | true
517 | false
518 | true
519 | false
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 | Referer
529 | ${scheme}://${host}:${port}/
530 |
531 |
532 | Accept-Language
533 | en-US,en;q=0.5
534 |
535 |
536 | Origin
537 | ${scheme}://${host}:${port}
538 |
539 |
540 | Content-Type
541 | application/json
542 |
543 |
544 | Accept-Encoding
545 | gzip, deflate
546 |
547 |
548 | User-Agent
549 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0
550 |
551 |
552 | Accept
553 | application/json, text/plain, */*
554 |
555 |
556 |
557 |
558 |
559 |
560 | 200
561 |
562 |
563 | Assertion.response_code
564 | false
565 | 8
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
--------------------------------------------------------------------------------
/integration/scripts/check_admin_extension_status_code.js:
--------------------------------------------------------------------------------
1 | var responseCode = +prev.getResponseCode()
2 | var isAdminExtension = vars.get("isAdminExtension")
3 |
4 |
5 | if (isAdminExtension === 'true') {
6 | if (responseCode !== 200 && responseCode !== 403) {
7 | AssertionResult.setFailureMessage(
8 | "Expected response code 200 for 401 for admin extension but got: " +
9 | responseCode
10 | );
11 | AssertionResult.setFailure(true);
12 | }
13 | } else if (responseCode !== 200) {
14 | AssertionResult.setFailureMessage(
15 | "Expected response code 200 but got: " + responseCode
16 | );
17 | AssertionResult.setFailure(true);
18 | }
19 |
--------------------------------------------------------------------------------
/integration/scripts/check_price_in_sats_vs_millisats.js:
--------------------------------------------------------------------------------
1 | var priceInSats = +vars.get("priceInSats");
2 | var priceInMillisats = +vars.get("priceInMillisats");
3 |
4 | if (priceInSats * 1000 !== priceInMillisats) {
5 | var message =
6 | "Expected price: " + priceInSats + ", but got: " + priceInMillisats;
7 | AssertionResult.setFailureMessage(message);
8 | AssertionResult.setFailure(true);
9 | }
10 |
--------------------------------------------------------------------------------
/integration/scripts/extract_extension_data.js:
--------------------------------------------------------------------------------
1 | var extensions = vars.get("extensions")
2 | var adminExtensions = vars.get("adminExtensions")
3 | var extensionIndex = vars.get("extensionIndex")
4 | var extesion = extensions.split(",")[extensionIndex - 1]
5 | var isAdminExtension = adminExtensions.split(",").indexOf(extesion) !== -1
6 |
7 | vars.put("extension", extesion)
8 | vars.put("isAdminExtension", isAdminExtension)
9 |
10 | var responseCode = prev.getResponseCode()
11 | vars.put("responseCode", responseCode)
--------------------------------------------------------------------------------
/integration/scripts/extract_extension_release_by_version.js:
--------------------------------------------------------------------------------
1 | var releases = JSON.parse(prev.getResponseDataAsString());
2 |
3 | var ext_id = vars.get("extension");
4 | var ext_version = vars.get("ext_version");
5 |
6 | vars.put("ext_id", ext_id);
7 |
8 | for (var i = 0; i < releases.length; i++) {
9 | var release = releases[i];
10 | if (release.version === ext_version) {
11 | vars.put("archive", release.archive);
12 | vars.put("source_repo", release.source_repo);
13 | vars.put("version", release.version);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/integration/scripts/extract_extensions_ids.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString());
2 | var excludedExtensions = (vars.get("excludedExtensions") || "").split(",");
3 |
4 | var extensions = {};
5 |
6 | for (var i = 0; i < resp.extensions.length; i++) {
7 | var ext = resp.extensions[i];
8 | if (excludedExtensions.indexOf(ext.id) === -1) {
9 | extensions[ext.id] = true;
10 | }
11 | }
12 |
13 |
14 |
15 | extensionList = Object.keys(extensions);
16 |
17 | vars.put("extensions", extensionList);
18 | vars.put("extensionsLength", extensionList.length);
19 |
--------------------------------------------------------------------------------
/integration/scripts/extract_latest_extension_release.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 | var latestRelease = resp[resp.length - 1]
3 |
4 | var ext_id = vars.get("extension")
5 |
6 | vars.put("ext_id", ext_id)
7 | vars.put("archive", latestRelease.archive)
8 | vars.put("source_repo", latestRelease.source_repo)
9 | vars.put("version", latestRelease.version)
10 |
--------------------------------------------------------------------------------
/integration/scripts/lndhub_check_balance.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 |
4 | var count = +vars.get("defaultPaymentCount")
5 | var paymentDelta = 20
6 |
7 | if (resp.BTC.AvailableBalance !== paymentDelta * count) {
8 | AssertionResult.setFailureMessage("Expected balance "+paymentDelta * count+" but got: " + resp.BTC.AvailableBalance);
9 | AssertionResult.setFailure(true)
10 | }
--------------------------------------------------------------------------------
/integration/scripts/lndhub_check_balance_after_payments.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 |
4 | var count = +vars.get("defaultPaymentCount")
5 | var paymentDelta = 20
6 | var mobilePaymentDelta = 20
7 |
8 | if (resp.BTC.AvailableBalance !== paymentDelta * count + mobilePaymentDelta) {
9 | AssertionResult.setFailureMessage("Expected balance "+paymentDelta * count+" but got: " + resp.BTC.AvailableBalance);
10 | AssertionResult.setFailure(true)
11 | }
--------------------------------------------------------------------------------
/integration/scripts/lndhub_check_payments.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 |
4 | var count = +vars.get("defaultPaymentCount")
5 |
6 | if (resp.length !== count) {
7 | AssertionResult.setFailureMessage("Expected "+ count+" transactions but got: " + resp.length);
8 | AssertionResult.setFailure(true)
9 | } else {
10 | for (var i=0;i 100) {
7 | var message =
8 | "Referer balance and referer bonus exceeds tolerated coversion error.";
9 | message += " Balance: " + balanceAlan + " bonus: " + refererBonus;
10 | AssertionResult.setFailureMessage(message);
11 |
12 | AssertionResult.setFailure(true);
13 | }
14 |
--------------------------------------------------------------------------------
/integration/scripts/satspay_check_charge_details.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 | var tipCounter = +vars.get("tipCounter")
4 | var name = "Hal"
5 | var description = "Let's go ..." + tipCounter + "!"
6 |
7 | if (resp.description !== description) {
8 | AssertionResult.setFailureMessage("Expected name to be 'Hal', but was: " + resp.name);
9 | AssertionResult.setFailure(true)
10 | }
11 |
12 | if (resp.description !== description) {
13 | AssertionResult.setFailureMessage("Expected description to be 'Let's go!', but was: " + resp.description);
14 | AssertionResult.setFailure(true)
15 | }
16 |
--------------------------------------------------------------------------------
/integration/scripts/satspay_check_charge_webhook_call.js:
--------------------------------------------------------------------------------
1 | var payment = JSON.parse(prev.getResponseDataAsString())
2 |
3 | if (!payment.paid || !payment.lnbitswallet || !payment.webhook){
4 | AssertionResult.setFailureMessage("Charge not correctly updated after paiment: "+ payment.id);
5 | AssertionResult.setFailure(true)
6 | }
7 |
8 | var extra = JSON.parse(payment.extra)
9 | if (!extra.webhook_response) {
10 | AssertionResult.setFailureMessage("Webhook response missing for payment: "+ payment.id);
11 | AssertionResult.setFailure(true)
12 | }
13 |
14 | var separatorIndex = extra.webhook_response.indexOf("\r\n\r\n")
15 | if (separatorIndex == -1) {
16 | AssertionResult.setFailureMessage("Webhook response has wrong format"+ extra.webhook_response);
17 | AssertionResult.setFailure(true)
18 | }
19 | var headers = extra.webhook_response.substring(0, separatorIndex)
20 | var bodyStr = extra.webhook_response.substring(separatorIndex)
21 |
22 | var body = JSON.parse(bodyStr)
23 |
24 |
25 | if (vars.get("lightningChargeId") !== body.id) {
26 | AssertionResult.setFailureMessage("Wrong webhook charge id. Expected: "+ vars.get("lightningChargeId") + ", but got "+ body.id);
27 | AssertionResult.setFailure(true)
28 | }
29 |
30 | if (vars.get("paymentRequest") !== body.payment_request) {
31 | AssertionResult.setFailureMessage("Wrong webhook charge payment request. Expected: "+ vars.get("paymentRequest") + ", but got "+ body.payment_request);
32 | AssertionResult.setFailure(true)
33 | }
34 |
35 |
36 |
--------------------------------------------------------------------------------
/integration/scripts/satspay_check_charges_count.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 | var paidChargeCount = +vars.get('paidChargeCount')
4 |
5 | var totalChargeCount = paidChargeCount + 2; // one onchain and one to expire
6 | if (resp.length !== totalChargeCount) {
7 | AssertionResult.setFailureMessage("Expected charges count to be "+totalChargeCount+", got " + resp.length);
8 | AssertionResult.setFailure(true)
9 | }
10 |
--------------------------------------------------------------------------------
/integration/scripts/satspay_check_payments_status.js:
--------------------------------------------------------------------------------
1 | var resp = JSON.parse(prev.getResponseDataAsString())
2 |
3 | var paidChargeCount = +vars.get('paidChargeCount')
4 | // one payment is the expired one
5 | var totalPaymentsCount = paidChargeCount + 1
6 |
7 | if (resp.length != totalPaymentsCount) {
8 | AssertionResult.setFailureMessage("Expected total "+totalPaymentsCount+" paymet, but got: "+ resp.length);
9 | AssertionResult.setFailure(true)
10 | }
11 |
12 | var pendingCount = 0
13 |
14 | for (var i=0; i",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/lnbits/lnbits-extensions/issues"
31 | },
32 | "homepage": "https://extensions.lnbits.com/",
33 | "devDependencies": {
34 | "prettier": "^3.3.2",
35 | "sass": "^1.77.6",
36 | "vite": "^5.3.1",
37 | "vite-plugin-solid": "^2.10.2"
38 | },
39 | "dependencies": {
40 | "@solidjs/router": "^0.13.6",
41 | "showdown": "^2.1.0",
42 | "solid-icons": "^1.1.0",
43 | "solid-js": "^1.8.17"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lnbits/lnbits-extensions/d788af945bc03655304d3cc2994e735b41ef55e4/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "lnbits-extensions"
3 | version = "1.0.0"
4 | description = ""
5 | authors = ["Alan Bits "]
6 | readme = "README.md"
7 |
8 | [tool.poetry.dependencies]
9 | python = "^3.9"
10 |
11 |
12 | [build-system]
13 | requires = ["poetry-core"]
14 | build-backend = "poetry.core.masonry.api"
15 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { createSignal, For, onMount } from "solid-js";
2 | import { render } from "solid-js/web";
3 | import "./index.scss";
4 | import data from "../extensions.json";
5 | import metadata from "../metadata.json";
6 | import { Router, Route, useParams, A, useNavigate } from "@solidjs/router";
7 | import { FaSolidUsers, FaBrandsGithub, FaSolidDownload } from "solid-icons/fa";
8 | import { BiSolidBolt } from "solid-icons/bi";
9 | import { Converter } from "showdown";
10 | import {
11 | RiArrowsArrowLeftSLine,
12 | RiArrowsArrowRightSLine,
13 | } from "solid-icons/ri";
14 |
15 | const extensions = [
16 | ...new Map(data.extensions.map((ext) => [ext.id, ext])).values(),
17 | ];
18 | extensions.forEach((ext) => {
19 | const meta = metadata.find((m) => m.id === ext.id);
20 | if (meta) {
21 | Object.assign(ext, meta);
22 | }
23 | if (!ext.contributors) ext.contributors = [];
24 | });
25 |
26 | const Gallery = ({ images }) => {
27 | const [activeIndex, setActiveIndex] = createSignal(0);
28 | const [fullScreen, setFullScreen] = createSignal(false);
29 | const next = () => setActiveIndex((i) => (i + 1) % images.length);
30 | const prev = () =>
31 | setActiveIndex((i) => (i - 1 + images.length) % images.length);
32 |
33 | let touchstartX, touchendX;
34 | const touchStart = (e) => (touchstartX = e.changedTouches[0].screenX);
35 | const touchEnd = (e) => {
36 | touchendX = e.changedTouches[0].screenX;
37 | if (touchendX < touchstartX) prev();
38 | if (touchendX > touchstartX) next();
39 | // if (touchendX === touchstartX) clickAction();
40 | };
41 | let mouseStartX, mouseEndX;
42 | const mouseStart = (e) => (mouseStartX = e.screenX);
43 | const mouseEnd = (e) => {
44 | mouseEndX = e.screenX;
45 | if (mouseEndX < mouseStartX) prev();
46 | if (mouseEndX > mouseStartX) next();
47 | // if (mouseEndX === mouseStartX) clickAction();
48 | };
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 | {(img, i) => (
58 |
64 |
}>
70 |
71 |
76 |
77 |
78 |
79 | )}
80 |
81 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | const Details = () => {
96 | let descriptionRef, termsRef;
97 | const params = useParams();
98 | const [activeTab, setActiveTab] = createSignal("description");
99 | const extensions = [
100 | ...new Map(data.extensions.map((ext) => [ext.id, ext])).values(),
101 | ];
102 | const ext_versions = data.extensions
103 | .filter((ext) => ext.id === params.name)
104 | .reverse();
105 | const ext = extensions.find((ext) => ext.id === params.name);
106 | const fetch_and_render_markup = (url, ref) => {
107 | fetch(url)
108 | .then((res) => res.text())
109 | .then((data) => {
110 | const converter = new Converter();
111 | const html = converter.makeHtml(data);
112 | ref.innerHTML = html;
113 | });
114 | };
115 |
116 | onMount(() => {
117 | fetch_and_render_markup(ext.description_md, descriptionRef);
118 | fetch_and_render_markup(ext.terms_and_conditions_md, termsRef);
119 | });
120 |
121 | return (
122 |
123 |
165 |
166 | setActiveTab("description")}>
169 | Description
170 |
171 | setActiveTab("gallery")}>
174 | Gallery
175 |
176 | setActiveTab("terms")}>
179 | Terms and Conditions
180 |
181 | setActiveTab("versions")}>
184 | Versions
185 |
186 |
187 |
192 |
193 |
194 |
195 |
198 |
199 | {ext_versions.map((ext) => (
200 |
201 |
213 |
214 | Minimum LNbits version:{" "}
215 | {ext.min_lnbits_version || "0.0.0"}
216 |
217 |
218 | Maximum LNbits version:{" "}
219 | {ext.max_lnbits_version}
220 |
221 |
222 | Hash: {ext.hash}
223 |
224 |
225 | ))}
226 |
227 |
228 | );
229 | };
230 |
231 | const Home = () => {
232 | const navigate = useNavigate();
233 | return (
234 |
235 |
236 | {(ext) => (
237 | navigate(`/${ext.id}`)}>
240 |
245 |
274 |
275 | )}
276 |
277 |
278 | );
279 | };
280 |
281 | const App = (props) => (
282 | <>
283 |
289 | {props.children}
290 |
302 | >
303 | );
304 |
305 | render(
306 | () => (
307 |
308 |
309 |
310 |
311 | ),
312 | document.getElementById("root"),
313 | );
314 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | $pink: #ff1ee6;
2 | $bg: #1f2234;
3 |
4 | html {
5 | background: $bg;
6 | background: radial-gradient(
7 | 89.59% 89.59% at 50% 0%,
8 | rgb(62, 5, 55) 0%,
9 | rgba(37, 124, 255, 0.01) 47.4%,
10 | rgba(13, 16, 23, 0) 100%
11 | ),
12 | #0d1017;
13 | background-repeat: no-repeat;
14 | }
15 |
16 | html,
17 | body {
18 | color: white;
19 | font-family: sans-serif;
20 | }
21 | body {
22 | max-width: 1200px;
23 | margin: 0 auto;
24 | padding: 64px 21px;
25 | }
26 |
27 | header {
28 | display: flex;
29 | justify-content: space-between;
30 | }
31 |
32 | a {
33 | color: $pink;
34 | text-decoration: none;
35 | &:hover {
36 | color: white;
37 | text-decoration: underline;
38 | }
39 | }
40 |
41 | .logo {
42 | max-width: 210px;
43 | }
44 |
45 | #extension-list {
46 | display: flex;
47 | flex-wrap: wrap;
48 | justify-content: center;
49 | max-width: 1200px;
50 | margin: 42px auto;
51 | }
52 |
53 | .min-version,
54 | .version {
55 | background: $pink;
56 | border-radius: 21px;
57 | padding: 2px 8px;
58 | display: inline-block;
59 | font-size: 14px;
60 | margin-left: 8px;
61 | }
62 | .min-version {
63 | background: $bg;
64 | }
65 |
66 |
67 | .extension {
68 | width: 46%;
69 | padding: 1%;
70 | margin: 1%;
71 | background: rgba(0, 0, 0, 0.5);
72 | border-radius: 5px;
73 | display: flex;
74 | cursor: pointer;
75 | &:hover {
76 | background: rgba(0, 0, 0, 1);
77 | }
78 | h2 {
79 | margin-top: 3px;
80 | a {
81 | color: white;
82 | }
83 | }
84 | img {
85 | width: 130px;
86 | margin-right: 22px;
87 | display: block;
88 | }
89 | }
90 |
91 | @media (max-width: 768px) {
92 | .extension {
93 | width: 100%;
94 | }
95 | }
96 |
97 |
98 |
99 | .btn {
100 | color: white;
101 | display: inline-block;
102 | text-decoration: none;
103 | border-radius: 5px;
104 | padding: 3px 8px;
105 | background: $bg;
106 | &:hover {
107 | background: $pink;
108 | text-decoration: none;
109 | }
110 | margin-left: 6px;
111 | margin-bottom: 8px;
112 | }
113 |
114 | footer {
115 | border-top: 1px solid rgba(255, 255, 255, 0.3);
116 | margin: 86px 0 0 0;
117 | padding: 8px 0 0 0;
118 | }
119 |
120 | #gallery {
121 | display: flex;
122 | gap: 20px;
123 | margin: 32px 0 64px;
124 | img {
125 | width: 100%;
126 | border-radius: 5px;
127 | }
128 | .gallery-images {
129 | width: 100%;
130 | position: relative;
131 | }
132 | .overlay {
133 | cursor: pointer;
134 | position: absolute;
135 | top: 0;
136 | left: 0;
137 | right: 0;
138 | bottom: 0;
139 | }
140 | }
141 | .gallery-arrow {
142 | cursor: pointer;
143 | display: flex;
144 | align-items: center;
145 | font-size: 32px;
146 | }
147 |
148 | .gallery-item {
149 | display: none;
150 | }
151 | .gallery-item-active {
152 | display: block;
153 | }
154 |
155 | .ext-header {
156 | display: flex;
157 | gap: 20px;
158 | background: rgba(0, 0, 0, 0.4);
159 | padding: 21px;
160 | border-radius: 10px;
161 | margin: 32px 0 64px;
162 | }
163 | @media (max-width: 768px) {
164 | header,
165 | .ext-header {
166 | flex-direction: column;
167 | }
168 | }
169 |
170 | #contributors small {
171 | color: white;
172 | }
173 |
174 | .ext-image img {
175 | width: 150px;
176 | }
177 |
178 | .ext-header h1 {
179 | margin: 0;
180 | }
181 |
182 | #tabs {
183 | display: flex;
184 | border-bottom: 1px solid rgba(255, 255, 255, 0.4);
185 | }
186 |
187 | #tabs span {
188 | position: relative;
189 | top: 1px;
190 | padding: 8px 16px;
191 | background: rgba(0, 0, 0, 0.3);
192 | border-radius: 5px;
193 | cursor: pointer;
194 | border: 1px solid #ffffff3d;
195 | padding: 8px 16px;
196 | background: rgba(0, 0, 0, 0.4);
197 | border-radius: 5px 5px 0 0;
198 | cursor: pointer;
199 | border: 1px solid rgba(255, 255, 255, 0.3);
200 | margin-right: 3px;
201 | &.active {
202 | border-bottom: 1px solid #0d1017;
203 | }
204 | }
205 | @media (max-width: 768px) {
206 | #tabs {
207 | flex-wrap: wrap;
208 | & span {
209 | flex-grow: 1;
210 | margin-right: 0;
211 | width: 100%;
212 | }
213 | }
214 | }
215 |
216 | .tab {
217 | display: none;
218 | }
219 | .active-tab {
220 | display: block;
221 | background: #0000005e;
222 | padding: 21px;
223 | border: 1px solid #ffffff38;
224 | border-top: none;
225 | }
226 |
227 | .embed-youtube {
228 | position: relative;
229 | padding-bottom: 56.25%; /* - 16:9 aspect ratio (most common) */
230 | padding-top: 30px;
231 | height: 0;
232 | overflow: hidden;
233 | }
234 |
235 | .embed-youtube iframe,
236 | .embed-youtube object,
237 | .embed-youtube embed {
238 | border: 0;
239 | position: absolute;
240 | top: 0;
241 | left: 0;
242 | width: 100%;
243 | height: 100%;
244 | z-index: 9999;
245 | }
246 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | files=$( ls ./integration/*.jmx)
3 | echo "Files: $files"
4 | for file in $files; do
5 | echo "Cleaning logs"
6 | rm -r reports logs
7 | echo "Running test with $file"
8 | date
9 | filename=$(basename "$file" ".jmx")
10 | echo "Using Extension Manifest: $EXTENSIONS_MANIFEST_PATH"
11 | ./apache-jmeter-5.6.3/bin/jmeter -DextensionsManifestPath="$EXTENSIONS_MANIFEST_PATH" -n -t $file -l logs/$filename.log -e -o reports ;
12 | error_count=$(cat jmeter.log | grep "summary =" | grep "Err: 1" | wc -l)
13 | echo "Error count: '$error_count'"
14 | echo "##########"
15 | echo "$error_count" == "0"
16 | echo "###########$error_count ###########"
17 | if [ "$error_count" = "0" ]; then
18 | echo "Test $filename OK."
19 | else
20 | echo "Test $filename failed. Error count: '$error_count'."
21 | cat logs/$filename.log
22 | exit 1
23 | fi
24 | done
25 | echo "Done"
26 | date
27 |
--------------------------------------------------------------------------------
/toc.md:
--------------------------------------------------------------------------------
1 | Terms and Conditions for LNbits Extension
2 |
3 | ## 1. Acceptance of Terms
4 |
5 | By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
6 |
7 | ## 2. License
8 |
9 | The Extension is free and open-source software, released under MIT. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
10 |
11 | ## 3. No Warranty
12 |
13 | The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
14 |
15 | ## 4. Limitation of Liability
16 |
17 | In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
18 |
19 | ## 5. Modification of Terms
20 |
21 | The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
22 |
23 | ## 6. General Provisions
24 |
25 | If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
26 |
27 | ## 7. Contact Information
28 |
29 | If you have any questions about these Terms, please contact the LNbits developers.
30 |
--------------------------------------------------------------------------------
/update_version.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | from hashlib import sha256
4 | from io import BytesIO
5 | from os.path import basename
6 | from zipfile import ZipFile
7 | import urllib.request
8 |
9 | if len(sys.argv) < 3:
10 | print("Usage: python update_version.py ")
11 | sys.exit(1)
12 |
13 | repo_name = sys.argv[1]
14 | version = sys.argv[2]
15 |
16 | repo = f"https://github.com/lnbits/{repo_name}"
17 | archive = f"{repo}/archive/refs/tags/{version}.zip"
18 | with urllib.request.urlopen(archive) as f:
19 | data = f.read()
20 |
21 | archive_zip = ZipFile(BytesIO(data))
22 | archive_hash = sha256(data).hexdigest()
23 |
24 | prefix = f"{basename(repo_name)}-{version.replace('v', '')}"
25 | config = json.load(archive_zip.open(f"{prefix}/config.json"))
26 |
27 | ext_name = config.get("id") or basename(repo_name)
28 |
29 | with open("extensions.json", "r+") as ext_json:
30 | extensions = json.load(ext_json)
31 |
32 | # find lastest version of extension
33 | latest_extension = None
34 | latest_index = None
35 | for i, extension in enumerate(extensions["extensions"]):
36 | if extension["id"] == ext_name:
37 | latest_extension = extension
38 | latest_index = i
39 |
40 | # if not found create a new extension
41 | if not latest_extension or not latest_index:
42 | raw_url = f"https://raw.githubusercontent.com/lnbits/{repo_name}/main"
43 | icon = config.get("tile", None)
44 | if icon:
45 | icon = raw_url + icon.replace(f"/{repo_name}", "", 1)
46 | else:
47 | icon = raw_url + "/static/image/icon.png"
48 | new_ext = {
49 | "id": ext_name,
50 | "repo": repo,
51 | "version": version.replace("v", ""),
52 | "archive": archive,
53 | "hash": archive_hash,
54 | "min_lnbits_version": config.get("min_lnbits_version"),
55 | "name": config.get("name"),
56 | "short_description": config.get("short_description"),
57 | "icon": icon,
58 | "details_link": f"{raw_url}/config.json",
59 | }
60 | extensions["extensions"].append(new_ext)
61 | # check if min_lnbits_version is different
62 | elif latest_extension["min_lnbits_version"] != config.get("min_lnbits_version"):
63 | new_ext = latest_extension.copy()
64 | new_ext["version"] = version.replace("v", "")
65 | new_ext["archive"] = archive
66 | new_ext["hash"] = archive_hash
67 | new_ext["min_lnbits_version"] = config.get("min_lnbits_version")
68 | new_ext["name"] = config.get("name")
69 | new_ext["short_description"] = config.get("short_description")
70 | # remove max version from latest release
71 | new_ext.pop("max_lnbits_version", None)
72 | # include the new extension in the list at the proper index
73 | _extensions = extensions["extensions"][:]
74 | _extensions.insert(latest_index+1, new_ext)
75 | extensions["extensions"] = _extensions
76 | # update the max version of the previous release
77 | extensions["extensions"][latest_index]["max_lnbits_version"] = version.replace("v", "")
78 | else:
79 | # else its not a breaking change, just update the version and archive
80 | extensions["extensions"][latest_index]["version"] = version.replace("v", "")
81 | extensions["extensions"][latest_index]["archive"] = archive
82 | extensions["extensions"][latest_index]["hash"] = archive_hash
83 | # update the config.json into the next release
84 | extensions["extensions"][latest_index]["name"] = config.get("name")
85 | extensions["extensions"][latest_index]["short_description"] = config.get("short_description")
86 |
87 | # update the extension.json file
88 | with open("extensions.json", "w") as ext_json:
89 | ext_json.write(json.dumps(extensions, indent=4))
90 | ext_json.close()
91 |
--------------------------------------------------------------------------------
/util.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # clones all extensions from extensions.json into extensions folder
4 | jq_ext_cmd='[.extensions[] | .repo + " " + .id] | unique | .[]'
5 | clone() {
6 | mkdir -p extensions
7 | cd extensions
8 | jq -r "$jq_ext_cmd" ../extensions.json | while read -r line
9 | do
10 | repo=$(echo $line | cut -f1 -d" " | sed 's/https:\/\//git@/' | sed 's/\//:/')
11 | id=$(echo $line | cut -f2 -d" ")
12 | git clone $repo $id
13 | if command -v poetry &> /dev/null
14 | then
15 | poetry env use python3.10
16 | poetry lock
17 | poetry install
18 | npm install
19 | fi
20 | done
21 | cd ..
22 | }
23 |
24 | # pulls all extensions from extensions.json
25 | pull() {
26 | cd extensions
27 | jq -r "$jq_ext_cmd" ../extensions.json | while read -r line
28 | do
29 | # repo=$(echo $line | cut -f1 -d" " | sed 's/https:\/\//git@/' | sed 's/\//:/')
30 | id=$(echo $line | cut -f2 -d" ")
31 | cd $id
32 | git pull
33 | if command -v poetry && command -v npm &> /dev/null
34 | then
35 | poetry env use python3.10
36 | poetry lock
37 | poetry install
38 | npm install
39 | fi
40 | cd ..
41 | done
42 | cd ..
43 | }
44 |
45 | # lint
46 | lint() {
47 | cd extensions
48 | EXTS_DIR=$(pwd)
49 | jq -r "$jq_ext_cmd" ../extensions.json | while read -r line
50 | do
51 | id=$(echo $line | cut -f2 -d" ")
52 | cd $EXTS_DIR/$id
53 | make
54 | done
55 | cd ../..
56 | }
57 |
58 | # gives you LNbits env variables for all extensions
59 | env() {
60 | env=$(jq -rj '[.extensions[].id] | unique | .[]+","' ./extensions.json | sed 's/.$//')
61 | echo "LNBITS_EXTENSIONS_DEFAULT_INSTALL=\"$env\""
62 | }
63 |
64 | # param: extension id (example)
65 | # param: version (v0.0.0)
66 | update_extension() {
67 | python update_version.py $1 $2
68 | }
69 |
70 | # execute functions
71 | $@
72 |
73 | # example
74 | # sh util.sh clone
75 | # sh util.sh pull
76 | # sh util.sh update_extension example "v0.0.0"
77 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import solidPlugin from "vite-plugin-solid";
3 |
4 | export default defineConfig({
5 | plugins: [solidPlugin()],
6 | server: {
7 | cors: { origin: "*" },
8 | },
9 | build: {
10 | commonjsOptions: {
11 | transformMixedEsModules: true,
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------