desktop: add Electron app scaffold (UI + preload bridge)

This commit is contained in:
MoCipher 2026-02-20 09:52:34 +01:00
parent b49225b0a5
commit 40aa19391d
6 changed files with 168 additions and 0 deletions

27
electron/main.js Normal file
View file

@ -0,0 +1,27 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});

13
electron/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "cryptec-electron",
"version": "0.1.0",
"private": true,
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "node ./test/electron-smoke.js"
},
"devDependencies": {
"electron": "^25.0.0"
}
}

56
electron/preload.js Normal file
View file

@ -0,0 +1,56 @@
const { contextBridge } = require('electron');
let nativeBindings = null;
try {
// Try to require the Node bindings package (napi). If not available, fall back to a JS shim.
nativeBindings = require(pathJoin(__dirname, '..', 'bindings'));
} catch (e) {
// ignore
}
function pathJoin(...parts) {
return parts.join('/');
}
const shim = {
generate_mnemonic: (strength = 128) => {
// simple in-process fallback
const crypto = require('crypto');
// use 128-bit entropy => 12 words via bip39 library if available
try {
const bip39 = require('bip39');
return bip39.generateMnemonic(strength);
} catch (e) {
return crypto.randomBytes(16).toString('hex');
}
},
first_receive_address: (mnemonic) => {
try {
const bip39 = require('bip39');
const bitcoin = require('bitcoinjs-lib');
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bitcoin.bip32.fromSeed(seed, bitcoin.networks.testnet);
const child = root.derivePath("m/84'/1'/0'/0/0");
const { address } = bitcoin.payments.p2wpkh({ pubkey: child.publicKey, network: bitcoin.networks.testnet });
return address || '';
} catch (e) {
return '';
}
},
create_psbt: (_mnemonic, toAddress, satoshis) => {
return Buffer.from(JSON.stringify({ type: 'psbt-mock', to: toAddress, sats: satoshis })).toString('base64');
},
sign_psbt: (_mnemonic, psbtB64) => {
const decoded = Buffer.from(psbtB64, 'base64').toString('utf8');
return Buffer.from(JSON.stringify({ signed: true, original: decoded })).toString('base64');
}
};
const api = nativeBindings ? nativeBindings : shim;
contextBridge.exposeInMainWorld('cryptec', {
generateMnemonic: (strength) => api.generate_mnemonic ? api.generate_mnemonic(strength) : api.generate_mnemonic(strength),
firstReceiveAddress: (mnemonic) => api.first_receive_address ? api.first_receive_address(mnemonic) : api.first_receive_address(mnemonic),
createPsbt: (mnemonic, to, sats) => api.create_psbt ? api.create_psbt(mnemonic, to, sats) : api.create_psbt(mnemonic, to, sats),
signPsbt: (mnemonic, psbtB64) => api.sign_psbt ? api.sign_psbt(mnemonic, psbtB64) : api.sign_psbt(mnemonic, psbtB64)
});

View file

@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>CrypteCipher Desktop</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' data:;" />
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin: 0; padding: 20px; }
.container { max-width: 820px; margin: 0 auto; }
.addr { background: #f6f6f6; padding: 12px; border-radius: 6px; font-family: monospace; }
.section { margin-top: 18px; }
</style>
</head>
<body>
<div class="container">
<h1>CrypteCipher — Desktop (Electron)</h1>
<div>
<button id="new">Create / Generate mnemonic</button>
<button id="addr">Get first receive address</button>
</div>
<div class="section">
<h3>Mnemonic</h3>
<div id="mn" class="addr">(none)</div>
</div>
<div class="section">
<h3>Receive Address</h3>
<div id="addrbox" class="addr">(none)</div>
</div>
<div class="section">
<h3>Send (mock)</h3>
<input id="to" placeholder="to address" style="width:60%" />
<input id="sats" placeholder="satoshis" style="width:20%" />
<button id="psbt">Create PSBT</button>
</div>
<pre id="out"></pre>
</div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
const $ = (id) => document.getElementById(id);
$('new').addEventListener('click', async () => {
const m = await window.cryptec.generateMnemonic(128);
$('mn').innerText = m;
});
$('addr').addEventListener('click', () => {
const mn = $('mn').innerText;
if (!mn) return alert('generate or enter mnemonic first');
const a = window.cryptec.firstReceiveAddress(mn);
$('addrbox').innerText = a;
});
$('psbt').addEventListener('click', async () => {
const mn = $('mn').innerText;
const to = $('to').value || 'tb1qexampleaddress';
const sats = parseInt($('sats').value || '1000', 10);
const psbt = await window.cryptec.createPsbt(mn, to, sats);
$('out').innerText = psbt;
});

View file

@ -0,0 +1,12 @@
const { execSync } = require('child_process');
const path = require('path');
try {
// smoke: ensure electron module can be required
const electron = require('electron');
console.log('electron available:', !!electron);
console.log('Electron smoke test passed');
process.exit(0);
} catch (e) {
console.error('Electron smoke test failed', e.message);
process.exit(1);
}