import { ipcMain, dialog, app } from 'electron' import fs from 'fs' import path from 'path' import { encrypt, fromEncryptedJson } from './utils/keystore' import logger from './logger' const neptuneNative = require('@neptune/native') const WALLETS_DIR = path.resolve(app.getPath('userData'), 'wallets') fs.mkdirSync(WALLETS_DIR, { recursive: true }) function assertBasename(name: string) { if (!name || name !== path.basename(name)) { throw new Error('Invalid file name') } } function safeResolvePath(fileName: string) { assertBasename(fileName) const candidate = path.join(WALLETS_DIR, fileName) const realBase = fs.realpathSync(WALLETS_DIR) const realCandidate = fs.existsSync(candidate) ? fs.realpathSync(candidate) : candidate if (!(realCandidate === realBase || realCandidate.startsWith(realBase + path.sep))) { throw new Error('Access denied') } return realCandidate } ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => { try { const keystore = await encrypt(seed, password) const timestamp = Date.now() const fileName = `neptune-wallet-${timestamp}.json` const filePath = path.join(WALLETS_DIR, fileName) fs.writeFileSync(filePath, keystore, 'utf-8') return { success: true, fileName } } catch (error: any) { logger.error('Error creating keystore:', error.message) throw error } }) ipcMain.handle('wallet:saveKeystoreAs', async (_event, fileName: string) => { try { const pathToFileName = safeResolvePath(fileName) const keystore = fs.readFileSync(pathToFileName, 'utf-8') const timestamp = Date.now() const defaultName = `neptune-wallet-${timestamp}.json` const { canceled, filePath } = await dialog.showSaveDialog({ title: 'Save Keystore File', defaultPath: path.join(app.getPath('documents'), defaultName), filters: [{ name: 'JSON', extensions: ['json'] }], }) if (canceled || !filePath) return { success: false, filePath: null } if (path.extname(filePath).toLowerCase() !== '.json') { throw new Error('Invalid file extension. Please save as a .json file.') } fs.mkdirSync(path.dirname(filePath), { recursive: true }) fs.writeFileSync(filePath, keystore, 'utf-8') return { success: true, filePath } } catch (error: any) { logger.error('Error saving keystore (Save As):', error.message) throw error } }) ipcMain.handle('wallet:decryptKeystore', async (_event, fileName: string, password: string) => { try { const filePath = safeResolvePath(fileName) const json = fs.readFileSync(filePath, 'utf-8') const phrase = await fromEncryptedJson(json, password) return { phrase } } catch (error: any) { logger.error('Error decrypting keystore ipc:', error.message) throw error } }) ipcMain.handle('wallet:checkKeystore', async () => { try { if (!fs.existsSync(WALLETS_DIR)) return { exists: false, fileName: null } const newestFile = fs .readdirSync(WALLETS_DIR) .filter((f) => f.endsWith('.json')) .sort( (a, b) => fs.statSync(path.join(WALLETS_DIR, b)).mtime.getTime() - fs.statSync(path.join(WALLETS_DIR, a)).mtime.getTime() )[0] if (!newestFile) return { exists: false, fileName: null } const resolvedPath = safeResolvePath(newestFile) let minBlockHeight: number | null = null try { const json = fs.readFileSync(resolvedPath, 'utf-8') const data = JSON.parse(json) const height = data?.minBlockHeight if (Number.isFinite(height)) minBlockHeight = height } catch (error: any) { logger.warn('Unable to read minBlockHeight from keystore:', error.message) } return { exists: true, fileName: newestFile, minBlockHeight } } catch (error: any) { logger.error('Error checking keystore ipc:', error.message) return { exists: false, fileName: null, minBlockHeight: null, error: error.message } } }) ipcMain.handle( 'wallet:updateMinBlockHeight', async (_event, fileName: string | null, minBlockHeight: number | null) => { if (!fileName) { return { success: false, error: 'No keystore file name provided.' } } try { const filePath = safeResolvePath(fileName) if (!fs.existsSync(filePath)) { return { success: false, error: 'Keystore file not found.' } } const fileContents = fs.readFileSync(filePath, 'utf-8') const walletJson = JSON.parse(fileContents) walletJson.minBlockHeight = Number.isFinite(minBlockHeight) ? minBlockHeight : null fs.writeFileSync(filePath, JSON.stringify(walletJson, null, 2), 'utf-8') return { success: true, minBlockHeight } } catch (error: any) { logger.error('Error updating min block height:', error.message) return { success: false, error: error.message } } } ) ipcMain.handle('wallet:getMinBlockHeight', async (_event, fileName: string | null) => { if (!fileName) { return { success: false, error: 'No keystore file name provided.', minBlockHeight: null } } try { const filePath = safeResolvePath(fileName) if (!fs.existsSync(filePath)) { return { success: false, error: 'Keystore file not found.', minBlockHeight: null } } const fileContents = fs.readFileSync(filePath, 'utf-8') const walletJson = JSON.parse(fileContents) const height = walletJson?.minBlockHeight return { success: true, minBlockHeight: Number.isFinite(height) ? height : null, } } catch (error: any) { logger.error('Error reading min block height:', error.message) return { success: false, error: error.message, minBlockHeight: null } } }) ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[]) => { try { const wallet = new neptuneNative.WalletManager() return JSON.parse(wallet.generateKeysFromSeed(seedPhrase)) } catch (error: any) { logger.error('Error generating keys from seed ipc:', error.message) throw error } }) ipcMain.handle('wallet:buildTransaction', async (_event, args) => { const { spendingKeyHex, inputAdditionRecords, outputAddresses, outputAmounts, fee } = args try { if (typeof spendingKeyHex !== 'string' || !/^(0x)?[0-9a-fA-F]+$/.test(spendingKeyHex)) { throw new Error('Invalid spending key') } if ( !Array.isArray(inputAdditionRecords) || !inputAdditionRecords.every((r) => typeof r === 'string') ) { throw new Error('Invalid inputAdditionRecords') } if ( !Array.isArray(outputAddresses) || !outputAddresses.every((a) => typeof a === 'string') ) { throw new Error('Invalid outputAddresses') } if ( !Array.isArray(outputAmounts) || !outputAmounts.every((a) => typeof a === 'string' && /^\d+(\.\d+)?$/.test(a)) ) { throw new Error('Invalid outputAmounts') } if (typeof fee !== 'string' || !/^\d+(\.\d+)?$/.test(fee)) { throw new Error('Invalid fee') } const builder = new neptuneNative.SimpleTransactionBuilder() const result = await builder.buildTransaction( import.meta.env.VITE_APP_API, spendingKeyHex, inputAdditionRecords, typeof args?.minBlockHeight === 'number' && Number.isFinite(args.minBlockHeight) ? args.minBlockHeight : 0, outputAddresses, outputAmounts, fee ) return JSON.parse(result) } catch (error: any) { logger.error('Error building transaction ipc:', error.message) throw error } }) ipcMain.on('log:info', (_, ...msg: string[]) => logger.info(...msg)) ipcMain.on('log:warn', (_, ...msg: string[]) => logger.warn(...msg)) ipcMain.on('log:error', (_, ...msg: string[]) => logger.error(...msg))