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

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