feat(docker): add dev.sh with auto-funding and ATM app setup

- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root

The dev.sh script now supports:
- ./dev.sh up --fund  # Start regtest and auto-fund ATM
- ./dev.sh fund       # Fund existing ATM app
- ./dev.sh status     # Show environment status
- ./dev.sh reset      # Clean restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-15 14:19:16 -05:00
commit c98f126ba7
180 changed files with 2695 additions and 9587 deletions

434
docs/clink-protocol.md Normal file
View file

@ -0,0 +1,434 @@
# CLINK Protocol
CLINK (Custodial Lightning Keys) is a Nostr-based protocol for Lightning payments. It enables wallets and services to communicate payment requests and authorizations through Nostr relays.
## Overview
CLINK defines three Nostr event kinds:
| Kind | Name | Purpose |
| ----- | ------ | -------------------------------- |
| 21001 | Offer | Request and receive invoices |
| 21002 | Debit | Authorize outgoing payments |
| 21003 | Manage | Create and revoke payment offers |
And two encoding formats:
| Format | Purpose | Example Use |
| ------ | ----------------------------------- | ------------------------- |
| noffer | Encode a payment offer (receivable) | ATM displays "pay me" QR |
| ndebit | Encode a debit authorization | ATM displays "pay you" QR |
## noffer (Payment Offer)
A `noffer` encodes information needed to **request a payment** from someone. When scanned, the payer's wallet sends a Kind 21001 to request an invoice.
### Format
```
noffer1<bech32-encoded-data>
```
### Encoded Data (TLV)
| Field | Type | Description |
| ----------- | -------- | ----------------------------------------------- |
| pubkey | 32 bytes | Recipient's Nostr pubkey (who receives payment) |
| relay | string | Relay URL for communication |
| price_type | uint8 | 0=fixed, 1=variable, 2=spontaneous |
| amount | uint64 | Amount in sats (if fixed) |
| description | string | Human-readable description |
| pointer | string | Optional identifier (e.g., user ID) |
### Example: ATM Cash-Out noffer
The ATM wants to receive payment from a customer:
```typescript
import { encodeNoffer } from '@lamassu/clink'
// ATM creates a noffer for receiving payment
const noffer = encodeNoffer({
pubkey: 'abc123...', // ATM's pubkey (or Lightning.Pub's pubkey)
relay: 'wss://relay.example.com',
priceType: 'spontaneous', // Customer chooses amount
description: 'Lamassu ATM - Cash Out',
})
// Result: noffer1qqs8h5mrx8v9...
// Display as QR code
```
### Flow: Customer Pays noffer
```
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Customer │ │ Relay │ │ ATM │
│ (Wallet) │ │ │ │ │
└──────┬───────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. Scan noffer QR │ │
│ │ │
│ 2. Send Kind 21001 request │ │
│ ───────────────────────────────►│ │
│ {amount: 50000, ...} │───────────────────────────────►│
│ │ │
│ │ 3. ATM creates invoice │
│ │ │
│ │ 4. Send Kind 21001 response │
│ │◄───────────────────────────────│
│◄────────────────────────────────│ {invoice: "lnbc..."} │
│ │ │
│ 5. Pay invoice │ │
│ ════════════════════════════════════════════════════════════════►│
│ (Lightning Network) │ │
│ │ │
│ │ 6. Payment received │
│ │ ATM dispenses cash │
│ │ │
```
## ndebit (Debit Authorization)
An `ndebit` encodes information needed to **authorize a payment** from someone's account. When scanned, the payer's wallet sends a Kind 21002 to authorize the payment.
### Format
```
ndebit1<bech32-encoded-data>
```
### Encoded Data (TLV)
| Field | Type | Description |
| ------- | -------- | ---------------------------------------- |
| pubkey | 32 bytes | Custodian's pubkey (who holds the funds) |
| relay | string | Relay URL for communication |
| pointer | string | Account identifier at the custodian |
### Example: ATM Cash-In ndebit
The ATM wants to pay the customer (customer inserted cash, wants Bitcoin):
```typescript
import { encodeNdebit, formatNdebitUri } from '@lamassu/clink'
// ATM creates an ndebit for the customer to authorize withdrawal
const ndebit = encodeNdebit({
pubkey: '4be8e203...', // Lightning.Pub's pubkey (custodian)
relay: 'wss://relay.example.com',
pointer: 'atm', // ATM's account identifier in Lightning.Pub
})
// Add amount as query parameter
const uri = formatNdebitUri(ndebit, 50000) // 50,000 sats
// Result: clink:ndebit1qqs8h5mrx8v9...?amount=50000
// Display as QR code
```
### Flow: Customer Authorizes ndebit
```
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Customer │ │ Relay │ │Lightning.Pub│
│ (Wallet) │ │ │ │ │
└──────┬───────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. Scan ndebit QR │ │
│ (shows: "ATM wants to │ │
│ send you 50,000 sats") │ │
│ │ │
│ 2. User confirms in wallet │ │
│ │ │
│ 3. Wallet sends Kind 21002 │ │
│ ───────────────────────────────►│ │
│ {amount: 50000, │───────────────────────────────►│
│ invoice: "lnbc...", │ │
│ pointer: "atm"} │ │
│ │ │
│ │ 4. Lightning.Pub verifies │
│ │ and pays the invoice │
│ │ │
│ │ 5. Send Kind 21002 response │
│ │◄───────────────────────────────│
│◄────────────────────────────────│ {preimage: "abc..."} │
│ │ │
│ 6. Customer receives sats │ │
│ │ │
```
## Kind 21001: Offer (Invoice Request/Response)
Used to request and receive Lightning invoices.
### Request (Payer → Recipient)
```json
{
"kind": 21001,
"pubkey": "<payer_pubkey>",
"created_at": 1706540000,
"tags": [
["p", "<recipient_pubkey>"],
["e", "<noffer_event_id>", "", "root"]
],
"content": "<nip44_encrypted>{
\"amount_sats\": 50000,
\"description\": \"ATM withdrawal\"
}"
}
```
### Response (Recipient → Payer)
```json
{
"kind": 21001,
"pubkey": "<recipient_pubkey>",
"created_at": 1706540001,
"tags": [
["p", "<payer_pubkey>"],
["e", "<request_event_id>", "", "reply"]
],
"content": "<nip44_encrypted>{
\"invoice\": \"lnbc500u1p...\",
\"amount_sats\": 50000
}"
}
```
### Error Response
```json
{
"kind": 21001,
"pubkey": "<recipient_pubkey>",
"content": "<nip44_encrypted>{
\"error\": {
\"code\": 1,
\"message\": \"Insufficient balance\"
}
}"
}
```
### Error Codes
| Code | Name | Description |
| ---- | ----------------- | ----------------------------- |
| 0 | Unknown | Unknown error |
| 1 | InsufficientFunds | Not enough balance |
| 2 | InvalidAmount | Amount out of range |
| 3 | Expired | Offer has expired |
| 4 | Unauthorized | Not authorized for this offer |
| 5 | TemporaryFailure | Try again later |
## Kind 21002: Debit (Payment Authorization)
Used to authorize a custodian to pay an invoice on your behalf.
### Request (Payer → Custodian)
```json
{
"kind": 21002,
"pubkey": "<payer_pubkey>",
"created_at": 1706540000,
"tags": [
["p", "<custodian_pubkey>"]
],
"content": "<nip44_encrypted>{
\"amount_sats\": 50000,
\"invoice\": \"lnbc500u1p...\",
\"pointer\": \"atm\"
}"
}
```
The `pointer` identifies which account at the custodian should pay.
### Response (Custodian → Payer)
Success:
```json
{
"kind": 21002,
"pubkey": "<custodian_pubkey>",
"created_at": 1706540001,
"tags": [
["p", "<payer_pubkey>"],
["e", "<request_event_id>", "", "reply"]
],
"content": "<nip44_encrypted>{
\"preimage\": \"0123456789abcdef...\",
\"amount_sats\": 50000
}"
}
```
Error:
```json
{
"kind": 21002,
"content": "<nip44_encrypted>{
\"error\": {
\"code\": 1,
\"message\": \"Insufficient balance in ATM account\"
}
}"
}
```
## Kind 21003: Manage (Offer Management)
Used to create, update, or revoke payment offers.
### Create Offer
```json
{
"kind": 21003,
"pubkey": "<merchant_pubkey>",
"tags": [
["p", "<custodian_pubkey>"],
["action", "create"]
],
"content": "<nip44_encrypted>{
\"price_type\": \"fixed\",
\"amount_sats\": 10000,
\"description\": \"Coffee\",
\"max_uses\": 100,
\"expires_at\": 1707000000
}"
}
```
### Revoke Offer
```json
{
"kind": 21003,
"pubkey": "<merchant_pubkey>",
"tags": [
["p", "<custodian_pubkey>"],
["action", "revoke"],
["e", "<offer_event_id>"]
],
"content": ""
}
```
## ATM Use Cases
### Cash-Out (Customer Buys Cash with Bitcoin)
1. Customer approaches ATM, selects "Sell Bitcoin"
2. ATM displays **noffer** QR (or invoice directly)
3. Customer scans with wallet
4. Wallet sends **Kind 21001** request
5. ATM responds with **Kind 21001** containing invoice
6. Customer pays invoice
7. ATM detects payment, dispenses cash
```typescript
// ATM generates noffer for cash-out
const noffer = clink.createOffer({
priceType: 'spontaneous',
description: 'Lamassu ATM - Cash Out',
})
// Display QR code with noffer
displayQR(noffer)
// Listen for offer requests
clink.onOfferRequest(async (request, senderPubkey) => {
// Create invoice for requested amount
const invoice = await lightningPub.createInvoice({
amountSats: request.amount_sats,
})
// Response is sent automatically by CLINK client
return createOfferSuccess(invoice.paymentRequest)
})
```
### Cash-In (Customer Buys Bitcoin with Cash)
1. Customer approaches ATM, selects "Buy Bitcoin"
2. Customer inserts cash bills
3. ATM displays **ndebit** QR
4. Customer scans with wallet
5. Wallet prompts: "ATM wants to send you 50,000 sats. Approve?"
6. Customer confirms, wallet sends **Kind 21002** with their invoice
7. Lightning.Pub pays the invoice
8. Customer receives sats
```typescript
// ATM generates ndebit for cash-in
const ndebit = encodeNdebit({
pubkey: LIGHTNING_PUB_PUBKEY, // Custodian who will pay
relay: RELAY_URL,
pointer: 'atm', // ATM's account at Lightning.Pub
})
const uri = formatNdebitUri(ndebit, satsAmount)
// Display QR code
displayQR(uri)
// Lightning.Pub handles the Kind 21002 automatically
// and pays the customer's invoice
```
## Encryption
All `content` fields are encrypted using **NIP-44** (XChaCha20-Poly1305):
1. Derive shared secret from sender's private key + recipient's public key
2. Encrypt content with XChaCha20-Poly1305
3. Encode as base64
This ensures only the intended recipient can read payment details.
## Security Considerations
1. **Always verify pubkeys** - Ensure the pubkey in noffer/ndebit matches expected recipient/custodian
2. **Check amounts** - Validate amount is within acceptable range before authorizing
3. **Verify relay** - Use trusted relays to prevent MITM attacks
4. **Timestamp validation** - Reject old events to prevent replay attacks
5. **Rate limiting** - Implement rate limits on offer requests
## Libraries
### JavaScript/TypeScript
```typescript
import {
encodeNoffer,
decodeNoffer,
encodeNdebit,
decodeNdebit,
formatNdebitUri,
CLINKClient,
createOfferSuccess,
createOfferError,
} from '@lamassu/clink'
```
### Reference Implementation
- [CLINK Protocol Spec](https://github.com/shocknet/clink)
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
- [@lamassu/clink](../packages/clink/) - TypeScript implementation
## Related NIPs
| NIP | Purpose |
| ------ | ------------------------- |
| NIP-01 | Basic event structure |
| NIP-04 | Encrypted DMs (legacy) |
| NIP-44 | Encrypted payloads (used) |
| NIP-19 | Bech32 encoding |