const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const { ethers } = require('ethers'); const keystore = require('./src/core/keystore'); const chains = require('./src/core/chains'); const balances = require('./src/core/balances'); const KEYSTORE_PATH = path.join(app.getPath('userData'), 'keystore.json'); const config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8')); // Holds the derived accounts ONLY while unlocked, in memory. Never written // to disk in plaintext. Cleared on lock/quit. let session = null; function createWindow() { const win = new BrowserWindow({ width: 1180, height: 780, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, 'src/renderer/index.html')); } app.whenReady().then(createWindow); app.on('window-all-closed', () => { session = null; if (process.platform !== 'darwin') app.quit(); }); // ---------- IPC handlers ---------- ipcMain.handle('wallet:hasKeystore', () => fs.existsSync(KEYSTORE_PATH)); ipcMain.handle('wallet:generateMnemonic', () => keystore.generateMnemonic()); function createOrImportWallet(mnemonic, password) { if (!keystore.validateMnemonic(mnemonic)) { throw new Error('That recovery phrase does not look valid. Check the word list and try again.'); } const encrypted = keystore.encryptMnemonic(mnemonic.trim(), password); keystore.saveKeystore(KEYSTORE_PATH, encrypted); session = { accounts: chains.deriveAllChains(mnemonic.trim()) }; return publicAccounts(); } ipcMain.handle('wallet:create', (_evt, { mnemonic, password }) => createOrImportWallet(mnemonic, password)); ipcMain.handle('wallet:import', (_evt, { mnemonic, password }) => createOrImportWallet(mnemonic, password)); ipcMain.handle('wallet:unlock', (_evt, { password }) => { if (!fs.existsSync(KEYSTORE_PATH)) throw new Error('No wallet found on this device.'); const stored = keystore.loadKeystore(KEYSTORE_PATH); let mnemonic; try { mnemonic = keystore.decryptMnemonic(stored, password); } catch { throw new Error('Incorrect password.'); } session = { accounts: chains.deriveAllChains(mnemonic) }; return publicAccounts(); }); ipcMain.handle('wallet:lock', () => { session = null; return true; }); ipcMain.handle('wallet:balances', async () => { requireSession(); const { ethereum, bitcoin, solana } = session.accounts; const [eth, btc, sol] = await Promise.allSettled([ balances.getEthBalance(ethereum.address, config.ethereum.rpcUrl), balances.getBtcBalance(bitcoin.address, config.bitcoin.apiBase), balances.getSolBalance(solana.address, config.solana.rpcUrl), ]); return { ethereum: eth.status === 'fulfilled' ? eth.value : null, bitcoin: btc.status === 'fulfilled' ? btc.value : null, solana: sol.status === 'fulfilled' ? sol.value : null, }; }); // Ethereum send is wired up as the reference implementation; Bitcoin/Solana // sending follow the same shape but need UTXO selection / instruction // building respectively — see README for what's left to fill in. ipcMain.handle('wallet:sendEth', async (_evt, { to, amountEth }) => { requireSession(); const provider = new ethers.JsonRpcProvider(config.ethereum.rpcUrl); const wallet = new ethers.Wallet(session.accounts.ethereum.privateKey, provider); const tx = await wallet.sendTransaction({ to, value: ethers.parseEther(amountEth) }); return { hash: tx.hash }; }); function requireSession() { if (!session) throw new Error('Wallet is locked.'); } function publicAccounts() { // Strip private keys before sending anything to the renderer process. const { ethereum, bitcoin, solana } = session.accounts; return { ethereum: { address: ethereum.address, path: ethereum.path }, bitcoin: { address: bitcoin.address, path: bitcoin.path }, solana: { address: solana.address, path: solana.path }, }; }