feat(machine): production safety — disable mock fallback and ndebit
When VITE_ALLOW_MOCK_FALLBACK is unset (production default), the ATM now shows a maintenance screen instead of silently falling back to mock services when hardware or Lightning initialization fails. Also disables ndebit/CLINK in production since the static ndebit pointer is replayable — cash-in uses LNURL-withdraw only (single-use by design). - Add allowMockFallback config field (Electron IPC + types) - Add strict config validation (no localhost, require private key) - Gate all catch-block fallbacks behind allowMockFallback - Disable debit approval service and ndebit generation in production - Add maintenance screen in App.vue when initError is set Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
34179a4e34
commit
e460765355
7 changed files with 238 additions and 120 deletions
|
|
@ -41,6 +41,15 @@ VITE_LIGHTNING_PUB_API_URL=http://localhost:1776
|
|||
# If not set, generates ephemeral identity on each restart
|
||||
VITE_ATM_PRIVATE_KEY=
|
||||
|
||||
# =============================================================================
|
||||
# Mock Fallback (Production Safety)
|
||||
# =============================================================================
|
||||
|
||||
# Allow fallback to mock services when hardware/Lightning fails (default: false)
|
||||
# Set to 'true' for development/demo environments only
|
||||
# When false (production default), initialization failures show a maintenance screen
|
||||
# VITE_ALLOW_MOCK_FALLBACK=true
|
||||
|
||||
# =============================================================================
|
||||
# Development Only
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ ipcMain.handle('get-config', () => {
|
|||
validatorDevice: process.env.VITE_LAMASSU_VALIDATOR_DEVICE,
|
||||
dispenserDevice: process.env.VITE_LAMASSU_DISPENSER_DEVICE,
|
||||
cassettes: process.env.VITE_LAMASSU_CASSETTES,
|
||||
allowMockFallback: process.env.VITE_ALLOW_MOCK_FALLBACK === 'true',
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface RuntimeConfig {
|
|||
validatorDevice?: string
|
||||
dispenserDevice?: string
|
||||
cassettes?: string
|
||||
allowMockFallback: boolean
|
||||
}
|
||||
|
||||
// Expose protected methods to renderer
|
||||
|
|
|
|||
|
|
@ -25,12 +25,17 @@ const formattedBtcPrice = computed(() => {
|
|||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (isElectron) {
|
||||
await atmStore.initializeForProduction()
|
||||
} else {
|
||||
await atmStore.initializeWithLightning()
|
||||
try {
|
||||
if (isElectron) {
|
||||
await atmStore.initializeForProduction()
|
||||
} else {
|
||||
await atmStore.initializeWithLightning()
|
||||
}
|
||||
atmStore.startPricePolling()
|
||||
} catch (error) {
|
||||
console.error('[App] Initialization failed:', error)
|
||||
atmStore.initError = error instanceof Error ? error.message : 'Initialization failed'
|
||||
}
|
||||
atmStore.startPricePolling()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -50,116 +55,148 @@ function toggleLiveServices() {
|
|||
|
||||
<template>
|
||||
<div class="flex h-dvh w-screen flex-col overflow-hidden bg-background font-sans text-foreground">
|
||||
<router-view />
|
||||
|
||||
<!-- Connection status + balance (top right) -->
|
||||
<!-- Maintenance screen: shown when initialization fails in production -->
|
||||
<div
|
||||
class="fixed right-2 top-2 z-50 flex flex-wrap items-center justify-end gap-1 sm:right-4 sm:top-4 sm:gap-2"
|
||||
v-if="atmStore.initError"
|
||||
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
|
||||
>
|
||||
<Badge
|
||||
v-if="formattedBtcPrice"
|
||||
class="bg-primary/10 font-mono text-primary border border-primary/30 text-xs sm:text-sm"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-24 w-24 text-warning"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
{{ formattedBtcPrice }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="atmStore.balanceSats !== null"
|
||||
class="bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-xs sm:text-sm"
|
||||
>
|
||||
bal: {{ formatSats(atmStore.balanceSats) }} sats
|
||||
</Badge>
|
||||
<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>
|
||||
<Badge
|
||||
v-if="network !== 'mainnet'"
|
||||
class="bg-warning/20 text-warning border border-warning/30"
|
||||
>
|
||||
{{ network }}
|
||||
</Badge>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<h1 class="text-3xl font-bold">ATM Unavailable</h1>
|
||||
<p class="max-w-md text-center text-lg text-muted-foreground">
|
||||
This machine is temporarily out of service. Please try again later or use another machine.
|
||||
</p>
|
||||
<p v-if="atmStore.debugMode" class="max-w-lg text-center font-mono text-sm text-destructive">
|
||||
{{ atmStore.initError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Debug overlay (dev only) -->
|
||||
<div
|
||||
v-if="atmStore.debugMode"
|
||||
class="shrink-0 bg-popover pb-[env(safe-area-inset-bottom)] font-mono text-xs text-popover-foreground"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 px-4 py-2 sm:gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 px-1 text-xs text-muted-foreground"
|
||||
@click="debugExpanded = !debugExpanded"
|
||||
<template v-else>
|
||||
<router-view />
|
||||
|
||||
<!-- Connection status + balance (top right) -->
|
||||
<div
|
||||
class="fixed right-2 top-2 z-50 flex flex-wrap items-center justify-end gap-1 sm:right-4 sm:top-4 sm:gap-2"
|
||||
>
|
||||
<Badge
|
||||
v-if="formattedBtcPrice"
|
||||
class="bg-primary/10 font-mono text-primary border border-primary/30 text-xs sm:text-sm"
|
||||
>
|
||||
{{ debugExpanded ? '▼' : '▲' }}
|
||||
</Button>
|
||||
<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 class="flex flex-wrap items-center gap-1">
|
||||
{{ formattedBtcPrice }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="atmStore.balanceSats !== null"
|
||||
class="bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-xs sm:text-sm"
|
||||
>
|
||||
bal: {{ formatSats(atmStore.balanceSats) }} sats
|
||||
</Badge>
|
||||
<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>
|
||||
<Badge
|
||||
v-if="network !== 'mainnet'"
|
||||
class="bg-warning/20 text-warning border border-warning/30"
|
||||
>
|
||||
{{ network }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Debug overlay (dev only) -->
|
||||
<div
|
||||
v-if="atmStore.debugMode"
|
||||
class="shrink-0 bg-popover pb-[env(safe-area-inset-bottom)] font-mono text-xs text-popover-foreground"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 px-4 py-2 sm:gap-4">
|
||||
<Button
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:variant="currentTheme === theme.id ? 'default' : 'outline'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="currentTheme = theme.id"
|
||||
class="h-5 px-1 text-xs text-muted-foreground"
|
||||
@click="debugExpanded = !debugExpanded"
|
||||
>
|
||||
{{ theme.label }}
|
||||
{{ debugExpanded ? '▼' : '▲' }}
|
||||
</Button>
|
||||
<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 class="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
v-for="theme in themes"
|
||||
:key="theme.id"
|
||||
:variant="currentTheme === theme.id ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="currentTheme = theme.id"
|
||||
>
|
||||
{{ theme.label }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
:variant="colorMode === 'light' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'light'"
|
||||
>
|
||||
Light
|
||||
</Button>
|
||||
<Button
|
||||
:variant="colorMode === 'dark' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'dark'"
|
||||
>
|
||||
Dark
|
||||
</Button>
|
||||
<Button
|
||||
:variant="colorMode === 'system' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'system'"
|
||||
>
|
||||
System
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="ml-auto h-6 text-xs text-muted-foreground"
|
||||
@click="atmStore.toggleDebug"
|
||||
>
|
||||
Hide Debug
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
:variant="colorMode === 'light' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'light'"
|
||||
>
|
||||
Light
|
||||
</Button>
|
||||
<Button
|
||||
:variant="colorMode === 'dark' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'dark'"
|
||||
>
|
||||
Dark
|
||||
</Button>
|
||||
<Button
|
||||
:variant="colorMode === 'system' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@click="colorMode = 'system'"
|
||||
>
|
||||
System
|
||||
</Button>
|
||||
<div v-if="debugExpanded" class="max-h-48 overflow-auto px-4 pb-4">
|
||||
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="ml-auto h-6 text-xs text-muted-foreground"
|
||||
@click="atmStore.toggleDebug"
|
||||
>
|
||||
Hide Debug
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="debugExpanded" class="max-h-48 overflow-auto px-4 pb-4">
|
||||
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -768,7 +768,9 @@ type PaymentReceivedCallback = (preimage: string) => void
|
|||
/**
|
||||
* Initialize Lightning services
|
||||
*/
|
||||
export async function initializeLightningServices(): Promise<LightningServices> {
|
||||
export async function initializeLightningServices(options?: {
|
||||
strict?: boolean
|
||||
}): Promise<LightningServices> {
|
||||
console.log('[Lightning] Initializing services...')
|
||||
|
||||
// Load configuration (async for Electron runtime config)
|
||||
|
|
@ -777,6 +779,26 @@ export async function initializeLightningServices(): Promise<LightningServices>
|
|||
console.log('[Lightning] Relay URL:', CONFIG.relayUrl)
|
||||
console.log('[Lightning] Lightning.Pub pubkey:', CONFIG.lightningPubPubkey || '(not configured)')
|
||||
|
||||
// Strict mode: validate config is production-ready (no localhost, no ephemeral identity)
|
||||
if (options?.strict) {
|
||||
const errors: string[] = []
|
||||
if (/localhost|127\.0\.0\.1/.test(CONFIG.relayUrl)) {
|
||||
errors.push('VITE_RELAY_URL contains localhost')
|
||||
}
|
||||
if (/localhost|127\.0\.0\.1/.test(CONFIG.lightningPubApiUrl)) {
|
||||
errors.push('VITE_LIGHTNING_PUB_API_URL contains localhost')
|
||||
}
|
||||
if (!CONFIG.atmPrivateKey) {
|
||||
errors.push('VITE_ATM_PRIVATE_KEY is not set (ephemeral identity not allowed in production)')
|
||||
}
|
||||
if (!CONFIG.lightningPubPubkey) {
|
||||
errors.push('VITE_LIGHTNING_PUB_PUBKEY is not set')
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new Error('[Lightning] Production config validation failed:\n- ' + errors.join('\n- '))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required configuration
|
||||
if (!CONFIG.lightningPubPubkey) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
const snapshot = ref<SnapshotFrom<ATMMachine> | null>(null)
|
||||
// Show mock bill simulator when no real hardware
|
||||
const debugMode = ref(true)
|
||||
const allowMockFallback = ref(true) // default true for browser dev
|
||||
const initError = ref<string | null>(null) // fatal error → maintenance screen
|
||||
const fiatCode = ref('USD')
|
||||
const useLiveServices = ref(false)
|
||||
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>(
|
||||
|
|
@ -274,7 +276,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.log('[ATM] Connecting to Lightning.Pub...')
|
||||
|
||||
try {
|
||||
const services = await initializeLightningServices()
|
||||
const services = await initializeLightningServices({ strict: !allowMockFallback.value })
|
||||
useLiveServices.value = true
|
||||
connectionStatus.value = 'connected'
|
||||
console.log('[ATM] Connected to Lightning.Pub!')
|
||||
|
|
@ -334,9 +336,18 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
}
|
||||
})
|
||||
|
||||
// In production, disable ndebit/CLINK (security: ndebit is replayable)
|
||||
if (!allowMockFallback.value) {
|
||||
services.stopDebitApproval()
|
||||
}
|
||||
|
||||
// Inject DB-backed inventory into services
|
||||
const servicesWithInventory: ATMServices = {
|
||||
...services.atmServices,
|
||||
// In production, ndebit is disabled — return empty so QR shows LNURL only
|
||||
...(!allowMockFallback.value && {
|
||||
generateNdebit: async () => '',
|
||||
}),
|
||||
getInventory: async () => {
|
||||
const fresh = await loadInventoryFromDb()
|
||||
return Object.keys(fresh).length > 0 ? fresh : services.atmServices.getInventory()
|
||||
|
|
@ -349,10 +360,14 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
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)
|
||||
if (allowMockFallback.value) {
|
||||
// Fall back to mock services
|
||||
console.log('[ATM] Falling back to mock services')
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
} else {
|
||||
initError.value = error instanceof Error ? error.message : 'Lightning initialization failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +537,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.log('[ATM] HAL hardware initialized')
|
||||
|
||||
// Initialize Lightning services
|
||||
const lightning = await initializeLightningServices()
|
||||
const lightning = await initializeLightningServices({ strict: !allowMockFallback.value })
|
||||
useLiveServices.value = true
|
||||
connectionStatus.value = 'connected'
|
||||
lightningPub.value = lightning.lightningPub
|
||||
|
|
@ -547,10 +562,19 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
}
|
||||
})
|
||||
|
||||
// In production, disable ndebit/CLINK (security: ndebit is replayable)
|
||||
if (!allowMockFallback.value) {
|
||||
lightning.stopDebitApproval()
|
||||
}
|
||||
|
||||
// Merge HAL hardware services with Lightning payment services
|
||||
const mergedServices: ATMServices = {
|
||||
...lightning.atmServices,
|
||||
...hal.atmServices, // Override dispenseCash and getInventory with real hardware
|
||||
// In production, ndebit is disabled — return empty so QR shows LNURL only
|
||||
...(!allowMockFallback.value && {
|
||||
generateNdebit: async () => '',
|
||||
}),
|
||||
// If DB has inventory, use it; otherwise fall back to HAL
|
||||
getInventory: async () => {
|
||||
const fresh = await loadInventoryFromDb()
|
||||
|
|
@ -604,10 +628,14 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.error('[ATM] HAL initialization failed:', error)
|
||||
connectionStatus.value = 'error'
|
||||
|
||||
// Fall back to mock services
|
||||
console.log('[ATM] Falling back to mock services')
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
if (allowMockFallback.value) {
|
||||
// Fall back to mock services
|
||||
console.log('[ATM] Falling back to mock services')
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
} else {
|
||||
initError.value = error instanceof Error ? error.message : 'HAL initialization failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -628,6 +656,10 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
|
||||
// Get runtime config from Electron main process (.env file)
|
||||
const runtimeConfig = await api.getConfig()
|
||||
allowMockFallback.value = runtimeConfig.allowMockFallback
|
||||
if (!allowMockFallback.value) {
|
||||
debugMode.value = false
|
||||
}
|
||||
const model = (runtimeConfig.machineModel || 'sintra') as MachineModel
|
||||
const runtimeFiatCode = runtimeConfig.fiatCode || 'USD'
|
||||
fiatCode.value = runtimeFiatCode
|
||||
|
|
@ -689,7 +721,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
console.log('[ATM] HAL initialized in main process')
|
||||
|
||||
// Initialize Lightning services
|
||||
const lightning = await initializeLightningServices()
|
||||
const lightning = await initializeLightningServices({ strict: !allowMockFallback.value })
|
||||
useLiveServices.value = true
|
||||
connectionStatus.value = 'connected'
|
||||
lightningPub.value = lightning.lightningPub
|
||||
|
|
@ -753,10 +785,19 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
},
|
||||
}
|
||||
|
||||
// In production, disable ndebit/CLINK (security: ndebit is replayable)
|
||||
if (!allowMockFallback.value) {
|
||||
lightning.stopDebitApproval()
|
||||
}
|
||||
|
||||
// Merge HAL hardware services with Lightning payment services
|
||||
const mergedServices: ATMServices = {
|
||||
...lightning.atmServices,
|
||||
...halAtmServices,
|
||||
// In production, ndebit is disabled — return empty so QR shows LNURL only
|
||||
...(!allowMockFallback.value && {
|
||||
generateNdebit: async () => '',
|
||||
}),
|
||||
getInventory: halAtmServices.getInventory,
|
||||
}
|
||||
|
||||
|
|
@ -823,15 +864,19 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
} catch (error) {
|
||||
console.error('[ATM] HAL initialization failed:', error)
|
||||
|
||||
// Fall back to Lightning-only mode (real Lightning, mock hardware)
|
||||
console.log('[ATM] Falling back to Lightning-only mode (mock hardware)')
|
||||
try {
|
||||
await initializeWithLightning()
|
||||
} catch (lightningError) {
|
||||
console.error('[ATM] Lightning also failed:', lightningError)
|
||||
connectionStatus.value = 'error'
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
if (allowMockFallback.value) {
|
||||
// Fall back to Lightning-only mode (real Lightning, mock hardware)
|
||||
console.log('[ATM] Falling back to Lightning-only mode (mock hardware)')
|
||||
try {
|
||||
await initializeWithLightning()
|
||||
} catch (lightningError) {
|
||||
console.error('[ATM] Lightning also failed:', lightningError)
|
||||
connectionStatus.value = 'error'
|
||||
useLiveServices.value = false
|
||||
initialize(mockServices)
|
||||
}
|
||||
} else {
|
||||
initError.value = error instanceof Error ? error.message : 'Hardware initialization failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -946,6 +991,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
actor,
|
||||
snapshot,
|
||||
debugMode,
|
||||
allowMockFallback,
|
||||
initError,
|
||||
fiatCode,
|
||||
useLiveServices,
|
||||
connectionStatus,
|
||||
|
|
|
|||
1
apps/machine/src/types/electron.d.ts
vendored
1
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -15,6 +15,7 @@ export interface RuntimeConfig {
|
|||
validatorDevice?: string
|
||||
dispenserDevice?: string
|
||||
cassettes?: string
|
||||
allowMockFallback: boolean
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue