diff --git a/assets/installer.nsh b/assets/installer.nsh index ab544c8d..db9f820d 100644 --- a/assets/installer.nsh +++ b/assets/installer.nsh @@ -1,3 +1,13 @@ +!macro customHeader + RequestExecutionLevel admin +!macroend + !macro customUnInstall - ExecWait "TaskKill /IM proxy-router.exe /F" + ${ifNot} ${isUpdated} + ExecWait "TaskKill /IM proxy-router.exe /F" + ExecWait "sc stop proxySeller" + ExecWait "sc stop proxyBuyer" + ExecWait "sc delete proxySeller" + ExecWait "sc delete proxyBuyer" + ${endIf} !macroend \ No newline at end of file diff --git a/executables/nssm.zip b/executables/nssm.zip new file mode 100644 index 00000000..3cc5b517 Binary files /dev/null and b/executables/nssm.zip differ diff --git a/package.json b/package.json index c9f21e82..da876e5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumerin-wallet-desktop", - "version": "1.1.31", + "version": "1.1.32", "engines": { "node": ">=14" }, @@ -45,6 +45,7 @@ "@lumerin/wallet-core": "git+ssh://git@github.com:Lumerin-protocol/WalletCore.git#1.0.62", "@reach/menu-button": "0.17.0", "@tabler/icons": "1.119.0", + "@vscode/sudo-prompt": "9.3.1", "axios": "0.27.2", "babel-preset-env": "1.7.0", "babel-preset-es2015": "6.24.1", @@ -128,7 +129,8 @@ "executables/**/*" ], "nsis": { - "include": "assets/installer.nsh" + "include": "assets/installer.nsh", + "allowElevation": true }, "mac": { "artifactName": "${name}.${ext}", @@ -155,6 +157,7 @@ "artifactName": "${name}.${ext}", "target": "nsis", "icon": "./assets/lumerin.png", + "requestedExecutionLevel": "requireAdministrator", "legalTrademarks": "" } }, diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index d5155228..f51ce2be 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -1,5 +1,7 @@ +const os = require("os"); + const pTimeout = require("p-timeout"); const logger = require("../../../logger"); const auth = require("../auth"); @@ -154,12 +156,15 @@ function createWallet(data, core, isOpen = true) { const restartProxyRouter = async (data, { emitter, api }) => { const password = await auth.getSessionPassword(); - api["proxy-router"].kill(config.chain.buyerProxyPort).catch(logger.error); - await api["proxy-router"] - .kill(config.chain.sellerProxyPort) - .catch(logger.error); - - emitter.emit("open-proxy-router", { password }); + if (['darwin', 'win32', 'linux'].includes(os.platform()) ) { + emitter.emit("open-proxy-router", { password, restartDaemon: true }); + } else { + api["proxy-router"].kill(config.chain.buyerProxyPort).catch(logger.error); + await api["proxy-router"] + .kill(config.chain.sellerProxyPort) + .catch(logger.error); + emitter.emit("open-proxy-router", { password }); + } }; async function openWallet({ emitter }, password) { @@ -310,7 +315,6 @@ const getLmrTransferGasLimit = (data, { api }) => api.lumerin.estimateGasTransfer(data); const getAddressAndPrivateKey = async (data, { api }) => { - const isValid = await auth.isValidPassword(data.password); if (!isValid) { return { error: new WalletError("Invalid password") }; @@ -343,14 +347,20 @@ const revealSecretPhrase = async (password) => { if (!isValid) { return { error: new WalletError("Invalid password") }; } - + const entropy = wallet.getEntropy(password); const mnemonic = keys.entropyToMnemonic(entropy); return mnemonic; -} +}; function getPastTransactions({ address, page, pageSize }, { api }) { - return api.explorer.getPastCoinTransactions(0, undefined, address, page, pageSize); + return api.explorer.getPastCoinTransactions( + 0, + undefined, + address, + page, + pageSize + ); } module.exports = { diff --git a/public/main/client/index.js b/public/main/client/index.js index 5a8a5943..cdfd4d86 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -1,8 +1,9 @@ "use strict"; -const { ipcMain } = require("electron"); +const { ipcMain, app, dialog } = require("electron"); const createCore = require("@lumerin/wallet-core"); const stringify = require("json-stringify-safe"); +const os = require("os"); const logger = require("../../logger"); const subscriptions = require("./subscriptions"); @@ -18,7 +19,11 @@ const { runProxyRouter, PROXY_ROUTER_MODE, isProxyRouterHealthy, + getResourcesPath, } = require("./proxyRouter"); +const { runMacosDaemons } = require("./proxyRouter/macos/daemon"); +const { runWindowsServices } = require("./proxyRouter/windows/service"); +const { runLinuxDaemons } = require("./proxyRouter/linux/daemon"); function startCore({ chain, core, config: coreConfig }, webContent) { logger.verbose(`Starting core ${chain}`); @@ -61,9 +66,13 @@ function startCore({ chain, core, config: coreConfig }, webContent) { send("transactions-scan-started", {}); return api.explorer - .syncTransactions(0, address, (number) => - storage.setSyncBlock(number, chain) - , page, pageSize) + .syncTransactions( + 0, + address, + (number) => storage.setSyncBlock(number, chain), + page, + pageSize + ) .then(function() { send("transactions-scan-finished", { success: true }); @@ -81,7 +90,9 @@ function startCore({ chain, core, config: coreConfig }, webContent) { success: false, }); - emitter.once("coin-block", () => syncTransactions({ address }, page, pageSize)); + emitter.once("coin-block", () => + syncTransactions({ address }, page, pageSize) + ); }); } @@ -93,7 +104,29 @@ function startCore({ chain, core, config: coreConfig }, webContent) { ); }); - emitter.on("open-proxy-router", async ({ password }) => { + const shouldRestartProxyRouterAfterWalletUpdate = () => { + const prevAppVersion = settings.getAppVersion(); + const isAppVersionChanged = prevAppVersion !== app.getVersion(); + + if (!isAppVersionChanged) { + return false; + } + + settings.setAppVersion(app.getVersion()); + const choice = dialog.showMessageBoxSync(null, { + type: "question", + buttons: ["Restart", "Later"], + title: "Confirm", + message: + "The wallet was updated and requires the restart of background service. Would you like to do it right now or restart manually later? The currently running contracts will be affected.", + }); + if (choice === 0) { + return true; + } + return false; + }; + + emitter.on("open-proxy-router", async ({ password, restartDaemon }) => { const proxyRouterUserConfig = settings.getProxyRouterConfig(); if (!proxyRouterUserConfig.useHostedProxyRouter) { const { address, privateKey } = await getAddressAndPrivateKey( @@ -106,26 +139,53 @@ function startCore({ chain, core, config: coreConfig }, webContent) { ...coreConfig.chain, ...proxyRouterUserConfig, }; + const shouldRestartProxy = shouldRestartProxyRouterAfterWalletUpdate(); const isSellerHealthy = await isProxyRouterHealthy( api, config.localSellerProxyRouterUrl ); - if (!isSellerHealthy) { - logger.debug("Seller is not healhy, restart..."); - await proxyRouterApi.kill(config.sellerProxyPort).catch(logger.error); - runProxyRouter(config, PROXY_ROUTER_MODE.Seller); - } - const isBuyerHealthy = await isProxyRouterHealthy( api, config.localBuyerProxyRouterUrl ); - if (!isBuyerHealthy) { - logger.debug("Buyer is not healhy, restart..."); - await proxyRouterApi.kill(config.buyerProxyPort).catch(logger.error); - runProxyRouter(config, PROXY_ROUTER_MODE.Buyer); + + if ( + !isSellerHealthy || + !isBuyerHealthy || + restartDaemon || + shouldRestartProxy + ) { + logger.debug("Proxy is not healhy, restart..."); + if (os.platform() === "darwin") { + await proxyRouterApi.kill(config.sellerProxyPort).catch(logger.error); + await proxyRouterApi.kill(config.buyerProxyPort).catch(logger.error); + await runMacosDaemons(getResourcesPath(), config); + } else if (os.platform() === "win32") { + await proxyRouterApi.kill(config.sellerProxyPort).catch(logger.error); + await proxyRouterApi.kill(config.buyerProxyPort).catch(logger.error); + await runWindowsServices(getResourcesPath(), config); + } else if (os.platform() === "linux") { + await proxyRouterApi.kill(config.sellerProxyPort).catch(logger.error); + await proxyRouterApi.kill(config.buyerProxyPort).catch(logger.error); + await runLinuxDaemons(getResourcesPath(), config); + } else { + if (!isSellerHealthy) { + await proxyRouterApi + .kill(config.sellerProxyPort) + .catch(logger.error); + runProxyRouter(config, PROXY_ROUTER_MODE.Seller); + } + + if (!isBuyerHealthy) { + await proxyRouterApi + .kill(config.buyerProxyPort) + .catch(logger.error); + runProxyRouter(config, PROXY_ROUTER_MODE.Buyer); + } + } } + send("proxy-router-type-changed", { isLocal: true, }); diff --git a/public/main/client/proxyRouter.js b/public/main/client/proxyRouter.js index 553c2196..841dbce4 100644 --- a/public/main/client/proxyRouter.js +++ b/public/main/client/proxyRouter.js @@ -1,14 +1,19 @@ const { app } = require("electron"); const fs = require("fs"); -const { spawn } = require("child_process"); - const logger = require("../../logger.js"); +const { spawn } = require("child_process"); const PROXY_ROUTER_MODE = { Buyer: "buyer", Seller: "seller", }; +const getResourcesPath = () => { + return process.env.NODE_ENV === "production" + ? process.resourcesPath // Prod Mode + : `${__dirname}/../../..`; // Dev Mode +}; + const openLogFile = (name, retry = true) => { try { const path = `${app.getPath("logs")}/${name}.log`; @@ -61,10 +66,7 @@ const runProxyRouter = (config, mode = PROXY_ROUTER_MODE.Seller) => { }; try { - const resourcePath = - process.env.NODE_ENV === "production" - ? process.resourcesPath // Prod Mode - : `${__dirname}/../../..`; // Dev Mode + const resourcePath = getResourcesPath() const out = openLogFile(`${mode}-out`); const err = openLogFile(`${mode}-err`); @@ -101,4 +103,4 @@ const runProxyRouter = (config, mode = PROXY_ROUTER_MODE.Seller) => { } }; -module.exports = { runProxyRouter, PROXY_ROUTER_MODE, isProxyRouterHealthy }; +module.exports = { runProxyRouter, PROXY_ROUTER_MODE, isProxyRouterHealthy, getResourcesPath }; diff --git a/public/main/client/proxyRouter/config.js b/public/main/client/proxyRouter/config.js new file mode 100644 index 00000000..595de66a --- /dev/null +++ b/public/main/client/proxyRouter/config.js @@ -0,0 +1,40 @@ +const PROXY_ROUTER_MODE = { + Buyer: "buyer", + Seller: "seller", +}; + +const getProxyRouterEnvs = (config, mode) => { + const modes = { + [PROXY_ROUTER_MODE.Buyer]: [ + ["PROXY_ADDRESS", `0.0.0.0:${config.buyerProxyPort}`], + ["WEB_ADDRESS", `0.0.0.0:${config.buyerWebPort}`], + ["IS_BUYER", true], + ["POOL_ADDRESS", `"${config.buyerDefaultPool}"`], + ["HASHRATE_DIFF_THRESHOLD", 0.1], + ], + [PROXY_ROUTER_MODE.Seller]: [ + ["PROXY_ADDRESS", `0.0.0.0:${config.sellerProxyPort}`], + ["WEB_ADDRESS", `0.0.0.0:${config.sellerWebPort}`], + ["IS_BUYER", false], + ["POOL_ADDRESS", `"${config.sellerDefaultPool}"`], + ["HASHRATE_DIFF_THRESHOLD", 0.03], + ], + }; + return [ + ["CLONE_FACTORY_ADDRESS", `"${config.cloneFactoryAddress}"`], + ["ETH_NODE_ADDRESS", `"${config.wsApiUrl}"`], + ["MINER_VETTING_DURATION", "1m"], + ["POOL_CONN_TIMEOUT", "15m"], + ["POOL_MAX_DURATION", "7m"], + ["POOL_MIN_DURATION", "2m"], + ["STRATUM_SOCKET_BUFFER_SIZE", 4], + ["VALIDATION_BUFFER_PERIOD", "10m"], + ["WALLET_ADDRESS", `"${config.walletAddress}"`], + ["WALLET_PRIVATE_KEY", `"${config.privateKey}"`], + ["LOG_LEVEL", "debug"], + ["MINER_SUBMIT_ERR_LIMIT", 0], + ...(modes[mode] || []), + ]; +}; + +module.exports = { getProxyRouterEnvs, PROXY_ROUTER_MODE }; diff --git a/public/main/client/proxyRouter/linux/daemon.js b/public/main/client/proxyRouter/linux/daemon.js new file mode 100644 index 00000000..fdc1d0f6 --- /dev/null +++ b/public/main/client/proxyRouter/linux/daemon.js @@ -0,0 +1,54 @@ +const { getProxyRouterEnvs, PROXY_ROUTER_MODE } = require("../config"); +const { linuxInstallScript } = require("./installScript"); +const { sudo } = require("../sudoPrompt"); + +const getInstallLinuxServiceCommand = async (daemonName, pathToExecutable) => { + const config = linuxInstallScript + .replaceAll("{serviceName}", daemonName) + .replaceAll("{pathToExecutable}", `${pathToExecutable}/proxy-router`) + + const path = `/etc/systemd/system/${daemonName}.service`; + return `touch ${path} && echo '${config}' > ${path}`; +}; + +const getCommandToRunDaemon = async (serviceName, envs) => { + const setEnvsCommand = envs + .map((e) => `sudo systemctl set-environment ${e[0]}=${e[1]}`) + .join(";"); + return `sudo systemctl daemon-reload; ${setEnvsCommand}; sudo service ${serviceName} restart`; +}; + +const runLinuxDaemons = async (resourcePath, config) => { + const sellerServiceName = "proxyRouterSeller"; + const buyerServiceName = "proxyRouterBuyer"; + const installSellerCommand = await getInstallLinuxServiceCommand( + sellerServiceName, + `${resourcePath}/executables` + ); + const installBuyerCommand = await getInstallLinuxServiceCommand( + buyerServiceName, + `${resourcePath}/executables` + ); + + const sellerRunCommand = await getCommandToRunDaemon( + sellerServiceName, + getProxyRouterEnvs(config, PROXY_ROUTER_MODE.Seller) + ); + const buyerRunCommand = await getCommandToRunDaemon( + buyerServiceName, + getProxyRouterEnvs(config, PROXY_ROUTER_MODE.Buyer) + ); + + const commands = [ + installSellerCommand, + installBuyerCommand, + sellerRunCommand, + buyerRunCommand, + ]; + + await sudo(commands.join(";")); +}; + +module.exports = { + runLinuxDaemons, +}; diff --git a/public/main/client/proxyRouter/linux/installScript.js b/public/main/client/proxyRouter/linux/installScript.js new file mode 100644 index 00000000..278e1084 --- /dev/null +++ b/public/main/client/proxyRouter/linux/installScript.js @@ -0,0 +1,16 @@ +const linuxInstallScript = ` +[Unit] +Description={serviceName} +After=network.target + +[Service] +Type=simple +ExecStart="{pathToExecutable}" +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +`; + +module.exports = { linuxInstallScript }; \ No newline at end of file diff --git a/public/main/client/proxyRouter/macos/daemon.js b/public/main/client/proxyRouter/macos/daemon.js new file mode 100644 index 00000000..ee948fce --- /dev/null +++ b/public/main/client/proxyRouter/macos/daemon.js @@ -0,0 +1,63 @@ +const { macosInstallScript } = require("./installScript"); +const { getProxyRouterEnvs, PROXY_ROUTER_MODE } = require("../config"); +const { execSync } = require('child_process'); + +const getInstallMacosDaemonCommand = async (daemonName, pathToExecutable) => { + pathToExecutable = pathToExecutable.replaceAll(' ', '\\ ') + const config = macosInstallScript + .replace("{serviceName}", daemonName) + .replace("{pathToExecutable}", `${pathToExecutable}/proxy-router`) + .replace("{workingDir}", pathToExecutable) + .replace("{logFilePath}", `${pathToExecutable}/${daemonName}.log`); + + const path = `~/Library/LaunchAgents/${daemonName}.plist`; + return `touch ${path} && echo '${config}' > ${path}`; +}; + +const getMacosDaemonPath = (daemonName) => { + const path = `~/Library/LaunchAgents/${daemonName}.plist`; + return path; +}; + +const getCommandToRunDaemon = async (pathToDaemon, envs) => { + const setEnvsCommand = envs + .map((e) => `launchctl setenv LMR_${e[0]} ${e[1]}`) + .join(";"); + return `launchctl unload -F ${pathToDaemon}; ${setEnvsCommand}; launchctl load -F ${pathToDaemon}`; +}; + +const runMacosDaemons = async (resourcePath, config) => { + const sellerServiceName = "com.proxy.router.seller"; + const buyerServiceName = "com.proxy.router.buyer"; + + const installSellerCommand = await getInstallMacosDaemonCommand( + sellerServiceName, + `${resourcePath}/executables` + ); + const installBuyerCommand = await getInstallMacosDaemonCommand( + buyerServiceName, + `${resourcePath}/executables` + ); + + const sellerRunCommand = await getCommandToRunDaemon( + getMacosDaemonPath(sellerServiceName), + getProxyRouterEnvs(config, PROXY_ROUTER_MODE.Seller) + ); + const buyerRunCommand = await getCommandToRunDaemon( + getMacosDaemonPath(buyerServiceName), + getProxyRouterEnvs(config, PROXY_ROUTER_MODE.Buyer) + ); + + const commands = [ + installSellerCommand, + installBuyerCommand, + sellerRunCommand, + buyerRunCommand, + ]; + + execSync(commands.join(";")); +}; + +module.exports = { + runMacosDaemons, +}; diff --git a/public/main/client/proxyRouter/macos/installScript.js b/public/main/client/proxyRouter/macos/installScript.js new file mode 100644 index 00000000..2b98955a --- /dev/null +++ b/public/main/client/proxyRouter/macos/installScript.js @@ -0,0 +1,21 @@ +const macosInstallScript = ` + + + + +Label{serviceName} +ProgramArguments + +/bin/zsh +-c +CLONE_FACTORY_ADDRESS=$LMR_CLONE_FACTORY_ADDRESS ETH_NODE_ADDRESS=$LMR_ETH_NODE_ADDRESS HASHRATE_DIFF_THRESHOLD=$LMR_HASHRATE_DIFF_THRESHOLD MINER_VETTING_DURATION=$LMR_MINER_VETTING_DURATION POOL_CONN_TIMEOUT=$LMR_POOL_CONN_TIMEOUT POOL_MAX_DURATION=$LMR_POOL_MAX_DURATION POOL_MIN_DURATION=$LMR_POOL_MIN_DURATION STRATUM_SOCKET_BUFFER_SIZE=$LMR_STRATUM_SOCKET_BUFFER_SIZE VALIDATION_BUFFER_PERIOD=$LMR_VALIDATION_BUFFER_PERIOD WALLET_ADDRESS=$LMR_WALLET_ADDRESS WALLET_PRIVATE_KEY=$LMR_WALLET_PRIVATE_KEY LOG_LEVEL=$LMR_LOG_LEVEL PROXY_ADDRESS=$LMR_PROXY_ADDRESS WEB_ADDRESS=$LMR_WEB_ADDRESS POOL_ADDRESS=$LMR_POOL_ADDRESS IS_BUYER=$LMR_IS_BUYER {pathToExecutable} + +WorkingDirectory{workingDir} +StandardOutPath{logFilePath} +KeepAlive +Disabled + + +`; + +module.exports = { macosInstallScript }; \ No newline at end of file diff --git a/public/main/client/proxyRouter/sudoPrompt.js b/public/main/client/proxyRouter/sudoPrompt.js new file mode 100644 index 00000000..3eee14ca --- /dev/null +++ b/public/main/client/proxyRouter/sudoPrompt.js @@ -0,0 +1,18 @@ +const sudoPrompt = require("@vscode/sudo-prompt"); + +const options = { + name: "Proxy Router", +}; + +const sudo = async (command) => { + return await new Promise((resolve, reject) => { + sudoPrompt.exec(command, options, (error) => { + if (error) { + return reject(error); + } + resolve(); + }); + }); +}; + +module.exports = { sudo }; diff --git a/public/main/client/proxyRouter/windows/service.js b/public/main/client/proxyRouter/windows/service.js new file mode 100644 index 00000000..e7190c7a --- /dev/null +++ b/public/main/client/proxyRouter/windows/service.js @@ -0,0 +1,69 @@ +const { getProxyRouterEnvs, PROXY_ROUTER_MODE } = require("../config"); +const { sudo } = require("../sudoPrompt"); + +const getInstallNssmServiceCommand = (pathToExecutable) => { + return `powershell -command "Expand-Archive -LiteralPath ${pathToExecutable}/nssm.zip -DestinationPath ${pathToExecutable}/nssm"`; +} + +const getInstallServiceCommand = (serviceName, pathToExecutable) => { + pathToExecutable = pathToExecutable.replaceAll(" ", "\\ "); + + const commands = [ + `${pathToExecutable}\\nssm\\nssm-2.24\\win64\\nssm.exe install ${serviceName} ${pathToExecutable}/proxy-router.exe`, + `${pathToExecutable}\\nssm\\nssm-2.24\\win64\\nssm.exe set ${serviceName} AppStdout ${pathToExecutable}/${serviceName}.log`, + `${pathToExecutable}\\nssm\\nssm-2.24\\win64\\nssm.exe set ${serviceName} AppStderr ${pathToExecutable}/${serviceName}-err.log`, + ]; + + return commands.join(" ; "); +}; + +const getEnvsFromConfig = (config, mode) => { + return getProxyRouterEnvs(config, mode) + .map((e) => `${e[0]}=${e[1]}`) + .join(" "); +}; + +const getCommandToSetEnv = (serviceName, envs, resourcePath) => { + return `${resourcePath}\\nssm\\nssm-2.24\\win64\\nssm.exe set ${serviceName} AppEnvironmentExtra ${envs} ; ${resourcePath}\\nssm\\nssm-2.24\\win64\\nssm.exe restart ${serviceName}`; +}; + +const runWindowsServices = async (resourcePath, config) => { + const pathToExecutables = `${resourcePath}/executables`; + const sellerServiceName = 'proxySeller'; + const buyerServiceName = 'proxyBuyer'; + + const installSellerCommand = await getInstallServiceCommand( + sellerServiceName, + pathToExecutables, + resourcePath + ); + const installBuyerCommand = await getInstallServiceCommand( + buyerServiceName, + pathToExecutables, + resourcePath + ); + + const sellerRunCommand = await getCommandToSetEnv( + sellerServiceName, + getEnvsFromConfig(config, PROXY_ROUTER_MODE.Seller), + pathToExecutables + ); + const buyerRunCommand = await getCommandToSetEnv( + buyerServiceName, + getEnvsFromConfig(config, PROXY_ROUTER_MODE.Buyer), + pathToExecutables + ); + + const commands = [ + getInstallNssmServiceCommand(pathToExecutables), + installSellerCommand, + installBuyerCommand, + sellerRunCommand, + buyerRunCommand, + ]; + + await sudo(commands.join(" ; ")); +}; + +module.exports = { runWindowsServices }; + diff --git a/public/main/client/settings/index.js b/public/main/client/settings/index.js index 7c413a9e..25be3736 100644 --- a/public/main/client/settings/index.js +++ b/public/main/client/settings/index.js @@ -15,6 +15,9 @@ function setKey(key, value) { logger.verbose("Settings changed", key); } +const getAppVersion = () => getKey("app.version"); +const setAppVersion = (value) => setKey("app.version", value); + const getPasswordHash = () => getKey("user.passwordHash"); function setPasswordHash(hash) { @@ -99,4 +102,6 @@ module.exports = { setProxyRouterConfig, getProxyRouterConfig, cleanupDb, + getAppVersion, + setAppVersion, };