From 36e326a27e3c9eb1441267c26c1f051d08599a47 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Sat, 24 Jan 2026 13:18:11 -0500 Subject: [PATCH] Add QR code display and CLINK offer request handler - Add qrcode.vue dependency for QR code rendering - Create QRCode.vue component for displaying payment codes - Update CashInView and CashOutView to use real QR codes - Add CLINK offer request handler to respond when wallets scan noffers - Wire payment received callback to state machine - Auto-complete transaction when payment is detected Co-Authored-By: Claude Opus 4.5 --- lamassu-next/apps/machine/package.json | 1 + .../apps/machine/src/components/QRCode.vue | 20 ++++++ .../apps/machine/src/services/lightning.ts | 70 ++++++++++++++++++- lamassu-next/apps/machine/src/stores/atm.ts | 12 ++++ .../apps/machine/src/views/CashInView.vue | 14 +--- .../apps/machine/src/views/CashOutView.vue | 14 +--- lamassu-next/pnpm-lock.yaml | 12 ++++ 7 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 lamassu-next/apps/machine/src/components/QRCode.vue diff --git a/lamassu-next/apps/machine/package.json b/lamassu-next/apps/machine/package.json index f56d5cb..134d3a5 100644 --- a/lamassu-next/apps/machine/package.json +++ b/lamassu-next/apps/machine/package.json @@ -23,6 +23,7 @@ "clsx": "^2.1.1", "lucide-vue-next": "^0.563.0", "pinia": "^2.2.0", + "qrcode.vue": "^3.6.0", "reka-ui": "^2.7.0", "tailwind-merge": "^3.4.0", "vue": "^3.5.0", diff --git a/lamassu-next/apps/machine/src/components/QRCode.vue b/lamassu-next/apps/machine/src/components/QRCode.vue new file mode 100644 index 0000000..f698325 --- /dev/null +++ b/lamassu-next/apps/machine/src/components/QRCode.vue @@ -0,0 +1,20 @@ + + + diff --git a/lamassu-next/apps/machine/src/services/lightning.ts b/lamassu-next/apps/machine/src/services/lightning.ts index ea6ce1f..55ccdae 100644 --- a/lamassu-next/apps/machine/src/services/lightning.ts +++ b/lamassu-next/apps/machine/src/services/lightning.ts @@ -6,7 +6,8 @@ import { NostrClient, generateIdentity, type MachineIdentity } from '@lamassu/nostr-client' import { LightningPubClient } from '@lamassu/lightning' -import { CLINKClient } from '@lamassu/clink' +import { CLINKClient, createOfferSuccess, createOfferError, CLINKErrorCode } from '@lamassu/clink' +import type { OfferRequest } from '@lamassu/clink' import type { ATMServices, ATMContext } from '@lamassu/state-machine' // Development infrastructure configuration @@ -28,8 +29,18 @@ interface LightningServices { clink: CLINKClient identity: MachineIdentity atmServices: ATMServices + /** Set callback for when offer requests are received */ + onOfferRequest: (callback: OfferRequestCallback) => void + /** Set callback for when payments are received */ + onPaymentReceived: (callback: PaymentReceivedCallback) => void } +/** Callback for incoming offer requests */ +type OfferRequestCallback = (request: OfferRequest, senderPubkey: string) => void + +/** Callback for payment received */ +type PaymentReceivedCallback = (preimage: string) => void + /** * Initialize Lightning services */ @@ -67,8 +78,57 @@ export async function initializeLightningServices(): Promise relays: [DEV_CONFIG.relayUrl], }) + // Callbacks for events + let offerRequestCallback: OfferRequestCallback | null = null + let paymentReceivedCallback: PaymentReceivedCallback | null = null + + // Set up CLINK offer request handler + // When someone scans our noffer and requests an invoice, we respond + clink.onOfferRequest(async (request, senderPubkey) => { + console.log('[CLINK] Received offer request from', senderPubkey.slice(0, 16) + '...') + console.log('[CLINK] Request:', request) + + // Notify the ATM about the incoming request + if (offerRequestCallback) { + offerRequestCallback(request, senderPubkey) + } + + // Generate an invoice for the requested amount + try { + const amountSats = request.amount_sats + if (!amountSats || amountSats <= 0) { + console.log('[CLINK] Invalid amount requested') + return createOfferError(CLINKErrorCode.InvalidAmount, 'Invalid amount') + } + + console.log('[CLINK] Creating invoice for', amountSats, 'sats') + const invoice = await lightningPub.createInvoice({ + amountSats, + description: request.description || 'Lamassu ATM Payment', + }) + + console.log('[CLINK] Invoice created:', invoice.paymentRequest.slice(0, 32) + '...') + + // Watch for payment + lightningPub.watchInvoice(invoice.paymentHash, (paidInvoice) => { + console.log('[CLINK] Payment received! Preimage:', paidInvoice.preimage) + if (paidInvoice.preimage && paymentReceivedCallback) { + paymentReceivedCallback(paidInvoice.preimage) + } + }) + + return createOfferSuccess(invoice.paymentRequest) + } catch (error) { + console.error('[CLINK] Failed to create invoice:', error) + return createOfferError( + CLINKErrorCode.TemporaryFailure, + error instanceof Error ? error.message : 'Failed to create invoice' + ) + } + }) + clink.startListening() - console.log('[Lightning] CLINK client initialized') + console.log('[Lightning] CLINK client initialized with offer handler') // Create ATM services const atmServices = createATMServices(lightningPub, clink, identity) @@ -79,6 +139,12 @@ export async function initializeLightningServices(): Promise clink, identity, atmServices, + onOfferRequest: (callback: OfferRequestCallback) => { + offerRequestCallback = callback + }, + onPaymentReceived: (callback: PaymentReceivedCallback) => { + paymentReceivedCallback = callback + }, } } diff --git a/lamassu-next/apps/machine/src/stores/atm.ts b/lamassu-next/apps/machine/src/stores/atm.ts index 5e99828..4137b10 100644 --- a/lamassu-next/apps/machine/src/stores/atm.ts +++ b/lamassu-next/apps/machine/src/stores/atm.ts @@ -112,6 +112,18 @@ export const useAtmStore = defineStore('atm', () => { connectionStatus.value = 'connected' console.log('[ATM] Connected to Lightning.Pub!') + // Wire up payment received callback + services.onPaymentReceived((preimage) => { + console.log('[ATM] Payment received via CLINK, preimage:', preimage.slice(0, 16) + '...') + paymentReceived(preimage) + }) + + // Wire up offer request callback (for logging/debugging) + services.onOfferRequest((request, sender) => { + console.log('[ATM] Offer request from:', sender.slice(0, 16) + '...') + console.log('[ATM] Amount requested:', request.amount_sats, 'sats') + }) + // Initialize state machine with real services initialize(services.atmServices) } catch (error) { diff --git a/lamassu-next/apps/machine/src/views/CashInView.vue b/lamassu-next/apps/machine/src/views/CashInView.vue index 1510ccc..af54b36 100644 --- a/lamassu-next/apps/machine/src/views/CashInView.vue +++ b/lamassu-next/apps/machine/src/views/CashInView.vue @@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Skeleton } from '@/components/ui/skeleton' +import QRCode from '@/components/QRCode.vue' const router = useRouter() const atmStore = useAtmStore() @@ -143,18 +144,7 @@ function formatSats(sats: number): string { -
-

