diff --git a/package-lock.json b/package-lock.json index 8c7acda..7cf13df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@koa/router": "^10.1.1", "cross-zip": "^4.0.0", + "electron-prompt": "^1.7.0", "electron-squirrel-startup": "^1.0.0", "env-paths": "2", "ethereumjs-wallet": "^1.0.2", @@ -40,6 +41,7 @@ "@electron-forge/publisher-github": "^6.0.0-beta.64", "@kayahr/jest-electron-runner": "^4.4.1", "@types/cross-zip": "^4.0.0", + "@types/electron-prompt": "^1.6.5", "@types/fs-extra": "^9.0.13", "@types/jest": "^27.4.1", "@types/js-yaml": "^4.0.5", @@ -3291,6 +3293,15 @@ "integrity": "sha512-jZRaaM3aib7qgVhCWeo76vVNJW+8+ujJzP/JHPPA4WUp5YBNMsqiyu5uYWg2eS05+jgSfb5NN8eqrAG72Ab2kA==", "dev": true }, + "node_modules/@types/electron-prompt": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/electron-prompt/-/electron-prompt-1.6.5.tgz", + "integrity": "sha512-CuhgXY5xQZ2lzoXdofCHrLEHoQdf6DIZelmRlDf+uvWhF7z00vIOjXxlXcp3rUAJ28qkM8MDdSFh9NyU8vKi0g==", + "dev": true, + "dependencies": { + "electron": "latest" + } + }, "node_modules/@types/express": { "version": "4.17.17", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", @@ -6872,6 +6883,11 @@ "node": ">=8" } }, + "node_modules/electron-prompt": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/electron-prompt/-/electron-prompt-1.7.0.tgz", + "integrity": "sha512-IfqJYEgcRO6NuyPROo8AtdkAiZ6N9I1lQEf4dJAkPuhV5YgOHdmLqZJf6OXumZJfzrjpzCM5jHeYOrhGdgbnEA==" + }, "node_modules/electron-squirrel-startup": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz", @@ -17502,6 +17518,15 @@ "integrity": "sha512-jZRaaM3aib7qgVhCWeo76vVNJW+8+ujJzP/JHPPA4WUp5YBNMsqiyu5uYWg2eS05+jgSfb5NN8eqrAG72Ab2kA==", "dev": true }, + "@types/electron-prompt": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/electron-prompt/-/electron-prompt-1.6.5.tgz", + "integrity": "sha512-CuhgXY5xQZ2lzoXdofCHrLEHoQdf6DIZelmRlDf+uvWhF7z00vIOjXxlXcp3rUAJ28qkM8MDdSFh9NyU8vKi0g==", + "dev": true, + "requires": { + "electron": "latest" + } + }, "@types/express": { "version": "4.17.17", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", @@ -20360,6 +20385,11 @@ } } }, + "electron-prompt": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/electron-prompt/-/electron-prompt-1.7.0.tgz", + "integrity": "sha512-IfqJYEgcRO6NuyPROo8AtdkAiZ6N9I1lQEf4dJAkPuhV5YgOHdmLqZJf6OXumZJfzrjpzCM5jHeYOrhGdgbnEA==" + }, "electron-squirrel-startup": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz", diff --git a/package.json b/package.json index 10a15ca..6d614d5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "main": "dist/desktop/src/index.js", "scripts": { "start": "concurrently -n \"desktop,ui\" \"cross-env-shell NODE_ENV=development 'npm run clean && npm run build && electron-forge start'\" \"cd ui && npm start\"", + "start:backend": "npm run build:desktop && electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", @@ -54,6 +55,7 @@ "@electron-forge/publisher-github": "^6.0.0-beta.64", "@kayahr/jest-electron-runner": "^4.4.1", "@types/cross-zip": "^4.0.0", + "@types/electron-prompt": "^1.6.5", "@types/fs-extra": "^9.0.13", "@types/jest": "^27.4.1", "@types/js-yaml": "^4.0.5", @@ -90,6 +92,7 @@ "dependencies": { "@koa/router": "^10.1.1", "cross-zip": "^4.0.0", + "electron-prompt": "^1.7.0", "electron-squirrel-startup": "^1.0.0", "env-paths": "2", "ethereumjs-wallet": "^1.0.2", diff --git a/src/.sentry.json b/src/.sentry.json deleted file mode 100644 index 2f65237..0000000 --- a/src/.sentry.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "KEY": "" -} diff --git a/src/config.ts b/src/config.ts index 05300c2..f2ab3dc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,8 +14,8 @@ export function configYamlExists(): boolean { return existsSync(getPath('config.yaml')) } -export function readConfigYaml(): Record { - const raw = readFileSync(getPath('config.yaml'), 'utf-8') +export function readConfigYaml(fileName = 'config.yaml'): Record { + const raw = readFileSync(getPath(fileName), 'utf-8') const data = load(raw, { schema: FAILSAFE_SCHEMA, }) @@ -23,18 +23,18 @@ export function readConfigYaml(): Record { return data as Record } -export function writeConfigYaml(newValues: Record) { - const data = readConfigYaml() +export function writeConfigYaml(newValues: Record, fileName = 'config.yaml') { + const data = readConfigYaml(fileName) for (const [key, value] of Object.entries(newValues)) { data[key] = value } - writeFileSync(getPath('config.yaml'), dump(data)) + writeFileSync(getPath(fileName), dump(data)) } -export function deleteKeyFromConfigYaml(key: string) { - const data = readConfigYaml() +export function deleteKeyFromConfigYaml(key: string, fileName = 'config.yaml') { + const data = readConfigYaml(fileName) delete data[key] - writeFileSync(getPath('config.yaml'), dump(data)) + writeFileSync(getPath(fileName), dump(data)) } export function getDesktopVersionFromFile(): string | undefined { diff --git a/src/electron.ts b/src/electron.ts index aba4113..ede552c 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -1,8 +1,10 @@ import { app, Menu, nativeTheme, Tray } from 'electron' import opener from 'opener' import { openDashboardInBrowser, openEtherjotInBrowser } from './browser' +import { readConfigYaml } from './config' import { runLauncher } from './launcher' import { BeeManager } from './lifecycle' +import { getCurrentNetwork, toggleNetwork } from './network' import { createNotification } from './notify' import { getAssetPath, paths } from './path' @@ -28,6 +30,12 @@ export function rebuildElectronTray() { } }, }, + { + label: getCurrentNetwork() === 'main' ? 'Switch to Testnet' : 'Switch to Mainnet', + click: () => { + toggleNetwork() + }, + }, { type: 'separator' }, { type: 'submenu', @@ -46,6 +54,15 @@ export function rebuildElectronTray() { opener(paths.log) }, }, + { + label: 'Data Directory', + click: async () => { + const config = readConfigYaml() + if (typeof config['data-dir'] === 'string') { + opener(config['data-dir']) + } + }, + }, { label: 'Quit', click: async () => { diff --git a/src/index.ts b/src/index.ts index e4d5ab6..67e6808 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { getStatus } from './status' // @ts-ignore import squirrelInstallingExecution from 'electron-squirrel-startup' import { runMigrations } from './migration' +import { initializeMultinetConfig } from './network' import { initSplash, Splash } from './splash' runMigrations() @@ -74,10 +75,15 @@ async function main() { await initializeBee() } + await initializeMultinetConfig() + runLauncher().catch(errorHandler) runElectronTray() - if (process.env.NODE_ENV !== 'development') openDashboardInBrowser() + if (process.env.NODE_ENV !== 'development') { + openDashboardInBrowser() + } + splash.hide() splash = undefined diff --git a/src/launcher.ts b/src/launcher.ts index b55e348..0419eef 100644 --- a/src/launcher.ts +++ b/src/launcher.ts @@ -58,6 +58,10 @@ export async function runLauncher() { mkdirSync(getPath('data-dir')) } + if (!checkPath('data-dir-test')) { + mkdirSync(getPath('data-dir-test')) + } + BeeManager.setUserIntention(true) const subprocess = launchBee(abortController).catch(reason => { logger.error(reason) diff --git a/src/network.ts b/src/network.ts new file mode 100644 index 0000000..6bd4129 --- /dev/null +++ b/src/network.ts @@ -0,0 +1,93 @@ +import { dialog } from 'electron' +import prompt from 'electron-prompt' +import { cpSync, existsSync } from 'fs' +import fetch from 'node-fetch' +import { deleteKeyFromConfigYaml, readConfigYaml, writeConfigYaml } from './config' +import { runLauncher } from './launcher' +import { BeeManager } from './lifecycle' +import { getPath } from './path' + +export function getCurrentNetwork(): 'test' | 'main' { + const config = readConfigYaml() + if (config.mainnet === true || config.mainnet === 'true') { + return 'main' + } + return 'test' +} + +export function initializeMultinetConfig() { + if (!existsSync(getPath('config.main.yaml'))) { + cpSync(getPath('config.yaml'), getPath('config.main.yaml')) + } + if (!existsSync(getPath('config.test.yaml'))) { + cpSync(getPath('config.yaml'), getPath('config.test.yaml')) + writeConfigYaml( + { + mainnet: false, + 'swap-enable': false, + 'chequebook-enable': false, + 'data-dir': getPath('data-dir-test'), + }, + 'config.test.yaml', + ) + deleteKeyFromConfigYaml('blockchain-rpc-endpoint', 'config.test.yaml') + } +} + +export async function toggleNetwork() { + const currentNetwork = getCurrentNetwork() + if (currentNetwork === 'main') { + await switchToTestnet() + } else { + await switchToMainnet() + } +} + +export async function switchToTestnet() { + const testConfig = readConfigYaml('config.test.yaml') + if (!testConfig['blockchain-rpc-endpoint']) { + const answer = await prompt({ + title: 'Sepolia JSON RPC endpoint', + label: 'URL:', + value: 'http://example.org', + inputAttrs: { + type: 'url', + }, + type: 'input', + }).catch(console.error) + if (!answer) { + dialog.showErrorBox('Error', 'Sepolia JSON RPC endpoint is required') + return + } + const response = await fetch(answer, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_chainId', params: [], id: 1 }), + }).catch(() => { + dialog.showErrorBox('Error', 'Sepolia JSON RPC endpoint is not reachable') + }) + if (!response) { + return + } + const data = await response.json().catch(() => { + dialog.showErrorBox('Error', 'Sepolia JSON RPC endpoint is not returning valid JSON') + }) + if (!data) { + return + } + writeConfigYaml({ 'blockchain-rpc-endpoint': answer }, 'config.test.yaml') + } + BeeManager.stop() + await BeeManager.waitForSigtermToFinish() + cpSync(getPath('config.yaml'), getPath('config.main.yaml')) + cpSync(getPath('config.test.yaml'), getPath('config.yaml')) + runLauncher() +} + +export async function switchToMainnet() { + BeeManager.stop() + await BeeManager.waitForSigtermToFinish() + cpSync(getPath('config.yaml'), getPath('config.test.yaml')) + cpSync(getPath('config.main.yaml'), getPath('config.yaml')) + runLauncher() +} diff --git a/src/server.ts b/src/server.ts index 0b4a6cb..cec2e04 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,7 +15,7 @@ import { readConfigYaml, readWalletPasswordOrThrow, writeConfigYaml } from './co import { runLauncher } from './launcher' import { BeeManager } from './lifecycle' import { logger, readBeeDesktopLogs, readBeeLogs, subscribeLogServerRequests } from './logger' -import { getPath } from './path' +import { getPath, paths } from './path' import { port } from './port' import { getStatus } from './status' import { swap } from './swap' @@ -145,6 +145,15 @@ export function runServer() { context.body = { message: 'Failed to swap', error } } }) + router.post('/open/logs', async context => { + opener(paths.log) + context.body = { success: true } + }) + router.post('/open/data-dir', async context => { + const config = readConfigYaml() + opener(config['data-dir']) + context.body = { success: true } + }) app.use(router.routes()) app.use(router.allowedMethods())