From 4314f408069d7f9bb6b0247eddf3befc61d558ae Mon Sep 17 00:00:00 2001 From: vkrasutsky Date: Tue, 26 Nov 2019 13:58:06 +0300 Subject: [PATCH] [BLP-408] Local chain (#40) * [BLP-408] Echo node * [BLP-408] Chain * [BLP-408] Chain * [BLP-408] Chain * [BLP-408] Chain * [BLP-408] Chain * [BLP-408] Refactor * [BLP-408] Refactor * [BLP-408] Chain * [BLP-408] Chain * [BLP-481] echo 0.13 * [BLP-452] add default delegate_share * devnet * change network * [BLP-408] Local chain * [BLP-408] Local chain * [BLP-408] Local chain * [BLP-408] Local chain * Fix path whitespace, add travis mac, only testnet * Fix path whitespace, add travis mac, only testnet * Fix path whitespace, add travis mac, only testnet * Up echo_node version * rm unused param --- .travis.yml | 12 +- app/actions/account-actions.js | 2 + app/actions/auth-actions.js | 1 + app/actions/balance-actions.js | 12 +- app/actions/global-actions.js | 6 +- app/constants/chain-constants.js | 3 +- app/constants/global-constants.js | 2 +- app/logic-components/db/models/network.js | 13 +- app/main.dev.js | 186 +++++++++++----------- app/main/echo-node.js | 97 ++++++++--- app/main/time-offset.js | 26 --- app/services/blockchain.js | 129 +++++++-------- app/services/node.file.encryptor.js | 111 +++++++++++++ app/services/user-storage-service.js | 22 ++- babel.config.js | 1 - internals/scripts/DownloadBuild.js | 42 +++-- package.json | 7 +- 17 files changed, 417 insertions(+), 255 deletions(-) delete mode 100644 app/main/time-offset.js create mode 100644 app/services/node.file.encryptor.js diff --git a/.travis.yml b/.travis.yml index cd0045e..e301f4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,21 +27,17 @@ jobs: - npm run build - npm run test - npm run lint - - - - - stage: build name: "Build Linux App" env: - - DOWNLOAD_ECHO_NODE_URL= "https://github.com/echoprotocol/echo/releases/download/0.6.1/echo_node" + - DOWNLOAD_ECHO_NODE_URL= "https://d14s13k07yt1gw.cloudfront.net/echo-linux.0.13.5.tar" if: tag IS present script: - sudo apt-get install rpm - npm config set unsafe-perm true - rm -rf node_modules/node-sass - npm i - - DEBUG_PROD=true npm run package-linux + - DEBUG_PROD=true npm run package-linux-with-download-build deploy: provider: releases api_key: $GITHUB_OAUTH_TOKEN @@ -77,11 +73,13 @@ jobs: - stage: build os: osx name: "Build macOS App" + env: + - DOWNLOAD_ECHO_NODE_URL= "https://d14s13k07yt1gw.cloudfront.net/echo-mac.0.13.5.tar" if: tag IS present script: - npm config set unsafe-perm true - npm i - - DEBUG_PROD=true npm run package-mac + - DEBUG_PROD=true npm run package-mac-with-download-build deploy: provider: releases api_key: $GITHUB_OAUTH_TOKEN diff --git a/app/actions/account-actions.js b/app/actions/account-actions.js index 8682f13..eca06b0 100644 --- a/app/actions/account-actions.js +++ b/app/actions/account-actions.js @@ -396,6 +396,7 @@ export const logoutAccount = (accountId) => async (dispatch, getState) => { dispatch(WalletReducer.actions.set({ field: 'tokens', value: tokens })); dispatch(subscribeTokens()); + dispatch(setAccounts()); }; /** * @@ -441,4 +442,5 @@ export const removeAllAccounts = () => async (dispatch) => { dispatch(WalletReducer.actions.clear({ field: 'hiddenAssets' })); dispatch(subscribeTokens()); + dispatch(setAccounts()); }; diff --git a/app/actions/auth-actions.js b/app/actions/auth-actions.js index 7202099..781ad70 100644 --- a/app/actions/auth-actions.js +++ b/app/actions/auth-actions.js @@ -81,6 +81,7 @@ export const loadRegistrators = () => async (dispatch, getState) => { ], []); await Services.getEcho().api.getObjects(objectIds); + }; /** diff --git a/app/actions/balance-actions.js b/app/actions/balance-actions.js index b65ec14..77771f9 100644 --- a/app/actions/balance-actions.js +++ b/app/actions/balance-actions.js @@ -47,13 +47,13 @@ export const initTokens = () => async (dispatch, getState) => { try { const tokens = await getBalances(accounts); - if (!tokens || !tokens.data.getBalances.length) { + if (!tokens || !tokens.data || !tokens.data.getBalances.length) { return false; } dispatch(setValue('tokens', fromJS(tokens.data.getBalances.filter((t) => t.type === TOKEN_TYPE)))); } catch (e) { - console.log(e); + console.error(e); } return true; @@ -92,7 +92,13 @@ export const clear = (field) => (dispatch) => { export const updateBalance = () => async (dispatch, getState) => { const accounts = getState().global.get('accounts'); - const selectedAccounts = await Services.getEcho().api.getFullAccounts([...accounts.keys()]); + if (!Services.getEcho().api) { + return; + } + + let selectedAccounts = await Services.getEcho().api.getFullAccounts([...accounts.keys()]); + + selectedAccounts = selectedAccounts.filter((account) => account); const objectIds = selectedAccounts.reduce((balances, account) => { const result = Object.entries(account.balances).reduce((arr, b) => [...arr, ...b], []); diff --git a/app/actions/global-actions.js b/app/actions/global-actions.js index bd6b167..7404cd8 100644 --- a/app/actions/global-actions.js +++ b/app/actions/global-actions.js @@ -1,6 +1,5 @@ /* eslint-disable no-undef */ import { Map, fromJS } from 'immutable'; -import { PrivateKey } from 'echojs-lib'; import GlobalReducer from '../reducers/global-reducer'; import Services from '../services'; @@ -102,6 +101,7 @@ export const setAccounts = () => (async () => { const userStorage = Services.getUserStorage(); const accounts = await userStorage.getAllAccounts(); const networkId = await userStorage.getNetworkId(); + const chainToken = await userStorage.getChainToken(); const keyPromises = accounts.map((account) => new Promise(async (resolve) => { @@ -109,7 +109,7 @@ export const setAccounts = () => (async () => { return resolve(keys.map((key) => ({ id: account.id, - key: PrivateKey.fromWif(key.wif).toPrivateKeyString(), + key: key.wif, }))); })); @@ -123,7 +123,7 @@ export const setAccounts = () => (async () => { }); }); - Services.getEcho().setOptions(accountsKeys, networkId); + Services.getEcho().setOptions(accountsKeys, networkId, chainToken); }); diff --git a/app/constants/chain-constants.js b/app/constants/chain-constants.js index 7e20858..6cdcf5a 100644 --- a/app/constants/chain-constants.js +++ b/app/constants/chain-constants.js @@ -1,8 +1,7 @@ export const DATA_DIR = 'echo/data'; export const SEED_NODE = 'node1.devnet.echo-dev.io:6310'; export const RESTART_PAUSE_MS = 5000; -export const DIFF_TIME_SYNC_MS = 30000; -export const SYNC_MONITOR_MS = 1000; +export const SYNC_MONITOR_MS = 2000; export const RESTART_TIME_CHECKING_NODE_MS = 5000; export const CHAIN_MIN_RANGE_PORT = 3000; export const CHAIN_MAX_RANGE_PORT = 5000; diff --git a/app/constants/global-constants.js b/app/constants/global-constants.js index 2fe17f5..bc4e0f1 100644 --- a/app/constants/global-constants.js +++ b/app/constants/global-constants.js @@ -108,4 +108,4 @@ export const APP_WINDOW_WIDTH = 1024; export const APP_WINDOW_HEIGHT = 728; export const APP_WINDOW_MIN_WIDTH = 1024; export const APP_WINDOW_MIN_HEIGHT = 728; -export const TIMEOUT_BEFORE_APP_PROCESS_EXITS_MS = 12000; +export const TIMEOUT_BEFORE_APP_PROCESS_EXITS_MS = 30000; diff --git a/app/logic-components/db/models/network.js b/app/logic-components/db/models/network.js index 0f965bf..f9e19eb 100644 --- a/app/logic-components/db/models/network.js +++ b/app/logic-components/db/models/network.js @@ -5,19 +5,22 @@ class Network { * * @param {Array} accounts * @param {Array} keys + * @param {Object} chainToken */ - constructor(accounts, keys) { + constructor(accounts, keys, chainToken = null) { this.accounts = accounts; this.keys = keys; + this.chainToken = chainToken; } /** * * @param {Array} accounts * @param {Array} keys + * @param {Object} chainToken */ - static create(accounts, keys) { - return new Network(accounts, keys); + static create(accounts, keys, chainToken = null) { + return new Network(accounts, keys, chainToken); } /** @@ -76,6 +79,10 @@ class Network { return this.keys; } + getChainToken() { + return this.chainToken; + } + } export default Network; diff --git a/app/main.dev.js b/app/main.dev.js index b7676a9..1adb75b 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -16,22 +16,24 @@ import { } from 'electron'; import { autoUpdater } from 'electron-updater'; import getPort from 'get-port'; +import { xor } from 'lodash'; import log from 'electron-log'; import appRootDir from 'app-root-dir'; import notifier from 'node-notifier'; import { join as joinPath, dirname } from 'path'; import rimraf from 'rimraf'; import i18next from 'i18next'; +import { PrivateKey } from 'echojs-lib'; +import { Subject, from } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; -import TimeOffset from './main/time-offset'; import MenuBuilder from './menu'; import EchoNode from './main/echo-node'; import getPlatform from './main/get-platform'; + import { DATA_DIR, - // SEED_NODE, - RESTART_PAUSE_MS, CHAIN_MIN_RANGE_PORT, CHAIN_MAX_RANGE_PORT, } from './constants/chain-constants'; @@ -43,14 +45,14 @@ import { APP_WINDOW_MIN_WIDTH, TIMEOUT_BEFORE_APP_PROCESS_EXITS_MS, DEFAULT_NETWORK_ID, - LOCAL_NODE, } from './constants/global-constants'; import { WIN_PLATFORM } from './constants/platform-constants'; import EN_TRANSLATION from './translations/en'; -app.commandLine.appendSwitch('js-flags', '--max-old-space-size=8096'); +app.commandLine.appendSwitch('js-flags', '--max-old-space-size=8096'); +let quited = false; export default class AppUpdater { constructor() { @@ -115,21 +117,11 @@ app.on('window-all-closed', () => { } }); -const echoNode = new EchoNode(); +let lastNode = null; let restartTimer = null; let tray = null; async function createWindow() { - const timeOffset = new TimeOffset(); - - ipcMain.on('getTimeOffset', async (event) => { - try { - const offset = await timeOffset.getOffset(); - event.sender.send('getTimeOffset', { result: offset }); - } catch (e) { - event.sender.send('getTimeOffset', { error: e }); - } - }); const execPath = process.env.NODE_ENV === 'production' ? joinPath(dirname(appRootDir.get()), 'icons') : joinPath(appRootDir.get(), 'resources', 'icons'); @@ -165,7 +157,7 @@ async function createWindow() { await i18next.init({ lng: 'en', - debug: true, + debug: false, resources: { en: { translation: EN_TRANSLATION, @@ -249,96 +241,98 @@ async function createWindow() { sendPort(); - const countAttempts = {}; - let startingError = false; - let processCounterId = 1; - const options = { - echorand: null, - 'data-dir': `${app.getPath('userData')}/${DATA_DIR}/${DEFAULT_NETWORK_ID}`, - 'rpc-endpoint': `127.0.0.1:${port}`, - 'seed-node': NETWORKS[DEFAULT_NETWORK_ID][LOCAL_NODE].seed, - }; - - const tryStart = (processId, params, accounts) => { + const subject = new Subject(); + let previousPublicKeys = []; + let removeBeforeStart; + let prevNetwork; - if (processId < processCounterId) { - return false; - } + function removeFolderAndRetrySyncNode(dataDir) { + return new Promise((resolve) => { + previousPublicKeys = []; + lastNode = null; - clearTimeout(restartTimer); + if (removeBeforeStart) { + removeBeforeStart = false; + return rimraf(dataDir, () => resolve()); + } - echoNode.stop() - .then(() => { - restartTimer = setTimeout(() => { - /* eslint-disable no-use-before-define */ - startNode(processId, params, accounts); - }, RESTART_PAUSE_MS); - }); - - return true; - - }; + return resolve(); + }); + } - const incrementCounterId = (processId) => { - if (!countAttempts[processId]) { - countAttempts[processId] = 0; + subject.pipe( + switchMap((data) => { + const promise = data.lastNode ? data.lastNode.stop() : Promise.resolve(); + return from(promise.then(() => removeFolderAndRetrySyncNode(data.networkOptions['data-dir'])).then(() => ({ + networkOptions: data.networkOptions, + accounts: data.accounts, + chainToken: data.chainToken, + networkId: data.networkId, + }))); + }), + ).subscribe((data) => { + + mainWindow.webContents.send('startEchoNode', { networkId: data.networkId }); + + if (data.networkId === 'devnet') { + return; } - countAttempts[processId] += 1; - }; + lastNode = new EchoNode(); + lastNode.start(data.networkOptions, data.accounts, data.chainToken).then(() => { + if (!quited && !lastNode.stopInProcess) { + removeBeforeStart = true; + } + }).catch(() => { + if (!quited && !lastNode.stopInProcess) { + removeBeforeStart = true; + } + }); + }); + + ipcMain.on('startNode', async (_, args) => { - const startNode = (processId, params, accounts) => { + const NETWORK_ID = args && args.networkId ? args.networkId : DEFAULT_NETWORK_ID; + const chainToken = args && args.chainToken ? args.chainToken : null; - clearTimeout(restartTimer); + const networkOptions = { + 'data-dir': `"${app.getPath('userData')}/${DATA_DIR}/${NETWORK_ID}"`.replace(/(\s+)/g, '%20'), + 'rpc-endpoint': `127.0.0.1:${port}`, + // testnet: null, + // 'replay-blockchain': null, + // devnet: null, + // 'seed-node': 'node1.devnet.echo-dev.io:6310', + }; - if (startingError) { - console.warn('startingError', startingError); - return false; - } + switch (NETWORK_ID) { + case 'testnet': + networkOptions.testnet = null; + break; + case 'devnet': + networkOptions.devnet = null; + networkOptions['seed-node'] = 'node1.devnet.echo-dev.io:6310'; + break; + default: - try { - incrementCounterId(processId); - echoNode.start(params, accounts) - .then((data) => { - console.log('[NODE] child then', data); - }) - .catch((err) => { - console.log('[NODE] child err', err); - if (countAttempts[processId] === 1) { - console.info('TRY TO START'); - tryStart(processId, params, accounts); - } else if (countAttempts[processId] === 2) { - rimraf(params['data-dir'], () => { - console.info('TRY TO REMOVE FOLDER'); - tryStart(processId, params, accounts); - }); - } else { - startingError = true; - } - - }); - - } catch (e) { - console.log('[NODE] error:', e); } - return true; - }; - - startNode(processCounterId += 1, options, []); + const accounts = args && args.accounts ? args.accounts : []; - ipcMain.on('startNode', async (event, args) => { + const receivedPublicKeys = accounts.map(({ key }) => PrivateKey.fromWif(key).toPublicKey().toString()); - const NETWORK_ID = args && args.networkId ? args.networkId : DEFAULT_NETWORK_ID; + if (prevNetwork !== NETWORK_ID || !lastNode || previousPublicKeys.length !== receivedPublicKeys.length || xor(receivedPublicKeys, previousPublicKeys).length) { + subject.next({ + lastNode, + networkOptions, + accounts, + chainToken, + networkId: NETWORK_ID, + }); + prevNetwork = NETWORK_ID; + } - const networkOptions = { - echorand: null, - 'data-dir': `${app.getPath('userData')}/${DATA_DIR}/${NETWORK_ID}`, - 'rpc-endpoint': `127.0.0.1:${port}`, - 'seed-node': NETWORKS[NETWORK_ID][LOCAL_NODE].seed, - }; + previousPublicKeys = receivedPublicKeys; - tryStart(processCounterId += 1, networkOptions, args && args.accounts ? args.accounts : []); }); }); @@ -363,23 +357,25 @@ async function createWindow() { } app.on('before-quit', (event) => { + quited = true; + if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } - console.log('Caught before-quit. Exiting in 5 seconds.'); + console.log(`Caught before-quit. Exiting in ${TIMEOUT_BEFORE_APP_PROCESS_EXITS_MS} seconds.`); event.preventDefault(); - if (echoNode.child) { - echoNode.child.then(() => { + if (lastNode && lastNode.child) { + lastNode.child.then(() => { process.exit(0); }).catch(() => { process.exit(0); }); - echoNode.stop(); + lastNode.stop(); setTimeout(() => { process.exit(0); }, TIMEOUT_BEFORE_APP_PROCESS_EXITS_MS); diff --git a/app/main/echo-node.js b/app/main/echo-node.js index 85adbf4..c74ec0d 100644 --- a/app/main/echo-node.js +++ b/app/main/echo-node.js @@ -1,13 +1,17 @@ import appRootDir from 'app-root-dir'; import { join as joinPath, dirname } from 'path'; import _spawn from 'cross-spawn'; +import mkdirp from 'mkdirp'; +import fs from 'fs'; import getPlatform from './get-platform'; +import NodeFileEncryptor from '../services/node.file.encryptor'; class EchoNode { constructor() { this.child = null; + this.stopInProcess = false; } /** @@ -16,19 +20,68 @@ class EchoNode { * @param {Array} accounts * @return {Promise.<*>} */ - async start(params, accounts = []) { + async start(params, accounts = [], chainToken) { - await this.stop(); + const execPath = process.env.NODE_ENV === 'production' ? joinPath(dirname(appRootDir.get()), 'bin') : joinPath(appRootDir.get(), 'resources', getPlatform(), 'bin'); - console.log('appRootDir.get()', appRootDir.get()); + const binPath = `${joinPath(execPath, 'echo_node')}`; - const execPath = process.env.NODE_ENV === 'production' ? joinPath(dirname(appRootDir.get()), 'bin') : joinPath(appRootDir.get(), 'resources', getPlatform(), 'bin'); + const keyConfigPath = `${params['data-dir']}/.key.config`; - console.log('execPath', execPath); + const fileExists = await new Promise((resolve) => { + fs.stat(keyConfigPath, (err) => { + if (err) { + return resolve(false); + } - const binPath = `${joinPath(execPath, 'echo_node')}`; + return resolve(true); + + }); + }); + + let bytes = null; - const child = this.spawn(binPath, params, accounts); + if (fileExists) { + bytes = await new Promise((resolve, reject) => { + fs.readFile(keyConfigPath, (err, data) => { + if (err) { + return reject(err); + } + + return resolve(data.toString('hex')); + }); + }); + + await new Promise((resolve, reject) => { + fs.unlink(keyConfigPath, (err) => { + if (err) { + return reject(err); + } + + return resolve(); + }); + }); + + } + + await mkdirp(dirname(keyConfigPath)); + + if (chainToken && chainToken.token) { + const fileHex = NodeFileEncryptor.getFileBytes(chainToken.token, accounts); + + if (bytes !== fileHex) { + await new Promise((resolve, reject) => { + fs.writeFile(keyConfigPath, Buffer.from(fileHex, 'hex'), (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } + } + + const child = this.spawn(binPath, params, chainToken); this.child = child; @@ -38,6 +91,8 @@ class EchoNode { async stop() { return new Promise((resolve) => { + this.stopInProcess = true; + const { child } = this; if (!child) { @@ -58,7 +113,10 @@ class EchoNode { return resolve(); }); - child.kill('SIGINT'); + if (!child.siginted) { + child.siginted = true; + child.kill('SIGINT'); + } return true; @@ -91,28 +149,22 @@ class EchoNode { * * @param {String} binPath * @param {Object} opts - * @param {Array} accounts * @return {*} */ - spawn(binPath, opts, accounts = []) { + spawn(binPath, opts, chainToken) { const args = this.flags(opts); - const env = Object.create(process.env); - - let accountsStr = ''; - - accounts.forEach((account) => { - accountsStr += `--account-info=${JSON.stringify([account.id, account.key])} `; - }); + console.info(`spawning: echo_node ${args.join(' ')}`); - args.push(accountsStr); + const env = {}; - console.info(`spawning: echo_node ${args.join(' ')}`); + if (chainToken) { + env.ECHO_KEY_PASSWORD = chainToken.token; + } const start = Date.now(); const child = _spawn(binPath, args, { - // shell: true, detached: true, env, }); @@ -121,14 +173,9 @@ class EchoNode { child.unref(); - process.once('exit', () => { - child.kill('SIGINT'); - }); - if (child.stdout) { child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr); - } const promise = new Promise((resolve, reject) => { diff --git a/app/main/time-offset.js b/app/main/time-offset.js deleted file mode 100644 index 39ada1c..0000000 --- a/app/main/time-offset.js +++ /dev/null @@ -1,26 +0,0 @@ -import Sntp from '@hapi/sntp'; - -class TimeOffset { - - constructor() { - this.offset = null; - } - - /** - * - * @return {Promise.} - */ - async getOffset() { - - if (this.offset) { - return this.offset; - } - const time = await Sntp.time(); - this.offset = time.t; - - return this.offset; - } - -} - -export default TimeOffset; diff --git a/app/services/blockchain.js b/app/services/blockchain.js index d0deede..30098b0 100644 --- a/app/services/blockchain.js +++ b/app/services/blockchain.js @@ -11,7 +11,7 @@ import { LOCAL_NODE, CONNECT_STATUS, } from '../constants/global-constants'; -import { DIFF_TIME_SYNC_MS, SYNC_MONITOR_MS, RESTART_TIME_CHECKING_NODE_MS } from '../constants/chain-constants'; +import { SYNC_MONITOR_MS, RESTART_TIME_CHECKING_NODE_MS } from '../constants/chain-constants'; let ipcRenderer; @@ -38,7 +38,6 @@ class Blockchain { this.api = null; this.emitter = emitter; this.isOnline = window.navigator.onLine; - this.timeOffset = null; this.isRemoteConnected = false; this.isLocalConnected = false; this.localNodeUrl = false; @@ -46,6 +45,10 @@ class Blockchain { this.store = null; this.localNodePercent = 0; this.localNodeDiffSyncTime = 10e9; + + + this.localBlockNumber = 0; + this.remoteBlockNumber = 0; } /** @@ -71,7 +74,7 @@ class Blockchain { async checkSwitching() { - if ((this.localNodeDiffSyncTime >= 0 && this.localNodeDiffSyncTime <= DIFF_TIME_SYNC_MS && this.isLocalConnected) || (this.isLocalConnected && !this.isRemoteConnected)) { + if (this.isOnline && this.isLocalConnected && this.remoteBlockNumber > 0 && this.localBlockNumber >= this.remoteBlockNumber - 1) { if (this.isLocalConnected) { await this.switchToLocal(); @@ -81,7 +84,7 @@ class Blockchain { await this.switchToRemote(); } - console.info(`[BLOCKCHAIN] Check switching. Current: ${this.current}. Connected ${this.isConnected}`); + console.info(`[BLOCKCHAIN] Check switching. Current: ${this.current}. Connected ${this.isConnected}. isOnline: ${this.isOnline}. isLocalConnected: ${this.isLocalConnected}. isRemoteConnected: ${this.isRemoteConnected}`); } @@ -144,6 +147,17 @@ class Blockchain { }); ipcRenderer.send('subscribePort'); + + ipcRenderer.on('startEchoNode', (_, data) => { + + if (this.networkId !== data.networkId) { + this.remoteBlockNumber = 0; + this.localBlockNumber = 0; + this.notifyLocalNodePercent(); + } + + this.networkId = data.networkId; + }); } await this.startCheckingRemote(); @@ -156,61 +170,45 @@ class Blockchain { } - async checkNodeSync() { - - + /** + * + * @param {string} node + */ + async setBlockNumber(node) { try { - const timeOffset = await this.getTimeOffset(); - const localGlobalObject = await this.local.api.getObject('2.1.0'); - let found = false; - let blockNum = 1; - - while (!found && localGlobalObject.head_block_number >= blockNum) { - /* eslint-disable no-await-in-loop */ - const block = await this.local.api.getBlock(blockNum); - - if (!block) { - return false; - } - - if (!block || (block && block.timestamp === '1970-01-01T00:00:00')) { - blockNum += 1; - } else { - found = true; - } + const globalObject = await this[node].api.getObject(constants.DYNAMIC_GLOBAL_OBJECT_ID, true); + if (globalObject && globalObject.head_block_number) { + this[`${node}BlockNumber`] = globalObject.head_block_number; } - const firstBlock = await this.local.api.getBlock(blockNum); - - - if (!firstBlock) { - return false; - } + } catch (e) { + console.warn(`${node} checkNodeSync error`, e); + } + } - const firstBlockTime = new Date(`${firstBlock.timestamp}Z`).getTime(); - const chainTime = new Date(`${localGlobalObject.time}Z`).getTime(); - const now = Date.now() + timeOffset; - const percent = (chainTime - firstBlockTime) / (now - firstBlockTime) * 100; + async checkNodeSync() { - console.info(`[BLOCKCHAIN] Percent: ${percent}%. Diff time: ${now - chainTime}. Height: ${localGlobalObject.head_block_number}`); + if (!this.local || !this.remote) { + return; + } - this.localNodeDiffSyncTime = now - chainTime; - this.localNodePercent = percent; + await this.setBlockNumber('remote'); + await this.setBlockNumber('local'); - this.notifyLocalNodePercent(); + this.notifyLocalNodePercent(); - this.checkSwitching(); - } catch (e) { - console.warn('checkNodeSync error', e); - } + this.checkSwitching(); - return true; } notifyLocalNodePercent() { - this.emitter.emit('setLocalNodePercent', this.isOnline && this.isLocalConnected && this.localNodeDiffSyncTime >= 0 && this.localNodeDiffSyncTime < DIFF_TIME_SYNC_MS ? 100 : Math.floor(this.localNodePercent * 100) / 100); + const percent = this.remoteBlockNumber && this.localBlockNumber ? this.localBlockNumber / this.remoteBlockNumber * 100 : 0; + + console.log('percent', percent, 'localBlockNumber', this.localBlockNumber, 'remoteBlockNumber', this.remoteBlockNumber); + + this.emitter.emit('setLocalNodePercent', Math.min(percent, 100)); } switchToLocal() { @@ -276,6 +274,10 @@ class Blockchain { return true; } + /** + * @method _overrideApi + * @param {Object} node + */ _overrideApi(node) { this.api = node.api; this.api.createTransaction = node.createTransaction.bind(node); @@ -316,31 +318,6 @@ class Blockchain { return true; } - async getTimeOffset() { - return new Promise((resolve, reject) => { - - if (this.timeOffset) { - return resolve(this.timeOffset); - } - - ipcRenderer.once('getTimeOffset', (event, arg) => { - if (arg.result) { - if (!this.timeOffset) { - this.timeOffset = arg.result; - } - return resolve(this.timeOffset); - } - - return reject(arg.error); - - }); - - ipcRenderer.send('getTimeOffset', 'ping'); - - return true; - }); - } - startSyncMonitor() { setInterval(async () => { this.checkNodeSync(); @@ -355,7 +332,8 @@ class Blockchain { } if (!this.localNodeUrl) { - return console.log('[LOCAL NODE] URL is empty'); + console.log('[LOCAL NODE] URL is empty'); + return false; } if (this.localConnecting) { @@ -416,7 +394,7 @@ class Blockchain { } async _localStart() { - + // TODO:: local switch unsubscribe previous!! this.local = await this._createConnection(this.localNodeUrl); console.info('[LOCAL NODE] Connected'); @@ -440,6 +418,8 @@ class Blockchain { async _remoteStart() { + // TODO:: remote switch unsubscribe previous!! + this.remote = await this._createConnection(NETWORKS[this.network][REMOTE_NODE].url, { pingInterval: PING_INTERVAL, pingTimeout: PING_TIMEOUT }); this.remote.cache.setStore(this.store); @@ -512,15 +492,16 @@ class Blockchain { * * @param {Array} accounts * @param {String} networkId + * @param {Object} chainToken * @return {boolean} */ - setOptions(accounts = [], networkId) { + setOptions(accounts = [], networkId, chainToken) { if (!ipcRenderer) { return false; } - ipcRenderer.send('startNode', { accounts, networkId }); + ipcRenderer.send('startNode', { accounts, networkId, chainToken }); return true; } diff --git a/app/services/node.file.encryptor.js b/app/services/node.file.encryptor.js new file mode 100644 index 0000000..53b37fc --- /dev/null +++ b/app/services/node.file.encryptor.js @@ -0,0 +1,111 @@ +import { serializers, PrivateKey } from 'echojs-lib'; +import crypto from 'crypto'; + + +class NodeFileEncryptor { + + /** + * + * @param {String} password + */ + static hashPassword(password) { + + const hash = crypto.createHash('sha512'); + const data = hash.update(password, 'utf-8'); + const sha512Result = data.digest('hex'); + + return sha512Result; + } + + /** + * + * @param {String} passwordHash + * @param {String} hex + */ + static getEncryptedHex(passwordHash, hex) { + const iv = Buffer.from(passwordHash.substring(64, 96), 'hex'); + const key = Buffer.from(passwordHash.substring(0, 64), 'hex'); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + let encrypted = cipher.update(hex, 'hex', 'hex'); + encrypted += cipher.final('hex'); + + return encrypted; + } + + + /** + * + * @param {Array.<{wif, account}>} echornadKeys + * @param {Array} sidechainKeys + */ + static getSerializedKeysHex(echornadKeys, sidechainKeys) { + + const sidechainAccountKey = serializers.collections.struct({ + account: serializers.chain.ids.protocol.accountId, + key: serializers.basic.bytes(32), + }); + + const echorandAccountKey = serializers.collections.struct({ + account: serializers.chain.ids.protocol.accountId, + key: serializers.basic.bytes(32), + }); + + const echoKeyConfig = serializers.collections.struct({ + echorand_accounts_keys: serializers.collections.vector(echorandAccountKey), + sidechain_accounts_keys: serializers.collections.vector(sidechainAccountKey), + }); + + const hex = echoKeyConfig.serialize({ + echorand_accounts_keys: echornadKeys.map(({ key, id }) => { + const pKey = PrivateKey.fromWif(key); + return { + account: id, + key: pKey.toHex(), + }; + }), + sidechain_accounts_keys: sidechainKeys, + }).toString('hex'); + + return hex; + } + + /** + * + * @param {String} password + * @param {Array.<{wif, account}>} echornadKeys + * @param {Array} sidechainKeys + */ + static getFileBytes(password, echornadKeys = [], sidechainKeys = []) { + + const passwordHash = NodeFileEncryptor.hashPassword(password); + const hex = NodeFileEncryptor.getSerializedKeysHex(echornadKeys, sidechainKeys); + const encrypted = NodeFileEncryptor.getEncryptedHex(passwordHash, hex); + + const encriptionArrayBytes = encrypted.split(/(?=(?:..)*$)/); + + const serializableEncryption = serializers.collections.struct({ + sha512Key: serializers.basic.bytes(passwordHash.length / 2), + encryption: serializers.collections.vector(serializers.basic.bytes(1)), + }); + + const checkBytesHex = serializableEncryption.serialize({ + sha512Key: passwordHash, + encryption: encriptionArrayBytes, + }).toString('hex'); + + const hash = crypto.createHash('sha512'); + const data = hash.update(checkBytesHex, 'hex', 'hex'); + const checkBytesResult = data.digest('hex'); + + const fileHex = serializableEncryption.serialize({ + sha512Key: checkBytesResult, + encryption: encriptionArrayBytes, + }).toString('hex'); + + return fileHex; + + } + +} + +export default NodeFileEncryptor; diff --git a/app/services/user-storage-service.js b/app/services/user-storage-service.js index f40d8c6..5c6aae1 100644 --- a/app/services/user-storage-service.js +++ b/app/services/user-storage-service.js @@ -1,3 +1,4 @@ +import CryptoService from './crypto-service'; import StorageService from './storage-service'; import Storage from '../logic-components/db/storage'; import { DB_NAME, STORE } from '../constants/global-constants'; @@ -146,6 +147,17 @@ class UserStorageService { } + async getChainToken(params) { + this.checkNetwork(); + + const decryptedData = await this.getCurrentScheme().getDecryptedData(params); + + const networkId = this.getNetworkId(); + const network = await this.getNetworkFromDecryptedData(networkId, decryptedData); + + return network.getChainToken(); + } + /** * * @param {Array} accounts @@ -354,12 +366,12 @@ class UserStorageService { if (!decryptedData.data.networks) { decryptedData.data.networks = {}; - network = Network.create([], []); + network = Network.create([], [], { token: this.getRandomToken() }); } else if (!decryptedData.data.networks[networkId]) { - network = Network.create([], []); + network = Network.create([], [], { token: this.getRandomToken() }); } else { const rawNetwork = decryptedData.data.networks[networkId]; - network = Network.create(rawNetwork.accounts.map((account) => Account.create(account.id, account.name, account.selected, account.primary)), rawNetwork.keys.map((key) => Key.create(key.publicKey, key.wif, key.accountId))); + network = Network.create(rawNetwork.accounts.map((account) => Account.create(account.id, account.name, account.selected, account.primary)), rawNetwork.keys.map((key) => Key.create(key.publicKey, key.wif, key.accountId)), rawNetwork.chainToken); } decryptedData.data.networks[networkId] = network; @@ -368,6 +380,10 @@ class UserStorageService { } + getRandomToken() { + return CryptoService.randomBytes(256).toString('hex'); + } + } UserStorageService.SCHEMES = { diff --git a/babel.config.js b/babel.config.js index 83c80d1..6b326a7 100644 --- a/babel.config.js +++ b/babel.config.js @@ -29,7 +29,6 @@ module.exports = (api) => { corejs: 'core-js@3', }, ], - require('@babel/preset-flow'), [require('@babel/preset-react'), { development }], ], plugins: [ diff --git a/internals/scripts/DownloadBuild.js b/internals/scripts/DownloadBuild.js index 313b7fb..8045c5b 100644 --- a/internals/scripts/DownloadBuild.js +++ b/internals/scripts/DownloadBuild.js @@ -1,27 +1,51 @@ +import path from "path"; import fs from "fs"; import download from "download"; +import { parse } from "url"; +import tar from "tar"; /** * * @param {String} url - * @param {String} os + * @param {String} os + * @param {String} filename */ const downloadBuild = async (url, os, filename) => { - console.log(`[${os}] Downloading build... ${url}`); + console.log(`[${os}] Downloading the build... ${url}`); - const destination = `resources/${os}/bin`; + const destination = `resources/${os}/bin`; + + const parsed = parse(url); + const urlFilename = path.basename(parsed.pathname); + const tarDestination = `${destination}/${urlFilename}`; if (fs.existsSync(url)) { // check local file fs.copyFileSync(url, `${destination}/${filename}`); } else { - await download(url, destination, { filename }); - } - + await download(url, destination); + } + + const extractedFolder = urlFilename.split('.').slice(0, -1).join('.'); + + console.log(`[${os}] Extracting to ${destination}/${extractedFolder}...`); + + await tar.x({ + file: tarDestination, + C: `${destination}`, + }); + + const nodeFilePath = `${destination}/${extractedFolder}/${filename}`; + + console.log(`[${os}] Copying to ${destination}/${filename}...`); + + fs.copyFileSync(nodeFilePath, `${destination}/${filename}`); + + console.log(`[${os}] Setting permissions ${filename}...`); - fs.chmodSync(`${destination}/${filename}`, 0o777); + fs.chmodSync(`${destination}/${filename}`, 0o777); - console.log(`[${os}] Downloaded.`); + console.log(`[${os}] Done.`); }; @@ -39,7 +63,7 @@ const downloadBuild = async (url, os, filename) => { } if (!downloadOS) { - throw new Error('You need to set process.env.DOWNLOAD_ECHO_NODE_OS'); + throw new Error('You need to set process.env.DOWNLOAD_ECHO_NODE_OS'); } await downloadBuild(downloadUrl, downloadOS, 'echo_node'); diff --git a/package.json b/package.json index d5a4119..533b509 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test-e2e-live": "node -r @babel/register ./internals/scripts/CheckBuiltsExist.js && cross-env NODE_ENV=test testcafe-live electron:./ ./test/e2e/HomePage.e2e.js", "test-watch": "yarn test --watch", "download-linux-build": "DOWNLOAD_ECHO_NODE_OS=linux node -r @babel/register ./internals/scripts/DownloadBuild.js", - "download-max-build": "DOWNLOAD_ECHO_NODE_OS=mac node -r @babel/register ./internals/scripts/DownloadBuild.js" + "download-mac-build": "DOWNLOAD_ECHO_NODE_OS=mac node -r @babel/register ./internals/scripts/DownloadBuild.js" }, "lint-staged": { "*.{js,jsx}": [ @@ -253,11 +253,11 @@ "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.14", "webpack-merge": "^4.1.5", - "yarn": "^1.12.3" + "yarn": "^1.12.3", + "tar": "^4.4.10" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.6.3", - "@hapi/sntp": "^3.1.1", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", @@ -313,6 +313,7 @@ "redux-modules": "^1.1.1", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", + "rxjs": "^6.5.3", "scrypt-js": "^2.0.4", "semantic-ui-react": "^0.86.0", "source-map-support": "^0.5.9",