📱

-

QR Code

- - {{ context.clinkOffer.slice(0, 24) }}... - -
+

Open your Lightning wallet and scan the code

diff --git a/lamassu-next/apps/machine/src/views/CashOutView.vue b/lamassu-next/apps/machine/src/views/CashOutView.vue index 80ee0f9..af22593 100644 --- a/lamassu-next/apps/machine/src/views/CashOutView.vue +++ b/lamassu-next/apps/machine/src/views/CashOutView.vue @@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Skeleton } from '@/components/ui/skeleton' +import QRCode from '@/components/QRCode.vue' const router = useRouter() const atmStore = useAtmStore() @@ -115,18 +116,7 @@ function formatSats(sats: number): string { -
-

📱

-

QR Code

- - {{ context.invoice.slice(0, 24) }}... - -
+

Waiting for payment...

diff --git a/lamassu-next/pnpm-lock.yaml b/lamassu-next/pnpm-lock.yaml index efe5f40..c153c23 100644 --- a/lamassu-next/pnpm-lock.yaml +++ b/lamassu-next/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: pinia: specifier: ^2.2.0 version: 2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) + qrcode.vue: + specifier: ^3.6.0 + version: 3.6.0(vue@3.5.27(typescript@5.9.3)) reka-ui: specifier: ^2.7.0 version: 2.7.0(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)) @@ -1459,6 +1462,11 @@ packages: engines: {node: '>=14'} hasBin: true + qrcode.vue@3.6.0: + resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==} + peerDependencies: + vue: ^3.0.0 + reka-ui@2.7.0: resolution: {integrity: sha512-m+XmxQN2xtFzBP3OAdIafKq7C8OETo2fqfxcIIxYmNN2Ch3r5oAf6yEYCIJg5tL/yJU2mHqF70dCCekUkrAnXA==} peerDependencies: @@ -2701,6 +2709,10 @@ snapshots: prettier@3.8.1: {} + qrcode.vue@3.6.0(vue@3.5.27(typescript@5.9.3)): + dependencies: + vue: 3.5.27(typescript@5.9.3) + reka-ui@2.7.0(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)): dependencies: '@floating-ui/dom': 1.7.4