diff --git a/compatible.csv b/compatible.csv new file mode 100644 index 0000000000..98e98ab39d --- /dev/null +++ b/compatible.csv @@ -0,0 +1,5 @@ +CKB,0.110,0.109,0.108,0.107,0.106,0.105,0.104,0.103 +Neuron,,,,,,,, +0.110,yes,yes,no,no,no,no,no,no +0.106,no,no,yes,yes,yes,yes,no,no +0.103,no,no,no,no,no,no,yes,yes diff --git a/packages/neuron-ui/src/containers/Main/index.tsx b/packages/neuron-ui/src/containers/Main/index.tsx index 03d52d0d43..0f2175d155 100644 --- a/packages/neuron-ui/src/containers/Main/index.tsx +++ b/packages/neuron-ui/src/containers/Main/index.tsx @@ -57,6 +57,7 @@ const MainContent = () => { show={!!globalAlertDialog} title={globalAlertDialog?.title} message={globalAlertDialog?.message} + action={globalAlertDialog?.action} type={globalAlertDialog?.type ?? 'success'} onCancel={onCancelGlobalDialog} /> diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx index a784f781c6..d9d8b5477c 100644 --- a/packages/neuron-ui/src/containers/Navbar/index.tsx +++ b/packages/neuron-ui/src/containers/Navbar/index.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useLocation, NavLink, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { NeuronWalletActions, useDispatch, useState as useGlobalState } from 'states' -import { checkForUpdates } from 'services/remote' +import { NeuronWalletActions, showGlobalAlertDialog, useDispatch, useState as useGlobalState } from 'states' +import { VerifyExternalCkbNodeRes, checkForUpdates, getVersion, verifyExternalCkbNode } from 'services/remote' import { AppUpdater as AppUpdaterSubject } from 'services/subjects' import Badge from 'widgets/Badge' import Logo from 'widgets/Icons/Logo.png' @@ -18,7 +18,7 @@ import { ArrowOpenRight, MenuExpand, } from 'widgets/Icons/icon' -import { RoutePath, clsx, useOnLocaleChange } from 'utils' +import { RoutePath, clsx, isSuccessResponse, useOnLocaleChange } from 'utils' import Tooltip from 'widgets/Tooltip' import styles from './navbar.module.scss' @@ -77,7 +77,7 @@ const Navbar = () => { const { wallet: { name }, settings: { wallets = [] }, - updater: { version }, + updater: { version, isUpdated }, } = neuronWallet const [t, i18n] = useTranslation() useOnLocaleChange(i18n) @@ -105,6 +105,42 @@ const Navbar = () => { } }, []) + const [verifyCkbResult, setVerifyCkbResult] = useState() + + useEffect(() => { + verifyExternalCkbNode().then(res => { + if (isSuccessResponse(res) && res.result) { + setVerifyCkbResult(res.result) + } + }) + }, []) + + useEffect(() => { + // isUpdated is true or version is not empty means check update has return + if (!verifyCkbResult || (isUpdated !== true && !version)) { + return + } + if (version && verifyCkbResult.shouldUpdate) { + showGlobalAlertDialog({ + type: 'warning', + message: t('navbar.update-neuron-with-ckb', { version: getVersion() }), + action: 'ok', + })(dispatch) + } else if (!verifyCkbResult.ckbIsCompatible) { + showGlobalAlertDialog({ + type: 'warning', + message: t('navbar.ckb-node-compatible', { version: getVersion() }), + action: 'ok', + })(dispatch) + } else if (!verifyCkbResult.withIndexer) { + showGlobalAlertDialog({ + type: 'warning', + message: t('navbar.ckb-without-indexer'), + action: 'ok', + })(dispatch) + } + }, [verifyCkbResult, version, isUpdated]) + useEffect(() => { if (pathname.includes(RoutePath.Settings)) { setIsClickedSetting(true) diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index d26f07816d..ac19c04625 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -22,7 +22,10 @@ "sync-not-start": "Sync not started yet", "connecting": "Connecting", "experimental-functions": "Experimental", - "s-udt": "Asset Accounts" + "s-udt": "Asset Accounts", + "update-neuron-with-ckb": "The version of the CKB node does not match Neuron (v{{ version }}), which may cause compatibility issues. Please update to the latest version of Neuron.", + "ckb-node-compatible": "CKB node is not compatible with Neuron (v{{ version }}), please check before further operation.", + "ckb-without-indexer": "Please add '--indexer' option to start local node" }, "network-status": { "tooltip": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 57491b8bab..fbb50dd194 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -22,7 +22,10 @@ "sync-not-start": "同步尚未開始", "connecting": "正在連接節點", "experimental-functions": "實驗性功能", - "s-udt": "資產賬戶" + "s-udt": "資產賬戶", + "update-neuron-with-ckb": "CKB 節點版本與 Neuron (v{{ version }}) 不匹配,可能導致兼容性問題。請更新到最新版 Neuron。", + "ckb-node-compatible": "CKB 節點版本與 Neuron (v{{ version }}) 不兼容,請謹慎使用。", + "ckb-without-indexer": "請添加 '--indexer' 參數來啟動本地節點" }, "network-status": { "tooltip": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index f09bb86c42..27cb6525dc 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -22,7 +22,10 @@ "sync-not-start": "同步尚未开始", "connecting": "正在连接节点", "experimental-functions": "实验性功能", - "s-udt": "资产账户" + "s-udt": "资产账户", + "update-neuron-with-ckb": "CKB 节点版本与 Neuron (v{{ version }}) 不匹配,可能导致兼容性问题。请更新到最新版 Neuron。", + "ckb-node-compatible": "CKB 节点版本与 Neuron (v{{ version }}) 不兼容,请谨慎使用。", + "ckb-without-indexer": "请添加 '--indexer' 参数来启动本地节点" }, "network-status": { "tooltip": { diff --git a/packages/neuron-ui/src/services/remote/app.ts b/packages/neuron-ui/src/services/remote/app.ts index 167c4893fa..65e69fa9ca 100644 --- a/packages/neuron-ui/src/services/remote/app.ts +++ b/packages/neuron-ui/src/services/remote/app.ts @@ -24,6 +24,15 @@ export const stopProcessMonitor = remoteApi<'ckb'>('stop-process-monitor') export const startProcessMonitor = remoteApi<'ckb'>('start-process-monitor') export const getIsCkbRunExternal = remoteApi('is-ckb-run-external') +export type VerifyExternalCkbNodeRes = + | { + ckbIsCompatible: boolean | undefined + withIndexer: boolean + shouldUpdate: boolean + } + | undefined +export const verifyExternalCkbNode = remoteApi('verify-external-ckb-node') + export const clearCellCache = remoteApi('clear-cache') export const invokeShowErrorMessage = remoteApi<{ title: string; content: string }>('show-error-message') diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index ff31d10f1e..d8f798b382 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -49,6 +49,7 @@ type Action = | 'is-dark' | 'set-theme' | 'is-ckb-run-external' + | 'verify-external-ckb-node' // Wallets | 'get-all-wallets' | 'get-current-wallet' diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 99d9f47c26..dfa736f910 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -139,6 +139,7 @@ declare namespace State { title?: string message?: string type: 'success' | 'failed' | 'warning' + action?: 'ok' | 'cancel' | 'all' onClose?: () => void onOk?: () => void onCancel?: () => void diff --git a/packages/neuron-ui/src/types/Subject/index.d.ts b/packages/neuron-ui/src/types/Subject/index.d.ts index 3a4c4ed450..fcba2123dd 100644 --- a/packages/neuron-ui/src/types/Subject/index.d.ts +++ b/packages/neuron-ui/src/types/Subject/index.d.ts @@ -67,5 +67,6 @@ declare namespace Subject { title?: string message?: string type: 'success' | 'failed' | 'warning' + action?: 'ok' | 'cancel' } } diff --git a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx index 1f5d459e00..747d75d11f 100644 --- a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx +++ b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import React, { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useDialog } from 'utils' import Button from 'widgets/Button' @@ -8,6 +8,7 @@ import Tips from 'widgets/Icons/Tips.png' import styles from './alertDialog.module.scss' type AlertType = 'success' | 'failed' | 'warning' +type Action = 'ok' | 'cancel' | 'all' const AlertDialog = ({ show, @@ -17,6 +18,7 @@ const AlertDialog = ({ onClose, onOk, onCancel, + action, }: { show?: boolean title?: string @@ -25,10 +27,17 @@ const AlertDialog = ({ onClose?: () => void onOk?: () => void onCancel?: () => void + action?: Action }) => { const [t] = useTranslation() const dialogRef = useRef(null) useDialog({ show, dialogRef, onClose: onClose || (() => {}) }) + const actions = useMemo<('cancel' | 'ok')[]>(() => { + if (action) { + return action === 'all' ? ['cancel', 'ok'] : [action] + } + return type === 'warning' ? ['cancel', 'ok'] : ['ok'] + }, [action, type]) return ( @@ -38,13 +47,12 @@ const AlertDialog = ({

{title}

{message}

- {type === 'failed' &&
diff --git a/packages/neuron-wallet/electron-builder.yml b/packages/neuron-wallet/electron-builder.yml index 6cc88df861..6318166a0c 100644 --- a/packages/neuron-wallet/electron-builder.yml +++ b/packages/neuron-wallet/electron-builder.yml @@ -13,7 +13,7 @@ afterSign: scripts/notarize.js files: - from: "../.." to: "." - filter: ["!**/*", ".ckb-version", ".ckb-light-version", "ormconfig.json"] + filter: ["!**/*", ".ckb-version", ".ckb-light-version", "ormconfig.json", "compatible.csv"] - package.json - dist - ".env" diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index 5a95b76015..400aa53405 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -34,6 +34,11 @@ "prettier --ignore-path ../../.prettierignore --write", "eslint --fix", "git add" + ], + "test/**/*.{js,cjs,mjs,jsx,ts,tsx}": [ + "prettier --ignore-path ../../.prettierignore --write", + "eslint --fix", + "git add" ] }, "dependencies": { diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 30f8514ba1..6d6f56208a 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -296,6 +296,13 @@ export default class ApiController { } }) + handle('verify-external-ckb-node', async () => { + return { + status: ResponseCode.Success, + result: await NodeService.getInstance().verifyExternalCkbNode(), + } + }) + // Wallets handle('get-all-wallets', async () => { diff --git a/packages/neuron-wallet/src/locales/en.ts b/packages/neuron-wallet/src/locales/en.ts index a7ec296857..2b919a62b2 100644 --- a/packages/neuron-wallet/src/locales/en.ts +++ b/packages/neuron-wallet/src/locales/en.ts @@ -218,12 +218,6 @@ export default { cancel: 'Cancel', }, }, - 'node-version-different': { - message: 'The node version is inconsistent with Neuron(v {{ version }}), please use after confirmation', - }, - 'ckb-without-indexer': { - message: "Please add '--indexer' option to start local node", - }, }, prompt: { password: { diff --git a/packages/neuron-wallet/src/locales/zh-tw.ts b/packages/neuron-wallet/src/locales/zh-tw.ts index 4a7ee1e5b5..1c7d1d4ff1 100644 --- a/packages/neuron-wallet/src/locales/zh-tw.ts +++ b/packages/neuron-wallet/src/locales/zh-tw.ts @@ -205,12 +205,6 @@ export default { cancel: '取消', }, }, - 'node-version-different': { - message: '節點版本與 Neuron(v {{ version }}) 不壹致,請確認後使用', - }, - 'ckb-without-indexer': { - message: "請添加 '--indexer' 參數來啟動本地節點", - }, }, prompt: { password: { diff --git a/packages/neuron-wallet/src/locales/zh.ts b/packages/neuron-wallet/src/locales/zh.ts index e1f0dc5a37..b1a62625c2 100644 --- a/packages/neuron-wallet/src/locales/zh.ts +++ b/packages/neuron-wallet/src/locales/zh.ts @@ -206,12 +206,6 @@ export default { cancel: '取消', }, }, - 'node-version-different': { - message: '节点版本与 Neuron(v {{ version }}) 不一致,请确认后使用', - }, - 'ckb-without-indexer': { - message: "请添加 '--indexer' 参数来启动本地节点", - }, }, prompt: { password: { diff --git a/packages/neuron-wallet/src/models/subjects/show-global-dialog.ts b/packages/neuron-wallet/src/models/subjects/show-global-dialog.ts index 1c71462098..2ed7b79a55 100644 --- a/packages/neuron-wallet/src/models/subjects/show-global-dialog.ts +++ b/packages/neuron-wallet/src/models/subjects/show-global-dialog.ts @@ -4,6 +4,7 @@ const ShowGlobalDialogSubject = new Subject<{ title?: string message?: string type: 'success' | 'failed' | 'warning' + action?: 'ok' | 'cancel' }>() export default ShowGlobalDialogSubject diff --git a/packages/neuron-wallet/src/services/node.ts b/packages/neuron-wallet/src/services/node.ts index c998d5331b..b2b257add2 100644 --- a/packages/neuron-wallet/src/services/node.ts +++ b/packages/neuron-wallet/src/services/node.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { BI } from '@ckb-lumos/bi' -import { app as electronApp, dialog, shell } from 'electron' +import { app as electronApp, dialog, shell, app } from 'electron' import { t } from 'i18next' import { interval, BehaviorSubject, merge } from 'rxjs' import { distinctUntilChanged, sampleTime, flatMap, delay, retry, debounceTime } from 'rxjs/operators' @@ -21,6 +21,13 @@ import { generateRPC } from '../utils/ckb-rpc' import startMonitor from './monitor' import { CKBLightRunner } from './light-runner' +export enum VerifyCkbVersionResult { + Same, + Compatible, + ShouldUpdate, + Incompatible, +} + class NodeService { private static instance: NodeService @@ -137,16 +144,27 @@ class NodeService { const isDefaultCKBNeedStart = await this.isDefaultCKBNeedRestart() if (isDefaultCKBNeedStart) { logger.info('CKB:\texternal RPC on default uri not detected, starting bundled CKB node.') + this._isCkbNodeExternal = false const redistReady = await redistCheck() await (redistReady ? this.startNode() : this.showGuideDialog()) await startMonitor() } else { logger.info('CKB:\texternal RPC on default uri detected, skip starting bundled CKB node.') this._isCkbNodeExternal = true - const network = NetworksService.getInstance().getCurrent() - if (network.type !== NetworkType.Light) { - await this.verifyNodeVersion() - await this.verifyStartWithIndexer() + } + } + + public async verifyExternalCkbNode() { + const network = NetworksService.getInstance().getCurrent() + if (this._isCkbNodeExternal && network.type !== NetworkType.Light) { + const localNodeInfo = await new RpcService(network.remote).localNodeInfo() + const internalNodeVersion = this.getInternalNodeVersion() + const neuronVersion = app.getVersion() + if (!internalNodeVersion || !localNodeInfo.version) return + return { + ckbIsCompatible: this.isCkbCompatibility(neuronVersion, localNodeInfo.version), + withIndexer: await this.isStartWithIndexer(), + shouldUpdate: this.verifyCKbNodeShouldUpdate(internalNodeVersion, localNodeInfo.version), } } } @@ -219,38 +237,54 @@ class NodeService { } } - private async verifyNodeVersion() { - const network = NetworksService.getInstance().getCurrent() - const localNodeInfo = await new RpcService(network.remote).localNodeInfo() - const internalNodeVersion = this.getInternalNodeVersion() - const [internalMajor, internalMinor] = internalNodeVersion?.split('.') ?? [] - const [externalMajor, externalMinor] = localNodeInfo.version?.split('.') ?? [] - - if (internalMajor !== externalMajor || (externalMajor === '0' && internalMinor !== externalMinor)) { - dialog.showMessageBox({ - type: 'warning', - message: t('messageBox.node-version-different.message', { version: internalNodeVersion }), - }) + private getNeuronCompatibilityCKB() { + const appPath = electronApp.isPackaged ? electronApp.getAppPath() : path.join(__dirname, '../../../..') + const compatiblePath = path.join(appPath, 'compatible.csv') + if (fs.existsSync(compatiblePath)) { + try { + const content = fs.readFileSync(compatiblePath, 'utf8')?.split('\n') + const ckbVersions = content?.[0]?.split(',')?.slice(1) + const neuronCompatible = content?.slice(2) + const result: Record> = {} + for (let index = 0; index < neuronCompatible.length; index++) { + const [neuronVersion, ...campatibleValues] = neuronCompatible[index].split(',') + result[neuronVersion] = {} + campatibleValues.forEach((v, idx) => { + result[neuronVersion][ckbVersions[idx]] = v === 'yes' + }) + } + return result + } catch (err) { + logger.error('App\t: get compatible table failed') + } } } - private async verifyStartWithIndexer() { + private isCkbCompatibility(neuronVersion: string, externalCKBVersion: string) { + const compatibilities = this.getNeuronCompatibilityCKB() + const neuronCompatibleVersion = neuronVersion.split('.').slice(0, 2).join('.') + const externalCKBCompatibleVersion = externalCKBVersion.split('.').slice(0, 2).join('.') + return compatibilities?.[neuronCompatibleVersion]?.[externalCKBCompatibleVersion] + } + + private verifyCKbNodeShouldUpdate(neuronCKBVersion: string, externalCKBVersion: string) { + const [internalMajor, internalMinor] = neuronCKBVersion.split('.')?.map(v => +v) ?? [] + const [externalMajor, externalMinor] = externalCKBVersion.split('.')?.map(v => +v) ?? [] + return internalMajor < externalMajor || (internalMajor === externalMajor && internalMinor < externalMinor) + } + + private async isStartWithIndexer() { const network = NetworksService.getInstance().getCurrent() try { const res = await rpcRequest<{ error?: { code: number } }>(network.remote, { method: 'get_indexer_tip' }) if (res.error?.code === START_WITHOUT_INDEXER) { logger.info('Node:\tthe ckb node does not start with --indexer') - dialog.showMessageBox({ - type: 'warning', - message: t('messageBox.ckb-without-indexer.message'), - }) + return false } + return true } catch (error) { logger.info('Node:\tcalling get_indexer_tip failed') - dialog.showMessageBox({ - type: 'warning', - message: t('messageBox.ckb-without-indexer.message'), - }) + return false } } } diff --git a/packages/neuron-wallet/tests/services/node.test.ts b/packages/neuron-wallet/tests/services/node.test.ts index 5cf5b39d24..32b7d35f6e 100644 --- a/packages/neuron-wallet/tests/services/node.test.ts +++ b/packages/neuron-wallet/tests/services/node.test.ts @@ -22,6 +22,7 @@ describe('NodeService', () => { const getAppPathMock = jest.fn() const showMessageBoxMock = jest.fn() const shellMock = jest.fn() + const getVersionMock = jest.fn() const startMonitorMock = jest.fn() const rpcRequestMock = jest.fn() const getChainMock = jest.fn() @@ -91,7 +92,7 @@ describe('NodeService', () => { return { getTipBlockNumber: stubbedGetTipBlockNumber, } - } + }, } }) jest.doMock('rxjs/operators', () => { @@ -123,6 +124,7 @@ describe('NodeService', () => { return isPackagedMock() }, getAppPath: getAppPathMock, + getVersion: getVersionMock, }, dialog: { showMessageBox: showMessageBoxMock, @@ -147,7 +149,7 @@ describe('NodeService', () => { return function () { return { getChain: getChainMock, - localNodeInfo: getLocalNodeInfoMock + localNodeInfo: getLocalNodeInfoMock, } } }) @@ -160,8 +162,8 @@ describe('NodeService', () => { start: stubbedStartLightNode, stop: stubbedStopLightNode, } - } - } + }, + }, } }) @@ -225,8 +227,8 @@ describe('NodeService', () => { beforeEach(async () => { const NodeService = require('../../src/services/node').default nodeService = new NodeService() - nodeService.verifyNodeVersion = () => {} - nodeService.verifyStartWithIndexer = () => {} + nodeService.isCkbCompatibility = () => {} + nodeService.isStartWithIndexer = () => {} stubbedNetworsServiceGet.mockReturnValueOnce({ remote: BUNDLED_CKB_URL }) }) @@ -282,7 +284,7 @@ describe('NodeService', () => { beforeEach(async () => { stubbedStartCKBNode.mockRejectedValue(new Error()) await nodeService.startNode() - }); + }) it('logs error', () => { expect(stubbedLoggerInfo).toHaveBeenCalledWith('CKB: fail to start bundled CKB with error:') expect(stubbedLoggerError).toHaveBeenCalledWith(new Error()) @@ -295,19 +297,19 @@ describe('NodeService', () => { startedBundledNode: false, }) }) - }); + }) describe('start light node', () => { beforeEach(() => { stubbedNetworsServiceGet.mockReset() }) it('start light node', async () => { - stubbedNetworsServiceGet.mockReturnValueOnce({type: NetworkType.Light}) + stubbedNetworsServiceGet.mockReturnValueOnce({ type: NetworkType.Light }) await nodeService.startNode() expect(stubbedStartLightNode).toBeCalled() expect(stubbedStopCkbNode).toBeCalled() }) }) - }); + }) describe('CurrentNetworkIDSubject#subscribe', () => { let eventCallback: any const stubbedTipNumberSubjectCallback = jest.fn() @@ -315,8 +317,8 @@ describe('NodeService', () => { const NodeService = require('../../src/services/node').default nodeService = new NodeService() nodeService.tipNumberSubject.subscribe(stubbedTipNumberSubjectCallback) - nodeService.verifyNodeVersion = () => {} - nodeService.verifyStartWithIndexer = () => {} + nodeService.isCkbCompatibility = () => {} + nodeService.isStartWithIndexer = () => {} eventCallback = stubbedCurrentNetworkIDSubjectSubscribe.mock.calls[0][0] }) it('emits disconnected event in ConnectionStatusSubject', () => { @@ -337,7 +339,7 @@ describe('NodeService', () => { nodeService.ckb.node.url = bundledNodeUrl }) stubbedStartCKBNode.mockResolvedValue(true) - stubbedNetworsServiceGet.mockReturnValue({remote: bundledNodeUrl}) + stubbedNetworsServiceGet.mockReturnValue({ remote: bundledNodeUrl }) getLocalNodeInfoMock.mockRejectedValue('not start') await nodeService.tryStartNodeOnDefaultURI() @@ -355,8 +357,8 @@ describe('NodeService', () => { describe('switches to other network', () => { beforeEach(async () => { stubbedConnectionStatusSubjectNext.mockReset() - stubbedNetworsServiceGet.mockReturnValue({remote: fakeHTTPUrl}) - await eventCallback({currentNetworkID: 'network2'}) + stubbedNetworsServiceGet.mockReturnValue({ remote: fakeHTTPUrl }) + await eventCallback({ currentNetworkID: 'network2' }) jest.advanceTimersByTime(10000) }) it('sets startedBundledNode to true in ConnectionStatusSubject', () => { @@ -367,8 +369,8 @@ describe('NodeService', () => { startedBundledNode: false, }) }) - }); - }); + }) + }) describe('with invalid url', () => { beforeEach(() => { stubbedNetworsServiceGet.mockReturnValueOnce({ remote: 'invalidurl' }) @@ -424,52 +426,92 @@ describe('NodeService', () => { expect(nodeService.getInternalNodeVersion()).toBe('0.107.0') }) }) - describe('test verify node version', () => { + describe('test verify external ckb node', () => { beforeEach(() => { const NodeService = require('../../src/services/node').default nodeService = new NodeService() - stubbedNetworsServiceGet.mockReturnValueOnce({ remote: BUNDLED_CKB_URL }) - }) - it('get internal version failed', async () => { - existsSyncMock.mockReturnValue(false) - getLocalNodeInfoMock.mockResolvedValue({}) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(0) + nodeService.getNeuronCompatibilityCKB = () => ({ + '0.110': { + '0.110': true, + '0.109': true, + '0.108': false, + '0.107': false, + }, + }) + stubbedNetworsServiceGet.mockReturnValue({ remote: BUNDLED_CKB_URL }) }) - it('get internal version success and same', async () => { + it('the ckb is running external and not light client', async () => { + nodeService._isCkbNodeExternal = true existsSyncMock.mockReturnValue(true) + getVersionMock.mockReturnValueOnce('0.110.0') readFileSyncMock.mockReturnValue('v0.107.0') getLocalNodeInfoMock.mockResolvedValue({ version: '0.107.0 (30e1255 2023-01-30)' }) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(0) + const res = await nodeService.verifyExternalCkbNode() + expect(res).toStrictEqual({ + ckbIsCompatible: false, + withIndexer: false, + shouldUpdate: false, + }) }) - it('get internal version success and patch not same', async () => { - existsSyncMock.mockReturnValue(true) - readFileSyncMock.mockReturnValue('v0.107.1') - getLocalNodeInfoMock.mockResolvedValue({ version: '0.107.0 (30e1255 2023-01-30)' }) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(0) + it('the ckb is running external and not light client, but get version failed', async () => { + nodeService._isCkbNodeExternal = true + existsSyncMock.mockReturnValue(false) + getLocalNodeInfoMock.mockResolvedValue({}) + const res = await nodeService.verifyExternalCkbNode() + expect(res).toBeUndefined() }) - it('major is same and minor is not same with major 0', async () => { - existsSyncMock.mockReturnValue(true) - readFileSyncMock.mockReturnValue('v0.107.0') - getLocalNodeInfoMock.mockResolvedValue({ version: '0.108.0 (30e1255 2023-01-30)' }) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(1) + it('the ckb type is light client', async () => { + nodeService._isCkbNodeExternal = true + stubbedNetworsServiceGet.mockReturnValueOnce({ remote: BUNDLED_CKB_URL, type: NetworkType.Light }) + const res = await nodeService.verifyExternalCkbNode() + expect(res).toBeUndefined() }) - it('major is same and minor is not same with major 1', async () => { - existsSyncMock.mockReturnValue(true) - readFileSyncMock.mockReturnValue('v1.107.0') - getLocalNodeInfoMock.mockResolvedValue({ version: '1.108.0 (30e1255 2023-01-30)' }) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(0) + it('the ckb is running internal', async () => { + nodeService._isCkbNodeExternal = false + const res = await nodeService.verifyExternalCkbNode() + expect(res).toBeUndefined() }) - it('major is not same', async () => { - existsSyncMock.mockReturnValue(true) - readFileSyncMock.mockReturnValue('v1.107.0') - getLocalNodeInfoMock.mockResolvedValue({ version: '0.108.0 (30e1255 2023-01-30)' }) - await nodeService.verifyNodeVersion() - expect(showMessageBoxMock).toBeCalledTimes(1) + }) + describe('test get compatibility', () => { + beforeEach(() => { + const NodeService = require('../../src/services/node').default + nodeService = new NodeService() + nodeService.getNeuronCompatibilityCKB = () => ({ + '0.110': { + '0.110': true, + '0.109': true, + '0.108': false, + '0.107': false, + }, + }) + stubbedNetworsServiceGet.mockReturnValueOnce({ remote: BUNDLED_CKB_URL }) + }) + it('neuron version not exist in compatible table', () => { + expect(nodeService.isCkbCompatibility('0.107.0', '0.107.0 (30e1255 2023-01-30)')).toBeFalsy() + }) + it('ckb version is not in compatible table', () => { + expect(nodeService.isCkbCompatibility('0.110.1', '0.106.0 (30e1255 2023-01-30)')).toBeFalsy() + }) + it('is not compatible', () => { + expect(nodeService.isCkbCompatibility('0.110.0', '0.108.0 (30e1255 2023-01-30)')).toBeFalsy() + }) + it('is compatible', () => { + expect(nodeService.isCkbCompatibility('0.110.0', '0.109.0 (30e1255 2023-01-30)')).toBeTruthy() + }) + }) + describe('test should update', () => { + beforeEach(() => { + const NodeService = require('../../src/services/node').default + nodeService = new NodeService() + }) + it('neuron ckb version > running ckb version', () => { + expect(nodeService.verifyCKbNodeShouldUpdate('0.108.0', '0.107.0 (30e1255 2023-01-30)')).toBeFalsy() + }) + it('neuron ckb version = running ckb version', () => { + expect(nodeService.verifyCKbNodeShouldUpdate('0.107.0', '0.107.0 (30e1255 2023-01-30)')).toBeFalsy() + }) + it('neuron ckb version < running ckb version', () => { + expect(nodeService.verifyCKbNodeShouldUpdate('0.106.0', '0.107.0 (30e1255 2023-01-30)')).toBeTruthy() }) }) describe('test verify start with indexer', () => { @@ -480,18 +522,48 @@ describe('NodeService', () => { }) it('start with indexer', async () => { rpcRequestMock.mockResolvedValue({}) - await nodeService.verifyStartWithIndexer() - expect(showMessageBoxMock).toBeCalledTimes(0) + const res = await nodeService.isStartWithIndexer() + expect(res).toBe(true) }) it('start without indexer', async () => { rpcRequestMock.mockResolvedValue({ error: { code: START_WITHOUT_INDEXER } }) - await nodeService.verifyStartWithIndexer() - expect(showMessageBoxMock).toBeCalledTimes(1) + const res = await nodeService.isStartWithIndexer() + expect(res).toBe(false) }) it('get indexer rpc failed', async () => { rpcRequestMock.mockRejectedValue('get tip header error') - await nodeService.verifyStartWithIndexer() - expect(showMessageBoxMock).toBeCalledTimes(1) + const res = await nodeService.isStartWithIndexer() + expect(res).toBe(false) + }) + }) + describe('test get Neuron compatibility CKB', () => { + beforeEach(() => { + const NodeService = require('../../src/services/node').default + nodeService = new NodeService() + }) + it('no compatibility file', () => { + existsSyncMock.mockReturnValue(false) + expect(nodeService.getNeuronCompatibilityCKB()).toBeUndefined() + }) + it('read file error', () => { + existsSyncMock.mockReturnValue(true) + readFileSyncMock.mockReturnValue(new Error('read failed')) + expect(nodeService.getNeuronCompatibilityCKB()).toBeUndefined() + }) + it('ckb version content is wrong', async () => { + existsSyncMock.mockReturnValue(true) + readFileSyncMock.mockReturnValue('') + expect(nodeService.getNeuronCompatibilityCKB()).toStrictEqual({}) + }) + it('success', async () => { + existsSyncMock.mockReturnValue(true) + readFileSyncMock.mockReturnValue('ckb,0.110,0.109\nNeuron,,\n0.109,yes,no') + expect(nodeService.getNeuronCompatibilityCKB()).toStrictEqual({ + '0.109': { + '0.110': true, + '0.109': false, + }, + }) }) }) })