Feature: Expose Hold Invoice API via Nostr RPC (Kind 21000) #2

Open
opened 2026-01-25 19:56:15 +00:00 by padreug · 0 comments
Owner

Overview

For ATM cash-out operations, we need hold invoices to ensure safe dispensing. The current flow has a risk window: user pays invoice, but if dispenser jams, user loses funds. Hold invoices solve this by delaying settlement until cash is physically dispensed.

Problem: Current Flow (Risky)

User pays invoice ──> Invoice settles ──> Dispense attempted ──> JAM!
                            │
                            └── User lost funds, ATM owes them

Solution: Hold Invoice Flow (Safe)

User pays invoice ──> Invoice HELD ──> Dispense attempted ──> Success ──> SETTLE
                           │                    │
                           │                    └── JAM! ──> CANCEL (funds returned)
                           │
                           └── Funds locked but not settled

Technical Background

LND supports hold invoices via:

  • AddHoldInvoice: Create invoice with known preimage hash
  • SettleInvoice: Release funds (ATM provides preimage)
  • CancelInvoice: Return funds to payer

Current Status in Lightning.Pub

Lightning.Pub's codebase includes LND protobuf definitions for hold invoices:

// From proto/lnd/invoices.ts
interface AddHoldInvoiceRequest {
  hash: Uint8Array // SHA256 of preimage we choose
  value: string // Amount in sats
  memo: string
  expiry: string
  // ...
}

interface SettleInvoiceMsg {
  preimage: Uint8Array // Reveal to settle
}

interface CancelInvoiceMsg {
  payment_hash: Uint8Array
}

However: These are not currently exposed in Lightning.Pub's HTTP/Nostr API.

Proposed Implementation

Expose hold invoice methods via Nostr RPC (kind 21000):

// New RPC methods needed
interface HoldInvoiceMethods {
  // Create hold invoice (doesn't settle automatically)
  createHoldInvoice(params: {
    amount_sats: number
    memo: string
    hash: string // Client provides the hash
  }): Promise<{ invoice: string }>

  // Settle after successful operation
  settleHoldInvoice(params: { preimage: string }): Promise<void>

  // Cancel if operation fails
  cancelHoldInvoice(params: { hash: string }): Promise<void>
}

Example Usage Flow

async function cashOutWithHoldInvoice(amount: number) {
  // 1. Generate preimage and hash
  const preimage = crypto.randomBytes(32)
  const hash = sha256(preimage)

  // 2. Create hold invoice via Lightning.Pub
  const invoice = await lightningPub.createHoldInvoice({
    amount_sats: amount,
    memo: `ATM Cash-Out $${amount / 100}`,
    hash: hash.toString('hex'),
  })

  // 3. Display invoice, wait for payment
  displayQR(invoice)
  await waitForHtlcAccepted(hash) // Payment received but not settled

  // 4. Attempt to dispense
  try {
    await dispenser.dispense(calculateBills(amount))

    // 5a. Success - settle the invoice
    await lightningPub.settleHoldInvoice({ preimage: preimage.toString('hex') })
    return { success: true }
  } catch (error) {
    // 5b. Failure - cancel the invoice, funds return to user
    await lightningPub.cancelHoldInvoice({ hash: hash.toString('hex') })
    return { success: false, error: 'Dispense failed, payment cancelled' }
  }
}

Timeout Handling

  • Hold invoices have expiry (default 10 minutes)
  • If ATM crashes mid-transaction, invoice eventually expires
  • User's funds return automatically after timeout
  • No manual intervention needed

Benefits

  • Safety: No funds lost on dispenser failures
  • Ecosystem: Benefits all Lightning.Pub users, not just ATMs
  • Clean Architecture: Single payment backend (no direct LND connection needed)

References

## Overview For ATM cash-out operations, we need hold invoices to ensure safe dispensing. The current flow has a risk window: user pays invoice, but if dispenser jams, user loses funds. Hold invoices solve this by delaying settlement until cash is physically dispensed. ## Problem: Current Flow (Risky) ``` User pays invoice ──> Invoice settles ──> Dispense attempted ──> JAM! │ └── User lost funds, ATM owes them ``` ## Solution: Hold Invoice Flow (Safe) ``` User pays invoice ──> Invoice HELD ──> Dispense attempted ──> Success ──> SETTLE │ │ │ └── JAM! ──> CANCEL (funds returned) │ └── Funds locked but not settled ``` ## Technical Background LND supports hold invoices via: - `AddHoldInvoice`: Create invoice with known preimage hash - `SettleInvoice`: Release funds (ATM provides preimage) - `CancelInvoice`: Return funds to payer ## Current Status in Lightning.Pub Lightning.Pub's codebase includes LND protobuf definitions for hold invoices: ```typescript // From proto/lnd/invoices.ts interface AddHoldInvoiceRequest { hash: Uint8Array // SHA256 of preimage we choose value: string // Amount in sats memo: string expiry: string // ... } interface SettleInvoiceMsg { preimage: Uint8Array // Reveal to settle } interface CancelInvoiceMsg { payment_hash: Uint8Array } ``` **However**: These are not currently exposed in Lightning.Pub's HTTP/Nostr API. ## Proposed Implementation Expose hold invoice methods via Nostr RPC (kind 21000): ```typescript // New RPC methods needed interface HoldInvoiceMethods { // Create hold invoice (doesn't settle automatically) createHoldInvoice(params: { amount_sats: number memo: string hash: string // Client provides the hash }): Promise<{ invoice: string }> // Settle after successful operation settleHoldInvoice(params: { preimage: string }): Promise<void> // Cancel if operation fails cancelHoldInvoice(params: { hash: string }): Promise<void> } ``` ## Example Usage Flow ```typescript async function cashOutWithHoldInvoice(amount: number) { // 1. Generate preimage and hash const preimage = crypto.randomBytes(32) const hash = sha256(preimage) // 2. Create hold invoice via Lightning.Pub const invoice = await lightningPub.createHoldInvoice({ amount_sats: amount, memo: `ATM Cash-Out $${amount / 100}`, hash: hash.toString('hex'), }) // 3. Display invoice, wait for payment displayQR(invoice) await waitForHtlcAccepted(hash) // Payment received but not settled // 4. Attempt to dispense try { await dispenser.dispense(calculateBills(amount)) // 5a. Success - settle the invoice await lightningPub.settleHoldInvoice({ preimage: preimage.toString('hex') }) return { success: true } } catch (error) { // 5b. Failure - cancel the invoice, funds return to user await lightningPub.cancelHoldInvoice({ hash: hash.toString('hex') }) return { success: false, error: 'Dispense failed, payment cancelled' } } } ``` ## Timeout Handling - Hold invoices have expiry (default 10 minutes) - If ATM crashes mid-transaction, invoice eventually expires - User's funds return automatically after timeout - No manual intervention needed ## Benefits - **Safety**: No funds lost on dispenser failures - **Ecosystem**: Benefits all Lightning.Pub users, not just ATMs - **Clean Architecture**: Single payment backend (no direct LND connection needed) ## References - [LND Hold Invoices](https://docs.lightning.engineering/lightning-network-tools/lnd/hold-invoices) - Original issue: https://git.atitlan.io/aiolabs/lamassu-next/issues/3
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: aiolabs/lightning-pub#2
No description provided.