Skip to content

Architecture

WoompaLoompa edited this page Aug 10, 2026 · 3 revisions

Architecture

clink/
├── __init__.py          extension entry point, registers background tasks + ClinkWallet
├── migrations.py        SQL migrations (m001…m004)
├── models.py            pydantic models (Offer, Debit, Plan, Subscription, …)
├── crud.py              database access (Database("ext_clink"))
├── node.py              receiving node service: relay listener + 21001/21002 handlers
├── pay.py               outgoing Pay Offers (kind 21001 payer flow)
├── subscriptions.py     recurring-charge poller (kind 21002 app flow)
├── account.py           Lightning.pub user API client (kind 21000, NIP-44 v1)
├── wallet.py            ClinkWallet LNbits funding source (create_invoice/pay_invoice/status)
├── views.py             HTML pages
├── views_api.py         REST API
├── nostr/               minimal CLINK protocol stack
│   ├── nip44.py         NIP-44 v2 encryption (offers/debits payloads)
│   ├── nip44v1.py       NIP-44 v1 encryption (Lightning.pub user API, libsodium vectors)
│   ├── bech32.py        noffer/ndebit/nprofile pointer codecs
│   ├── events.py        build/sign/verify kind 21001/21002/21000 events
│   ├── keys.py          key helpers over lnbits.utils.nostr
│   └── relay.py         async relay client (websockets)
├── static/js/           Vue/Quasar frontends
├── templates/clink/     Jinja templates
└── tests/               pytest suite

Background tasks

The extension registers two permanent tasks in clink_start() (started with LNbits, cancelled in clink_stop()):

Task Name What it does
clink_listener clink_listener Holds a relay subscription per enabled relay. Receives kind 21001 (offers) and 21002 (debits), verifies signatures, decrypts with NIP-44, and answers with encrypted responses. Reconnects with a 5 s backoff.
clink_subscriptions clink_subscriptions Every 60 s, finds active subscriptions whose period has ended and bills them (kind 21002 direct payment), advancing the period on ok and cancelling after 3 failed attempts.

Data model (SQLite via LNbits)

  • clink.offers — stored offers with their noffer1..., keypair, and active flag.
  • clink.debits — debit pointers with rules, budget, frequency, state.
  • clink.plans — recurring plans (amount_msat, frequency_number/unit).
  • clink.subscriptions — plan + payer ndebit, period bounds, attempt counter, last payment/error.
  • clink.relays — configured relays.
  • clink.node_keys — one CLINK keypair per wallet (used for debit pointers).
  • clink.debit_usage — per-period spend tracking for budgets ((debit_id, period_start)).
  • clink.k1s — single-use session identifiers for debit requests.
  • clink.invoices — invoice status tracking for ClinkWallet (v0.1.5).

Migrations run in order (m001 tables, m002 node keys, m003 subscription fields, m004 invoices).

Request handling (node side)

Incoming events are routed by kind after signature verification (verify_event):

  1. Offer request (21001) — matched to an offer whose pubkey equals the p tag. The payload is decrypted with the offer's keypair, the amount is resolved (resolve_offer_amount) against the pointer's price type, an invoice is created (create_invoice), and the response is encrypted back to the requestor.
  2. Debit request (21002) — matched to the wallet's node key, then:
    • age check (> 30 s → GFY 3)
    • pointer resolution (explicit pointer, or single-debit fallback)
    • active-state and allowed_pubkeys rule checks
    • session k1 single-use check (clink.k1s)
    • direct payment (decode bolt11, validate amount/fixed/budget, pay_invoice, budget rollback on failure) or budget request (frequency + budget validation, ack).

Outgoing flows

  • Pay Offers (pay.py) — decode noffer → resolve amount → build a kind 21001 request with a fresh ephemeral keypair → request_response on the noffer relay + enabled relays → parse the encrypted response → pay_invoice → await preimage.
  • Subscriptions (subscriptions.py) — for a due subscription: create_invoice on the service wallet → kind 21002 direct-payment request to the payer's node → request_response → advance period / count attempts.
  • Funding source (wallet.py + account.py) — ClinkWallet is injected into lnbits.wallets at extension load. create_invoice requests a BOLT11 invoice from the configured noffer's node over kind 21001; with an account string, pay_invoice/status talk to the node's Nostr user API (kind 21000, NIP-44 v1 envelope via account.py) for PayInvoice/GetUserInfo/GetPaymentState.

request_response (in nostr/relay.py) subscribes on every relay before publishing the request so a fast response cannot be missed; it returns the first event matching #e + #p (plus a caller-supplied match predicate, e.g. the user API's response envelope).

Frontend

The UI is plain Vue + Quasar served from LNbits templates. Templates must not use {{ }} for Vue — Jinja consumes it; the code uses v-text, v-if, etc. instead. See templates/clink/*.html and static/js/*.js.

Clone this wiki locally