Skip to content

Development

WoompaLoompa edited this page Aug 10, 2026 · 8 revisions

Development

Repository layout

  • 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

Running tests

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.py

The 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.

Linting & formatting

python -m ruff check .
python -m black --check .
  • Target: Python >= 3.10, line length 88.
  • nostr/ and tests/vectors/ are excluded from mypy; ruff/black run over the whole tree.

Frontend compatibility (LNbits 1.5.x g object)

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.

Frontend rendering: never self-close component tags

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 empty

Icons: only use single-glyph Material Icons ligatures

LNbits 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).

Funding source (ClinkWallet)

v0.1.4 makes CLINK a selectable LNbits backend wallet.

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 = ClinkWallet

Because 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. The node is configured with an env var (no UI field — the /admin funding-source card only renders inputs for core-bundled classes):

LNBITS_CLINK_FUNDING_NOFFER=noffer1...

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.

Scope (receive-first). create_invoice requests a BOLT11 invoice from the node's offer service over kind 21001 (reusing the clink/pay.py request flow) — the noffer alone is enough to receive. pay_invoice returns a clear "receive-only" error and get_invoice_status/status report pending/zero, because sending and balance reporting need a Lightning.pub account on the node (connection string + user API) — planned for the next milestone. 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, status() behaviour, the pre-network amount checks, and the receive-only payment response (no live relay calls).

Release pipeline

.github/workflows/release.yml is triggered by a version tag (e.g. v0.1.3):

  1. Builds a release artifact (the extension zip) and drafts a GitHub release.
  2. Runs tools/update_extensions.py, which updates the registry and opens a pull request against lnbits/lnbits-extensions so the extension shows up in the LNbits extension manager.

To cut a release:

git tag v0.1.3
git push origin v0.1.3

Each 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_GITHUB secret (a token with access to the lnbits/lnbits-extensions fork) configured in the repository settings.

Conventions

  • Commit messages follow feat: … / fix: … style (see git log).
  • All commits are authored under a dedicated extension identity (WoompaLoompa / 06bc1a977d@atomicmail.io).
  • No new Python dependencies: protocol primitives are implemented in clink/nostr on top of LNbits' bundled packages.

Clone this wiki locally