diff --git a/app/actions/DaemonActions.js b/app/actions/DaemonActions.js index b1b1e5631a..c47dca1e66 100644 --- a/app/actions/DaemonActions.js +++ b/app/actions/DaemonActions.js @@ -89,7 +89,10 @@ export const checkDecreditonVersion = () => (dispatch, getState) => { } }) .catch(function (error) { - console.log("Unable to check latest decrediton release version.", error); + console.error( + "Unable to check latest decrediton release version.", + error + ); }); }; @@ -260,7 +263,7 @@ export const shutdownApp = () => async (dispatch, getState) => { return dispatch(finalShutdown()); } catch (error) { const openOrder = - error.indexOf("cannot log out with active orders", 0) > -1; + String(error).indexOf("cannot log out with active orders", 0) > -1; dispatch({ type: DEX_LOGOUT_FAILED, error, openOrder }); dispatch(showCantCloseModal()); } diff --git a/app/actions/DexActions.js b/app/actions/DexActions.js index 8808dc6cc3..b66ceb6b39 100644 --- a/app/actions/DexActions.js +++ b/app/actions/DexActions.js @@ -9,8 +9,8 @@ import { EXTERNALREQUEST_DEX } from "main_dev/externalRequests"; import * as configConstants from "constants/config"; import { makeRandomString } from "helpers"; -const sendSync = (...args) => { - const res = ipcRenderer.sendSync(...args); +const invoke = async (...args) => { + const res = await ipcRenderer.invoke(...args); if (res instanceof Error) { throw res; } @@ -56,7 +56,7 @@ export const DEX_STARTUP_ATTEMPT = "DEX_STARTUP_ATTEMPT"; export const DEX_STARTUP_FAILED = "DEX_STARTUP_FAILED"; export const DEX_STARTUP_SUCCESS = "DEX_STARTUP_SUCCESS"; -export const startDex = () => (dispatch, getState) => { +export const startDex = () => async (dispatch, getState) => { dispatch({ type: DEX_STARTUP_ATTEMPT }); const isTestnet = sel.isTestNet(getState()); const { @@ -65,7 +65,7 @@ export const startDex = () => (dispatch, getState) => { const walletPath = getWalletPath(isTestnet, walletName); try { - const res = sendSync("start-dex", walletPath, isTestnet); + const res = await invoke("start-dex", walletPath, isTestnet); dispatch({ type: DEX_STARTUP_SUCCESS, serverAddress: res }); dispatch(dexCheckInit()); } catch (error) { @@ -78,10 +78,10 @@ export const DEX_CHECKINIT_ATTEMPT = "DEX_CHECKINIT_ATTEMPT"; export const DEX_CHECKINIT_FAILED = "DEX_CHECKINIT_FAILED"; export const DEX_CHECKINIT_SUCCESS = "DEX_CHECKINIT_SUCCESS"; -export const dexCheckInit = () => (dispatch) => { +export const dexCheckInit = () => async (dispatch) => { dispatch({ type: DEX_CHECKINIT_ATTEMPT }); try { - const res = sendSync("check-init-dex"); + const res = await invoke("check-init-dex"); dispatch({ type: DEX_CHECKINIT_SUCCESS, res }); } catch (error) { dispatch({ type: DEX_CHECKINIT_FAILED, error }); @@ -96,7 +96,7 @@ export const stopDex = () => (dispatch, getState) => { return; } - ipcRenderer.send("stop-dex"); + invoke("stop-dex"); dispatch({ type: DEX_STOPPED }); }; @@ -104,14 +104,14 @@ export const DEX_INIT_ATTEMPT = "DEX_INIT_ATTEMPT"; export const DEX_INIT_SUCCESS = "DEX_INIT_SUCCESS"; export const DEX_INIT_FAILED = "DEX_INIT_FAILED"; -export const initDex = (passphrase) => (dispatch, getState) => { +export const initDex = (passphrase) => async (dispatch, getState) => { dispatch({ type: DEX_INIT_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_INIT_FAILED, error: "Dex isn't active" }); return; } try { - sendSync("init-dex", passphrase); + await invoke("init-dex", passphrase); dispatch({ type: DEX_INIT_SUCCESS }); // Request current user information dispatch(userDex()); @@ -125,14 +125,14 @@ export const DEX_LOGIN_ATTEMPT = "DEX_LOGIN_ATTEMPT"; export const DEX_LOGIN_SUCCESS = "DEX_LOGIN_SUCCESS"; export const DEX_LOGIN_FAILED = "DEX_LOGIN_FAILED"; -export const loginDex = (passphrase) => (dispatch, getState) => { +export const loginDex = (passphrase) => async (dispatch, getState) => { dispatch({ type: DEX_LOGIN_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_LOGIN_FAILED, error: "Dex isn't active" }); return; } try { - sendSync("login-dex", passphrase); + await invoke("login-dex", passphrase); dispatch({ type: DEX_LOGIN_SUCCESS }); // Request current user information dispatch(userDex()); @@ -146,24 +146,17 @@ export const DEX_LOGOUT_ATTEMPT = "DEX_LOGOUT_ATTEMPT"; export const DEX_LOGOUT_SUCCESS = "DEX_LOGOUT_SUCCESS"; export const DEX_LOGOUT_FAILED = "DEX_LOGOUT_FAILED"; -export const logoutDex = () => - new Promise((resolve, reject) => { - try { - sendSync("logout-dex"); - return resolve(true); - } catch (error) { - return reject(error); - } - }); +export const logoutDex = () => invoke("logout-dex"); export const DEX_CREATEWALLET_ATTEMPT = "DEX_CREATEWALLET_ATTEMPT"; export const DEX_CREATEWALLET_SUCCESS = "DEX_CREATEWALLET_SUCCESS"; export const DEX_CREATEWALLET_FAILED = "DEX_CREATEWALLET_FAILED"; -export const createWalletDex = (passphrase, appPassphrase, accountName) => ( - dispatch, - getState -) => { +export const createWalletDex = ( + passphrase, + appPassphrase, + accountName +) => async (dispatch, getState) => { dispatch({ type: DEX_CREATEWALLET_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_CREATEWALLET_FAILED, error: "Dex isn't active" }); @@ -180,7 +173,7 @@ export const createWalletDex = (passphrase, appPassphrase, accountName) => ( const rpclisten = rpcCreds.rpcListen; const rpccert = rpcCreds.rpcCert; const assetID = 42; - sendSync( + await invoke( "create-wallet-dex", assetID, passphrase, @@ -208,7 +201,7 @@ export const btcCreateWalletDex = ( passphrase, appPassphrase, btcWalletName -) => (dispatch, getState) => { +) => async (dispatch, getState) => { dispatch({ type: BTC_CREATEWALLET_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: BTC_CREATEWALLET_FAILED, error: "Dex isn't active" }); @@ -226,7 +219,7 @@ export const btcCreateWalletDex = ( ? btcConfig.test.rpcbind + ":" + btcConfig.test.rpcport : btcConfig.rpcbind + ":" + btcConfig.rpcport; const assetID = 0; - sendSync( + await invoke( "create-wallet-dex", assetID, passphrase, @@ -257,14 +250,14 @@ export const DEX_USER_ATTEMPT = "DEX_USER_ATTEMPT"; export const DEX_USER_SUCCESS = "DEX_USER_SUCCESS"; export const DEX_USER_FAILED = "DEX_USER_FAILED"; -export const userDex = () => (dispatch, getState) => { +export const userDex = () => async (dispatch, getState) => { dispatch({ type: DEX_USER_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_USER_FAILED, error: "Dex isn't active" }); return; } try { - const user = sendSync("user-dex"); + const user = await invoke("user-dex"); dispatch({ type: DEX_USER_SUCCESS, user }); } catch (error) { dispatch({ type: DEX_USER_FAILED, error }); @@ -276,14 +269,14 @@ export const DEX_GETCONFIG_ATTEMPT = "DEX_GETCONFIG_ATTEMPT"; export const DEX_GETCONFIG_SUCCESS = "DEX_GETCONFIG_SUCCESS"; export const DEX_GETCONFIG_FAILED = "DEX_GETCONFIG_FAILED"; -export const getConfigDex = (addr) => (dispatch, getState) => { +export const getConfigDex = (addr) => async (dispatch, getState) => { dispatch({ type: DEX_GETCONFIG_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_GETCONFIG_FAILED, error: "Dex isn't active" }); return; } try { - const config = sendSync("get-config-dex", addr); + const config = await invoke("get-config-dex", addr); dispatch({ type: DEX_GETCONFIG_SUCCESS, config, addr }); } catch (error) { dispatch({ type: DEX_GETCONFIG_FAILED, error }); @@ -295,7 +288,7 @@ export const DEX_REGISTER_ATTEMPT = "DEX_REGISTER_ATTEMPT"; export const DEX_REGISTER_SUCCESS = "DEX_REGISTER_SUCCESS"; export const DEX_REGISTER_FAILED = "DEX_REGISTER_FAILED"; -export const registerDex = (appPass) => (dispatch, getState) => { +export const registerDex = (appPass) => async (dispatch, getState) => { dispatch({ type: DEX_REGISTER_ATTEMPT }); if (!sel.dexActive(getState())) { dispatch({ type: DEX_REGISTER_FAILED, error: "Dex isn't acteive" }); @@ -309,7 +302,7 @@ export const registerDex = (appPass) => (dispatch, getState) => { } const fee = config.feeAsset.amount; try { - sendSync("register-dex", appPass, addr, fee); + await invoke("register-dex", appPass, addr, fee); dispatch({ type: DEX_REGISTER_SUCCESS }); // Request current user information dispatch(userDex()); @@ -331,7 +324,7 @@ export const DEX_LAUNCH_WINDOW_ATTEMPT = "DEX_LAUNCH_WINDOW_ATTEMPT"; export const DEX_LAUNCH_WINDOW_SUCCESS = "DEX_LAUNCH_WINDOW_SUCCESS"; export const DEX_LAUNCH_WINDOW_FAILED = "DEX_LAUNCH_WINDOW_FAILED"; -export const launchDexWindow = () => (dispatch, getState) => { +export const launchDexWindow = () => async (dispatch, getState) => { const { dex: { dexServerAddress } } = getState(); @@ -342,7 +335,7 @@ export const launchDexWindow = () => (dispatch, getState) => { } try { const serverAddress = dexServerAddress; - sendSync("launch-dex-window", serverAddress); + await invoke("launch-dex-window", serverAddress); dispatch({ type: DEX_LAUNCH_WINDOW_SUCCESS }); // Request current user information dispatch(userDex()); @@ -360,10 +353,10 @@ export const CHECK_BTC_CONFIG_SUCCESS_UPDATE_NEEDED = export const CHECK_BTC_CONFIG_SUCCESS_NEED_INSTALL = "CHECK_BTC_CONFIG_SUCCESS_NEED_INSTALL"; -export const checkBTCConfig = () => (dispatch, getState) => { +export const checkBTCConfig = () => async (dispatch, getState) => { dispatch({ type: CHECK_BTC_CONFIG_ATTEMPT }); try { - const res = sendSync("check-btc-config"); + const res = await invoke("check-btc-config"); if ( res.rpcuser && res.rpcpassword && @@ -397,7 +390,7 @@ export const UPDATE_BTC_CONFIG_ATTEMPT = "UPDATE_BTC_CONFIG_ATTEMPT"; export const UPDATE_BTC_CONFIG_SUCCESS = "UPDATE_BTC_CONFIG_SUCCESS"; export const UPDATE_BTC_CONFIG_FAILED = "UPDATE_BTC_CONFIG_FAILED"; -export const updateBTCConfig = () => (dispatch, getState) => { +export const updateBTCConfig = () => async (dispatch, getState) => { dispatch({ type: UPDATE_BTC_CONFIG_ATTEMPT }); try { const rpcuser = makeRandomString(12); @@ -405,7 +398,7 @@ export const updateBTCConfig = () => (dispatch, getState) => { const rpcbind = "127.0.0.1"; const rpcport = sel.isTestNet(getState()) ? "18332" : "8332"; const testnet = sel.isTestNet(getState()); - const res = sendSync( + const res = await invoke( "update-btc-config", rpcuser, rpcpassword, diff --git a/app/actions/LNActions.js b/app/actions/LNActions.js index 93fd53bd93..fd7cbe9ba8 100644 --- a/app/actions/LNActions.js +++ b/app/actions/LNActions.js @@ -6,7 +6,8 @@ import { getWalletCfg } from "../config"; import { getWalletPath } from "main_dev/paths"; import { getNextAccountAttempt } from "./ControlActions"; import * as cfgConstants from "constants/config"; -import { isString, isNumber } from "lodash"; +import { isNumber } from "lodash"; +import { invoke } from "helpers/electronRenderer"; export const CLOSETYPE_COOPERATIVE_CLOSE = 0; export const CLOSETYPE_LOCAL_FORCE_CLOSE = 1; @@ -97,7 +98,7 @@ export const startDcrlnd = ( stage: LNWALLET_STARTUPSTAGE_STARTDCRLND, type: LNWALLET_STARTUP_CHANGEDSTAGE }); - const res = ipcRenderer.sendSync( + const res = await invoke( "start-dcrlnd", lnAccount, walletPort, @@ -106,9 +107,6 @@ export const startDcrlnd = ( isTestnet, autopilotEnabled ); - if (isString(res) || res instanceof Error) { - throw res; - } dcrlndCreds = res; } catch (error) { dispatch({ type: LNWALLET_STARTUP_FAILED }); @@ -120,7 +118,7 @@ export const startDcrlnd = ( // dcrlnd is already running so if some error occurs we need to shut it down. const cleanup = () => { // Force dcrlnd to stop. - ipcRenderer.send("stop-dcrlnd"); + invoke("stop-dcrlnd"); dispatch({ type: LNWALLET_STARTUP_FAILED }); if (creating) { @@ -223,7 +221,7 @@ export const stopDcrlnd = () => (dispatch, getState) => { return; } - ipcRenderer.send("stop-dcrlnd"); + invoke("stop-dcrlnd"); dispatch({ type: LNWALLET_DCRLND_STOPPED }); }; @@ -240,7 +238,7 @@ export const checkLnWallet = () => async (dispatch) => { } // Check whether the app knows of a previously running dcrlnd instance. - const creds = ipcRenderer.sendSync("dcrlnd-creds"); + const creds = await invoke("dcrlnd-creds"); if (!creds) { return; } diff --git a/app/actions/StatisticsActions.js b/app/actions/StatisticsActions.js index b26362c551..45026aefe0 100644 --- a/app/actions/StatisticsActions.js +++ b/app/actions/StatisticsActions.js @@ -1,6 +1,6 @@ import * as wallet from "wallet"; import * as sel from "selectors"; -import fs from "fs"; +import { fs } from "wallet-preload-shim"; import { isNumber, isNil, isUndefined } from "lodash"; import { endOfDay, formatLocalISODate, isSameDate } from "helpers"; import { @@ -380,7 +380,7 @@ export const exportStatToCSV = (opts) => (dispatch, getState) => { const seriesNames = allSeries.map((s) => s.name); const headerLine = csvLine(["time", ...seriesNames]); - fd = fs.openSync(csvFilename, "w", 0o600); + fd = fs.openWritable(csvFilename); fs.writeSync(fd, headerLine); fs.writeSync(fd, ln); } catch (err) { diff --git a/app/components/views/GetStartedPage/hooks.js b/app/components/views/GetStartedPage/hooks.js index c0960faeb3..e5584715ca 100644 --- a/app/components/views/GetStartedPage/hooks.js +++ b/app/components/views/GetStartedPage/hooks.js @@ -226,16 +226,24 @@ export const useGetStarted = () => { }); const getError = useCallback((serviceError) => { if (!serviceError) return; + // We can return errors in the form of react component, which are objects. // So we handle them first. if (React.isValidElement(serviceError)) { return serviceError; } - // If the errors is an object but not a react component, we strigfy it so we can - // render. + + // If the error is an instance of the Error class, extract the message. + if (serviceError instanceof Error) { + return serviceError.message; + } + + // If the error is an object but not a react component, we stringify it so + // we can render it. if (isObject(serviceError)) { return JSON.stringify(serviceError); } + return serviceError; }, []); const error = useMemo( diff --git a/app/components/views/LNPage/WalletTab/WalletTab.jsx b/app/components/views/LNPage/WalletTab/WalletTab.jsx index ef986870a5..d9afff09a2 100644 --- a/app/components/views/LNPage/WalletTab/WalletTab.jsx +++ b/app/components/views/LNPage/WalletTab/WalletTab.jsx @@ -3,7 +3,6 @@ import { FormattedMessage as T } from "react-intl"; import { DescriptionHeader } from "layout"; import { Subtitle } from "shared"; import { InfoDocModalButton } from "buttons"; -import { ConfirmModal } from "modals"; import BalanceHeader from "./BalanceHeader/BalanceHeader"; import BackupInfoHeader from "./BackupInfoHeader/BackupInfoHeader"; import BackupInfoDetails from "./BackupInfoDetails/BackupInfoDetails"; @@ -27,11 +26,8 @@ const WalletTab = () => { info, scbPath, scbUpdatedTime, - confirmFileOverwrite, onBackup, - onVerifyBackup, - onCancelFileOverwrite, - onConfirmFileOverwrite + onVerifyBackup } = useWalletTab(); const { confirmedBalance, unconfirmedBalance, totalBalance } = walletBalances; @@ -71,28 +67,6 @@ const WalletTab = () => { onVerifyBackup={onVerifyBackup} /> - - } - modalContent={ - <> - {confirmFileOverwrite} - }} - /> - - } - /> ); }; diff --git a/app/components/views/LNPage/WalletTab/hooks.js b/app/components/views/LNPage/WalletTab/hooks.js index 252028f608..048e181abb 100644 --- a/app/components/views/LNPage/WalletTab/hooks.js +++ b/app/components/views/LNPage/WalletTab/hooks.js @@ -1,6 +1,5 @@ import { ipcRenderer } from "electron"; -import fs from "fs"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useLNPage } from "../hooks"; export function useWalletTab() { @@ -14,38 +13,16 @@ export function useWalletTab() { scbUpdatedTime } = useLNPage(); - const [confirmFileOverwrite, setConfirmFileOverwrite] = useState(null); - useEffect(() => { setTimeout(() => updateWalletBalances(), 1000); }, [updateWalletBalances]); - const onConfirmFileOverwrite = async () => { - const filePath = confirmFileOverwrite; - if (!filePath) { - return; - } - setConfirmFileOverwrite(null); - await exportBackup(filePath); - }; - - const onCancelFileOverwrite = () => { - setConfirmFileOverwrite(null); - }; - const onBackup = async () => { - setConfirmFileOverwrite(null); const { filePath } = await ipcRenderer.invoke("show-save-dialog"); if (!filePath) { return; } - // If this file already exists, show the confirmation modal. - if (fs.existsSync(filePath)) { - setConfirmFileOverwrite(filePath); - return; - } - await exportBackup(filePath); }; @@ -64,10 +41,7 @@ export function useWalletTab() { info, scbPath, scbUpdatedTime, - confirmFileOverwrite, onBackup, - onVerifyBackup, - onCancelFileOverwrite, - onConfirmFileOverwrite + onVerifyBackup }; } diff --git a/app/helpers/electronRenderer.js b/app/helpers/electronRenderer.js new file mode 100644 index 0000000000..5eb8a0ec29 --- /dev/null +++ b/app/helpers/electronRenderer.js @@ -0,0 +1,9 @@ +import { ipcRenderer } from "electron"; + +export const invoke = async (...args) => { + const res = await ipcRenderer.invoke(...args); + if (res instanceof Error) { + throw res; + } + return res; +}; diff --git a/app/i18n/extracted/static/dialogs.json b/app/i18n/extracted/static/dialogs.json new file mode 100644 index 0000000000..3664a8e823 --- /dev/null +++ b/app/i18n/extracted/static/dialogs.json @@ -0,0 +1,14 @@ +[ + { + "id": "dialogs.confirmFileOverwrite", + "defaultMessage": "Overwrite contents of file {filename}?" + }, + { + "id": "dialogs.yesButton", + "defaultMessage": "Yes" + }, + { + "id": "dialogs.cancelButton", + "defaultMessage": "Cancel" + } +] diff --git a/app/i18n/extracted/static/index.js b/app/i18n/extracted/static/index.js index 9d0fdd8b5a..98c46cfa6a 100644 --- a/app/i18n/extracted/static/index.js +++ b/app/i18n/extracted/static/index.js @@ -1,5 +1,5 @@ // Add any new files to this array -const staticFilesData = [require("./menus.json")]; +const staticFilesData = [require("./menus.json"), require("./dialogs.json")]; // staticDefaults are the flattened messages for all statically-defined // messages (all messages in this directory). diff --git a/app/main.development.js b/app/main.development.js index 914d2a6b41..93a3af8c90 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -118,7 +118,7 @@ import { // setPath as decrediton app.setPath("userData", getAppDataDirectory()); -app.allowRendererProcessReuse = false; +app.allowRendererProcessReuse = true; // See if we can communicate with the dexc lib. const dexPingRes = __pingDex("__pong"); @@ -281,6 +281,28 @@ const installExtensions = async () => { const { ipcMain } = require("electron"); +// handleEvent listens on the given channel for ipcRenderer.invoke() calls and +// returns the result of the given function or an Error instance if the function +// failed. +const handleEvent = (channel, fn) => { + ipcMain.handle(channel, async (...args) => { + try { + return await fn(...args); + } catch (error) { + if (error instanceof Error) { + return error; + } else { + return new Error(err); + } + } + }); +}; + +// handle is the same as handleEvent but pops off the first arg ("event" object) +// before calling fn. +const handle = (channel, fn) => + handleEvent(channel, (event, ...args) => fn(...args)); + ipcMain.on("reload-allowed-external-request", (event) => { reloadAllowedExternalRequests(); event.returnValue = true; @@ -310,14 +332,13 @@ ipcMain.on("get-available-wallets", (event, network) => { event.returnValue = getAvailableWallets(network); }); -ipcMain.on("start-daemon", async (event, params, testnet) => { - const startedValues = await startDaemon(params, testnet, reactIPC); - event.sender.send("start-daemon-response", startedValues); -}); +handle("start-daemon", (params, testnet) => + startDaemon(params, testnet, reactIPC) +); -ipcMain.on("connect-daemon", (event, { rpcCreds }) => { - event.returnValue = connectRpcDaemon(mainWindow, rpcCreds); -}); +handle("connect-daemon", ({ rpcCreds }) => + connectRpcDaemon(mainWindow, rpcCreds) +); ipcMain.on("delete-daemon", (event, appData, testnet) => { event.returnValue = deleteDaemon(appData, testnet); @@ -344,9 +365,9 @@ ipcMain.on("stop-wallet", (event) => { event.returnValue = stopWallet(); }); -ipcMain.on("start-wallet", (event, walletPath, testnet, rpcCreds) => { +handle("start-wallet", (walletPath, testnet, rpcCreds) => { const { rpcUser, rpcPass, rpcListen, rpcCert } = rpcCreds; - event.returnValue = startWallet( + return startWallet( mainWindow, daemonIsAdvanced, testnet, @@ -359,177 +380,31 @@ ipcMain.on("start-wallet", (event, walletPath, testnet, rpcCreds) => { ); }); -ipcMain.on( - "start-dcrlnd", - async ( - event, - walletAccount, - walletPort, - rpcCreds, - walletPath, - testnet, - autopilotEnabled - ) => { - try { - event.returnValue = await startDcrlnd( - walletAccount, - walletPort, - rpcCreds, - walletPath, - testnet, - autopilotEnabled - ); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } - } -); +handle("start-dcrlnd", startDcrlnd); -ipcMain.on("stop-dcrlnd", async (event) => { - event.returnValue = await stopDcrlnd(); -}); +handle("stop-dcrlnd", stopDcrlnd); -ipcMain.on("check-init-dex", async (event) => { - try { - event.returnValue = await checkInitDex(); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("check-init-dex", checkInitDex); -ipcMain.on("init-dex", async (event, passphrase) => { - try { - event.returnValue = await initDex(passphrase); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("init-dex", initDex); -ipcMain.on("login-dex", async (event, passphrase) => { - try { - event.returnValue = await loginDex(passphrase); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("login-dex", loginDex); -ipcMain.on("logout-dex", async (event) => { - try { - event.returnValue = await logoutDex(); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("logout-dex", logoutDex); -ipcMain.on( - "create-wallet-dex", - async ( - event, - assetID, - passphrase, - appPassphrase, - account, - rpcuser, - rpcpass, - rpclisten, - rpccert - ) => { - try { - event.returnValue = await createWalletDex( - assetID, - passphrase, - appPassphrase, - account, - rpcuser, - rpcpass, - rpclisten, - rpccert - ); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } - } -); +handle("create-wallet-dex", createWalletDex); -ipcMain.on("get-config-dex", async (event, addr) => { - try { - event.returnValue = await getConfigDex(addr); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("get-config-dex", getConfigDex); -ipcMain.on("register-dex", async (event, appPass, addr, fee) => { - try { - event.returnValue = await registerDex(appPass, addr, fee); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("register-dex", registerDex); -ipcMain.on("user-dex", async (event) => { - try { - event.returnValue = await userDex(); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("user-dex", userDex); -ipcMain.on("start-dex", async (event, walletPath, testnet) => { - try { - event.returnValue = await startDex(walletPath, testnet); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("start-dex", startDex); -ipcMain.on("stop-dex", async (event) => { - event.returnValue = await stopDex(); -}); +handle("stop-dex", stopDex); -ipcMain.on("launch-dex-window", async (event, serverAddress) => { - event.returnValue = await createDexWindow(serverAddress); -}); +handle("launch-dex-window", createDexWindow); function createDexWindow(serverAddress) { const child = new BrowserWindow({ @@ -543,88 +418,23 @@ function createDexWindow(serverAddress) { }); } -ipcMain.on("check-btc-config", async (event) => { - try { - event.returnValue = await getCurrentBitcoinConfig(); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("check-btc-config", getCurrentBitcoinConfig); -ipcMain.on( - "update-btc-config", - async (event, rpcuser, rpcpassword, rpcbind, rpcport, testnet) => { - try { - event.returnValue = await updateDefaultBitcoinConfig( - rpcuser, - rpcpassword, - rpcbind, - rpcport, - testnet - ); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } - } -); +handle("update-btc-config", updateDefaultBitcoinConfig); -ipcMain.on("dcrlnd-creds", (event) => { - if (GetDcrlndPID() && GetDcrlndPID() !== -1) { - event.returnValue = GetDcrlndCreds(); - } else { - event.returnValue = null; - } -}); +handle("dcrlnd-creds", () => (GetDcrlndPID() !== -1 ? GetDcrlndCreds() : null)); -ipcMain.on("ln-scb-info", (event, walletPath, testnet) => { - try { - event.returnValue = lnScbInfo(walletPath, testnet); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("ln-scb-info", lnScbInfo); -ipcMain.on("ln-remove-dir", (event, walletName, testnet) => { - try { - event.returnValue = removeDcrlnd(walletName, testnet); - } catch (error) { - if (!(error instanceof Error)) { - event.returnValue = new Error(error); - } else { - event.returnValue = error; - } - } -}); +handle("ln-remove-dir", removeDcrlnd); -ipcMain.on("check-daemon", () => { - getBlockChainInfo(); -}); +handle("check-daemon", getBlockChainInfo); -ipcMain.on("daemon-getinfo", () => { - getDaemonInfo(); -}); +handle("daemon-getinfo", getDaemonInfo); -ipcMain.on("clean-shutdown", async function (event) { - const stopped = await cleanShutdown( - mainWindow, - app, - GetDcrdPID(), - GetDcrwPID() - ); - event.sender.send("clean-shutdown-finished", stopped); -}); +handle("clean-shutdown", () => + cleanShutdown(mainWindow, app, GetDcrdPID(), GetDcrwPID()) +); let reactIPC; ipcMain.on("register-for-errors", function (event) { @@ -762,6 +572,26 @@ ipcMain.handle("show-open-dialog", async (event, opts) => { return await dialog.showOpenDialog(allowedOpts); }); +ipcMain.on("confirm-file-overwrite", (event, filename) => { + const cfgLocale = globalCfg.get(LOCALE); + const locale = locales.find((value) => value.key === cfgLocale); + const msgTemplate = locale.messages["dialogs.confirmFileOverwrite"]; + const msg = msgTemplate.replace("{filename}", filename); + const yesBtn = locale.messages["dialogs.yesButton"]; + const cancelBtn = locale.messages["dialogs.cancelButton"]; + const buttons = [cancelBtn, yesBtn]; + + const opts = { + message: msg, + type: "question", + buttons: buttons, + defaultId: buttons.indexOf(cancelBtn), + cancelId: buttons.indexOf(cancelBtn) + }; + const res = dialog.showMessageBoxSync(mainWindow, opts); + event.returnValue = res === buttons.indexOf(yesBtn); +}); + function setMenuLocale(locale) { //Removes previous listeners of "context-menu" event. mainWindow.webContents._events["context-menu"] = []; @@ -839,6 +669,8 @@ app.on("ready", async () => { } let url = `file://${__dirname}/dist/app.html`; + const path = require("path"); // eslint-disable-line + const preloadPath = path.resolve(__dirname, "dist", "wallet-preload.js"); if (process.env.NODE_ENV === "development") { // Load from the webpack dev server with hot module replacement. const port = process.env.PORT || 3000; @@ -860,7 +692,8 @@ app.on("ready", async () => { devTools: true, contextIsolation: false, webSecurity: false, - enableRemoteModule + enableRemoteModule, + preload: preloadPath }, icon: __dirname + "/icon.png" }; diff --git a/app/main_dev/ipc.js b/app/main_dev/ipc.js index 5ab49e6e53..72ccf7d940 100644 --- a/app/main_dev/ipc.js +++ b/app/main_dev/ipc.js @@ -119,11 +119,10 @@ export const startDaemon = async (params, testnet, reactIPC) => { } const appdata = params && params.appdata; - const started = await launchDCRD(reactIPC, testnet, appdata); - return started; + return await launchDCRD(reactIPC, testnet, appdata); } catch (err) { logger.log("error", "error launching dcrd: " + err); - return { err }; + throw err; } }; @@ -159,7 +158,7 @@ export const removeWallet = (testnet, walletPath) => { } }; -export const startWallet = ( +export const startWallet = async ( mainWindow, daemonIsAdvanced, testnet, @@ -172,8 +171,7 @@ export const startWallet = ( ) => { if (GetDcrwPID()) { logger.log("info", "dcrwallet already started " + GetDcrwPID()); - mainWindow.webContents.send("dcrwallet-port", GetDcrwPort()); - return GetDcrwPID(); + return { pid: GetDcrwPID(), port: GetDcrwPort() }; } initWalletCfg(testnet, walletPath); checkNoLegacyWalletConfig( @@ -182,7 +180,7 @@ export const startWallet = ( rpcUser && rpcPass && rpcHost && rpcListen ); try { - return launchDCRWallet( + return await launchDCRWallet( mainWindow, daemonIsAdvanced, walletPath, @@ -195,6 +193,7 @@ export const startWallet = ( ); } catch (e) { logger.log("error", "error launching dcrwallet: " + e); + throw e; } }; diff --git a/app/main_dev/launch.js b/app/main_dev/launch.js index 6498526a54..266074abbb 100644 --- a/app/main_dev/launch.js +++ b/app/main_dev/launch.js @@ -59,6 +59,13 @@ let dcrdSocket, heightIsSynced, selectedWallet = null; +// These lists track resolve() functions for getinfo/getblockchaininfo daemon +// calls. When the renderer requests one of those calls, the corresponding +// resolve function is added to this list. When the response is received from +// dcrd, the resolve is shifted, so this behaves as a FIFO. +const getInfoResolves = []; +const getBlockchainInfoResolves = []; + const callDEX = (func, params) => { // TODO: this can be done globally once ipcRenderer doesn't import launch.js anymore. const { getNativeFunction, getBufferPointer } = require("sbffi"); @@ -589,7 +596,7 @@ const DecodeDaemonIPCData = (data, cb) => { } }; -export const launchDCRWallet = ( +export const launchDCRWallet = async ( mainWindow, daemonIsAdvanced, walletPath, @@ -628,18 +635,22 @@ export const launchDCRWallet = ( const dcrwExe = getExecutablePath("dcrwallet", argv.custombinpath); if (!fs.existsSync(dcrwExe)) { - logger.log( - "error", - "The dcrwallet executable does not exist. Expected to find it at " + - dcrwExe - ); - return; + const msg = `The dcrwallet executable does not exist. Expected to find it at ${dcrwExe}`; + logger.log("error", msg); + throw new Error(msg); } + // Create a promise that will be resolved once the dcrwallet port is determined. + let portResolve, portReject; + const portPromise = new Promise((resolve, reject) => { + portResolve = resolve; + portReject = reject; + }); + const notifyGrpcPort = (port) => { dcrwPort = port; - logger.log("info", "wallet grpc running on port", port); - mainWindow.webContents.send("dcrwallet-port", port); + logger.log("info", "wallet grpc running on port %d", port); + portResolve(dcrwPort); }; const decodeDcrwIPC = (data) => @@ -654,6 +665,7 @@ export const launchDCRWallet = ( "error", "GRPC port not found on IPC channel to dcrwallet: " + intf ); + portReject("dcrwallet gRPC port not found"); } } if (mtype === "issuedclientcertificate") { @@ -771,7 +783,8 @@ export const launchDCRWallet = ( logger.log("info", "dcrwallet started with pid:" + dcrwPID); dcrwallet.unref(); - return dcrwPID; + const port = await portPromise; + return { pid: dcrwPID, port: port }; }; export const launchDCRLnd = ( @@ -1073,90 +1086,88 @@ export const connectRpcDaemon = async (mainWindow, rpcCreds) => { rpc_port = rpcport; } - try { - // During the first startup, the rpc.cert file might not exist for a few - // seconds. In that case, we wait up to 30s before failing this call. - let tries = 0; - const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms)); - while (tries++ < 30 && !fs.existsSync(rpc_cert)) await sleep(1000); - if (!fs.existsSync(rpc_cert)) { - return mainWindow.webContents.send("connectRpcDaemon-response", { - error: new Error("rpc cert '" + rpc_cert + "' does not exist") - }); - } + // Return early if already connected. + if (dcrdSocket && dcrdSocket.readyState === dcrdSocket.OPEN) { + return { connected: true }; + } - const cert = fs.readFileSync(rpc_cert); - const url = `${rpc_host}:${rpc_port}`; - if (dcrdSocket && dcrdSocket.readyState === dcrdSocket.OPEN) { - return mainWindow.webContents.send("connectRpcDaemon-response", { - connected: true - }); - } - dcrdSocket = new webSocket(`wss://${url}/ws`, { - headers: { - Authorization: - "Basic " + Buffer.from(rpc_user + ":" + rpc_pass).toString("base64") - }, - cert: cert, - ecdhCurve: "secp521r1", - ca: [cert] - }); - dcrdSocket.on("open", function () { - logger.log("info", "decrediton has connected to dcrd instance"); - return mainWindow.webContents.send("connectRpcDaemon-response", { - connected: true - }); - }); - dcrdSocket.on("error", function (error) { - logger.log("error", `Error: ${error}`); - return mainWindow.webContents.send("connectRpcDaemon-response", { - connected: false, - error: error.toString() - }); - }); - dcrdSocket.on("message", function (data) { - const parsedData = JSON.parse(data); - const id = parsedData ? parsedData.id : ""; - switch (id) { - case "getinfo": - mainWindow.webContents.send( - "check-getinfo-response", - parsedData.result - ); - break; - case "getblockchaininfo": { - const dataResults = parsedData.result || {}; - const blockCount = dataResults.blocks; - const syncHeight = dataResults.syncheight; - mainWindow.webContents.send("check-daemon-response", { - blockCount, - syncHeight - }); - break; - } - } - }); - dcrdSocket.on("close", () => { - logger.log("info", "decrediton has disconnected to dcrd instance"); - }); - } catch (error) { - return mainWindow.webContents.send("connectRpcDaemon-response", { - connected: false, - error - }); + // During the first startup, the rpc.cert file might not exist for a few + // seconds. In that case, we wait up to 30s before failing this call. + let tries = 0; + const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms)); + while (tries++ < 30 && !fs.existsSync(rpc_cert)) await sleep(1000); + if (!fs.existsSync(rpc_cert)) { + throw new Error(`rpc cert ${rpc_cert}' does not exist`); } + + const cert = fs.readFileSync(rpc_cert); + const url = `${rpc_host}:${rpc_port}`; + + // Create a promise that will be resolved when the socket is opened. + let openResolve, openReject; + const openPromise = new Promise((resolve, reject) => { + openResolve = resolve; + openReject = reject; + }); + + // Attempt a new connection to dcrd. + dcrdSocket = new webSocket(`wss://${url}/ws`, { + headers: { + Authorization: + "Basic " + Buffer.from(rpc_user + ":" + rpc_pass).toString("base64") + }, + cert: cert, + ecdhCurve: "secp521r1", + ca: [cert] + }); + dcrdSocket.on("open", function () { + logger.log("info", "decrediton has connected to dcrd instance"); + openResolve({ connected: true }); + }); + dcrdSocket.on("error", function (error) { + logger.log("error", `Error: ${error}`); + openReject(error); + }); + dcrdSocket.on("message", function (data) { + const parsedData = JSON.parse(data); + const id = parsedData ? parsedData.id : ""; + switch (id) { + case "getinfo": + getInfoResolves.shift()(parsedData.result); + break; + case "getblockchaininfo": { + const dataResults = parsedData.result || {}; + const blockCount = dataResults.blocks; + const syncHeight = dataResults.syncheight; + getBlockchainInfoResolves.shift()({ + blockCount, + syncHeight + }); + break; + } + } + }); + dcrdSocket.on("close", () => { + logger.log("info", "decrediton has disconnected to dcrd instance"); + }); + + return openPromise; }; export const getDaemonInfo = () => - dcrdSocket.send( - '{"jsonrpc":"1.0","id":"getinfo","method":"getinfo","params":[]}' - ); + new Promise((resolve) => { + getInfoResolves.push(resolve); + dcrdSocket.send( + '{"jsonrpc":"1.0","id":"getinfo","method":"getinfo","params":[]}' + ); + }); export const getBlockChainInfo = () => new Promise((resolve) => { if (dcrdSocket && dcrdSocket.readyState === dcrdSocket.CLOSED) { return resolve({}); } + getBlockchainInfoResolves.push(resolve); dcrdSocket.send( '{"jsonrpc":"1.0","id":"getblockchaininfo","method":"getblockchaininfo","params":[]}' ); diff --git a/app/wallet-preload-shim.js b/app/wallet-preload-shim.js new file mode 100644 index 0000000000..c3bafd2130 --- /dev/null +++ b/app/wallet-preload-shim.js @@ -0,0 +1 @@ +export const fs = window.fs; diff --git a/app/wallet-preload.js b/app/wallet-preload.js new file mode 100644 index 0000000000..d92ae02c00 --- /dev/null +++ b/app/wallet-preload.js @@ -0,0 +1,17 @@ +import * as fs from "wallet/fs"; +import { contextBridge } from "electron"; + +// Elements in this object define the public API exported by the preload script. +const api = { + fs: fs +}; + +try { + Object.keys(api).forEach((key) => + contextBridge.exposeInMainWorld(key, api[key]) + ); +} catch (error) { + // This happens when contextIsolation == false. Expose directly in the global + // window object. + Object.keys(api).forEach((key) => (window[key] = api[key])); +} diff --git a/app/wallet/app.js b/app/wallet/app.js index 75914cb706..2c04fb4365 100644 --- a/app/wallet/app.js +++ b/app/wallet/app.js @@ -24,7 +24,9 @@ export const logOptionNoResponseData = (opts) => ({ // Formats a dynamic list of log arguments const formatLogArgs = (msg, args) => { const formatArg = (arg) => { - if (isObject(arg) && isFunction(arg.toObject)) { + if (arg instanceof Error) { + return arg.toString(); + } else if (isObject(arg) && isFunction(arg.toObject)) { // requests/responses on the grpc system have a toObejct() func return JSON.stringify(arg.toObject()); } else if (isUndefined(arg)) { diff --git a/app/wallet/daemon.js b/app/wallet/daemon.js index 1bee0107bb..2a0b628b23 100644 --- a/app/wallet/daemon.js +++ b/app/wallet/daemon.js @@ -3,20 +3,23 @@ import { ipcRenderer } from "electron"; import { withLog as log, logOptionNoResponseData } from "./app"; import { isString } from "lodash"; +// invoke calls ipcRenderer.invoke() with the given channel and args. If the +// call returns an Error instance, then it throws an exception. +const invoke = async (channel, ...args) => { + const res = await ipcRenderer.invoke(channel, ...args); + if (res instanceof Error) { + throw res; + } + return res; +}; + export const checkDecreditonVersion = log( () => Promise.resolve(ipcRenderer.sendSync("check-version")), "Check Decrediton release version" ); export const startDaemon = log( - (params, testnet) => - new Promise((resolve, reject) => { - ipcRenderer.send("start-daemon", params, testnet); - ipcRenderer.on("start-daemon-response", (event, started) => { - if (started && started.err) reject(started.err); - resolve(started); - }); - }), + (params, testnet) => invoke("start-daemon", params, testnet), "Start Daemon" ); @@ -26,14 +29,11 @@ export const deleteDaemonData = log( "Delete Daemon Data" ); -export const cleanShutdown = () => { - return new Promise((resolve) => { - ipcRenderer.send("clean-shutdown"); - ipcRenderer.on("clean-shutdown-finished", (event, stopped) => { - if (!stopped) throw "Error shutting down app"; - resolve(stopped); - }); - }); +export const cleanShutdown = async () => { + // FIXME: clean-shutdown currently never fails. Revise this. + const stopped = await invoke("clean-shutdown"); + if (!stopped) throw "Error shutting down app"; + return stopped; }; export const setIsWatchingOnly = log( @@ -97,22 +97,7 @@ export const stopWallet = log( export const startWallet = log( (walletPath, testnet, rpcCreds) => - new Promise((resolve, reject) => { - let port, - pid = ""; - - // resolveCheck must be done both on the dcrwallet-port event and on the - // return of the sendSync call because we can't be certain which will happen first - const resolveCheck = () => (pid && port ? resolve({ pid, port }) : null); - - ipcRenderer.once("dcrwallet-port", (e, p) => { - port = p; - resolveCheck(); - }); - pid = ipcRenderer.sendSync("start-wallet", walletPath, testnet, rpcCreds); - if (!pid) reject("Error starting wallet"); - resolveCheck(); - }), + invoke("start-wallet", walletPath, testnet, rpcCreds), "Start Wallet" ); @@ -127,22 +112,17 @@ export const getPreviousWallet = log( logOptionNoResponseData() ); -export const getBlockCount = log( - () => - new Promise((resolve) => { - ipcRenderer.once("check-daemon-response", (e, info) => { - const blockCount = isString(info.blockCount) - ? parseInt(info.blockCount.trim()) - : info.blockCount; - const syncHeight = isString(info.syncHeight) - ? parseInt(info.syncHeight.trim()) - : info.syncHeight; - resolve({ blockCount, syncHeight }); - }); - ipcRenderer.send("check-daemon"); - }), - "Get Block Count" -); +export const getBlockCount = log(async () => { + const info = await invoke("check-daemon"); + const blockCount = isString(info.blockCount) + ? parseInt(info.blockCount.trim()) + : info.blockCount; + const syncHeight = isString(info.syncHeight) + ? parseInt(info.syncHeight.trim()) + : info.syncHeight; + + return { blockCount, syncHeight }; +}, "Get Block Count"); export const setHeightSynced = log( () => @@ -155,17 +135,11 @@ export const setHeightSynced = log( "set height is synced" ); -export const getDaemonInfo = log( - (rpcCreds) => - new Promise((resolve) => { - ipcRenderer.once("check-getinfo-response", (e, info) => { - const isTestnet = info ? info.testnet : null; - resolve({ isTestnet }); - }); - ipcRenderer.send("daemon-getinfo", rpcCreds); - }), - "Get Daemon network info" -); +export const getDaemonInfo = log(async (rpcCreds) => { + const info = await invoke("daemon-getinfo", rpcCreds); + const isTestnet = info ? info.testnet : null; + return { isTestnet }; +}, "Get Daemon network info"); export const getAvailableWallets = log( (network) => @@ -202,21 +176,13 @@ export const allowStakePoolHost = log( "Allow StakePool Host" ); -export const connectDaemon = log( - (params) => - new Promise((resolve, reject) => { - ipcRenderer.once("connectRpcDaemon-response", (e, info) => { - if (info.connected) { - resolve({ connected: true }); - } - if (info.error) { - reject({ connected: false, error: info.error }); - } - }); - ipcRenderer.send("connect-daemon", params); - }), - "Connect Daemon" -); +export const connectDaemon = log(async (params) => { + try { + return await invoke("connect-daemon", params); + } catch (error) { + throw { connected: false, error: error.toString() }; + } +}, "Connect Daemon"); // TODO create a wallet/log and move those method not related to daemon to there. diff --git a/app/wallet/fs.js b/app/wallet/fs.js new file mode 100644 index 0000000000..534f2089bd --- /dev/null +++ b/app/wallet/fs.js @@ -0,0 +1,41 @@ +import fs from "fs"; +import { makeRandomString } from "helpers/strings"; +import { ipcRenderer } from "electron"; + +// This map decouples an fd (number) from an opaque identifier, so that the UI +// can only write to files previously opened through these functions. +const fds = {}; + +export const openWritable = (filename) => { + if (fs.existsSync(filename)) { + const confirmOverwrite = ipcRenderer.sendSync( + "confirm-file-overwrite", + filename + ); + if (!confirmOverwrite) throw new Error("User canceled file overwrite"); + } + + const fd = fs.openSync(filename, "w", 0o600); + const id = makeRandomString(32); + fds[id] = fd; + return id; +}; + +export const writeSync = (id, s) => { + const fd = fds[id]; + if (!fd) { + throw new Error(`id ${id} not a previously opened file`); + } + + return fs.writeSync(fd, s); +}; + +export const closeSync = (id) => { + const fd = fds[id]; + if (!fd) { + throw new Error(`id ${id} not a previously opened file`); + } + + delete fds[id]; + return fs.closeSync(fd); +}; diff --git a/app/wallet/ln/index.js b/app/wallet/ln/index.js index 624d783023..c7e2f94d3c 100644 --- a/app/wallet/ln/index.js +++ b/app/wallet/ln/index.js @@ -4,6 +4,7 @@ import { lnrpc as pb } from "middleware/ln/rpc_pb"; import { lnrpc as wupb } from "middleware/ln/walletunlocker_pb"; import { strHashToRaw } from "helpers/byteActions"; import { ipcRenderer } from "electron"; +import { invoke } from "helpers/electronRenderer"; export const getLightningClient = client.getLightningClient; export const getWatchtowerClient = client.getWatchtowerClient; @@ -304,16 +305,10 @@ export const stopDaemon = (client) => { }; export const removeDcrlnd = (walletName, testnet) => - new Promise((resolve, reject) => { - const res = ipcRenderer.sendSync("ln-remove-dir", walletName, testnet); - res instanceof Error ? reject(res) : resolve(res); - }); + invoke("ln-remove-dir", walletName, testnet); export const scbInfo = (walletPath, testnet) => - new Promise((resolve, reject) => { - const res = ipcRenderer.sendSync("ln-scb-info", walletPath, testnet); - res instanceof Error ? reject(res) : resolve(res); - }); + invoke("ln-scb-info", walletPath, testnet); export const exportBackup = (client, destPath) => new Promise((resolve, reject) => { @@ -324,6 +319,18 @@ export const exportBackup = (client, destPath) => return; } + // If this file already exists, show the confirmation modal. + if (fs.existsSync(destPath)) { + const confirmOverwrite = ipcRenderer.sendSync( + "confirm-file-overwrite", + destPath + ); + if (!confirmOverwrite) { + reject("User canceled file overwrite"); + return; + } + } + const data = resp.getMultiChanBackup().getMultiChanBackup(); try { fs.writeFileSync(destPath, data); diff --git a/package.json b/package.json index 9ec616d88f..757236a7c9 100644 --- a/package.json +++ b/package.json @@ -15,15 +15,17 @@ "build-main": "cross-env NODE_ENV=production node -r @babel/register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress=profile --color", "build-renderer": "cross-env NODE_ENV=production node -r @babel/register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress=profile --color", "build-trezor": "cross-env NODE_ENV=production node -r @babel/register ./node_modules/webpack/bin/webpack --config webpack.config.trezor.js --progress=profile --color", - "build": "npm run build-trezor && npm run build-main && npm run build-renderer", + "build-preload": "cross-env NODE_ENV=production node -r @babel/register ./node_modules/webpack/bin/webpack --config webpack.config.preload.js --progress=profile --color", + "build": "npm run build-trezor && npm run build-preload && npm run build-main && npm run build-renderer", "dexsite": "node ./scripts/dexsite.js", "rebuild-natives": "node_modules/.bin/electron-rebuild", "rebuild-dexc": "cd modules/dex && npm run install", "start": "cross-env NODE_ENV=production electron ./app/ --debug --custombinpath=./bin", "start-hot": "cross-env HOT=1 NODE_ENV=development electron -r @babel/register -r @babel/polyfill ./app/main.development", "start-hot-nosandbox": "cross-env HOT=1 NODE_ENV=development electron -r @babel/register -r @babel/polyfill ./app/main.development -r --no-sandbox", + "start-preload": "webpack -c webpack.config.preload.js --watch", "postinstall": "electron-builder install-app-deps && npm run rebuild-dexc && npm rum dexsite", - "dev": "npm run hot-server -- --start-hot", + "dev": "npm run build-preload && npm run hot-server -- --start-hot", "dev-nosandbox": "npm run hot-server -- --start-hot-nosandbox", "package": "npm run build && ./node_modules/.bin/electron-builder build --publish never", "package-win": "npm run build && ./node_modules/.bin/electron-builder build --win --x64 --ia32", @@ -47,7 +49,8 @@ "\\.module\\.css$": "identity-obj-proxy", "\\.css$": "/test/mocks/styleMock.js", "^grpc$": "/test/mocks/grpcMock.js", - "^electron$": "/test/mocks/electronMock.js" + "^electron$": "/test/mocks/electronMock.js", + "^electron-store$": "/test/mocks/electronStore.js" }, "transformIgnorePatterns": [ "/node_modules/", diff --git a/server.js b/server.js index 3908e3e1bf..b097cfc9a7 100644 --- a/server.js +++ b/server.js @@ -30,10 +30,16 @@ app.use(wdm); app.use(webpackHotMiddleware(compiler)); +let preloadProc; + const server = app.listen(PORT, "localhost", serverError => { if (serverError) { return console.error(serverError); } + + // Start a webpack run to watch for changes to the preload script. + preloadProc = spawn("npm", ["run", "start-preload"], { shell: true, env: process.env, stdio: "inherit" }); + if (argv["start-hot"]) { spawn("npm", [ "run", "start-hot" ], { shell: true, env: process.env, stdio: "inherit" }) .on("close", code => process.exit(code)) @@ -47,6 +53,10 @@ const server = app.listen(PORT, "localhost", serverError => { console.log(`Listening at http://localhost:${PORT}`); }); +process.on("exit", () => { + preloadProc && preloadProc.kill(); +}); + process.on("SIGTERM", () => { console.log("Stopping dev server"); wdm.close(); diff --git a/test/mocks/electronStore.js b/test/mocks/electronStore.js new file mode 100644 index 0000000000..9b51e56e35 --- /dev/null +++ b/test/mocks/electronStore.js @@ -0,0 +1,16 @@ +class MockElectronStore { + get(key) { + return this[key]; + } + set(key, value) { + return (this[key] = value); + } + delete(key) { + delete this[key]; + } + has(key) { + return !!this[key]; + } +} + +export default MockElectronStore; diff --git a/webpack.config.preload.js b/webpack.config.preload.js new file mode 100644 index 0000000000..6b74e43493 --- /dev/null +++ b/webpack.config.preload.js @@ -0,0 +1,74 @@ +/** + * Build config for trezor's iframe. This is used to contact trezor-bridge on a + * separate iframe in the wallet. + */ + +const path = require("path"); +const webpack = require("webpack"); +const TerserPlugin = require("terser-webpack-plugin"); + +module.exports = { + mode: "production", + + target: "electron-preload", + + entry: "./app/wallet-preload.js", + + devtool: "source-map", + + output: { + filename: "wallet-preload.js", + path: path.join(__dirname, "app/dist"), + publicPath: "./", + library: { + name: "_decrediton", + type: "var" + } + }, + + module: { + rules: [ + { + test: /\.js?$/, + exclude: /node_modules/, + use: ["babel-loader"] + } + ] + }, + + resolve: { + modules: ["node_modules"] + }, + + plugins: [ + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify("production"), + "process.env.NODE_DEBUG": false + }) + ], + + optimization: { + minimizer: [ + new TerserPlugin({ + parallel: true, + extractComments: false, + terserOptions: { + ecma: 6, + mangle: { + reserved: [ + "Array", + "BigInteger", + "Boolean", + "Buffer", + "ECPair", + "Function", + "Number", + "Point", + "Script" + ] + } + } + }) + ] + } +};