-
Notifications
You must be signed in to change notification settings - Fork 0
Development
-
clink/— extension package (see Architecture) -
tests/— pytest suite -
.github/workflows/release.yml— tag-triggered release pipeline -
tools/update_extensions.py— registry updater for the LNbits extension market
The repo basename lnbits-clink is not a valid Python identifier, and LNbits loads the extension from its extensions/clink folder. tests/run_tests.sh reproduces that by symlinking the repo into a temp dir named clink:
# with lnbits installed in the active environment
tests/run_tests.sh
# or pointing at a specific interpreter
PYTEST_PYTHON=/path/to/venv/bin/python tests/run_tests.sh
# run one file
tests/run_tests.sh -q clink/tests/test_subscriptions.pyThe suite covers the protocol layer (NIP-44 vectors, bech32 codecs, event sign/verify), the node service, Pay Offers, and the subscriptions engine. It also contains tests/vectors/nip44.vectors.json, a curated NIP-44 test vector set.
python -m ruff check .
python -m black --check .- Target: Python
>= 3.10, line length 88. -
nostr/andtests/vectors/are excluded from mypy; ruff/black run over the whole tree.
On LNbits 1.5.x windowMixin is empty ({}) and the g object (user,
wallets, settings) is injected into the core Vue app through a global
mixin (static/js/init-app.js). Extension pages create their own app
(window.app = Vue.createApp({...})), so they never receive that mixin and
this.g is undefined — reading this.g.user throws and the page renders
blank. Page components must therefore read window.g directly: a plain global
set in globals.js and populated with the logged-in user by
templates/base.html.
data() {
return {
wallets: window.g.user.wallets,
wallet: window.g.user.wallets[0]
}
}checkout.js is a public page and does not touch g.
Extension pages are in-DOM templates: templates/base.html renders the
{% block page %} markup straight into <div id="vue">, the browser parses it
as HTML, and Vue only then compiles #vue's innerHTML. The HTML parser does
not support self-closing syntax for non-void elements, so a tag like
<q-tab name="debits" ... /> is treated as an unclosed open tag that
swallows everything that follows it until a matching close tag appears. That is
exactly what happened before v0.1.2: the three q-tab elements rendered
nested inside each other (tabs stacked diagonally and overlapping), dialog
inputs overlapped, and the pay page collapsed to its header.
Rule: every component tag must use explicit closing tags — <q-tab ...></q-tab>,
<q-btn ...></q-btn>, <q-space></q-space>. Only real HTML void elements
(<img>, <br>, <input>) may be self-closing. After editing a template,
verify with:
grep -rnE '<(q-[a-z0-9-]+|qrcode-vue)\s*/>' templates/clink/ # must be emptyLNbits pages load the classic Material Icons font, where an icon name is a
ligature (e.g. bolt, event, pending_actions). If the requested name is
not a single glyph in the loaded font, the text renderer falls back to matching
substring ligatures, so event_repeat rendered as event + repeat (+ the
underscore) — i.e. three icons where one was expected (fixed in v0.1.3 by
using event). Newer Material Symbols names may or may not be present depending
on the served font version, so after choosing an icon, verify it renders as one
glyph on the live page (check the rendered icon element width — a split
ligature is ~2-3× the normal 24px).
v0.1.4 makes CLINK a selectable LNbits backend wallet; v0.1.5 makes it a
fully paying one with a Lightning.pub account.
How it hooks in. LNbits resolves the backend with
getattr(lnbits.wallets, settings.lnbits_backend_wallet_class) in
set_funding_source(). Extensions are imported before that call at startup
(lnbits/app.py), so clink/__init__.py injects the class directly:
import lnbits.wallets as _lnbits_wallets
if not hasattr(_lnbits_wallets, "ClinkWallet"):
_lnbits_wallets.ClinkWallet = ClinkWalletBecause the class lands on the same module object set_funding_source()
resolves against, lnbits_backend_wallet_class=ClinkWallet works without any
core changes. The class lives in clink/wallet.py and implements the
lnbits.wallets.base.Wallet ABC (same API in LNbits 1.5.6 and 1.6.x).
Configuration. Two env vars configure the node:
LNBITS_CLINK_FUNDING_NOFFER=noffer1...
LNBITS_CLINK_FUNDING_ACCOUNT=nprofile1...[:token] # optional; enables send + balance
The superuser must also add ClinkWallet to the allowed funding sources
(LNBITS_ALLOWED_FUNDING_SOURCES env var or the admin settings) so it appears
in the /admin dropdown.
Admin fields (core patch). The /admin → Funding Sources card only renders
inputs for bundled classes plus whatever the settings model exposes. To show
CLINK with Noffer + Account inputs, two core files are patched:
lnbits/settings.py (a ClinkFundingSource(LNbitsSettings) mixin with
lnbits_clink_funding_noffer / lnbits_clink_funding_account added to the
FundingSourcesSettings base list — making them valid on Settings,
UpdateSettings and AdminSettings) and
lnbits/static/bundle-components.min.js (a ["ClinkWallet", ...] entry in
rawFundingSources). This is a core change — it must be re-applied on any
image that does not carry it. On the demo Fly machine it is re-applied at every
boot by a sitecustomize on the volume (see
Installation#admin-fields-core-patch--boot-patch): /app/data/lnbits_patch/
holds sitecustomize.py + boot_patch.py, enabled via the
PYTHONPATH=/app/data/lnbits_patch secret, so it survives restarts and
redeploys. boot_patch.py is idempotent and skips (logging to
patch.log) if a future core version no longer has the anchors.
Scope (v0.1.5). create_invoice requests a BOLT11 invoice from the node's
offer service over kind 21001. With LNBITS_CLINK_FUNDING_ACCOUNT set,
pay_invoice pays via the node's Nostr user API (kind 21000, NIP-44 v1) and
status() reports the real node balance (GetUserInfo); invoice status is
tracked with GetPaymentState. Without the account, pay_invoice returns a
clear "receive-only" error and status() reports 0 msat. When the noffer is
unset, status() reports an error and the instance falls back to VoidWallet
after the startup retries (standard LNbits behavior).
Tests. tests/test_wallet.py covers config parsing, the account-fallback
path, status() balance reporting, and the pre-network amount checks;
tests/test_account.py exercises the Lightning.pub user API client
(envelope, NIP-44 encryption, response verification) with a monkeypatched
responder. No live relay calls.
.github/workflows/release.yml is triggered by a version tag (e.g. v0.1.5):
- Builds a release artifact (the extension zip) and drafts a GitHub release.
- Runs
tools/update_extensions.py, which updates the registry and opens a pull request againstlnbits/lnbits-extensionsso the extension shows up in the LNbits extension manager.
To cut a release:
git tag v0.1.5
git push origin v0.1.5Each tag creates a fresh branch + PR (
update-lnbits-clink-v<tag>). Close the previous PR when a newer version supersedes it.
The auto-PR needs an
EXT_GITHUBsecret (a token with access to thelnbits/lnbits-extensionsfork) configured in the repository settings.
- Commit messages follow
feat: …/fix: …style (seegit log). - All commits are authored under a dedicated extension identity (
WoompaLoompa/06bc1a977d@atomicmail.io). - No new Python dependencies: protocol primitives are implemented in
clink/nostron top of LNbits' bundled packages.
lnbits-clink — CLINK (Nostr-native Lightning) for LNbits.