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

Release pipeline

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

  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.2
git push origin v0.1.2

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