From 40aa19391d83a140f5020790d2b5d5177fdf3ead Mon Sep 17 00:00:00 2001 From: MoCipher Date: Fri, 20 Feb 2026 09:52:34 +0100 Subject: [PATCH] desktop: add Electron app scaffold (UI + preload bridge) --- electron/main.js | 27 ++++++++++++++++ electron/package.json | 13 ++++++++ electron/preload.js | 56 +++++++++++++++++++++++++++++++++ electron/renderer/index.html | 39 +++++++++++++++++++++++ electron/renderer/renderer.js | 21 +++++++++++++ electron/test/electron-smoke.js | 12 +++++++ 6 files changed, 168 insertions(+) create mode 100644 electron/main.js create mode 100644 electron/package.json create mode 100644 electron/preload.js create mode 100644 electron/renderer/index.html create mode 100644 electron/renderer/renderer.js create mode 100644 electron/test/electron-smoke.js diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..80474e2 --- /dev/null +++ b/electron/main.js @@ -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(); +}); \ No newline at end of file diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 0000000..f1b11fb --- /dev/null +++ b/electron/package.json @@ -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" + } +} diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 0000000..f8c82c8 --- /dev/null +++ b/electron/preload.js @@ -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) +}); \ No newline at end of file diff --git a/electron/renderer/index.html b/electron/renderer/index.html new file mode 100644 index 0000000..3028634 --- /dev/null +++ b/electron/renderer/index.html @@ -0,0 +1,39 @@ + + + + + CrypteCipher Desktop + + + + +
+

CrypteCipher — Desktop (Electron)

+
+ + +
+
+

Mnemonic

+
(none)
+
+
+

Receive Address

+
(none)
+
+
+

Send (mock)

+ + + +
+

+    
+ + + \ No newline at end of file diff --git a/electron/renderer/renderer.js b/electron/renderer/renderer.js new file mode 100644 index 0000000..65014a6 --- /dev/null +++ b/electron/renderer/renderer.js @@ -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; +}); diff --git a/electron/test/electron-smoke.js b/electron/test/electron-smoke.js new file mode 100644 index 0000000..de6dee9 --- /dev/null +++ b/electron/test/electron-smoke.js @@ -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); +}