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 <noreply@anthropic.com>
This commit is contained in:
parent
687717bfce
commit
36e326a27e
7 changed files with 117 additions and 26 deletions
|
|
@ -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",
|
||||
|
|
|
|||
20
lamassu-next/apps/machine/src/components/QRCode.vue
Normal file
20
lamassu-next/apps/machine/src/components/QRCode.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
value: string
|
||||
size?: number
|
||||
}>()
|
||||
|
||||
const qrSize = computed(() => props.size ?? 200)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center rounded-lg bg-white p-4">
|
||||
<QrcodeVue :value="value" :size="qrSize" level="M" render-as="svg" />
|
||||
<code class="mt-2 block max-w-full break-all px-2 text-center text-xs text-zinc-500">
|
||||
{{ value.slice(0, 32) }}...
|
||||
</code>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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<LightningServices>
|
|||
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<LightningServices>
|
|||
clink,
|
||||
identity,
|
||||
atmServices,
|
||||
onOfferRequest: (callback: OfferRequestCallback) => {
|
||||
offerRequestCallback = callback
|
||||
},
|
||||
onPaymentReceived: (callback: PaymentReceivedCallback) => {
|
||||
paymentReceivedCallback = callback
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
class="mx-auto flex h-52 w-52 flex-col items-center justify-center rounded-lg bg-white"
|
||||
>
|
||||
<p class="text-3xl">📱</p>
|
||||
<p class="text-sm text-black">QR Code</p>
|
||||
<code
|
||||
v-if="context?.clinkOffer"
|
||||
class="mt-2 block max-w-full break-all px-2 text-xs text-zinc-500"
|
||||
>
|
||||
{{ context.clinkOffer.slice(0, 24) }}...
|
||||
</code>
|
||||
</div>
|
||||
<QRCode v-if="context?.clinkOffer" :value="context.clinkOffer" :size="200" />
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
Open your Lightning wallet and scan the code
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
class="mx-auto flex h-52 w-52 flex-col items-center justify-center rounded-lg bg-white"
|
||||
>
|
||||
<p class="text-3xl">📱</p>
|
||||
<p class="text-sm text-black">QR Code</p>
|
||||
<code
|
||||
v-if="context?.invoice"
|
||||
class="mt-2 block max-w-full break-all px-2 text-xs text-zinc-500"
|
||||
>
|
||||
{{ context.invoice.slice(0, 24) }}...
|
||||
</code>
|
||||
</div>
|
||||
<QRCode v-if="context?.invoice" :value="context.invoice" :size="200" />
|
||||
<p class="text-center text-sm text-muted-foreground">Waiting for payment...</p>
|
||||
|
||||
<!-- Dev controls -->
|
||||
|
|
|
|||
12
lamassu-next/pnpm-lock.yaml
generated
12
lamassu-next/pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue