Implement noffer-based cash-out flow #9

Closed
opened 2026-06-13 21:59:06 +00:00 by padreug · 0 comments
Owner

Migrated from aiolabs/lamassu-next#9 — opened by @padreug on 2026-01-25.

Summary

Implement cash-out (user sells bitcoin → receives cash) using CLINK Offers protocol (noffer, Kind 21001) instead of direct invoice generation.

Current Implementation

The state machine currently uses generateInvoice service to create Lightning invoices directly:

cashOut.selectingAmount → cashOut.generatingInvoice → cashOut.displayingInvoice → cashOut.dispensingCash

This approach requires:

  • User to input amount on ATM
  • ATM to generate invoice for specific amount
  • User to scan and pay

Proposed noffer Implementation

Use CLINK Offers protocol (Kind 21001) for a more flexible flow:

cashOut.idle → cashOut.displayingNoffer → cashOut.awaitingPayment → cashOut.dispensingCash

Flow

  1. ATM displays noffer QR - Contains ATM's pubkey and relay info
  2. User's wallet sends Kind 21001 request - Includes desired amount
  3. ATM responds with invoice - Via Kind 21001 response
  4. User pays invoice - Wallet handles payment
  5. ATM detects payment - Via preimage or subscription
  6. ATM dispenses cash - Based on confirmed sats received

Benefits

  • Variable amounts: User enters amount in their wallet (any supported denomination)
  • Standard protocol: CLINK compliance for wallet interoperability
  • Better UX: Wallet-native flow, user stays in their app
  • Static QR option: Same noffer QR for multiple transactions
  • Rate display: Can show current exchange rate on ATM screen

Implementation Tasks

1. noffer Generation Service

// packages/clink/src/services/noffer.ts
interface NofferService {
  generateNoffer(config: { pubkey: string; relays: string[] }): string
  handleOfferRequest(event: NostrEvent): Promise<string> // returns invoice
}

2. State Machine Updates

// packages/state-machine/src/machine.ts
cashOut: {
  initial: 'idle',
  states: {
    idle: { on: { START_CASH_OUT: 'displayingNoffer' } },
    displayingNoffer: {
      invoke: { src: 'subscribeToOfferRequests' },
      on: { OFFER_REQUEST_RECEIVED: 'processingRequest' }
    },
    processingRequest: {
      invoke: { src: 'generateInvoiceForOffer' },
      on: { INVOICE_GENERATED: 'awaitingPayment' }
    },
    awaitingPayment: {
      invoke: { src: 'subscribeToPayment' },
      on: { PAYMENT_RECEIVED: 'dispensingCash' }
    },
    dispensingCash: {
      invoke: { src: 'dispenseCash' },
      on: { DISPENSE_COMPLETE: '#machine.idle' }
    }
  }
}

3. Nostr Subscription for Offer Requests

  • Subscribe to Kind 21001 events tagged with ATM's pubkey
  • Validate amount against available denominations
  • Rate limit requests

4. Invoice Response

  • Generate invoice via Lightning.Pub RPC
  • Send Kind 21001 response with invoice
  • Track pending invoices for payment confirmation

5. UI Updates

  • Display noffer QR code
  • Show current exchange rate
  • Display payment status
  • Confirm dispense amount

Security Considerations

  • Rate limiting: Prevent spam offer requests
  • Amount validation: Ensure requested amount is dispensable
  • Timeout handling: Cancel if payment not received within window
  • Double-dispense prevention: Track paid invoices
  • CLINK Offers spec: clink/specs/clink-offers.md
  • Current noffer implementation: packages/clink/src/noffer.ts
  • State machine: packages/state-machine/src/machine.ts
  • #8 (Session-scoped ndebit for cash-in)
> _Migrated from [aiolabs/lamassu-next#9](https://git.atitlan.io/aiolabs/lamassu-next/issues/9) — opened by @padreug on 2026-01-25._ ## Summary Implement cash-out (user sells bitcoin → receives cash) using CLINK Offers protocol (noffer, Kind 21001) instead of direct invoice generation. ## Current Implementation The state machine currently uses `generateInvoice` service to create Lightning invoices directly: ``` cashOut.selectingAmount → cashOut.generatingInvoice → cashOut.displayingInvoice → cashOut.dispensingCash ``` This approach requires: - User to input amount on ATM - ATM to generate invoice for specific amount - User to scan and pay ## Proposed noffer Implementation Use CLINK Offers protocol (Kind 21001) for a more flexible flow: ``` cashOut.idle → cashOut.displayingNoffer → cashOut.awaitingPayment → cashOut.dispensingCash ``` ### Flow 1. **ATM displays noffer QR** - Contains ATM's pubkey and relay info 2. **User's wallet sends Kind 21001 request** - Includes desired amount 3. **ATM responds with invoice** - Via Kind 21001 response 4. **User pays invoice** - Wallet handles payment 5. **ATM detects payment** - Via preimage or subscription 6. **ATM dispenses cash** - Based on confirmed sats received ### Benefits - **Variable amounts**: User enters amount in their wallet (any supported denomination) - **Standard protocol**: CLINK compliance for wallet interoperability - **Better UX**: Wallet-native flow, user stays in their app - **Static QR option**: Same noffer QR for multiple transactions - **Rate display**: Can show current exchange rate on ATM screen ## Implementation Tasks ### 1. noffer Generation Service ```typescript // packages/clink/src/services/noffer.ts interface NofferService { generateNoffer(config: { pubkey: string; relays: string[] }): string handleOfferRequest(event: NostrEvent): Promise<string> // returns invoice } ``` ### 2. State Machine Updates ```typescript // packages/state-machine/src/machine.ts cashOut: { initial: 'idle', states: { idle: { on: { START_CASH_OUT: 'displayingNoffer' } }, displayingNoffer: { invoke: { src: 'subscribeToOfferRequests' }, on: { OFFER_REQUEST_RECEIVED: 'processingRequest' } }, processingRequest: { invoke: { src: 'generateInvoiceForOffer' }, on: { INVOICE_GENERATED: 'awaitingPayment' } }, awaitingPayment: { invoke: { src: 'subscribeToPayment' }, on: { PAYMENT_RECEIVED: 'dispensingCash' } }, dispensingCash: { invoke: { src: 'dispenseCash' }, on: { DISPENSE_COMPLETE: '#machine.idle' } } } } ``` ### 3. Nostr Subscription for Offer Requests - Subscribe to Kind 21001 events tagged with ATM's pubkey - Validate amount against available denominations - Rate limit requests ### 4. Invoice Response - Generate invoice via Lightning.Pub RPC - Send Kind 21001 response with invoice - Track pending invoices for payment confirmation ### 5. UI Updates - Display noffer QR code - Show current exchange rate - Display payment status - Confirm dispense amount ## Security Considerations - **Rate limiting**: Prevent spam offer requests - **Amount validation**: Ensure requested amount is dispensable - **Timeout handling**: Cancel if payment not received within window - **Double-dispense prevention**: Track paid invoices ## Related - CLINK Offers spec: `clink/specs/clink-offers.md` - Current noffer implementation: `packages/clink/src/noffer.ts` - State machine: `packages/state-machine/src/machine.ts` - #8 (Session-scoped ndebit for cash-in)
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#9
No description provided.