-
Notifications
You must be signed in to change notification settings - Fork 0
Protocol
The extension implements a hand-rolled, minimal CLINK protocol stack in the clink/nostr package. It has no new dependencies — it reuses LNbits' bundled Nostr utilities (lnbits.utils.nostr), coincurve, websockets, and bech32.
CLINK is specified by the shocknet/CLINK repository:
-
specs/clink-offers.md— kind 21001 offers -
specs/clink-debits.md— kind 21002 debits -
specs/clink-manage.md— kind 21003 management delegation (not yet implemented here)
All content payloads are encrypted with NIP-44 v2, matching the audited paulmillr reference exactly:
- 16-bit (u16) length prefix; plaintext 1–65535 bytes, encoded 132–87472 bytes, payload 99–65603 bytes.
-
clink/nostr/nip44.pyimplementsencrypt/decrypt(andencrypt_with_keys/decrypt_with_keyshelpers) and is tested againsttests/vectors/nip44.vectors.json.
- Key generation/signing/verification reuse
lnbits.utils.nostr+coincurve. -
clink/nostr/keys.pyaddspubkey_from_privkey(hex_secret)→ the 64-hex, x-only (NIP-01) public key. - Outgoing requests (Pay Offers, subscription renewals) sign with a fresh ephemeral keypair per request, per the CLINK SDK's recommendation.
clink/nostr/bech32.py encodes/decodes both pointer types. The TLV layout is type (1 byte) | length (1 byte) | value, and encoding emits TLVs in descending type order to match the reference SDK byte-for-byte.
| TLV | Field | Notes |
|---|---|---|
0 |
Service pubkey | 32 bytes |
1 |
Relay URL | where the node listens |
2 |
Offer id | server-generated |
3 |
Price type |
0 fixed, 1 variable, 2 spontaneous |
4 |
Price | optional, 4-byte big-endian uint32 (sats) |
| TLV | Field | Notes |
|---|---|---|
0 |
Node service pubkey | 32 bytes |
1 |
Relay URL | where the node listens |
2 |
Pointer id | optional, opaque |
3 |
Session k1
|
optional, exactly 32 raw bytes |
A pointer without TLV 3 is a static pointer (subscriptions, pre-approved debits). A pointer with TLV 3 is a session pointer (one-shot payout); the requestor MUST echo the hex of k1 in the request payload.
All CLINK events carry a mandatory ["clink_version", "1"] tag.
| Kind | Direction | Purpose |
|---|---|---|
21001 |
request / response | Offer request (payer → service), invoice response |
21002 |
request / response | Debit request (app → wallet node), ok/GFY response |
21003 |
request / response | Manage delegation (spec reference only) |
Response events always include ["e", "<request_event_id>"] to reference the request.
Offer request (kind 21001, payer → service):
{
"offer": "<offer_id>",
"amount_sats": 10000,
"description": "optional"
}Response: {"bolt11": "<invoice>"} or an error payload.
Direct payment (kind 21002, app → wallet node):
{
"pointer": "<pointer_id>",
"amount_sats": 10000,
"bolt11": "<BOLT11>",
"description": "optional",
"k1": "<64-hex, only for session pointers>"
}Response: {"res": "ok", "preimage": "…"} (preimage omitted for internal settlement).
Budget request (kind 21002):
{
"pointer": "<pointer_id>",
"amount_sats": 50000,
"frequency": { "number": 1, "unit": "month" },
"description": "optional"
}Response: {"res": "ok"}.
Failures are returned as {"res": "GFY", "code": <n>, "error": "<reason>"}.
| Code | Meaning |
|---|---|
1 |
Request Denied (user/rule denied) |
2 |
Temporary Failure (node issue) |
3 |
Expired Request (too old, e.g. > 30 s delta) |
4 |
Rate Limited |
5 |
Invalid Amount (out of range/budget) |
6 |
Invalid Request (malformed/missing fields) |
The node rejects requests older than REQUEST_MAX_AGE_SECONDS = 30 with GFY(3) and uses the k1 table for single-use session correlation.
lnbits-clink — CLINK (Nostr-native Lightning) for LNbits.