Files
2026-08-27 18:27:44 +00:00

112 lines
3.9 KiB
JavaScript

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 },
};
}