diff --git a/package.json b/package.json index d6c1d8a7..13c5ba7b 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@types/react-router-dom": "^5.1.8", "@types/styled-components": "^5.1.11", "@types/tmp": "^0.2.2", - "electron": "18.3.15", - "electron-builder": "^22.14.5", + "electron": "^28.2.2", + "electron-builder": "^24.9.1", "ts-loader": "^9.2.3", "typescript": "^4.3.5", "webpack": "^5.88.2", diff --git a/src/electron/index.ts b/src/electron/index.ts index 1285a834..d253e178 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -3,11 +3,31 @@ * This typescript file contains the Electron app which renders the React app. */ -import { BrowserWindow, app, ipcMain, dialog, clipboard } from "electron"; +import { + BrowserWindow, + app, + clipboard, + dialog, + ipcMain, + shell, +} from "electron"; +import { OpenDialogOptions } from "electron/common"; +import { accessSync, constants } from "fs"; import path from "path"; +import { isAddress } from 'web3-utils'; -import { accessSync, constants } from "fs"; -import { OpenDialogOptions } from "electron/common"; +import { + doesDirectoryExist, + findFirstFile, + isDirectoryWritable, +} from './BashUtils'; +import { + createMnemonic, + generateBLSChange, + generateKeys, + validateBLSCredentials, + validateMnemonic, +} from './Eth2Deposit'; /** * VERSION and COMMITHASH are set by the git-revision-webpack-plugin module. @@ -27,7 +47,7 @@ const doesFileExist = (filename: string): boolean => { app.on("ready", () => { var iconPath = path.join("static", "icon.png"); const bundledIconPath = path.join(process.resourcesPath, "..", "static", "icon.png"); - + if (doesFileExist(bundledIconPath)) { iconPath = bundledIconPath; } @@ -66,15 +86,60 @@ app.on("ready", () => { * This logic closes the application when the window is closed, explicitly. * On MacOS this is not a default feature. */ - ipcMain.on('close', (evt, arg) => { + ipcMain.on('close', () => { app.quit(); - }) + }); + + /** + * Will grab the provide text and copy to the cipboard + */ + ipcMain.on('clipboardWriteText', (evt, ext, type) => { + clipboard.writeText(ext, type); + }); + + /** + * Will open a file explorer to the path provided + */ + ipcMain.on('shellShowItemInFolder', (event, fullPath: string) => { + shell.showItemInFolder(fullPath); + }); /** * Provides the renderer a way to call the dialog.showOpenDialog function using IPC. */ - ipcMain.handle('showOpenDialog', async (event, options) => { - return await dialog.showOpenDialog( options); + ipcMain.handle('showOpenDialog', async (event, options: OpenDialogOptions) => { + return await dialog.showOpenDialog(options); + }); + + /** + * Passthroughs for non-electron renderer calls + */ + ipcMain.handle('createMnemonic', async (event, ...args: Parameters) => { + return await createMnemonic(...args); + }); + ipcMain.handle('generateBLSChange', async (event, ...args: Parameters) => { + return await generateBLSChange(...args); + }); + ipcMain.handle('generateKeys', async (event, ...args: Parameters) => { + return await generateKeys(...args); + }); + ipcMain.handle('validateBLSCredentials', async (event, ...args: Parameters) => { + return await validateBLSCredentials(...args); + }); + ipcMain.handle('validateMnemonic', async (event, ...args: Parameters) => { + return await validateMnemonic(...args); + }); + ipcMain.handle('doesDirectoryExist', async (event, ...args: Parameters) => { + return await doesDirectoryExist(...args); + }); + ipcMain.handle('isDirectoryWritable', async (event, ...args: Parameters) => { + return await isDirectoryWritable(...args); + }); + ipcMain.handle('findFirstFile', async (event, ...args: Parameters) => { + return await findFirstFile(...args); + }); + ipcMain.handle('isAddress', async (event, ...args: Parameters) => { + return await isAddress(...args); }); /** diff --git a/src/electron/preload.ts b/src/electron/preload.ts index d63d0756..982bf8e7 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -5,49 +5,37 @@ import { contextBridge, - shell, - clipboard, ipcRenderer, - OpenDialogOptions, - OpenDialogReturnValue } from "electron"; -import Web3Utils from 'web3-utils'; - -import { createMnemonic, generateKeys, validateMnemonic, validateBLSCredentials, generateBLSChange } from './Eth2Deposit'; - -import { doesDirectoryExist, isDirectoryWritable, findFirstFile } from './BashUtils'; - -const ipcRendererSendClose = () => { - ipcRenderer.send('close'); -}; - -const invokeShowOpenDialog = (options: OpenDialogOptions): Promise => { - return ipcRenderer.invoke('showOpenDialog', options); -}; +import { + IBashUtilsAPI, + IElectronAPI, + IEth2DepositAPI, + IWeb3UtilsAPI, +} from "./renderer"; contextBridge.exposeInMainWorld('electronAPI', { - 'shellOpenExternal': shell.openExternal, - 'shellShowItemInFolder': shell.showItemInFolder, - 'clipboardWriteText': clipboard.writeText, - 'ipcRendererSendClose': ipcRendererSendClose, - 'invokeShowOpenDialog': invokeShowOpenDialog + 'clipboardWriteText': (...args: Parameters) => ipcRenderer.send('clipboardWriteText', ...args), + 'invokeShowOpenDialog': (...args: Parameters) => ipcRenderer.invoke('showOpenDialog', ...args), + 'ipcRendererSendClose': () => ipcRenderer.send('close'), + 'shellShowItemInFolder': (...args: Parameters) => ipcRenderer.send('shellShowItemInFolder', ...args), }); contextBridge.exposeInMainWorld('eth2Deposit', { - 'createMnemonic': createMnemonic, - 'generateKeys': generateKeys, - 'validateMnemonic': validateMnemonic, - 'validateBLSCredentials': validateBLSCredentials, - 'generateBLSChange': generateBLSChange + 'createMnemonic': (...args: Parameters) => ipcRenderer.invoke('createMnemonic', ...args), + 'generateBLSChange': (...args: Parameters) => ipcRenderer.invoke('generateBLSChange', ...args), + 'generateKeys': (...args: Parameters) => ipcRenderer.invoke('generateKeys', ...args), + 'validateBLSCredentials': (...args: Parameters) => ipcRenderer.invoke('validateBLSCredentials', ...args), + 'validateMnemonic': (...args: Parameters) => ipcRenderer.invoke('validateMnemonic', ...args), }); contextBridge.exposeInMainWorld('bashUtils', { - 'doesDirectoryExist': doesDirectoryExist, - 'isDirectoryWritable': isDirectoryWritable, - 'findFirstFile': findFirstFile + 'doesDirectoryExist': (...args: Parameters) => ipcRenderer.invoke('doesDirectoryExist', ...args), + 'findFirstFile': (...args: Parameters) => ipcRenderer.invoke('findFirstFile', ...args), + 'isDirectoryWritable': (...args: Parameters) => ipcRenderer.invoke('isDirectoryWritable', ...args), }); contextBridge.exposeInMainWorld('web3Utils', { - 'isAddress': Web3Utils.isAddress -}); \ No newline at end of file + 'isAddress': (...args: Parameters) => ipcRenderer.invoke('isAddress', ...args), +}); diff --git a/src/electron/renderer.d.ts b/src/electron/renderer.d.ts index 115a977f..ca9d1e38 100644 --- a/src/electron/renderer.d.ts +++ b/src/electron/renderer.d.ts @@ -24,30 +24,29 @@ import { } from "child_process" export interface IElectronAPI { - shellOpenExternal: (url: string, options?: Electron.OpenExternalOptions | undefined) => Promise, - shellShowItemInFolder: (fullPath: string) => void, clipboardWriteText: (ext: string, type?: "selection" | "clipboard" | undefined) => void, - ipcRendererSendClose: () => void, invokeShowOpenDialog: (options: OpenDialogOptions) => Promise + ipcRendererSendClose: () => void, + shellShowItemInFolder: (fullPath: string) => void, } export interface IEth2DepositAPI { createMnemonic: (language: string) => Promise, + generateBLSChange: (folder: string, chain: string, mnemonic: string, index: number, indices: string, withdrawal_credentials: string, execution_address: string) => Promise, generateKeys: (mnemonic: string, index: number, count: number, network: string, password: string, eth1_withdrawal_address: string, folder: string) => Promise, - validateMnemonic: (mnemonic: string) => Promise, validateBLSCredentials: (chain: string, mnemonic: string, index: number, withdrawal_credentials: string) => Promise, - generateBLSChange: (folder: string, chain: string, mnemonic: string, index: number, indices: string, withdrawal_credentials: string, execution_address: string) => Promise, + validateMnemonic: (mnemonic: string) => Promise, } export interface IBashUtilsAPI { doesDirectoryExist: (directory: string) => Promise, - isDirectoryWritable: (directory: string) => Promise, findFirstFile: (directory: string, startsWith: string) => Promise + isDirectoryWritable: (directory: string) => Promise, } export interface IWeb3UtilsAPI { - isAddress: (address: string, chainId?: number | undefined) => boolean + isAddress: (address: string, chainId?: number | undefined) => Promise } declare global { diff --git a/src/react/components/BTECConfigurationWizard.tsx b/src/react/components/BTECConfigurationWizard.tsx index a8e42395..2f9b763b 100644 --- a/src/react/components/BTECConfigurationWizard.tsx +++ b/src/react/components/BTECConfigurationWizard.tsx @@ -29,7 +29,7 @@ type Props = { /** * This is the wizard the user will navigate to configure their BLS to execution change. * It uses the notion of a 'step' to render specific pages within the flow. - * + * * @param props.onStepBack function to execute when stepping back * @param props.onStepForward function to execute when stepping forward * @param props.network the network for which to generate this BTEC @@ -112,9 +112,9 @@ const BTECConfigurationWizard: FC = (props): ReactElement => { } } - const validateInputs = () => { + const validateInputs = async () => { let isError = false; - + if (props.startIndex < 0) { setStartingIndexError(true); isError = true; @@ -179,7 +179,8 @@ const BTECConfigurationWizard: FC = (props): ReactElement => { } if (props.withdrawalAddress != "") { - if (!window.web3Utils.isAddress(props.withdrawalAddress)) { + const isValidAddress = await window.web3Utils.isAddress(props.withdrawalAddress); + if (!isValidAddress) { setWithdrawalAddressError(true); setWithdrawalAddressErrorMsg(errors.ADDRESS_FORMAT_ERROR); isError = true; diff --git a/src/react/components/KeyConfigurationWizard.tsx b/src/react/components/KeyConfigurationWizard.tsx index 41797a99..d356d2be 100644 --- a/src/react/components/KeyConfigurationWizard.tsx +++ b/src/react/components/KeyConfigurationWizard.tsx @@ -28,7 +28,7 @@ type Props = { /** * This is the wizard the user will navigate to configure their keys. * It uses the notion of a 'step' to render specific pages within the flow. - * + * * @param props.onStepBack function to execute when stepping back * @param props.onStepForward function to execute when stepping forward * @param props.keyGenerationStartIndex the index at which to start generating keys for the user @@ -121,7 +121,7 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { } } - const validateInputs = () => { + const validateInputs = async () => { let isError = false; if (props.numberOfKeys < 1 || props.numberOfKeys > 1000) { @@ -130,14 +130,14 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { } else { setNumberOfKeysError(false); } - + if (props.password.length < 8) { setPasswordStrengthError(true); isError = true; } else { setPasswordStrengthError(false); } - + if (props.keyGenerationStartIndex < 0) { setStartingIndexError(true); isError = true; @@ -146,7 +146,8 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { } if (props.withdrawalAddress != "") { - if (!window.web3Utils.isAddress(props.withdrawalAddress)) { + const isValidAddress = await window.web3Utils.isAddress(props.withdrawalAddress); + if (!isValidAddress) { setWithdrawalAddressFormatError(true); isError = true; } else { diff --git a/src/react/components/OnlineDetector.tsx b/src/react/components/OnlineDetector.tsx index 86871f7e..04fb2830 100644 --- a/src/react/components/OnlineDetector.tsx +++ b/src/react/components/OnlineDetector.tsx @@ -25,25 +25,32 @@ const FixedErrorButton = styled(Button)` const PulseAnimation = keyframes` 0% { - box-shadow: 0 0 0 0px rgba(250, 30, 14, 0.7); + background-color: rgba(250, 30, 14, 0.7); + width: 0px; + height: 0px; } 70% { - box-shadow: 0 0 0 22px rgba(250, 30, 14, 0); + background-color: rgba(250, 30, 14, 0); + width: 50px; + height: 50px; + margin: -25px; } 100% { - box-shadow: 0 0 0 0px rgba(250, 30, 14, 0); + background-color: rgba(250, 30, 14, 0); + width: 60px; + height: 60px; + margin: -30px; } ` const PusleCircle = styled(Box)` position: absolute; - width: 1px; - height: 1px; - left: 22px; - box-shadow: 0 0 0 0px rgba(250, 30, 14, 0.7); - opacity: 1; + width: 0px; + height: 0px; + left: 23px; + background-color: rgba(250, 30, 14, 0); animation: ${PulseAnimation} 3s infinite; border-radius: 1000px; `