fix(machine): production hardware fixes for Douro cash-out
- Fix cassette denominations: Douro uses Q100/Q200, not Q20 - Add hal:get-inventory IPC so renderer can read HAL cassette inventory - Add balance fetch/display to HAL+IPC init path and idle screen - Enable/disable bill validator via watch on nested state transitions - Pass fiatCode to state machine context (was hardcoded to USD) - Preserve currency across state machine resetContext - Add CANCEL handler to dispenseError state (was stuck) - Fix remaining hardcoded $ symbols in CashInView Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c874d9e2e3
commit
0ea9640f49
9 changed files with 102 additions and 18 deletions
|
|
@ -214,6 +214,11 @@ ipcMain.handle('hal:reject-bill', () => {
|
|||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:get-inventory', () => {
|
||||
if (!halInstance) return {}
|
||||
return halInstance.getInventory()
|
||||
})
|
||||
|
||||
ipcMain.handle('hal:cleanup', async () => {
|
||||
if (halInstance) {
|
||||
await halInstance.cleanup()
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
halDisableValidator: (): Promise<void> => ipcRenderer.invoke('hal:disable-validator'),
|
||||
halStackBill: (): Promise<void> => ipcRenderer.invoke('hal:stack-bill'),
|
||||
halRejectBill: (): Promise<void> => ipcRenderer.invoke('hal:reject-bill'),
|
||||
halGetInventory: (): Promise<Record<number, number>> => ipcRenderer.invoke('hal:get-inventory'),
|
||||
halCleanup: (): Promise<void> => ipcRenderer.invoke('hal:cleanup'),
|
||||
|
||||
// HAL event listeners (main process → renderer)
|
||||
|
|
@ -106,6 +107,7 @@ declare global {
|
|||
halDisableValidator: () => Promise<void>
|
||||
halStackBill: () => Promise<void>
|
||||
halRejectBill: () => Promise<void>
|
||||
halGetInventory: () => Promise<Record<number, number>>
|
||||
halCleanup: () => Promise<void>
|
||||
onHalBillRead: (callback: (denomination: number) => void) => void
|
||||
onHalBillInserted: (callback: (denomination: number) => void) => void
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@ export const MACHINE_PRESETS: Record<MachineModel, Omit<DeviceConfig, 'fiatCode'
|
|||
dispenser: {
|
||||
type: 'puloon',
|
||||
device: '/dev/ttyS1',
|
||||
cassettes: [{ denomination: 20, count: 50 }],
|
||||
cassettes: [
|
||||
{ denomination: 100, count: 50 },
|
||||
{ denomination: 200, count: 50 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
createATMMachine,
|
||||
createActor,
|
||||
|
|
@ -132,8 +132,9 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// State
|
||||
const actor = ref<ActorRefFrom<ATMMachine> | null>(null)
|
||||
const snapshot = ref<SnapshotFrom<ATMMachine> | null>(null)
|
||||
// Always on until real hardware bill validator is connected
|
||||
// Show mock bill simulator when no real hardware
|
||||
const debugMode = ref(true)
|
||||
const fiatCode = ref('USD')
|
||||
const useLiveServices = ref(false)
|
||||
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>(
|
||||
'disconnected'
|
||||
|
|
@ -164,6 +165,11 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
return snapshot.value?.context ?? null
|
||||
})
|
||||
|
||||
const fiatSymbol = computed(() => {
|
||||
const symbols: Record<string, string> = { USD: '$', GTQ: 'Q', EUR: '€', GBP: '£', MXN: '$' }
|
||||
return symbols[fiatCode.value] || fiatCode.value
|
||||
})
|
||||
|
||||
const isIdle = computed(() => currentState.value === 'idle')
|
||||
|
||||
const isCashIn = computed(() => {
|
||||
|
|
@ -189,7 +195,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// Actions
|
||||
function initialize(services: ATMServices = mockServices) {
|
||||
atmServicesRef = services
|
||||
const machine = createATMMachine(services)
|
||||
const machine = createATMMachine(services, { currency: fiatCode.value })
|
||||
actor.value = createActor(machine)
|
||||
|
||||
// Track previous state to detect transitions into 'complete'
|
||||
|
|
@ -620,7 +626,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// Get runtime config from Electron main process (.env file)
|
||||
const runtimeConfig = await api.getConfig()
|
||||
const model = (runtimeConfig.machineModel || 'sintra') as MachineModel
|
||||
const fiatCode = runtimeConfig.fiatCode || 'USD'
|
||||
const runtimeFiatCode = runtimeConfig.fiatCode || 'USD'
|
||||
fiatCode.value = runtimeFiatCode
|
||||
|
||||
// Build device config from runtime values
|
||||
const { getDeviceConfig, toHalConfig } = await import('@/config')
|
||||
|
|
@ -644,7 +651,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const devConfig = getDeviceConfig(model, fiatCode, overrides)
|
||||
const devConfig = getDeviceConfig(model, runtimeFiatCode, overrides)
|
||||
console.log('[ATM] Machine model:', devConfig.model)
|
||||
console.log('[ATM] Fiat currency:', devConfig.fiatCode)
|
||||
console.log('[ATM] Validator device:', devConfig.validator.device)
|
||||
|
|
@ -685,6 +692,21 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
lightningPub.value = lightning.lightningPub
|
||||
clinkClient.value = lightning.clink
|
||||
|
||||
// Fetch initial balance and subscribe to live updates
|
||||
try {
|
||||
const { balanceSats: initial } = await lightning.lightningPub.getBalance()
|
||||
balanceSats.value = initial
|
||||
console.log('[ATM] Initial balance:', initial, 'sats')
|
||||
} catch (e) {
|
||||
console.warn('[ATM] Failed to fetch initial balance:', e)
|
||||
}
|
||||
|
||||
stopBalanceWatch?.()
|
||||
stopBalanceWatch = lightning.lightningPub.watchBalance((newBalance) => {
|
||||
balanceSats.value = newBalance
|
||||
console.log('[ATM] Balance updated:', newBalance, 'sats')
|
||||
})
|
||||
|
||||
// Wire payment callbacks
|
||||
lightning.onPaymentReceived((preimage) => {
|
||||
paymentReceived(preimage)
|
||||
|
|
@ -711,8 +733,20 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
await api.halDispense(amounts)
|
||||
},
|
||||
getInventory: async () => {
|
||||
// Priority: DB inventory > HAL hardware inventory > empty
|
||||
const fresh = await loadInventoryFromDb()
|
||||
return Object.keys(fresh).length > 0 ? fresh : {}
|
||||
if (Object.keys(fresh).length > 0) return fresh
|
||||
// Fall back to HAL's cassette-based inventory
|
||||
try {
|
||||
const halInv = await api.halGetInventory()
|
||||
if (Object.keys(halInv).length > 0) {
|
||||
console.log('[ATM] Using HAL hardware inventory:', halInv)
|
||||
return halInv
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ATM] Failed to get HAL inventory:', e)
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -768,6 +802,20 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
send({ type: 'ERROR', error })
|
||||
})
|
||||
|
||||
// Watch state transitions to enable/disable validator hardware
|
||||
watch(nestedState, (newState, oldState) => {
|
||||
if (newState === 'insertingBills' && oldState !== 'insertingBills') {
|
||||
console.log('[ATM] Enabling bill validator')
|
||||
api.halEnableValidator()
|
||||
}
|
||||
if (oldState === 'insertingBills' && newState !== 'insertingBills') {
|
||||
console.log('[ATM] Disabling bill validator')
|
||||
api.halDisableValidator()
|
||||
}
|
||||
})
|
||||
|
||||
// Real hardware connected — disable mock bill simulator
|
||||
debugMode.value = false
|
||||
console.log('[ATM] Fully initialized with HAL (IPC) + Lightning')
|
||||
} catch (error) {
|
||||
console.error('[ATM] HAL initialization failed:', error)
|
||||
|
|
@ -871,6 +919,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
actor,
|
||||
snapshot,
|
||||
debugMode,
|
||||
fiatCode,
|
||||
useLiveServices,
|
||||
connectionStatus,
|
||||
balanceSats,
|
||||
|
|
@ -882,6 +931,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// Computed
|
||||
currentState,
|
||||
context,
|
||||
fiatSymbol,
|
||||
isIdle,
|
||||
isCashIn,
|
||||
isCashOut,
|
||||
|
|
|
|||
1
apps/machine/src/types/electron.d.ts
vendored
1
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -45,6 +45,7 @@ declare global {
|
|||
halDisableValidator: () => Promise<void>
|
||||
halStackBill: () => Promise<void>
|
||||
halRejectBill: () => Promise<void>
|
||||
halGetInventory: () => Promise<Record<number, number>>
|
||||
halCleanup: () => Promise<void>
|
||||
onHalBillRead: (callback: (denomination: number) => void) => void
|
||||
onHalBillInserted: (callback: (denomination: number) => void) => void
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
<CardHeader class="text-center">
|
||||
<CardTitle>Insert Cash</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
Rate: {{ formatSats(context.exchangeRate) }} sats/$
|
||||
Rate: {{ formatSats(context.exchangeRate) }} sats/{{ atmStore.fiatSymbol }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
|
|
@ -223,7 +223,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
<div class="mb-2 flex justify-between">
|
||||
<span class="text-muted-foreground">Inserted:</span>
|
||||
<span class="text-2xl font-bold text-green-400"
|
||||
>${{ context.fiatAmount / 100 }}</span
|
||||
>{{ atmStore.fiatSymbol }}{{ context.fiatAmount / 100 }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
|
|
@ -267,7 +267,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
:disabled="!canAcceptBill(denom)"
|
||||
@click="simulateBill(denom)"
|
||||
>
|
||||
${{ denom }}
|
||||
{{ atmStore.fiatSymbol }}{{ denom }}
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
|
|
@ -308,7 +308,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
|
|||
{{ formatSats(context?.satsAmount || 0) }} sats
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
${{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
|
||||
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,10 @@ function formatFiat(cents: number): string {
|
|||
<CardHeader class="text-center">
|
||||
<CardTitle>Select Cash Amount</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
Rate: <Badge variant="secondary">{{ formatSats(context.exchangeRate) }} sats/$</Badge>
|
||||
Rate:
|
||||
<Badge variant="secondary"
|
||||
>{{ formatSats(context.exchangeRate) }} sats/{{ atmStore.fiatSymbol }}</Badge
|
||||
>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
|
|
@ -135,7 +138,9 @@ function formatFiat(cents: number): string {
|
|||
:key="denom"
|
||||
class="flex items-center justify-between rounded-lg bg-white/5 px-4 py-3 sm:p-4"
|
||||
>
|
||||
<span class="text-xl font-bold text-green-400 sm:text-2xl">${{ denom }}</span>
|
||||
<span class="text-xl font-bold text-green-400 sm:text-2xl"
|
||||
>{{ atmStore.fiatSymbol }}{{ denom }}</span
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -166,7 +171,7 @@ function formatFiat(cents: number): string {
|
|||
<div class="rounded-lg bg-white/10 p-6 text-center">
|
||||
<p class="text-sm text-muted-foreground">You will receive</p>
|
||||
<p class="text-4xl font-bold text-green-400">
|
||||
${{ context ? formatFiat(context.fiatAmount) : '0.00' }}
|
||||
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
You pay:
|
||||
|
|
@ -226,7 +231,10 @@ function formatFiat(cents: number): string {
|
|||
<CardTitle>Scan to Pay</CardTitle>
|
||||
<CardDescription v-if="context">
|
||||
Pay <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> to
|
||||
receive <Badge class="bg-green-600">${{ formatFiat(context.fiatAmount) }}</Badge>
|
||||
receive
|
||||
<Badge class="bg-green-600"
|
||||
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
|
||||
>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
|
|
@ -296,7 +304,9 @@ function formatFiat(cents: number): string {
|
|||
<CardTitle class="text-green-400">Cash Ready!</CardTitle>
|
||||
<p v-if="context">
|
||||
Please take your
|
||||
<Badge class="bg-green-600 text-lg">${{ formatFiat(context.fiatAmount) }}</Badge>
|
||||
<Badge class="bg-green-600 text-lg"
|
||||
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
|
||||
>
|
||||
</p>
|
||||
<Button
|
||||
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black hover:from-orange-600 hover:to-yellow-500"
|
||||
|
|
|
|||
|
|
@ -84,6 +84,14 @@ function handleCashOut() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Balance -->
|
||||
<div v-if="atmStore.balanceSats !== null" class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Available Balance</p>
|
||||
<p class="text-xl font-bold text-primary sm:text-2xl">
|
||||
{{ atmStore.balanceSats.toLocaleString() }} sats
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:gap-6 md:gap-8">
|
||||
<!-- Buy Bitcoin -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue