-
Notifications
You must be signed in to change notification settings - Fork 0
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
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. |
-
clink.offers— stored offers with theirnoffer1..., 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 + payerndebit, 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 forClinkWallet(v0.1.5).
Migrations run in order (m001 tables, m002 node keys, m003 subscription
fields, m004 invoices).
Incoming events are routed by kind after signature verification (verify_event):
-
Offer request (21001) — matched to an offer whose pubkey equals the
ptag. 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. -
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_pubkeysrule checks - session
k1single-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).
-
Pay Offers (
pay.py) — decode noffer → resolve amount → build a kind 21001 request with a fresh ephemeral keypair →request_responseon the noffer relay + enabled relays → parse the encrypted response →pay_invoice→ await preimage. -
Subscriptions (
subscriptions.py) — for a due subscription:create_invoiceon 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) —ClinkWalletis injected intolnbits.walletsat extension load.create_invoicerequests a BOLT11 invoice from the configured noffer's node over kind 21001; with an account string,pay_invoice/statustalk to the node's Nostr user API (kind 21000, NIP-44 v1 envelope viaaccount.py) forPayInvoice/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).
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.
lnbits-clink — CLINK (Nostr-native Lightning) for LNbits.