Feature: Hold Invoices for Safe Dispensing #3

Open
opened 2026-06-13 21:58:16 +00:00 by padreug · 2 comments
Owner

Migrated from aiolabs/lamassu-next#3 — opened by @padreug on 2026-01-24.\n\n## Overview

For cash-out, 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.

Status: Achievable with Hybrid Approach
Complexity: High
Dependencies: LND hold invoices, Lightning.Pub extension or direct LND access

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

Lightning.Pub Status

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

// From Lightning.Pub/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.

Implementation Options

Contribute hold invoice support to Lightning.Pub:

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

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

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

Pros: Clean integration, benefits entire ecosystem
Cons: Requires upstream contribution, timeline uncertain

Option B: Hybrid Approach

ATM uses Lightning.Pub for accounting but connects to LND directly for hold invoices:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   ATM Machine   │────>│  Lightning.Pub  │────>│      LND        │
│                 │     │  (accounting)   │     │   (payments)    │
│                 │─────────────────────────────>│                 │
│                 │     direct gRPC for hold     │                 │
└─────────────────┘     invoices                 └─────────────────┘

Pros: Works today, no upstream changes needed
Cons: More complex, two connections to manage

Option C: Escrow Account

Use Lightning.Pub's internal accounts as escrow:

  1. Create "escrow" account in Lightning.Pub
  2. User pays to escrow account (instant settlement)
  3. On successful dispense: transfer escrow → operator
  4. On failed dispense: transfer escrow → user refund

Pros: Works within Lightning.Pub model
Cons: Requires user to have Lightning.Pub account for refund

We will contribute hold invoice support directly to Lightning.Pub (Option A). This:

  • Benefits the entire Nostr+Lightning ecosystem
  • Keeps our architecture clean (single payment backend)
  • Aligns with the open-source-first philosophy of this project

Action Item: Open PR to Lightning.Pub exposing hold invoice methods via Nostr RPC (kind 21000).

Cash-Out Flow with Hold Invoices

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

  // 2. Create hold invoice via LND
  const invoice = await lnd.addHoldInvoice({
    hash,
    value: amount,
    memo: `ATM Cash-Out $${amount / 100}`,
    expiry: 600, // 10 minutes
  })

  // 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 lnd.settleInvoice({ preimage })
    return { success: true }
  } catch (error) {
    // 5b. Failure - cancel the invoice, funds return to user
    await lnd.cancelInvoice({ payment_hash: hash })
    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

Priority

P2 - High effort, critical for production safety

References

> _Migrated from [aiolabs/lamassu-next#3](https://git.atitlan.io/aiolabs/lamassu-next/issues/3) — opened by @padreug on 2026-01-24._\n\n## Overview For cash-out, 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. **Status**: Achievable with Hybrid Approach **Complexity**: High **Dependencies**: LND hold invoices, Lightning.Pub extension or direct LND access ## 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 ## Lightning.Pub Status Lightning.Pub's codebase includes LND protobuf definitions for hold invoices: ```typescript // From Lightning.Pub/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. ## Implementation Options ### Option A: Lightning.Pub Extension (Recommended) Contribute hold invoice support to Lightning.Pub: ```typescript // New RPC methods needed interface LightningPubExtension { // Create hold invoice (doesn't settle automatically) createHoldInvoice(params: { amount_sats: number memo: string hash: string // We provide the hash }): Promise<{ invoice: string }> // Settle after successful dispense settleHoldInvoice(params: { preimage: string }): Promise<void> // Cancel if dispense fails cancelHoldInvoice(params: { hash: string }): Promise<void> } ``` **Pros**: Clean integration, benefits entire ecosystem **Cons**: Requires upstream contribution, timeline uncertain ### Option B: Hybrid Approach ATM uses Lightning.Pub for accounting but connects to LND directly for hold invoices: ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ ATM Machine │────>│ Lightning.Pub │────>│ LND │ │ │ │ (accounting) │ │ (payments) │ │ │─────────────────────────────>│ │ │ │ direct gRPC for hold │ │ └─────────────────┘ invoices └─────────────────┘ ``` **Pros**: Works today, no upstream changes needed **Cons**: More complex, two connections to manage ### Option C: Escrow Account Use Lightning.Pub's internal accounts as escrow: 1. Create "escrow" account in Lightning.Pub 2. User pays to escrow account (instant settlement) 3. On successful dispense: transfer escrow → operator 4. On failed dispense: transfer escrow → user refund **Pros**: Works within Lightning.Pub model **Cons**: Requires user to have Lightning.Pub account for refund ## Recommended Approach: Lightning.Pub Contribution We will contribute hold invoice support directly to Lightning.Pub (Option A). This: - Benefits the entire Nostr+Lightning ecosystem - Keeps our architecture clean (single payment backend) - Aligns with the open-source-first philosophy of this project **Action Item**: Open PR to Lightning.Pub exposing hold invoice methods via Nostr RPC (kind 21000). ## Cash-Out Flow with Hold Invoices ```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 LND const invoice = await lnd.addHoldInvoice({ hash, value: amount, memo: `ATM Cash-Out $${amount / 100}`, expiry: 600, // 10 minutes }) // 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 lnd.settleInvoice({ preimage }) return { success: true } } catch (error) { // 5b. Failure - cancel the invoice, funds return to user await lnd.cancelInvoice({ payment_hash: hash }) 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 ## Priority P2 - High effort, critical for production safety ## References - [LND Hold Invoices](https://docs.lightning.engineering/lightning-network-tools/lnd/hold-invoices) - [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
padreug changed title from [reserved] migration number alignment to Feature: Hold Invoices for Safe Dispensing 2026-06-14 06:54:35 +00:00
padreug reopened this issue 2026-06-14 06:54:35 +00:00
Author
Owner

@padreug commented on 2026-01-25 (lamassu-next#3):

Moved to lightning-pub repo: aiolabs/lightning-pub#2

This issue requires extending Lightning.Pub's API to expose hold invoice functionality and belongs in that repository.

> _@padreug commented on 2026-01-25 ([lamassu-next#3](https://git.atitlan.io/aiolabs/lamassu-next/issues/3#issuecomment-49)):_ **Moved to lightning-pub repo**: https://git.atitlan.io/aiolabs/lightning-pub/issues/2 This issue requires extending Lightning.Pub's API to expose hold invoice functionality and belongs in that repository.
Author
Owner

@padreug commented on 2026-05-31 (lamassu-next#3):

Re-opening this, need to update for new bitspire infra (no longer using lightning.pub)

> _@padreug commented on 2026-05-31 ([lamassu-next#3](https://git.atitlan.io/aiolabs/lamassu-next/issues/3#issuecomment-1735)):_ Re-opening this, need to update for new bitspire infra (no longer using lightning.pub)
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/bitspire#3
No description provided.