Wire Lightning.Pub services to ATM state machine
- Add lightning.ts services module with real LightningPubClient and CLINKClient - Connect to local relay (ws://localhost:7777) and Lightning.Pub on init - Generate real CLINK noffers for cash-in flow - Generate real Lightning invoices for cash-out flow - Add connection status indicator (Live/Connecting/Offline badge) - Add toggle between live and mock services in debug panel - Fall back to mock services on connection failure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f068cdf01d
commit
687717bfce
3 changed files with 283 additions and 16 deletions
|
|
@ -1,13 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useAtmStore } from '@/stores/atm'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const atmStore = useAtmStore()
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize the ATM state machine
|
||||
atmStore.initialize()
|
||||
onMounted(async () => {
|
||||
// Initialize the ATM state machine with Lightning.Pub
|
||||
await atmStore.initializeWithLightning()
|
||||
})
|
||||
|
||||
function toggleLiveServices() {
|
||||
if (atmStore.useLiveServices) {
|
||||
// Switch to mock
|
||||
atmStore.initialize()
|
||||
} else {
|
||||
// Switch to live
|
||||
atmStore.initializeWithLightning()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -16,12 +28,37 @@ onMounted(() => {
|
|||
>
|
||||
<router-view />
|
||||
|
||||
<!-- Connection status (top right) -->
|
||||
<div class="fixed right-4 top-4 z-50 flex items-center gap-2">
|
||||
<Badge
|
||||
v-if="atmStore.connectionStatus === 'connected'"
|
||||
class="bg-success text-success-foreground"
|
||||
>
|
||||
Live
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="atmStore.connectionStatus === 'connecting'"
|
||||
class="bg-warning text-warning-foreground"
|
||||
>
|
||||
Connecting...
|
||||
</Badge>
|
||||
<Badge v-else-if="atmStore.connectionStatus === 'error'" class="bg-destructive">
|
||||
Offline
|
||||
</Badge>
|
||||
<Badge v-else class="bg-muted text-muted-foreground">Mock</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Debug overlay (dev only) -->
|
||||
<div
|
||||
v-if="atmStore.debugMode"
|
||||
class="fixed inset-x-0 bottom-0 z-50 max-h-52 overflow-auto bg-popover p-4 font-mono text-xs text-popover-foreground"
|
||||
>
|
||||
<div class="mb-2 text-success">State: {{ atmStore.currentState }}</div>
|
||||
<div class="mb-2 flex items-center gap-4">
|
||||
<span class="text-success">State: {{ atmStore.currentState }}</span>
|
||||
<Button variant="outline" size="sm" class="h-6 text-xs" @click="toggleLiveServices">
|
||||
{{ atmStore.useLiveServices ? 'Switch to Mock' : 'Connect Live' }}
|
||||
</Button>
|
||||
</div>
|
||||
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
201
lamassu-next/apps/machine/src/services/lightning.ts
Normal file
201
lamassu-next/apps/machine/src/services/lightning.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Lightning Services
|
||||
*
|
||||
* Connects to Lightning.Pub and provides real ATMServices implementation.
|
||||
*/
|
||||
|
||||
import { NostrClient, generateIdentity, type MachineIdentity } from '@lamassu/nostr-client'
|
||||
import { LightningPubClient } from '@lamassu/lightning'
|
||||
import { CLINKClient } from '@lamassu/clink'
|
||||
import type { ATMServices, ATMContext } from '@lamassu/state-machine'
|
||||
|
||||
// Development infrastructure configuration
|
||||
// In production, these would come from environment or secure config
|
||||
const DEV_CONFIG = {
|
||||
// Lightning.Pub Nostr pubkey (from docker logs)
|
||||
lightningPubPubkey: '0943fe710a0f48c77a0bdd11bd186d320cd1b01615cf9ec36fa2dc45bd86e92f',
|
||||
// Local strfry relay
|
||||
relayUrl: 'ws://localhost:7777',
|
||||
// Admin token for HTTP API (development only)
|
||||
adminToken: 'lamassu-dev-admin-token',
|
||||
// Lightning.Pub HTTP API
|
||||
lightningPubApiUrl: 'http://localhost:1776',
|
||||
}
|
||||
|
||||
interface LightningServices {
|
||||
nostrClient: NostrClient
|
||||
lightningPub: LightningPubClient
|
||||
clink: CLINKClient
|
||||
identity: MachineIdentity
|
||||
atmServices: ATMServices
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Lightning services
|
||||
*/
|
||||
export async function initializeLightningServices(): Promise<LightningServices> {
|
||||
console.log('[Lightning] Initializing services...')
|
||||
|
||||
// Generate a machine identity (in production, load from secure storage)
|
||||
const identity = generateIdentity()
|
||||
console.log('[Lightning] Machine identity:', identity.publicKey.slice(0, 16) + '...')
|
||||
|
||||
// Create Nostr client
|
||||
const nostrClient = new NostrClient({
|
||||
relays: [{ url: DEV_CONFIG.relayUrl }],
|
||||
identity,
|
||||
})
|
||||
|
||||
// Connect to relay
|
||||
await nostrClient.connect()
|
||||
console.log('[Lightning] Connected to relay:', DEV_CONFIG.relayUrl)
|
||||
|
||||
// Create Lightning.Pub client
|
||||
const lightningPub = new LightningPubClient({
|
||||
accountPubkey: DEV_CONFIG.lightningPubPubkey,
|
||||
relays: [DEV_CONFIG.relayUrl],
|
||||
})
|
||||
|
||||
lightningPub.initialize(nostrClient, identity)
|
||||
console.log('[Lightning] Lightning.Pub client initialized')
|
||||
|
||||
// Create CLINK client
|
||||
const clink = new CLINKClient({
|
||||
nostrClient,
|
||||
identity,
|
||||
operatorPubkey: DEV_CONFIG.lightningPubPubkey, // Use Lightning.Pub as operator for dev
|
||||
relays: [DEV_CONFIG.relayUrl],
|
||||
})
|
||||
|
||||
clink.startListening()
|
||||
console.log('[Lightning] CLINK client initialized')
|
||||
|
||||
// Create ATM services
|
||||
const atmServices = createATMServices(lightningPub, clink, identity)
|
||||
|
||||
return {
|
||||
nostrClient,
|
||||
lightningPub,
|
||||
clink,
|
||||
identity,
|
||||
atmServices,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ATMServices implementation using real Lightning clients
|
||||
*/
|
||||
function createATMServices(
|
||||
lightningPub: LightningPubClient,
|
||||
clink: CLINKClient,
|
||||
_identity: MachineIdentity
|
||||
): ATMServices {
|
||||
return {
|
||||
/**
|
||||
* Generate a CLINK offer (noffer) for receiving payment
|
||||
*
|
||||
* For cash-in: customer scans QR to receive sats
|
||||
*/
|
||||
generateClinkOffer: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating CLINK offer for', context.satsAmount, 'sats')
|
||||
|
||||
// Create a noffer for this specific amount
|
||||
const noffer = clink.createOffer({
|
||||
priceType: 'fixed',
|
||||
amountSats: context.satsAmount,
|
||||
description: `Lamassu ATM - ${context.satsAmount} sats`,
|
||||
minAmountSats: context.satsAmount,
|
||||
maxAmountSats: context.satsAmount,
|
||||
})
|
||||
|
||||
console.log('[ATM Service] Generated noffer:', noffer.slice(0, 32) + '...')
|
||||
return noffer
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a Lightning invoice for payment
|
||||
*
|
||||
* For cash-out: customer pays this invoice to receive cash
|
||||
*/
|
||||
generateInvoice: async (amountMsat: number): Promise<string> => {
|
||||
console.log('[ATM Service] Generating invoice for', amountMsat, 'msats')
|
||||
|
||||
const amountSats = Math.floor(amountMsat / 1000)
|
||||
|
||||
try {
|
||||
const invoice = await lightningPub.createInvoice({
|
||||
amountSats,
|
||||
description: 'Lamassu ATM - Cash Out',
|
||||
})
|
||||
|
||||
console.log('[ATM Service] Generated invoice:', invoice.paymentRequest.slice(0, 32) + '...')
|
||||
return invoice.paymentRequest
|
||||
} catch (error) {
|
||||
console.error('[ATM Service] Failed to generate invoice:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current exchange rate
|
||||
*/
|
||||
getExchangeRate: async (currency: string): Promise<number> => {
|
||||
console.log('[ATM Service] Fetching exchange rate for', currency)
|
||||
|
||||
try {
|
||||
const rate = await lightningPub.getExchangeRate(currency)
|
||||
console.log('[ATM Service] Exchange rate:', rate.satsPerUnit, 'sats/' + currency)
|
||||
return rate.satsPerUnit
|
||||
} catch (error) {
|
||||
console.warn('[ATM Service] Failed to fetch rate, using fallback:', error)
|
||||
// Fallback rate for development
|
||||
return 2500 // sats per USD (~$40k BTC)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send receipt via Nostr DM
|
||||
*/
|
||||
sendNostrReceipt: async (context: ATMContext): Promise<void> => {
|
||||
console.log('[ATM Service] Sending receipt to', context.userNpub)
|
||||
|
||||
if (!context.userNpub) {
|
||||
console.log('[ATM Service] No user npub, skipping receipt')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement NIP-04/NIP-44 encrypted DM with receipt details
|
||||
console.log('[ATM Service] Receipt sending not yet implemented')
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispense cash (mock for development)
|
||||
*/
|
||||
dispenseCash: async (amounts: { denomination: number; count: number }[]): Promise<void> => {
|
||||
console.log('[ATM Service] Dispensing cash:', amounts)
|
||||
|
||||
// In production, this would interface with the Rust HAL
|
||||
// For now, simulate dispense delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
console.log('[ATM Service] Cash dispensed successfully')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for invoice payment
|
||||
*/
|
||||
export function watchInvoice(
|
||||
lightningPub: LightningPubClient,
|
||||
paymentHash: string,
|
||||
onPaid: (preimage: string) => void
|
||||
): void {
|
||||
lightningPub.watchInvoice(paymentHash, (invoice) => {
|
||||
if (invoice.preimage) {
|
||||
onPaid(invoice.preimage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { DEV_CONFIG }
|
||||
|
|
@ -5,48 +5,48 @@ import {
|
|||
createActor,
|
||||
type ATMContext,
|
||||
type ATMServices,
|
||||
type ActorRefFrom,
|
||||
type SnapshotFrom,
|
||||
type ATMMachine,
|
||||
} from '@lamassu/state-machine'
|
||||
import type { Actor, SnapshotFrom } from 'xstate'
|
||||
import { initializeLightningServices } from '@/services/lightning'
|
||||
|
||||
// Service implementations will be injected
|
||||
// For now, use mock implementations for development
|
||||
// Mock services for development/fallback
|
||||
const mockServices: ATMServices = {
|
||||
generateClinkOffer: async (context) => {
|
||||
console.log('[Mock] Generating CLINK offer for', context.satsAmount, 'sats')
|
||||
// Return a fake noffer for development
|
||||
return `noffer1mock${Date.now().toString(36)}`
|
||||
},
|
||||
|
||||
generateInvoice: async (amountMsat) => {
|
||||
console.log('[Mock] Generating invoice for', amountMsat, 'msats')
|
||||
// Return a fake invoice for development
|
||||
return `lnbc${amountMsat}n1mock${Date.now().toString(36)}`
|
||||
},
|
||||
|
||||
getExchangeRate: async (currency) => {
|
||||
console.log('[Mock] Fetching exchange rate for', currency)
|
||||
// Return mock rate: 2500 sats per USD (40k USD/BTC)
|
||||
return 2500
|
||||
},
|
||||
|
||||
sendNostrReceipt: async (context) => {
|
||||
console.log('[Mock] Sending Nostr receipt to', context.userNpub)
|
||||
return true
|
||||
},
|
||||
|
||||
dispenseCash: async (amounts) => {
|
||||
console.log('[Mock] Dispensing cash:', amounts)
|
||||
// Simulate dispense delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
export const useAtmStore = defineStore('atm', () => {
|
||||
// State
|
||||
const actor = ref<Actor<ReturnType<typeof createATMMachine>> | null>(null)
|
||||
const snapshot = ref<SnapshotFrom<ReturnType<typeof createATMMachine>> | null>(null)
|
||||
const actor = ref<ActorRefFrom<ATMMachine> | null>(null)
|
||||
const snapshot = ref<SnapshotFrom<ATMMachine> | null>(null)
|
||||
const debugMode = ref(import.meta.env.DEV)
|
||||
const useLiveServices = ref(false)
|
||||
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>(
|
||||
'disconnected'
|
||||
)
|
||||
|
||||
// Computed
|
||||
const currentState = computed(() => {
|
||||
|
|
@ -89,7 +89,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
actor.value = createActor(machine)
|
||||
|
||||
// Subscribe to state changes
|
||||
actor.value.subscribe((newSnapshot) => {
|
||||
actor.value.subscribe((newSnapshot: SnapshotFrom<ATMMachine>) => {
|
||||
snapshot.value = newSnapshot
|
||||
console.log('[ATM] State:', newSnapshot.value)
|
||||
})
|
||||
|
|
@ -99,6 +99,32 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.log('[ATM] State machine initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with real Lightning.Pub services
|
||||
*/
|
||||
async function initializeWithLightning() {
|
||||
connectionStatus.value = 'connecting'
|
||||
console.log('[ATM] Connecting to Lightning.Pub...')
|
||||
|
||||
try {
|
||||
const services = await initializeLightningServices()
|
||||
useLiveServices.value = true
|
||||
connectionStatus.value = 'connected'
|
||||
console.log('[ATM] Connected to Lightning.Pub!')
|
||||
|
||||
// Initialize state machine with real services
|
||||
initialize(services.atmServices)
|
||||
} catch (error) {
|
||||
console.error('[ATM] Failed to connect to Lightning.Pub:', error)
|
||||
connectionStatus.value = 'error'
|
||||
|
||||
// Fall back to mock services
|
||||
console.log('[ATM] Falling back to mock services')
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
}
|
||||
}
|
||||
|
||||
function send(event: Parameters<NonNullable<typeof actor.value>['send']>[0]) {
|
||||
if (!actor.value) {
|
||||
console.error('[ATM] Cannot send event: machine not initialized')
|
||||
|
|
@ -154,6 +180,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
actor,
|
||||
snapshot,
|
||||
debugMode,
|
||||
useLiveServices,
|
||||
connectionStatus,
|
||||
|
||||
// Computed
|
||||
currentState,
|
||||
|
|
@ -165,6 +193,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
|
||||
// Actions
|
||||
initialize,
|
||||
initializeWithLightning,
|
||||
send,
|
||||
selectCashIn,
|
||||
selectCashOut,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue