feat: configurable fee rates via VITE_CASH_IN_FEE and VITE_CASH_OUT_FEE

Accepts percentage (5.55) or decimal (0.0555) — auto-detected by
whether the value is >= 1. Defaults to 3.33% cash-in, 7.77% cash-out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-04-05 14:03:32 -04:00
commit 2a2faf41ef
4 changed files with 31 additions and 3 deletions

View file

@ -30,6 +30,12 @@ import { initializeHal, type HalInstance } from './hal-service.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
/** Parse fee value: accepts percentage (5.55 → 0.0555) or decimal (0.0555 → 0.0555) */
function parseFee(val: string): number {
const n = parseFloat(val)
return n >= 1 ? n / 100 : n
}
// Load .env file manually (Electron main process doesn't have Vite's env loading) // Load .env file manually (Electron main process doesn't have Vite's env loading)
function loadEnvFile() { function loadEnvFile() {
const envPath = path.join(__dirname, '..', '.env') const envPath = path.join(__dirname, '..', '.env')
@ -190,6 +196,10 @@ ipcMain.handle('get-config', () => {
// Maintenance mode — show "out of service" screen // Maintenance mode — show "out of service" screen
maintenanceMode: process.env.VITE_MAINTENANCE_MODE === 'true', maintenanceMode: process.env.VITE_MAINTENANCE_MODE === 'true',
// Fee rates — accepts percentage (5.55) or decimal (0.0555), auto-detected
cashInFeeRate: parseFee(process.env.VITE_CASH_IN_FEE || '0.0333'),
cashOutFeeRate: parseFee(process.env.VITE_CASH_OUT_FEE || '0.0777'),
} }
}) })

View file

@ -260,6 +260,8 @@ export const useAtmStore = defineStore('atm', () => {
const allowMockFallback = ref(true) // default true for browser dev const allowMockFallback = ref(true) // default true for browser dev
const initError = ref<string | null>(null) // fatal error → maintenance screen const initError = ref<string | null>(null) // fatal error → maintenance screen
const fiatCode = ref('USD') const fiatCode = ref('USD')
const cashInFeeRate = ref(0.0333)
const cashOutFeeRate = ref(0.0777)
const machineModel = ref('atm') const machineModel = ref('atm')
const useLiveServices = ref(false) const useLiveServices = ref(false)
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>( const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>(
@ -359,7 +361,11 @@ export const useAtmStore = defineStore('atm', () => {
// Actions // Actions
function initialize(services: ATMServices = mockServices) { function initialize(services: ATMServices = mockServices) {
atmServicesRef = services atmServicesRef = services
const machine = createATMMachine(services, { currency: fiatCode.value }) const machine = createATMMachine(services, {
currency: fiatCode.value,
cashInFeeRate: cashInFeeRate.value,
cashOutFeeRate: cashOutFeeRate.value,
})
actor.value = createActor(machine) actor.value = createActor(machine)
// Track previous state to detect transitions into 'complete' // Track previous state to detect transitions into 'complete'
@ -925,6 +931,9 @@ export const useAtmStore = defineStore('atm', () => {
machineModel.value = model machineModel.value = model
const runtimeFiatCode = runtimeConfig.fiatCode || 'USD' const runtimeFiatCode = runtimeConfig.fiatCode || 'USD'
fiatCode.value = runtimeFiatCode fiatCode.value = runtimeFiatCode
if (runtimeConfig.cashInFeeRate !== undefined) cashInFeeRate.value = runtimeConfig.cashInFeeRate
if (runtimeConfig.cashOutFeeRate !== undefined)
cashOutFeeRate.value = runtimeConfig.cashOutFeeRate
// Build device config from runtime values // Build device config from runtime values
const { getDeviceConfig, toHalConfig, MACHINE_PRESETS } = await import('@/config') const { getDeviceConfig, toHalConfig, MACHINE_PRESETS } = await import('@/config')

View file

@ -16,6 +16,8 @@ export interface RuntimeConfig {
allowMockFallback: boolean allowMockFallback: boolean
operatorPubkeys: string operatorPubkeys: string
maintenanceMode: boolean maintenanceMode: boolean
cashInFeeRate: number
cashOutFeeRate: number
} }
export interface AtmSecrets { export interface AtmSecrets {

View file

@ -20,7 +20,7 @@ import {
*/ */
export function createATMMachine( export function createATMMachine(
services: Partial<ATMServices> = {}, services: Partial<ATMServices> = {},
options?: { currency?: string } options?: { currency?: string; cashInFeeRate?: number; cashOutFeeRate?: number }
) { ) {
return setup({ return setup({
types: { types: {
@ -401,7 +401,14 @@ export function createATMMachine(
}).createMachine({ }).createMachine({
id: 'atm', id: 'atm',
initial: 'idle', initial: 'idle',
context: { ...initialContext, ...(options?.currency ? { currency: options.currency } : {}) }, context: {
...initialContext,
...(options?.currency ? { currency: options.currency } : {}),
...(options?.cashInFeeRate !== undefined ? { cashInFeePercent: options.cashInFeeRate } : {}),
...(options?.cashOutFeeRate !== undefined
? { cashOutFeePercent: options.cashOutFeeRate }
: {}),
},
states: { states: {
idle: { idle: {
entry: 'resetContext', entry: 'resetContext',