From 5650a1766c5b3d00ebcc072965f160c1a8fa1d2c Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Wed, 15 Jun 2022 16:29:45 +0800 Subject: [PATCH 01/13] Add heart beat and retry closed process (#2422) --- .../src/block-sync-renderer/task.ts | 2 +- .../src/controllers/app/index.ts | 3 + .../src/services/monitor/base.ts | 39 ++++++ .../services/monitor/ckb-indexer-monitor.ts | 24 ++++ .../src/services/monitor/ckb-monitor.ts | 14 ++ .../src/services/monitor/index.ts | 10 ++ packages/neuron-wallet/src/services/node.ts | 39 +++--- .../neuron-wallet/src/utils/rpc-request.ts | 34 ++++- .../tests/services/monitor.test.ts | 123 ++++++++++++++++++ .../tests/utils/rpc-request.test.ts | 31 ++++- 10 files changed, 296 insertions(+), 23 deletions(-) create mode 100644 packages/neuron-wallet/src/services/monitor/base.ts create mode 100644 packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts create mode 100644 packages/neuron-wallet/src/services/monitor/ckb-monitor.ts create mode 100644 packages/neuron-wallet/src/services/monitor/index.ts create mode 100644 packages/neuron-wallet/tests/services/monitor.test.ts diff --git a/packages/neuron-wallet/src/block-sync-renderer/task.ts b/packages/neuron-wallet/src/block-sync-renderer/task.ts index 63527e04ea..a8e23532a8 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/task.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/task.ts @@ -61,7 +61,7 @@ export const listener = async ({ type, id, channel, message }: WorkerMessage) => } case 'unmount': { - if (!syncQueue) { return } + if (!syncQueue) { process.exit(0); return } logger.debug("Sync:\tstopping") await syncQueue.stopAndWait() syncQueue = null diff --git a/packages/neuron-wallet/src/controllers/app/index.ts b/packages/neuron-wallet/src/controllers/app/index.ts index 59b86448aa..17431a6d13 100644 --- a/packages/neuron-wallet/src/controllers/app/index.ts +++ b/packages/neuron-wallet/src/controllers/app/index.ts @@ -15,6 +15,7 @@ import SyncApiController from 'controllers/sync-api' import { SETTINGS_WINDOW_TITLE } from 'utils/const' import IndexerService from 'services/indexer' import { stopCkbNode } from 'services/ckb-runner' +import startMonitor from 'services/monitor' const app = electronApp @@ -50,6 +51,8 @@ export default class AppController { SyncApiController.getInstance().mount() await this.openWindow() + + startMonitor() } /** diff --git a/packages/neuron-wallet/src/services/monitor/base.ts b/packages/neuron-wallet/src/services/monitor/base.ts new file mode 100644 index 0000000000..47aa052f3b --- /dev/null +++ b/packages/neuron-wallet/src/services/monitor/base.ts @@ -0,0 +1,39 @@ +import logger from 'utils/logger' +import { interval, timer, Subscription, race, from } from 'rxjs' +import { map } from 'rxjs/operators' + +export default abstract class Monitor { + interval: Subscription | null = null + + isReStarting: boolean = false + + name: string = '' + + abstract isLiving(): Promise + + abstract restart(): Promise + + startMonitor(intervalTime: number = 10000) { + this.interval = interval(intervalTime).subscribe(async () => { + if (this.isReStarting) { + return + } + const timeout = timer(intervalTime / 2).pipe(map(() => true)) + const isLiving = await race(timeout, from(this.isLiving())).toPromise() + if (!isLiving) { + logger.info(`Monitor: is restarting ${this.name} process`) + this.isReStarting = true + try { + await this.restart() + logger.info(`Monitor: Restarting ${this.name} process success`) + } finally { + this.isReStarting = false + } + } + }) + } + + clearMonitor() { + this.interval?.unsubscribe() + } +} diff --git a/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts new file mode 100644 index 0000000000..669431583d --- /dev/null +++ b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts @@ -0,0 +1,24 @@ +import { resetSyncTask } from 'block-sync-renderer/index' +import IndexerService from 'services/indexer' +import { rpcRequest } from 'utils/rpc-request' +import BaseMonitor from './base' + +export default class CkbIndexerMonitor extends BaseMonitor { + async isLiving(): Promise { + try { + await rpcRequest(IndexerService.LISTEN_URI, { method: 'get_tip' }) + return true + } catch (error) { + if (error?.code === 'ECONNREFUSED') { + return false + } + return true + } + } + + async restart(): Promise { + await resetSyncTask() + } + + name: string = 'ckb-indexer' +} diff --git a/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts b/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts new file mode 100644 index 0000000000..23bc6b7050 --- /dev/null +++ b/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts @@ -0,0 +1,14 @@ +import NodeService from '../node' +import BaseMonitor from './base' + +export default class CkbMonitor extends BaseMonitor { + async isLiving(): Promise { + return !(await NodeService.getInstance().isDefaultCKBNeedRestart()) + } + + async restart(): Promise { + return NodeService.getInstance().startNode() + } + + name: string = 'ckb' +} diff --git a/packages/neuron-wallet/src/services/monitor/index.ts b/packages/neuron-wallet/src/services/monitor/index.ts new file mode 100644 index 0000000000..f717e94980 --- /dev/null +++ b/packages/neuron-wallet/src/services/monitor/index.ts @@ -0,0 +1,10 @@ +import Base from './base' +import CkbIndexerMonitor from './ckb-indexer-monitor' +import CkbMonitor from './ckb-monitor' + +export default function startMonitor() { + const monitors = [new CkbIndexerMonitor(), new CkbMonitor()] + monitors.forEach((v: Base) => { + v.startMonitor() + }) +} diff --git a/packages/neuron-wallet/src/services/node.ts b/packages/neuron-wallet/src/services/node.ts index b02b70a7b1..c93130bee0 100644 --- a/packages/neuron-wallet/src/services/node.ts +++ b/packages/neuron-wallet/src/services/node.ts @@ -132,21 +132,38 @@ class NodeService { ) } - public async tryStartNodeOnDefaultURI(): Promise { + public async tryStartNodeOnDefaultURI() { + const isDefaultCKBNeedStart = await this.isDefaultCKBNeedRestart() + if (isDefaultCKBNeedStart) { + logger.info('CKB:\texternal RPC on default uri not detected, starting bundled CKB node.') + const redistReady = await redistCheck() + await (redistReady ? this.startNode() : this.showGuideDialog()) + } else { + logger.info('CKB:\texternal RPC on default uri detected, skip starting bundled CKB node.') + } + } + + public async isDefaultCKBNeedRestart() { let network = NetworksService.getInstance().getCurrent() if (network.remote !== BUNDLED_CKB_URL) { return false } try { await new RpcService(network.remote).getChain() - logger.info('CKB:\texternal RPC on default uri detected, skip starting bundled CKB node.') return false } catch (err) { - logger.info('CKB:\texternal RPC on default uri not detected, starting bundled CKB node.') - const redistReady = await redistCheck() - this.startedBundledNode = await (redistReady ? this.startNode() : this.showGuideDialog()) + return true + } + } - return this.startedBundledNode + public async startNode() { + try { + await startCkbNode() + this.startedBundledNode = true + } catch (error) { + this.startedBundledNode = false + logger.info('CKB:\tfail to start bundled CKB with error:') + logger.error(error) } } @@ -170,16 +187,6 @@ class NodeService { return false }) } - - private startNode = () => { - return startCkbNode() - .then(() => true) - .catch(err => { - logger.info('CKB:\tfail to start bundled CKB with error:') - logger.error(err) - return false - }) - } } export default NodeService diff --git a/packages/neuron-wallet/src/utils/rpc-request.ts b/packages/neuron-wallet/src/utils/rpc-request.ts index f869f43db6..14da636460 100644 --- a/packages/neuron-wallet/src/utils/rpc-request.ts +++ b/packages/neuron-wallet/src/utils/rpc-request.ts @@ -1,14 +1,41 @@ import axios from 'axios' +export const rpcRequest = async ( + url: string, + options: { + method: string + params?: any + } +): Promise => { + const res = await axios.post<{ id: number; error?: any; result: any }[]>( + url, + { + id: 0, + jsonrpc: '2.0', + method: options.method, + params: options.params + }, + { + headers: { + 'content-type': 'application/json' + } + } + ) + if (res.status !== 200) { + throw new Error(`indexer request failed with HTTP code ${res.status}`) + } + return res.data +} + export const rpcBatchRequest = async ( - ckbIndexerUrl: string, + url: string, options: { method: string params?: any }[] ): Promise => { const res = await axios.post<{ id: number; error?: any; result: any }[]>( - ckbIndexerUrl, + url, options.map((v, idx) => ({ id: idx, jsonrpc: '2.0', @@ -28,5 +55,6 @@ export const rpcBatchRequest = async ( } export default { - rpcBatchRequest + rpcBatchRequest, + rpcRequest } diff --git a/packages/neuron-wallet/tests/services/monitor.test.ts b/packages/neuron-wallet/tests/services/monitor.test.ts new file mode 100644 index 0000000000..3aa7f8c488 --- /dev/null +++ b/packages/neuron-wallet/tests/services/monitor.test.ts @@ -0,0 +1,123 @@ +import Monitor from '../../src/services/monitor/base' +import CkbMonitor from '../../src/services/monitor/ckb-monitor' +import CkbIndexerMonitor from '../../src/services/monitor/ckb-indexer-monitor' + +const isDefaultCKBNeedRestartMock = jest.fn().mockResolvedValue(true) +const startNodeMock = jest.fn() + +jest.mock('../../src/services/node', () => ({ + getInstance() { + return { + isDefaultCKBNeedRestart: isDefaultCKBNeedRestartMock, + startNode: startNodeMock + } + } +})) + +describe('ckb monitor', () => { + const monitor = new CkbMonitor() + it('is living', async () => { + const isLiving = await monitor.isLiving() + expect(isLiving).toBeFalsy() + }) + it('restart', async () => { + await monitor.restart() + expect(startNodeMock).toHaveBeenCalled() + }) +}) + +const rpcRequestMock = jest.fn() +jest.mock('../../src/utils/rpc-request', () => ({ + rpcRequest: () => rpcRequestMock() +})) + +const resetSyncTaskMock = jest.fn() +jest.mock('../../src/block-sync-renderer/index', () => ({ + resetSyncTask: () => resetSyncTaskMock() +})) + +describe('ckb indexer monitor', () => { + const monitor = new CkbIndexerMonitor() + describe('is living', () => { + it('rpc success', async () => { + const isLiving = await monitor.isLiving() + expect(isLiving).toBeTruthy() + }) + it('rpc failed with ECONNREFUSED', async () => { + rpcRequestMock.mockRejectedValueOnce({ code: 'ECONNREFUSED' }) + const isLiving = await monitor.isLiving() + expect(isLiving).toBeFalsy() + }) + it('rpc failed not ECONNREFUSED', async () => { + rpcRequestMock.mockRejectedValueOnce({}) + const isLiving = await monitor.isLiving() + expect(isLiving).toBeTruthy() + }) + }) + it('restart', async () => { + await monitor.restart() + expect(resetSyncTaskMock).toHaveBeenCalled() + }) +}) + +const isLivingMock = jest.fn() +const restartMock = jest.fn() +class MonitorTest extends Monitor { + isLiving(): Promise { + return isLivingMock() + } + + restart(): Promise { + return restartMock() + } +} + +function wait(times: number) { + return new Promise((resolve) => { + setTimeout(() => { resolve(times) }, times) + }) +} +describe('base monitor', () => { + const monitor = new MonitorTest() + + beforeEach(() => { + isLivingMock.mockReset() + restartMock.mockReset() + }) + + describe('start monitor', () => { + it('is living', async () => { + isLivingMock.mockResolvedValue(true) + monitor.startMonitor(100) + await wait(200) + expect(isLivingMock).toHaveBeenCalled() + expect(restartMock).toHaveBeenCalledTimes(0) + monitor.clearMonitor() + }) + it('not living', async () => { + isLivingMock.mockResolvedValue(true).mockResolvedValueOnce(false) + monitor.startMonitor(100) + await wait(200) + expect(isLivingMock).toHaveBeenCalled() + expect(restartMock).toHaveBeenCalled() + monitor.clearMonitor() + }) + it('isLiving timeout', async () => { + isLivingMock.mockImplementation(() => wait(200)) + monitor.startMonitor(100) + await wait(200) + expect(isLivingMock).toHaveBeenCalled() + expect(restartMock).toHaveBeenCalledTimes(0) + monitor.clearMonitor() + }) + it('not living wait restart', async () => { + isLivingMock.mockResolvedValueOnce(false) + restartMock.mockImplementation(() => wait(400)) + monitor.startMonitor(100) + await wait(400) + expect(isLivingMock).toHaveBeenCalled() + expect(restartMock).toHaveBeenCalledTimes(1) + monitor.clearMonitor() + }) + }) +}) diff --git a/packages/neuron-wallet/tests/utils/rpc-request.test.ts b/packages/neuron-wallet/tests/utils/rpc-request.test.ts index 0653e16690..b4d2e2f135 100644 --- a/packages/neuron-wallet/tests/utils/rpc-request.test.ts +++ b/packages/neuron-wallet/tests/utils/rpc-request.test.ts @@ -1,12 +1,12 @@ -import { rpcBatchRequest } from '../../src/utils/rpc-request' +import { rpcBatchRequest, rpcRequest } from '../../src/utils/rpc-request' const postMock = jest.fn() jest.mock('axios', () => ({ post: () => postMock() })) -describe('rpc-request', () => { +describe('rpc-batch-request', () => { const options = [ { method: 'get_block', @@ -47,4 +47,29 @@ describe('rpc-request', () => { } ]) }) -}) \ No newline at end of file +}) + +describe('rpc-request', () => { + const option = { + method: 'get_block', + params: 1 + } + it('fetch error', async () => { + postMock.mockResolvedValueOnce({ status: 500 }) + await expect(rpcRequest('url', option)).rejects.toThrow(new Error(`indexer request failed with HTTP code 500`)) + }) + it('fetch success', async () => { + postMock.mockResolvedValueOnce({ + status: 200, + data: { + id: 2, + result: 2 + } + }) + const res = await rpcRequest('url', option) + expect(res).toEqual({ + id: 2, + result: 2 + }) + }) +}) From 8cfb2d0be323435a23f9c2cb8b4db965dec32cd7 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Thu, 16 Jun 2022 17:06:52 +0800 Subject: [PATCH 02/13] fix: Fix open create hardware wallet after open import seed. --- .../src/components/ImportHardware/common.ts | 10 ++-- .../components/ImportHardware/confirming.tsx | 43 +++++++++-------- .../ImportHardware/detect-device.tsx | 18 +++----- .../ImportHardware/import-error.tsx | 18 +++++--- .../ImportHardware/import-success.tsx | 10 ++-- .../src/components/ImportHardware/index.tsx | 46 ++++++++++++++----- .../components/ImportHardware/name-wallet.tsx | 27 ++++++----- .../ImportHardware/select-model.tsx | 25 ++++------ 8 files changed, 107 insertions(+), 90 deletions(-) diff --git a/packages/neuron-ui/src/components/ImportHardware/common.ts b/packages/neuron-ui/src/components/ImportHardware/common.ts index 605eca45a4..18c4c5f29f 100644 --- a/packages/neuron-ui/src/components/ImportHardware/common.ts +++ b/packages/neuron-ui/src/components/ImportHardware/common.ts @@ -1,7 +1,7 @@ /* eslint-disable import/prefer-default-export */ import { FailureFromController } from 'services/remote/remoteApiWrapper' -export enum RoutePath { +export enum ImportStep { DetectDevice = '/detect-device', Comfirming = '/confirming', Error = '/error', @@ -15,12 +15,14 @@ export interface Model { product: string } -export interface LocationState { - entryPath: string - model: Model +export interface ImportHardwareState { + model?: Model extendedPublicKey?: { publicKey: string chainCode: string } error?: FailureFromController['message'] + step: ImportStep } + +export type ActionType = Partial diff --git a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx index b7d42b655c..7b8df42d14 100644 --- a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx @@ -1,42 +1,41 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { RouteComponentProps } from 'react-router-dom' import Button from 'widgets/Button' import { ReactComponent as PendingIcon } from 'widgets/Icons/Pending.svg' import { getDeviceExtendedPublickey } from 'services/remote' -import { isSuccessResponse, useDidMount } from 'utils' -import { RoutePath, LocationState } from './common' +import { isSuccessResponse } from 'utils' +import { ImportStep, ActionType } from './common' import styles from './findDevice.module.scss' -const Confirming = ({ history, location }: RouteComponentProps<{}, {}, LocationState>) => { +const Confirming = ({ dispatch }: { dispatch: React.Dispatch }) => { const [t] = useTranslation() - const { entryPath } = location.state const onBack = useCallback(() => { - history.push(entryPath) - }, [history, entryPath]) + dispatch({ step: ImportStep.ImportHardware }) + }, [dispatch]) - useDidMount(() => { + useEffect(() => { + let cancel = false getDeviceExtendedPublickey().then(res => { + if (cancel) { + return + } if (isSuccessResponse(res)) { - history.push({ - pathname: entryPath + RoutePath.NameWallet, - state: { - ...location.state, - extendedPublicKey: res.result!, - }, + dispatch({ + step: ImportStep.NameWallet, + extendedPublicKey: res.result!, }) } else { - history.push({ - pathname: entryPath + RoutePath.Error, - state: { - ...location.state, - error: res.message, - }, + dispatch({ + step: ImportStep.Error, + error: res.message, }) } }) - }) + return () => { + cancel = true + } + }, []) return (
diff --git a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx index edc6c211fa..d78ea2c5e4 100644 --- a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useState } from 'react' -import { useHistory, RouteComponentProps } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import { getDevices, getDeviceFirmwareVersion, getDeviceCkbAppVersion, connectDevice } from 'services/remote' @@ -12,7 +11,7 @@ import { DeviceNotFoundException, MultiDeviceException, } from 'exceptions' -import { RoutePath, LocationState } from './common' +import { ImportStep, ActionType, Model } from './common' import styles from './findDevice.module.scss' @@ -45,13 +44,11 @@ const Info = ( ) } -const DetectDevice = ({ history, location }: RouteComponentProps<{}, {}, LocationState>) => { +const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch; model: Model }) => { const [t] = useTranslation() - const histroy = useHistory() - const { model, entryPath } = location.state const onBack = useCallback(() => { - histroy.goBack() - }, [histroy]) + dispatch({ step: ImportStep.ImportHardware }) + }, [dispatch]) const [scaning, setScaning] = useState(true) const [error, setError] = useState('') @@ -99,11 +96,8 @@ const DetectDevice = ({ history, location }: RouteComponentProps<{}, {}, Locatio }) const onNext = useCallback(() => { - history.push({ - pathname: entryPath + RoutePath.Comfirming, - state: location.state, - }) - }, [history, entryPath, location.state]) + dispatch({ step: ImportStep.Comfirming }) + }, [dispatch]) const errorMsg = error.startsWith('messages.codes.') ? t(error) : error const ready = error === '' && appVersion !== '' diff --git a/packages/neuron-ui/src/components/ImportHardware/import-error.tsx b/packages/neuron-ui/src/components/ImportHardware/import-error.tsx index 43acebc40c..d79c596fd7 100644 --- a/packages/neuron-ui/src/components/ImportHardware/import-error.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/import-error.tsx @@ -1,22 +1,26 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { RouteComponentProps } from 'react-router-dom' import Button from 'widgets/Button' import CopyZone from 'widgets/CopyZone' import { ReactComponent as FailedInfo } from 'widgets/Icons/FailedInfo.svg' import { errorFormatter } from 'utils' -import { LocationState } from './common' +import { ActionType, ImportHardwareState, ImportStep } from './common' import styles from './findDevice.module.scss' -const ImportError = ({ history, location }: RouteComponentProps<{}, {}, LocationState>) => { +const ImportError = ({ + dispatch, + error, +}: { + dispatch: React.Dispatch + error: ImportHardwareState['error'] +}) => { const [t] = useTranslation() - const { error, entryPath } = location.state const onBack = useCallback(() => { - history.push(entryPath) - }, [history, entryPath]) + dispatch({ step: ImportStep.ImportHardware }) + }, [dispatch]) - const errorMsg = errorFormatter(error!, t) + const errorMsg = error ? errorFormatter(error, t) : '' return (
diff --git a/packages/neuron-ui/src/components/ImportHardware/import-success.tsx b/packages/neuron-ui/src/components/ImportHardware/import-success.tsx index 38e203d13b..25ca700091 100644 --- a/packages/neuron-ui/src/components/ImportHardware/import-success.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/import-success.tsx @@ -1,18 +1,16 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { RouteComponentProps } from 'react-router-dom' import Button from 'widgets/Button' import { ReactComponent as CompleteIcon } from 'widgets/Icons/Complete.svg' -import { RoutePath, LocationState } from './common' +import { ImportStep, ActionType } from './common' import styles from './findDevice.module.scss' -const ImportSuccess = ({ history, location }: RouteComponentProps<{}, {}, LocationState>) => { +const ImportSuccess = ({ dispatch }: { dispatch: React.Dispatch }) => { const [t] = useTranslation() - const { entryPath } = location.state const onClose = useCallback(() => { - history.push(entryPath.replace(RoutePath.ImportHardware, '')) - }, [history, entryPath]) + dispatch({ step: ImportStep.ImportHardware }) + }, []) return (
diff --git a/packages/neuron-ui/src/components/ImportHardware/index.tsx b/packages/neuron-ui/src/components/ImportHardware/index.tsx index ea779b4949..03ce99cbc3 100644 --- a/packages/neuron-ui/src/components/ImportHardware/index.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/index.tsx @@ -1,5 +1,4 @@ -import React, { useRef, useEffect } from 'react' -import { Switch, Route, RouteComponentProps } from 'react-router-dom' +import React, { useRef, useEffect, useReducer } from 'react' import Experimental from 'widgets/ExperimentalRibbon' import Comfirming from './confirming' import ImportError from './import-error' @@ -7,11 +6,41 @@ import SelectModel from './select-model' import DetectDevice from './detect-device' import ImportSuccess from './import-success' import NameWallet from './name-wallet' -import { RoutePath } from './common' +import { ImportStep, ImportHardwareState, ActionType } from './common' import styles from './findDevice.module.scss' -const ImportHardware = ({ match }: RouteComponentProps) => { +const reducer: React.Reducer = (state, action) => { + return { ...state, ...action } +} + +const Content = () => { + const [importHardwareStates, dispatch] = useReducer(reducer, { step: ImportStep.ImportHardware }) + switch (importHardwareStates.step) { + case ImportStep.ImportHardware: + return + case ImportStep.DetectDevice: + return + case ImportStep.Comfirming: + return + case ImportStep.Error: + return + case ImportStep.Success: + return + case ImportStep.NameWallet: + return ( + + ) + default: + return + } +} + +const ImportHardware = () => { const dialogRef = useRef(null) const EXPERIMENTAL_TAG = 'import-hardware' @@ -22,14 +51,7 @@ const ImportHardware = ({ match }: RouteComponentProps) => { return ( - - - - - - - - + ) } diff --git a/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx b/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx index c313d7e945..354cf3dcf1 100644 --- a/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx @@ -1,23 +1,29 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { RouteComponentProps } from 'react-router-dom' import Button from 'widgets/Button' import TextField from 'widgets/TextField' import { createHardwareWallet } from 'services/remote' import { isSuccessResponse } from 'utils' -import { RoutePath, LocationState } from './common' +import { ImportStep, ActionType, ImportHardwareState } from './common' import styles from './findDevice.module.scss' -const NameWallet = ({ history, location }: RouteComponentProps<{}, {}, LocationState>) => { +const NameWallet = ({ + dispatch, + model, + extendedPublicKey, +}: { + dispatch: React.Dispatch + model: ImportHardwareState['model'] + extendedPublicKey: ImportHardwareState['extendedPublicKey'] +}) => { const [t] = useTranslation() - const { entryPath, model, extendedPublicKey } = location.state - const [walletName, setWalletName] = useState(`${model.manufacturer} ${model.product}`) + const [walletName, setWalletName] = useState(`${model?.manufacturer} ${model?.product}`) const [errorMsg, setErrorMsg] = useState('') const onBack = useCallback(() => { - history.push(entryPath) - }, [history, entryPath]) + dispatch({ step: ImportStep.ImportHardware }) + }, [dispatch]) const onNext = useCallback( (e: React.FormEvent) => { @@ -27,16 +33,13 @@ const NameWallet = ({ history, location }: RouteComponentProps<{}, {}, LocationS walletName, }).then(res => { if (isSuccessResponse(res)) { - history.push({ - pathname: entryPath + RoutePath.Success, - state: location.state, - }) + dispatch({ step: ImportStep.Success }) } else { setErrorMsg(typeof res.message === 'string' ? res.message : res.message!.content!) } }) }, - [history, entryPath, walletName, location.state, extendedPublicKey] + [walletName, extendedPublicKey] ) const onInput = useCallback(e => { diff --git a/packages/neuron-ui/src/components/ImportHardware/select-model.tsx b/packages/neuron-ui/src/components/ImportHardware/select-model.tsx index fed15ee863..b714d0a878 100644 --- a/packages/neuron-ui/src/components/ImportHardware/select-model.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/select-model.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useState } from 'react' -import { RouteComponentProps } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import Select from 'widgets/Select' import { Text } from 'office-ui-fabric-react' +import { useHistory } from 'react-router-dom' +import { useGoBack } from 'utils' import styles from './findDevice.module.scss' -import { LocationState, Model, RoutePath } from './common' +import { ActionType, Model, ImportStep } from './common' const supportedHardwareModels = [ { @@ -26,23 +27,17 @@ const supportedHardwareModels = [ }, ] -const SelectModel = ({ match, history }: RouteComponentProps<{}, {}, LocationState>) => { +const SelectModel = ({ dispatch }: { dispatch: React.Dispatch }) => { const [t] = useTranslation() const [model, setModel] = useState() - - const onBack = useCallback(() => { - history.push(match.url.replace(RoutePath.ImportHardware, '')) - }, [history, match.url]) - + const history = useHistory() + const onBack = useGoBack(history) const onNext = useCallback(() => { - history.push({ - pathname: match.url + RoutePath.DetectDevice, - state: { - model: model!, - entryPath: match.url, - }, + dispatch({ + model, + step: ImportStep.DetectDevice, }) - }, [history, match.url, model]) + }, [dispatch, model]) const onDropDownChange = useCallback(({ data }) => { setModel(data) From 35454884e5e194c0e71525d6a3f4c204caf72845 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Mon, 20 Jun 2022 12:04:05 +0800 Subject: [PATCH 03/13] Fix local testcase (#2427) --- packages/neuron-wallet/tests/services/monitor.test.ts | 4 ++-- .../tests/services/tx/transaction-generator.test.ts | 6 ++++++ .../tests/services/tx/transaction-service.test.ts | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/neuron-wallet/tests/services/monitor.test.ts b/packages/neuron-wallet/tests/services/monitor.test.ts index 3aa7f8c488..af8ab977ca 100644 --- a/packages/neuron-wallet/tests/services/monitor.test.ts +++ b/packages/neuron-wallet/tests/services/monitor.test.ts @@ -111,10 +111,10 @@ describe('base monitor', () => { monitor.clearMonitor() }) it('not living wait restart', async () => { - isLivingMock.mockResolvedValueOnce(false) + isLivingMock.mockResolvedValue(true).mockResolvedValueOnce(false) restartMock.mockImplementation(() => wait(400)) monitor.startMonitor(100) - await wait(400) + await wait(800) expect(isLivingMock).toHaveBeenCalled() expect(restartMock).toHaveBeenCalledTimes(1) monitor.clearMonitor() diff --git a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts index 6b566f98b8..0527ce5def 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts @@ -72,6 +72,12 @@ jest.doMock('../../../src/services/wallets', () => ({ } } })) +jest.mock('../../../src/models/asset-account-info', () => { + const originalModule = jest.requireActual('../../../src/models/asset-account-info').default + return function() { + return new originalModule('0x92b197aa1fba0f63633922c61c92375c9c074a93e85963554f5499fe1450d0e6') + } +}) import TransactionGenerator from '../../../src/services/tx/transaction-generator' import HdPublicKeyInfo from '../../../src/database/chain/entities/hd-public-key-info' diff --git a/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts index f6118da58f..6ed5734137 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-service.test.ts @@ -10,6 +10,13 @@ import transactions from '../../setupAndTeardown/transactions.fixture' import { getConnection } from 'typeorm' import HdPublicKeyInfo from '../../../src/database/chain/entities/hd-public-key-info' +jest.mock('../../../src/models/asset-account-info', () => { + const originalModule = jest.requireActual('../../../src/models/asset-account-info').default + return function() { + return new originalModule('0x92b197aa1fba0f63633922c61c92375c9c074a93e85963554f5499fe1450d0e5') + } +}) + describe('Test TransactionService', () => { beforeAll(async () => { await initConnection() From b156c1b10e062e2e3b5c2d95bb4e64f94f8687ae Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 27 Jun 2022 11:42:10 +0800 Subject: [PATCH 04/13] chore: add issue template for synchronization issue --- .../ISSUE_TEMPLATE/synchronization-issue.yml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/synchronization-issue.yml diff --git a/.github/ISSUE_TEMPLATE/synchronization-issue.yml b/.github/ISSUE_TEMPLATE/synchronization-issue.yml new file mode 100644 index 0000000000..3a44b6ec99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/synchronization-issue.yml @@ -0,0 +1,55 @@ +name: Synchronization issue +description: Neuron doesn't synchronize +title: '[Synchronization] **brief description**' +labels: + - bug +assignees: + - Keith-CY +body: + - type: markdown + attributes: + value: | + Please check if these issues help + - [Synchronization doesnt' start](https://github.com/nervosnetwork/neuron/issues/2388) + - [Transactions keep pending on Neuron v0.101.3](https://github.com/nervosnetwork/neuron/issues/2384) + - [`Check for update` in settings doesn't work](https://github.com/nervosnetwork/neuron/issues/2372) + + - type: textarea + id: detial + validations: + required: true + attributes: + label: Detail of the issue + description: Elaborate on your issue in this field + + - type: markdown + attributes: + value: | + + --- + ## Please add neuron logs if possible, they can be exported by `Menu -> Help -> Export Debug Information` + + - type: textarea + id: bundled-ckb + validations: + required: false + attributes: + label: bundled-ckb.log + render: shell + + - type: textarea + id: main-log + validations: + required: false + attributes: + label: main.log + render: shell + + - type: textarea + id: status + validations: + required: false + attributes: + label: status.log + render: shell + From 7941525ed9a160eb52136aa0bf2aefec4ec9e5f0 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Mon, 4 Jul 2022 17:32:23 +0800 Subject: [PATCH 05/13] Feat add path setting (#2433) --- .../src/components/ClearCache/index.tsx | 41 ++++---- .../components/ClearCache/style.module.scss | 48 ++++++---- .../src/components/DataSetting/hooks.ts | 70 ++++++++++++++ .../components/DataSetting/index.module.scss | 66 +++++++++++++ .../src/components/DataSetting/index.tsx | 92 ++++++++++++++++++ .../src/components/ErrorBoundary/index.tsx | 4 +- .../src/components/GeneralSetting/index.tsx | 6 +- .../src/components/SettingTabs/index.tsx | 3 + packages/neuron-ui/src/locales/en.json | 23 ++++- packages/neuron-ui/src/locales/zh-tw.json | 23 ++++- packages/neuron-ui/src/locales/zh.json | 23 ++++- packages/neuron-ui/src/services/remote/app.ts | 17 +++- .../src/services/remote/remoteApiWrapper.ts | 7 ++ .../src/stories/GeneralSetting.stories.tsx | 2 +- packages/neuron-ui/src/utils/enums.ts | 1 + .../src/widgets/Icons/OpenFolder.svg | 2 + packages/neuron-ui/src/widgets/Icons/icon.tsx | 4 + packages/neuron-wallet/src/controllers/api.ts | 48 ++++++++++ .../src/controllers/export-debug.ts | 4 +- .../neuron-wallet/src/controllers/mercury.ts | 4 +- .../neuron-wallet/src/services/ckb-runner.ts | 13 +-- .../neuron-wallet/src/services/indexer.ts | 8 +- .../src/services/monitor/base.ts | 55 +++++++---- .../services/monitor/ckb-indexer-monitor.ts | 4 + .../src/services/monitor/ckb-monitor.ts | 5 + .../src/services/monitor/index.ts | 27 +++++- .../neuron-wallet/src/services/settings.ts | 31 +++++- .../index/resetSyncTask.test.ts | 3 +- .../tx-status-listener.intg.test.ts | 4 +- .../tests/controllers/asset-account.test.ts | 8 ++ .../tests/controllers/export-debug.test.ts | 5 +- .../tests/controllers/multisig.test.ts | 2 +- .../tests/services/ckb-runner.test.ts | 17 +++- .../tests/services/monitor.test.ts | 72 +++++++++++++- .../tests/services/setting.test.ts | 95 +++++++++++++++++++ 35 files changed, 722 insertions(+), 115 deletions(-) create mode 100644 packages/neuron-ui/src/components/DataSetting/hooks.ts create mode 100644 packages/neuron-ui/src/components/DataSetting/index.module.scss create mode 100644 packages/neuron-ui/src/components/DataSetting/index.tsx create mode 100644 packages/neuron-ui/src/widgets/Icons/OpenFolder.svg create mode 100644 packages/neuron-wallet/tests/services/setting.test.ts diff --git a/packages/neuron-ui/src/components/ClearCache/index.tsx b/packages/neuron-ui/src/components/ClearCache/index.tsx index b2b19a5eb5..8e9294d935 100644 --- a/packages/neuron-ui/src/components/ClearCache/index.tsx +++ b/packages/neuron-ui/src/components/ClearCache/index.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useCallback, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' -import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg' import WarningIcon from 'widgets/Icons/Warning.png' import { StateDispatch, addPopup } from 'states' import { clearCellCache } from 'services/remote' import { cacheClearDate } from 'services/localCache' import { isSuccessResponse, useDialog, uniformTimeFormatter } from 'utils' +import { Tooltip } from 'widgets/Icons/icon' import styles from './style.module.scss' const I18N_PATH = 'settings.clear-cache' @@ -71,28 +71,25 @@ const ClearCache = ({ dispatch }: { dispatch: StateDispatch }) => { return ( <> -
- {clearedDate ? ( -
{t('settings.general.cache-cleared-on', { date: clearedDate })}
- ) : null} -
- - {t('settings.general.clear-cache-description')} -
-
-
- +
+ {t('settings.data.cache')} + + + + :
+
{t('settings.data.cache-cleared-on', { date: clearedDate })}
+ warning
{t(`${I18N_PATH}.title`)}
diff --git a/packages/neuron-ui/src/components/ClearCache/style.module.scss b/packages/neuron-ui/src/components/ClearCache/style.module.scss index 937589c6e9..1b74c20c9a 100644 --- a/packages/neuron-ui/src/components/ClearCache/style.module.scss +++ b/packages/neuron-ui/src/components/ClearCache/style.module.scss @@ -1,30 +1,36 @@ @import '../../styles//mixin.scss'; .clearCache { - &.detail { - grid-area: clear-cache-detail; - .date { - display: flex; - align-items: center; - font-size: 0.875rem; - height: 1.125rem; - margin-bottom: 5px; - } - .desc { - display: flex; - font-size: 0.6875rem; - color: #666; + font-weight: bold; + display: flex; + + .tooltip { + position: relative; + display: inline-flex; + align-items: center; + padding-left: 4px; + + &::after { + display: none; + position: absolute; + left: -20px; + top: 120%; + content: attr(data-tooltip); + padding: 4px 12px; + border-radius: 6px; + background: #cccccc99; + font-weight: 300; + width: 600px; } - svg { - height: 0.6875rem; - width: 0.6875rem; - filter: grayscale(1) opacity(0.6); - margin: 2px 5px 0 0; + + &:hover::after { + display: block; } } - &.action { - grid-area: clear-cache-action; - } +} + +.clearedDate { + font-size: 12px; } .dialog { diff --git a/packages/neuron-ui/src/components/DataSetting/hooks.ts b/packages/neuron-ui/src/components/DataSetting/hooks.ts new file mode 100644 index 0000000000..44a07d733f --- /dev/null +++ b/packages/neuron-ui/src/components/DataSetting/hooks.ts @@ -0,0 +1,70 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + getCkbNodeDataPath, + getIndexerDataPath, + invokeShowOpenDialog, + startProcessMonitor, + stopProcessMonitor, + setCkbNodeDataPath, + setIndexerDataPath, +} from 'services/remote' +import { isSuccessResponse, useDialogWrapper, useDidMount } from 'utils' + +export const useDataPath = ( + getPath: typeof getCkbNodeDataPath | typeof getIndexerDataPath, + setPath: typeof setCkbNodeDataPath | typeof setIndexerDataPath, + type: Parameters[0] +) => { + const [t] = useTranslation() + const [prevPath, setPrevPath] = useState() + const [currentPath, setCurrentPath] = useState() + const { dialogRef, openDialog, closeDialog } = useDialogWrapper() + useDidMount(() => { + getPath(undefined).then(res => { + if (isSuccessResponse(res)) { + setPrevPath(res.result!) + } + }) + }) + const onSetting = useCallback(() => { + invokeShowOpenDialog({ + buttonLabel: t('settings.data.set', { lng: navigator.language }), + properties: ['openDirectory', 'createDirectory', 'promptToCreate', 'treatPackageAsDirectory'], + }).then(res => { + if (isSuccessResponse(res) && !res.result?.canceled && res.result?.filePaths?.length) { + setCurrentPath(res.result?.filePaths?.[0]) + stopProcessMonitor(type).then(stopRes => { + if (isSuccessResponse(stopRes)) { + openDialog() + } + }) + } + }) + }, [t, type]) + const onCancel = useCallback(() => { + startProcessMonitor(type).then(res => { + if (isSuccessResponse(res)) { + closeDialog() + } + }) + }, [closeDialog, type]) + const onConfirm = useCallback(() => { + setPath(currentPath!).then(res => { + if (isSuccessResponse(res)) { + setPrevPath(currentPath) + closeDialog() + } + }) + }, [currentPath, closeDialog, setPrevPath, setPath]) + return { + prevPath, + currentPath, + onSetting, + dialogRef, + onCancel, + onConfirm, + } +} + +export default useDataPath diff --git a/packages/neuron-ui/src/components/DataSetting/index.module.scss b/packages/neuron-ui/src/components/DataSetting/index.module.scss new file mode 100644 index 0000000000..63ebb162b1 --- /dev/null +++ b/packages/neuron-ui/src/components/DataSetting/index.module.scss @@ -0,0 +1,66 @@ +@import '../../styles/mixin.scss'; + +.root { + margin-top: 20px; + display: grid; + grid-template-columns: auto 1fr 140px; + grid-gap: 30px 20px; + align-items: center; + + .name { + font-weight: bold; + } + + .content { + font-size: 12px; + word-break: break-all; + } + + button { + line-height: 1; + height: 1.625rem; + } +} + +.dialog { + @include dialog-container; + padding: 30px 50px; + + .describe { + word-break: break-all; + font-size: 14px; + } + + .attention { + position: relative; + padding-left: 1rem; + font-size: 0.75rem; + letter-spacing: 0.5px; + margin: 14px 0 20px 0; + color: #666; + + svg { + position: absolute; + left: 0; + top: 0.13rem; + filter: grayscale(1) opacity(0.6); + width: 0.625rem; + height: 0.625rem; + } + } + + .action { + padding-top: 16px; + display: flex; + justify-content: space-between; + } +} + +.path { + display: flex; + align-items: center; + + & > svg { + flex-shrink: 0; + } +} \ No newline at end of file diff --git a/packages/neuron-ui/src/components/DataSetting/index.tsx b/packages/neuron-ui/src/components/DataSetting/index.tsx new file mode 100644 index 0000000000..eaf994a914 --- /dev/null +++ b/packages/neuron-ui/src/components/DataSetting/index.tsx @@ -0,0 +1,92 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Button from 'widgets/Button' +import ClearCache from 'components/ClearCache' +import { useDispatch } from 'states' +import { getCkbNodeDataPath, getIndexerDataPath, setCkbNodeDataPath, setIndexerDataPath } from 'services/remote' +import { ReactComponent as Attention } from 'widgets/Icons/ExperimentalAttention.svg' +import CopyZone from 'widgets/CopyZone' +import { OpenFolder } from 'widgets/Icons/icon' +import { shell } from 'electron' +import { useDataPath } from './hooks' + +import styles from './index.module.scss' + +const itemProps: Record< + 'ckbNode' | 'indexer', + { + type: 'ckb' | 'ckb-indexer' + getPath: typeof getCkbNodeDataPath | typeof getIndexerDataPath + setPath: typeof setCkbNodeDataPath | typeof setIndexerDataPath + titleI18nKey: string + tipI18nKey: string + } +> = { + ckbNode: { + type: 'ckb', + getPath: getCkbNodeDataPath, + setPath: setCkbNodeDataPath, + titleI18nKey: 'ckb-node-data', + tipI18nKey: 'remove-ckb-data-tip', + }, + indexer: { + type: 'ckb-indexer', + getPath: getIndexerDataPath, + setPath: setIndexerDataPath, + titleI18nKey: 'ckb-indexer-data', + tipI18nKey: 'remove-indexer-data-tip', + }, +} + +const SetItem = ({ type }: { type: keyof typeof itemProps }) => { + const props = itemProps[type] + const [t] = useTranslation() + const { onSetting, prevPath, currentPath, dialogRef, onCancel, onConfirm } = useDataPath( + props.getPath, + props.setPath, + props.type + ) + const openPath = useCallback(() => { + if (prevPath) { + shell.openPath(prevPath!) + } + }, [prevPath]) + return ( + <> +
{t(`settings.data.${props.titleI18nKey}`)}:
+
+ + {prevPath} + + +
+
+ + + ) +} + +const DataSetting = () => { + const dispatch = useDispatch() + return ( +
+ + + +
+ ) +} + +DataSetting.displayName = 'DataSetting' +export default DataSetting diff --git a/packages/neuron-ui/src/components/ErrorBoundary/index.tsx b/packages/neuron-ui/src/components/ErrorBoundary/index.tsx index 409a34f660..bed246d94c 100644 --- a/packages/neuron-ui/src/components/ErrorBoundary/index.tsx +++ b/packages/neuron-ui/src/components/ErrorBoundary/index.tsx @@ -5,7 +5,9 @@ import { handleViewError } from 'services/remote' const handleError = (error: Error) => { handleViewError(error.toString()) - window.location.reload() + if (process.env.NODE_ENV !== 'development') { + window.location.reload() + } return { hasError: true } } diff --git a/packages/neuron-ui/src/components/GeneralSetting/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/index.tsx index 133119882f..6b973b5834 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/index.tsx @@ -1,11 +1,9 @@ import React, { useCallback, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ProgressIndicator } from 'office-ui-fabric-react' -import ClearCache from 'components/ClearCache' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' import Dropdown from 'widgets/Dropdown' -import { StateDispatch } from 'states' import { checkForUpdates, downloadUpdate, installUpdate, setLocale, getVersion } from 'services/remote' import { CONSTANTS } from 'utils' @@ -72,10 +70,9 @@ const UpdateDownloadStatus = ({ progress = 0, newVersion = '', releaseNotes = '' interface GeneralSettingProps { updater: State.AppUpdater - dispatch: StateDispatch } -const GeneralSetting = ({ updater, dispatch }: GeneralSettingProps) => { +const GeneralSetting = ({ updater }: GeneralSettingProps) => { const [t, i18n] = useTranslation() const [lng, setLng] = useState(i18n.language) @@ -127,7 +124,6 @@ const GeneralSetting = ({ updater, dispatch }: GeneralSettingProps) => {
)} -
{t('settings.general.language')}
{ diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 8f2f3a889b..17c51a7193 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -312,17 +312,14 @@ "setting-tabs": { "general": "General", "wallets": "Wallets", - "network": "Network" + "network": "Network", + "data": "Data" }, "general": { - "clear-cache": "Clear Cache", - "clearing-cache": "Clearing...", - "clear-cache-description": "Clear cache if you encounter data sync or balance display problems. Neuron will rescan block data.", "show": "Show", "hide": "Hide", "version": "Version", "language": "Language", - "cache-cleared-on": "Cache cleared on {{date}}", "apply": "Apply" }, "wallet-manager": { @@ -372,6 +369,22 @@ "cancel": "Cancel", "ok": "OK" } + }, + "data": { + "ckb-node-data": "CKB Node Data", + "ckb-indexer-data": "CKB Indexer Data", + "set": "Set", + "cache": "Cache", + "clear-cache-description": "Clear cache if you encounter data sync or balance display problems. Neuron will rescan block data.", + "cache-cleared-on": "Cache cleared on {{date}}", + "clear-cache": "Clear Cache", + "clearing-cache": "Clearing...", + "remove-ckb-data-tip": "Please move CKB Node Data from {{prevPath}} to {{currentPath}} and then click on \"Data have been moved\" to start from the previously synchronized block; Or click on \"Synchronize from scratch\" to start with synchronizing from scratch.", + "remove-indexer-data-tip": "Please move Indexer Data from {{prevPath}} to {{currentPath}} and then click on \"Data have been moved\" to start from the previously synchronized block; Or click on \"Synchronize from scratch\" to start with synchronizing from scratch.", + "resync-describe": "Attention: Synchronize from scratch won't remove previously downloaded CKB Node(or Indexer) Data.", + "cancel": "Cancel", + "move-data-finish": "Data have been moved", + "re-sync": "Synchronize from scratch" } }, "password-request": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index c200a46f37..38fc1441ac 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -305,17 +305,14 @@ "setting-tabs": { "general": "通用", "wallets": "錢包", - "network": "網絡" + "network": "網絡", + "data": "數據" }, "general": { - "clear-cache": "清理緩存", - "clearing-cache": "清理中…", - "clear-cache-description": "當資料同步或餘額顯示出現問題時,可以清理緩存,Neuron 會重新同步所有塊數據。", "show": "顯示", "hide": "隱藏", "version": "版本", "language": "語言", - "cache-cleared-on": "上次清理緩存時間 {{date}}", "apply": "應用" }, "wallet-manager": { @@ -365,6 +362,22 @@ "cancel": "取消", "ok": "確認" } + }, + "data": { + "ckb-node-data": "節點數據", + "ckb-indexer-data": "Indexer 數據", + "set": "設置", + "cache": "緩存", + "clear-cache-description": "當資料同步或餘額顯示出現問題時,可以清理緩存,Neuron 會重新同步所有塊數據。", + "cache-cleared-on": "上次清理緩存時間 {{date}}", + "clear-cache": "清理緩存", + "clearing-cache": "清理中…", + "remove-ckb-data-tip": "請將 CKB Node 數據從 {{prevPath}} 移動至 {{currentPath}}然後點擊 \"數據轉移完成\" 開始從已知區塊同步; 否則點擊 \"重新同步\" 開始重新同步.", + "remove-indexer-data-tip": "請將 Indexer 數據從 {{prevPath}} 移動至 {{currentPath}}然後點擊 \"數據轉移完成\" 開始從已知區塊同步; 否則點擊 \"重新同步\" 開始重新同步.", + "cancel": "取消", + "resync-describe": "註意: 重新同步不會刪除當前已同步的 CKB Node(Indexer) 數據.", + "move-data-finish": "數據轉移完成", + "re-sync": "重新同步" } }, "password-request": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 050eed4f4f..e1446c1018 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -305,17 +305,14 @@ "setting-tabs": { "general": "通用", "wallets": "钱包", - "network": "网络" + "network": "网络", + "data": "数据" }, "general": { - "clear-cache": "清空缓存", - "clearing-cache": "清空中...", - "clear-cache-description": "当数据同步或余额显示出现问题时,可以清空缓存,Neuron 会重新同步所有块数据。", "show": "显示", "hide": "隐藏", "version": "版本", "language": "语言", - "cache-cleared-on": "上次清空缓存时间 {{date}}", "apply": "应用" }, "wallet-manager": { @@ -365,6 +362,22 @@ "cancel": "取消", "ok": "确认" } + }, + "data": { + "ckb-node-data": "节点数据", + "ckb-indexer-data": "Indexer 数据", + "set": "设置", + "cache": "缓存", + "clear-cache-description": "当数据同步或余额显示出现问题时,可以清空缓存,Neuron 会重新同步所有块数据。", + "cache-cleared-on": "上次清空缓存时间 {{date}}", + "clear-cache": "清空缓存", + "clearing-cache": "清空中...", + "remove-ckb-data-tip": "请将 CKB Node 数据从 {{prevPath}} 移动至 {{currentPath}}然后点击 \"数据转移完成\" 开始从已知区块同步; 否则点击 \"重新同步\" 开始重新同步.", + "remove-indexer-data-tip": "请将 Indexer 数据从 {{prevPath}} 移动至 {{currentPath}}然后点击 \"数据转移完成\" 开始从已知区块同步; 否则点击 \"重新同步\" 开始重新同步.", + "resync-describe": "注意: 重新同步不会删除当前已同步的 CKB Node(Indexer) 数据.", + "cancel": "取消", + "move-data-finish": "数据转移完成", + "re-sync": "重新同步" } }, "password-request": { diff --git a/packages/neuron-ui/src/services/remote/app.ts b/packages/neuron-ui/src/services/remote/app.ts index 6b0f8a050b..76340290cd 100644 --- a/packages/neuron-ui/src/services/remote/app.ts +++ b/packages/neuron-ui/src/services/remote/app.ts @@ -1,4 +1,12 @@ -import { OpenDialogOptions, MenuItemConstructorOptions, MenuItem, Size, OpenDialogReturnValue } from 'electron' +import { + OpenDialogOptions, + MenuItemConstructorOptions, + MenuItem, + Size, + OpenDialogReturnValue, + MessageBoxOptions, + MessageBoxReturnValue, +} from 'electron' import { LOCALES } from 'utils/const' import { remoteApi } from './remoteApiWrapper' @@ -9,6 +17,12 @@ export const requestOpenInExplorer = remoteApi('handle-view-error') export const showSettings = remoteApi('show-settings') export const setLocale = remoteApi('set-locale') +export const getCkbNodeDataPath = remoteApi('get-ckb-node-data-path') +export const setCkbNodeDataPath = remoteApi('set-ckb-node-data-path') +export const getIndexerDataPath = remoteApi('get-indexer-data-path') +export const setIndexerDataPath = remoteApi('set-indexer-data-path') +export const stopProcessMonitor = remoteApi<'ckb' | 'ckb-indexer'>('stop-process-monitor') +export const startProcessMonitor = remoteApi<'ckb' | 'ckb-indexer'>('start-process-monitor') export const clearCellCache = remoteApi('clear-cache') @@ -17,3 +31,4 @@ export const invokeShowOpenDialog = remoteApi('show-open-dialog-modal') export const invokeOpenContextMenu = remoteApi>('open-context-menu') export const invokeGetAllDisplaysSize = remoteApi('get-all-displays-size') +export const invokeShowMessageBox = remoteApi('show-message-box') diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index 864805a6d9..9895319787 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -42,6 +42,13 @@ type Action = | 'open-external' | 'open-context-menu' | 'get-all-displays-size' + | 'show-message-box' + | 'get-ckb-node-data-path' + | 'set-ckb-node-data-path' + | 'get-indexer-data-path' + | 'set-indexer-data-path' + | 'stop-process-monitor' + | 'start-process-monitor' // Wallets | 'get-all-wallets' | 'get-current-wallet' diff --git a/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx b/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx index f84b7b4236..a4ea98e16c 100644 --- a/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx +++ b/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx @@ -20,5 +20,5 @@ stories.addDecorator(withKnobs).add('With knobs', () => { const props = { ...initStates, } - return {}} /> + return }) diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts index 6ae91c2e34..e9ba2227f4 100644 --- a/packages/neuron-ui/src/utils/enums.ts +++ b/packages/neuron-ui/src/utils/enums.ts @@ -18,6 +18,7 @@ export enum RoutePath { SettingsGeneral = '/settings/general', SettingsWallets = '/settings/wallets', SettingsNetworks = '/settings/networks', + SettingsData = '/settings/data', CreateWallet = '/wallets/new', ImportWallet = '/wallets/import', ImportKeystore = '/keystore/import', diff --git a/packages/neuron-ui/src/widgets/Icons/OpenFolder.svg b/packages/neuron-ui/src/widgets/Icons/OpenFolder.svg new file mode 100644 index 0000000000..7634f4223f --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/OpenFolder.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/packages/neuron-ui/src/widgets/Icons/icon.tsx b/packages/neuron-ui/src/widgets/Icons/icon.tsx index 356fd75c23..5bae71d5ce 100644 --- a/packages/neuron-ui/src/widgets/Icons/icon.tsx +++ b/packages/neuron-ui/src/widgets/Icons/icon.tsx @@ -23,6 +23,8 @@ import { ReactComponent as KeystoreSvg } from './Keystore.svg' import { ReactComponent as SettingsSvg } from './Settings.svg' import { ReactComponent as PendingIconSvg } from './PendingIcon.svg' import { ReactComponent as NewTabSvg } from './new_tab.svg' +import { ReactComponent as TooltipSvg } from './Tooltip.svg' +import { ReactComponent as OpenFolderSvg } from './OpenFolder.svg' import styles from './icon.module.scss' @@ -60,3 +62,5 @@ export const Keystore = WrapSvg(KeystoreSvg) export const Settings = WrapSvg(SettingsSvg) export const PendingIcon = WrapSvg(PendingIconSvg) export const NewTab = WrapSvg(NewTabSvg) +export const Tooltip = WrapSvg(TooltipSvg) +export const OpenFolder = WrapSvg(OpenFolderSvg) diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index ef9d2022fd..4a02555d1a 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -53,6 +53,7 @@ import SUDTController from "controllers/sudt" import SyncedBlockNumber from 'models/synced-block-number' import IndexerService from 'services/indexer' import MultisigConfigModel from 'models/multisig-config' +import startMonitor, { stopMonitor } from 'services/monitor' export type Command = 'export-xpubkey' | 'import-xpubkey' | 'delete-wallet' | 'backup-wallet' | 'migrate-acp' // Handle channel messages from renderer process and user actions. @@ -453,6 +454,53 @@ export default class ApiController { await IndexerService.clearCache(params?.resetIndexerData) return { status: ResponseCode.Success, result: true } }) + + handle('get-ckb-node-data-path', () => { + return { + status: ResponseCode.Success, + result: SettingsService.getInstance().ckbDataPath + } + }) + + handle('set-ckb-node-data-path', (_, dataPath: string) => { + SettingsService.getInstance().ckbDataPath = dataPath + startMonitor('ckb', true) + return { + status: ResponseCode.Success, + result: SettingsService.getInstance().ckbDataPath + } + }) + + handle('get-indexer-data-path', () => { + return { + status: ResponseCode.Success, + result: SettingsService.getInstance().indexerDataPath + } + }) + + handle('set-indexer-data-path', (_, dataPath: string) => { + SettingsService.getInstance().indexerDataPath = dataPath + startMonitor('ckb-indexer', true) + return { + status: ResponseCode.Success, + result: SettingsService.getInstance().ckbDataPath + } + }) + + handle('start-process-monitor', (_, monitorName: string) => { + startMonitor(monitorName, true) + return { + status: ResponseCode.Success, + } + }) + + handle('stop-process-monitor', async (_, monitorName: string) => { + await stopMonitor(monitorName) + return { + status: ResponseCode.Success, + } + }) + // Sign and Verify handle('sign-message', async (_, params: Controller.Params.SignParams) => { return this.#signAndVerifyController.sign(params) diff --git a/packages/neuron-wallet/src/controllers/export-debug.ts b/packages/neuron-wallet/src/controllers/export-debug.ts index 26de6c516b..39ce8aa3d3 100644 --- a/packages/neuron-wallet/src/controllers/export-debug.ts +++ b/packages/neuron-wallet/src/controllers/export-debug.ts @@ -6,11 +6,11 @@ import CKB from '@nervosnetwork/ckb-sdk-core' import { app, dialog } from 'electron' import logger from 'utils/logger' import { t } from 'i18next' -import { ckbDataPath } from 'services/ckb-runner' import NetworksService from 'services/networks' import SyncedBlockNumber from 'models/synced-block-number' import AddressService from 'services/addresses' import redistCheck from 'utils/redist-check' +import SettingsService from 'services/settings' export default class ExportDebugController { // eslint-disable-next-line prettier/prettier @@ -101,7 +101,7 @@ export default class ExportDebugController { const SIZE_TO_READ = 32_000 return new Promise((resolve, reject) => { - const logPath = path.resolve(ckbDataPath(), 'data', 'logs', 'run.log') + const logPath = path.resolve(SettingsService.getInstance().ckbDataPath, 'data', 'logs', 'run.log') if (!fs.existsSync(logPath)) { return reject(new Error("File not found")) } const fileStats = fs.statSync(logPath) diff --git a/packages/neuron-wallet/src/controllers/mercury.ts b/packages/neuron-wallet/src/controllers/mercury.ts index 0862839802..930d6395ac 100644 --- a/packages/neuron-wallet/src/controllers/mercury.ts +++ b/packages/neuron-wallet/src/controllers/mercury.ts @@ -1,14 +1,14 @@ -import { ckbDataPath } from 'services/ckb-runner' import path from 'path' import fs from 'fs' import env from 'env' import { dialog } from 'electron' import { t } from 'i18next' +import SettingsService from 'services/settings' const { app } = env export const migrate = async () => { - const ckbPath = ckbDataPath() + const ckbPath = SettingsService.getInstance().ckbDataPath const lumosDataPath = path.resolve(app.getPath('userData'), app.isPackaged ? '' : 'dev', 'indexer_data') const ckbIndexerDataPath = path.resolve(app.getPath('userData'), app.isPackaged ? '' : 'dev', 'ckb-indexer') diff --git a/packages/neuron-wallet/src/services/ckb-runner.ts b/packages/neuron-wallet/src/services/ckb-runner.ts index 0716b94d55..a7eec6a131 100644 --- a/packages/neuron-wallet/src/services/ckb-runner.ts +++ b/packages/neuron-wallet/src/services/ckb-runner.ts @@ -4,6 +4,7 @@ import fs from 'fs' import { ChildProcess, spawn } from 'child_process' import process from 'process' import logger from 'utils/logger' +import SettingsService from './settings' const platform = (): string => { switch (process.platform) { @@ -30,19 +31,15 @@ const ckbBinary = (): string => { return platform() === 'win' ? binary + '.exe' : binary } -export const ckbDataPath = (): string => { - return path.resolve(app.getPath('userData'), 'chains/mainnet') -} - const initCkb = async () => { logger.info('CKB:\tInitializing node...') return new Promise((resolve, reject) => { - if (fs.existsSync(path.join(ckbDataPath(), 'ckb.toml'))) { + if (fs.existsSync(path.join(SettingsService.getInstance().ckbDataPath, 'ckb.toml'))) { logger.log('CKB:\tinit: config file detected, skip ckb init.') return resolve() } - const initCmd = spawn(ckbBinary(), ['init', '--chain', 'mainnet', '-C', ckbDataPath()]) + const initCmd = spawn(ckbBinary(), ['init', '--chain', 'mainnet', '-C', SettingsService.getInstance().ckbDataPath]) initCmd.stderr.on('data', data => { logger.error('CKB:\tinit fail:', data.toString()) }) @@ -71,7 +68,7 @@ export const startCkbNode = async () => { await initCkb() logger.info('CKB:\tstarting node...') - const options = ['run', '-C', ckbDataPath()] + const options = ['run', '-C', SettingsService.getInstance().ckbDataPath] if (app.isPackaged && process.env.CKB_NODE_ASSUME_VALID_TARGET) { options.push('--assume-valid-target', process.env.CKB_NODE_ASSUME_VALID_TARGET) } @@ -130,6 +127,6 @@ export const stopCkbNode = () => { */ export const clearCkbNodeCache = async () => { await stopCkbNode() - fs.rmSync(ckbDataPath(), { recursive: true, force: true }) + fs.rmSync(SettingsService.getInstance().ckbDataPath, { recursive: true, force: true }) await startCkbNode() } diff --git a/packages/neuron-wallet/src/services/indexer.ts b/packages/neuron-wallet/src/services/indexer.ts index 7b579c9c48..f2095a4661 100644 --- a/packages/neuron-wallet/src/services/indexer.ts +++ b/packages/neuron-wallet/src/services/indexer.ts @@ -12,6 +12,7 @@ import NetworksService from './networks' import CommonUtils from 'utils/common' import { resetSyncTask } from 'block-sync-renderer' import { clean as cleanChain } from 'database/chain' +import SettingsService from './settings' const platform = (): string => { switch (process.platform) { @@ -186,6 +187,11 @@ export default class IndexerService { #getDataPath = (network: Network): string => { - return path.resolve(env.fileBasePath, IndexerService.indexerDataFolder, 'data', `${network.genesisHash}`) + let indexerDataPath = SettingsService.getInstance().indexerDataPath + if (!indexerDataPath) { + indexerDataPath = path.resolve(env.fileBasePath, IndexerService.indexerDataFolder, 'data', `${network.genesisHash}`) + SettingsService.getInstance().indexerDataPath = indexerDataPath + } + return indexerDataPath } } diff --git a/packages/neuron-wallet/src/services/monitor/base.ts b/packages/neuron-wallet/src/services/monitor/base.ts index 47aa052f3b..5284c22e0b 100644 --- a/packages/neuron-wallet/src/services/monitor/base.ts +++ b/packages/neuron-wallet/src/services/monitor/base.ts @@ -1,11 +1,13 @@ import logger from 'utils/logger' -import { interval, timer, Subscription, race, from } from 'rxjs' +import { interval, timer, Subscription, race, from, Observable } from 'rxjs' import { map } from 'rxjs/operators' export default abstract class Monitor { - interval: Subscription | null = null + private interval: Observable | null = null - isReStarting: boolean = false + private subcription: Subscription | null = null + + private isReStarting: boolean = false name: string = '' @@ -13,27 +15,38 @@ export default abstract class Monitor { abstract restart(): Promise - startMonitor(intervalTime: number = 10000) { - this.interval = interval(intervalTime).subscribe(async () => { - if (this.isReStarting) { - return - } - const timeout = timer(intervalTime / 2).pipe(map(() => true)) - const isLiving = await race(timeout, from(this.isLiving())).toPromise() - if (!isLiving) { - logger.info(`Monitor: is restarting ${this.name} process`) - this.isReStarting = true - try { - await this.restart() - logger.info(`Monitor: Restarting ${this.name} process success`) - } finally { - this.isReStarting = false - } + abstract stop(): Promise + + monitor = async (intervalTime: number) => { + if (this.isReStarting) { + return + } + const timeout = timer(intervalTime / 2).pipe(map(() => true)) + const isLiving = await race(timeout, from(this.isLiving())).toPromise() + if (!isLiving) { + logger.info(`Monitor: is restarting ${this.name} process`) + this.isReStarting = true + try { + await this.restart() + logger.info(`Monitor: Restarting ${this.name} process success`) + } finally { + this.isReStarting = false } - }) + } + } + + startMonitor(intervalTime: number = 10000, startNow: boolean = false) { + this.interval = interval(intervalTime) + if (startNow) { + this.monitor(intervalTime) + } + if (!this.subcription?.closed) { + this.subcription?.unsubscribe() + } + this.subcription = this.interval.subscribe(async () => this.monitor(intervalTime)) } clearMonitor() { - this.interval?.unsubscribe() + this.subcription?.unsubscribe() } } diff --git a/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts index 669431583d..6c4b433cfc 100644 --- a/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts +++ b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts @@ -20,5 +20,9 @@ export default class CkbIndexerMonitor extends BaseMonitor { await resetSyncTask() } + async stop(): Promise { + await resetSyncTask(false) + } + name: string = 'ckb-indexer' } diff --git a/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts b/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts index 23bc6b7050..eb3fccee85 100644 --- a/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts +++ b/packages/neuron-wallet/src/services/monitor/ckb-monitor.ts @@ -1,3 +1,4 @@ +import { stopCkbNode } from 'services/ckb-runner' import NodeService from '../node' import BaseMonitor from './base' @@ -10,5 +11,9 @@ export default class CkbMonitor extends BaseMonitor { return NodeService.getInstance().startNode() } + async stop(): Promise { + await stopCkbNode() + } + name: string = 'ckb' } diff --git a/packages/neuron-wallet/src/services/monitor/index.ts b/packages/neuron-wallet/src/services/monitor/index.ts index f717e94980..89cd79621f 100644 --- a/packages/neuron-wallet/src/services/monitor/index.ts +++ b/packages/neuron-wallet/src/services/monitor/index.ts @@ -2,9 +2,26 @@ import Base from './base' import CkbIndexerMonitor from './ckb-indexer-monitor' import CkbMonitor from './ckb-monitor' -export default function startMonitor() { - const monitors = [new CkbIndexerMonitor(), new CkbMonitor()] - monitors.forEach((v: Base) => { - v.startMonitor() - }) +const monitors: Base[] = [] + +export default function startMonitor(name?: string, startNow?: boolean) { + if (!monitors.length) { + monitors.push(new CkbIndexerMonitor(), new CkbMonitor()) + } + monitors + .filter(v => !name || v.name === name) + .forEach((v: Base) => { + v.startMonitor(undefined, startNow) + }) +} + +export async function stopMonitor(name?: string) { + await Promise.all( + monitors + .filter(v => !name || v.name === name) + .map(v => { + v.clearMonitor() + return v.stop() + }) + ) } diff --git a/packages/neuron-wallet/src/services/settings.ts b/packages/neuron-wallet/src/services/settings.ts index 9ad562e9b8..2583f5fc0d 100644 --- a/packages/neuron-wallet/src/services/settings.ts +++ b/packages/neuron-wallet/src/services/settings.ts @@ -3,6 +3,9 @@ import env from 'env' import Store from 'models/store' import { changeLanguage } from 'locales/i18n' import { updateApplicationMenu } from 'controllers/app/menu' +import path from 'path' + +const { app } = env export const locales = ['zh', 'zh-TW', 'en', 'en-US'] as const export type Locale = typeof locales[number] @@ -31,8 +34,34 @@ export default class SettingsService extends Store { } } + get indexerDataPath() { + return this.readSync('indexerDataPath') + } + + set indexerDataPath(dataPath: string) { + this.writeSync('indexerDataPath', dataPath) + } + + get ckbDataPath() { + return this.readSync('ckbDataPath') + } + + set ckbDataPath(dataPath: string) { + this.writeSync('ckbDataPath', dataPath) + } + constructor() { - super('', 'settings.json', JSON.stringify({ locale: env.app.getLocale() })) + super( + '', + 'settings.json', + JSON.stringify({ + locale: app.getLocale(), + ckbDataPath: path.resolve(app.getPath('userData'), 'chains/mainnet') + }) + ) + if (!this.ckbDataPath) { + this.ckbDataPath = path.resolve(app.getPath('userData'), 'chains/mainnet') + } } private onLocaleChanged = (lng: Locale) => { diff --git a/packages/neuron-wallet/tests/block-sync-renderer/index/resetSyncTask.test.ts b/packages/neuron-wallet/tests/block-sync-renderer/index/resetSyncTask.test.ts index 9b90bd5a58..5639bd7cb6 100644 --- a/packages/neuron-wallet/tests/block-sync-renderer/index/resetSyncTask.test.ts +++ b/packages/neuron-wallet/tests/block-sync-renderer/index/resetSyncTask.test.ts @@ -1,8 +1,9 @@ describe(`Reset sync task`, () => { const stubbedmaintainAddressesIfNecessary = jest.fn() const stubbedSleep = jest.fn() + const stubbedTimeout = jest.fn().mockReturnValue(Promise.resolve(10)) jest.doMock('services/wallets', () => ({ getInstance: () => ({ maintainAddressesIfNecessary: stubbedmaintainAddressesIfNecessary }) })) - jest.doMock('utils/common', () => ({ sleep: stubbedSleep })) + jest.doMock('utils/common', () => ({ sleep: stubbedSleep, timeout: stubbedTimeout })) const blockSyncRenderer = require('block-sync-renderer') const spyCreateBlockSyncTask = jest.spyOn(blockSyncRenderer, 'createBlockSyncTask').mockImplementation(() => jest.fn()) diff --git a/packages/neuron-wallet/tests/block-sync-renderer/tx-status-listener.intg.test.ts b/packages/neuron-wallet/tests/block-sync-renderer/tx-status-listener.intg.test.ts index 7875379866..b2386e6f3b 100644 --- a/packages/neuron-wallet/tests/block-sync-renderer/tx-status-listener.intg.test.ts +++ b/packages/neuron-wallet/tests/block-sync-renderer/tx-status-listener.intg.test.ts @@ -39,7 +39,9 @@ jest.doMock('rxjs', () => { __esModule: true, interval: stubbedRxJsIntervalFn, ReplaySubject: jest.fn(), - Subject: jest.fn(), + Subject: jest.fn().mockReturnValue({ + pipe: jest.fn().mockReturnValue({ subscribe: jest.fn() }) + }), BehaviorSubject: jest.fn(), } }); diff --git a/packages/neuron-wallet/tests/controllers/asset-account.test.ts b/packages/neuron-wallet/tests/controllers/asset-account.test.ts index d9169d952f..b0d8a6027d 100644 --- a/packages/neuron-wallet/tests/controllers/asset-account.test.ts +++ b/packages/neuron-wallet/tests/controllers/asset-account.test.ts @@ -22,6 +22,14 @@ const resetMocks = () => { stubbedAssetAccountServiceDestoryAssetAccount.mockReset() } +jest.mock('../../src/services/settings', () => { + return { + getInstance() { + return {} + } + } +}) + describe('AssetAccountController', () => { let assetAccountController: any; let AssetAccountController: any; diff --git a/packages/neuron-wallet/tests/controllers/export-debug.test.ts b/packages/neuron-wallet/tests/controllers/export-debug.test.ts index 0b8a733e1c..7b361a65b7 100644 --- a/packages/neuron-wallet/tests/controllers/export-debug.test.ts +++ b/packages/neuron-wallet/tests/controllers/export-debug.test.ts @@ -33,7 +33,10 @@ jest.mock('../../src/services/addresses', () => { jest.mock('fs', () => { return { - createWriteStream: () => null + createWriteStream: () => null, + readFileSync: () => JSON.stringify({}), + writeFileSync: () => jest.fn(), + existsSync: () => jest.fn() } }) diff --git a/packages/neuron-wallet/tests/controllers/multisig.test.ts b/packages/neuron-wallet/tests/controllers/multisig.test.ts index 8aa8af7e96..5c56d98cba 100644 --- a/packages/neuron-wallet/tests/controllers/multisig.test.ts +++ b/packages/neuron-wallet/tests/controllers/multisig.test.ts @@ -63,7 +63,7 @@ const isMainnetMock = jest.fn().mockReturnValue(false) jest.mock('../../src/services/networks', () => ({ getInstance: () => ({ - isMainnet: isMainnetMock + isMainnet: () => isMainnetMock() }) })) diff --git a/packages/neuron-wallet/tests/services/ckb-runner.test.ts b/packages/neuron-wallet/tests/services/ckb-runner.test.ts index 0287219d0b..bf74450e83 100644 --- a/packages/neuron-wallet/tests/services/ckb-runner.test.ts +++ b/packages/neuron-wallet/tests/services/ckb-runner.test.ts @@ -55,6 +55,15 @@ jest.doMock('utils/logger', () => { jest.doMock('process', () => { return stubbedProcess }) + +const ckbDataPath = '/chains/mainnet' +jest.mock('../../src/services/settings', () => ({ + getInstance() { + return { + ckbDataPath + } + } +})) const { startCkbNode, stopCkbNode, getLookingValidTargetStatus } = require('../../src/services/ckb-runner') describe('ckb runner', () => { @@ -87,13 +96,13 @@ describe('ckb runner', () => { '--chain', 'mainnet', '-C', - expect.stringContaining(path.join('chains', 'mainnet')) + ckbDataPath ]) }) it('runs ckb binary', () => { expect(stubbedSpawn).toHaveBeenCalledWith( expect.stringContaining(path.join(platformPath, 'ckb')), - ['run', '-C', expect.stringContaining(path.join('chains', 'mainnet'))], + ['run', '-C', ckbDataPath], { stdio: ['ignore', 'pipe', 'pipe'] } ) }) @@ -116,13 +125,13 @@ describe('ckb runner', () => { '--chain', 'mainnet', '-C', - expect.stringContaining(path.join('chains', 'mainnet')) + ckbDataPath ]) }) it('runs ckb binary', () => { expect(stubbedSpawn).toHaveBeenCalledWith( expect.stringContaining(path.join(platformPath, 'ckb')), - ['run', '-C', expect.stringContaining(path.join('chains', 'mainnet'))], + ['run', '-C', ckbDataPath], { stdio: ['ignore', 'pipe', 'pipe'] } ) }) diff --git a/packages/neuron-wallet/tests/services/monitor.test.ts b/packages/neuron-wallet/tests/services/monitor.test.ts index af8ab977ca..96003a3a26 100644 --- a/packages/neuron-wallet/tests/services/monitor.test.ts +++ b/packages/neuron-wallet/tests/services/monitor.test.ts @@ -1,6 +1,7 @@ import Monitor from '../../src/services/monitor/base' import CkbMonitor from '../../src/services/monitor/ckb-monitor' import CkbIndexerMonitor from '../../src/services/monitor/ckb-indexer-monitor' +import startMonitor, { stopMonitor } from '../../src/services/monitor/index' const isDefaultCKBNeedRestartMock = jest.fn().mockResolvedValue(true) const startNodeMock = jest.fn() @@ -14,6 +15,11 @@ jest.mock('../../src/services/node', () => ({ } })) +const stopCkbNodeMock = jest.fn() +jest.mock('../../src/services/ckb-runner', () => ({ + stopCkbNode: () => stopCkbNodeMock() +})) + describe('ckb monitor', () => { const monitor = new CkbMonitor() it('is living', async () => { @@ -24,6 +30,10 @@ describe('ckb monitor', () => { await monitor.restart() expect(startNodeMock).toHaveBeenCalled() }) + it('stop', async () => { + await monitor.stop() + expect(stopCkbNodeMock).toHaveBeenCalled() + }) }) const rpcRequestMock = jest.fn() @@ -33,7 +43,7 @@ jest.mock('../../src/utils/rpc-request', () => ({ const resetSyncTaskMock = jest.fn() jest.mock('../../src/block-sync-renderer/index', () => ({ - resetSyncTask: () => resetSyncTaskMock() + resetSyncTask: (v: boolean) => resetSyncTaskMock(v) })) describe('ckb indexer monitor', () => { @@ -58,10 +68,15 @@ describe('ckb indexer monitor', () => { await monitor.restart() expect(resetSyncTaskMock).toHaveBeenCalled() }) + it('stop', async () => { + await monitor.stop() + expect(resetSyncTaskMock).toHaveBeenCalledWith(false) + }) }) const isLivingMock = jest.fn() const restartMock = jest.fn() +const stopMock = jest.fn() class MonitorTest extends Monitor { isLiving(): Promise { return isLivingMock() @@ -70,6 +85,10 @@ class MonitorTest extends Monitor { restart(): Promise { return restartMock() } + + stop(): Promise { + return stopMock() + } } function wait(times: number) { @@ -119,5 +138,56 @@ describe('base monitor', () => { expect(restartMock).toHaveBeenCalledTimes(1) monitor.clearMonitor() }) + it('start monitor with first', async () => { + isLivingMock.mockResolvedValue(true).mockResolvedValueOnce(false) + monitor.startMonitor(1000, true) + await wait(500) + expect(isLivingMock).toHaveBeenCalled() + expect(restartMock).toHaveBeenCalled() + monitor.clearMonitor() + }) + it('twice start monitor', async () => { + isLivingMock.mockReset() + isLivingMock.mockResolvedValue(true) + monitor.startMonitor(500) + monitor.startMonitor(500) + await wait(800) + expect(isLivingMock).toHaveBeenCalledTimes(1) + monitor.clearMonitor() + }) + }) +}) + +describe('monitor index', () => { + beforeEach(() => { + rpcRequestMock.mockReset() + stopCkbNodeMock.mockReset() + resetSyncTaskMock.mockReset() + }) + it('start ckb monitor', () => { + startMonitor('ckb', true) + expect(isDefaultCKBNeedRestartMock).toHaveBeenCalled() + stopMonitor('ckb') + }) + it('start indexer monitor', () => { + startMonitor('ckb-indexer', true) + expect(rpcRequestMock).toHaveBeenCalled() + stopMonitor('ckb-indexer') + }) + it('start and stop all', () => { + startMonitor(undefined, true) + expect(isDefaultCKBNeedRestartMock).toHaveBeenCalled() + expect(rpcRequestMock).toHaveBeenCalled() + stopMonitor() + expect(stopCkbNodeMock).toHaveBeenCalled() + expect(resetSyncTaskMock).toHaveBeenCalledWith(false) + }) + it('stop-ckb-monitor', () => { + stopMonitor('ckb') + expect(stopCkbNodeMock).toHaveBeenCalled() + }) + it('stop-ckb-indexer-monitor', () => { + stopMonitor('ckb-indexer') + expect(resetSyncTaskMock).toHaveBeenCalledWith(false) }) }) diff --git a/packages/neuron-wallet/tests/services/setting.test.ts b/packages/neuron-wallet/tests/services/setting.test.ts new file mode 100644 index 0000000000..4e3a48ff3c --- /dev/null +++ b/packages/neuron-wallet/tests/services/setting.test.ts @@ -0,0 +1,95 @@ +import SettingsService from '../../src/services/settings' + +const readSyncMock = jest.fn() +const writeSyncMock = jest.fn() +const resolveMock = jest.fn() +const getLocaleMock = jest.fn() +const getPathMock = jest.fn() +const updateApplicationMenuMock = jest.fn() + +function resetMock() { + readSyncMock.mockReset() + writeSyncMock.mockReset() + resolveMock.mockReset() + getLocaleMock.mockReset() + getPathMock.mockReset() + updateApplicationMenuMock.mockReset() +} + +jest.mock('../../src/models/store', () => { + function MockStore() {} + MockStore.prototype.readSync = (field: string) => readSyncMock(field) + MockStore.prototype.writeSync = (field: string, value: any) => writeSyncMock(field, value) + return MockStore +}) + +jest.mock('../../src/controllers/app/menu', () => ({ + updateApplicationMenu: () => updateApplicationMenuMock() +})) + +jest.mock('electron', () => ({ + BrowserWindow: { + getAllWindows: jest.fn().mockReturnValue([]) + } +})) + +jest.mock('path', () => ({ + resolve: () => resolveMock() +})) + +jest.mock('env', () => ({ + app: { + getLocale: () => getLocaleMock(), + getPath: () => getPathMock() + } +})) + +describe('SettingsService', () => { + beforeEach(() => { + resetMock() + }) + + it('constructor no ckbDataPath', () => { + const instance = SettingsService.getInstance() + readSyncMock.mockReturnValue('ckbDataPath') + expect(instance.ckbDataPath).toEqual('ckbDataPath') + }) + + describe('locale', () => { + it('get', () => { + SettingsService.getInstance().locale + expect(readSyncMock).toBeCalledWith('locale') + }) + it('set', () => { + SettingsService.getInstance().locale = 'zh' + expect(writeSyncMock).toBeCalledWith('locale', 'zh') + expect(updateApplicationMenuMock).toHaveBeenCalled() + }) + it('set exception', () => { + expect(() => SettingsService.getInstance().locale = 'zh11' as any).toThrow(new Error(`Locale zh11 not supported`)) + }) + }) + + + describe('indexer-path', () => { + it('get', () => { + SettingsService.getInstance().indexerDataPath + expect(readSyncMock).toBeCalledWith('indexerDataPath') + }) + it('set', () => { + SettingsService.getInstance().indexerDataPath = 'indexerDataPath' + expect(writeSyncMock).toBeCalledWith('indexerDataPath', 'indexerDataPath') + }) + }) + + describe('ckb-node-path', () => { + it('get', () => { + SettingsService.getInstance().ckbDataPath + expect(readSyncMock).toBeCalledWith('ckbDataPath') + }) + it('set', () => { + SettingsService.getInstance().ckbDataPath = 'ckbDataPath' + expect(writeSyncMock).toBeCalledWith('ckbDataPath', 'ckbDataPath') + }) + }) +}) From cd97173ec1b35546d16089b185bc6d45667592a5 Mon Sep 17 00:00:00 2001 From: Chen Yu Date: Mon, 4 Jul 2022 20:28:42 +0800 Subject: [PATCH 06/13] fix: fix link to nervos dao (#2436) --- packages/neuron-ui/src/components/NervosDAO/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/neuron-ui/src/components/NervosDAO/index.tsx b/packages/neuron-ui/src/components/NervosDAO/index.tsx index 40bdff1db3..635300d2d3 100644 --- a/packages/neuron-ui/src/components/NervosDAO/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAO/index.tsx @@ -31,8 +31,7 @@ import styles from './nervosDAO.module.scss' const { MIN_DEPOSIT_AMOUNT } = CONSTANTS -const DAO_DOCS_URL = - 'https://docs.nervos.org/docs/basics/guides/neuron#5-deposit-your-nervos-ckbyte-tokens-into-nervos-dao' +const DAO_DOCS_URL = 'https://docs.nervos.org/docs/basics/guides/crypto%20wallets/neuron/#deposit-ckb-into-nervos-dao' const NervosDAO = () => { const [focusedRecord, setFocusedRecord] = useState('') From 588125c9ac00c13cff9064937a6391089cf13bd4 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Tue, 5 Jul 2022 16:35:33 +0800 Subject: [PATCH 07/13] Fix duplicate address (#2438) --- .../src/block-sync-renderer/index.ts | 14 +++-- .../src/block-sync-renderer/sync/queue.ts | 9 ++- .../src/block-sync-renderer/task.ts | 2 +- ...1656930265386-RemoveDuplicateBlake160s.ts} | 4 +- .../src/database/chain/ormconfig.ts | 4 +- .../neuron-wallet/src/services/addresses.ts | 24 +++++--- .../neuron-wallet/src/services/indexer.ts | 6 +- .../services/monitor/ckb-indexer-monitor.ts | 6 +- .../neuron-wallet/src/services/wallets.ts | 14 ++++- packages/neuron-wallet/src/utils/queue.ts | 49 +++++++++++++++ .../index/switchNetwork.test.ts | 12 ++-- .../tests/block-sync-renderer/queue.test.ts | 2 +- .../tests/services/address.test.ts | 31 ++++++---- .../tests/services/monitor.test.ts | 16 ++--- .../tests/services/wallets.test.ts | 38 +++++++++--- .../neuron-wallet/tests/utils/queue.test.ts | 61 +++++++++++++++++++ 16 files changed, 232 insertions(+), 60 deletions(-) rename packages/neuron-wallet/src/database/chain/migrations/{1651133703535-RemoveDuplicateBlake160s.ts => 1656930265386-RemoveDuplicateBlake160s.ts} (89%) create mode 100644 packages/neuron-wallet/src/utils/queue.ts create mode 100644 packages/neuron-wallet/tests/utils/queue.test.ts diff --git a/packages/neuron-wallet/src/block-sync-renderer/index.ts b/packages/neuron-wallet/src/block-sync-renderer/index.ts index 3f4142439f..7c1effd206 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/index.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/index.ts @@ -14,6 +14,7 @@ import { LumosCellQuery, LumosCell } from './sync/indexer-connector' import { WorkerMessage, StartParams, QueryIndexerParams } from './task' import logger from 'utils/logger' import CommonUtils from 'utils/common' +import queueWrapper from 'utils/queue' import env from 'env' let network: Network | null @@ -65,6 +66,8 @@ export const resetSyncTask = async (startTask = true) => { } } +export const resetSyncTaskQueue = queueWrapper(resetSyncTask, 1, true) + export const switchToNetwork = async (newNetwork: Network, reconnected = false, shouldSync = true) => { if (!reconnected && network?.id === newNetwork.id && network?.genesisHash === newNetwork.genesisHash) { return @@ -78,7 +81,7 @@ export const switchToNetwork = async (newNetwork: Network, reconnected = false, logger.info('Network:\tswitched to:', network) } - await resetSyncTask(shouldSync) + await resetSyncTaskQueue.asyncPush(shouldSync) } export const queryIndexer = async (query: LumosCellQuery): Promise => { @@ -130,6 +133,9 @@ export const createBlockSyncTask = async () => { case 'cache-tip-block-updated': SyncApiController.emiter.emit('cache-tip-block-updated', message) break + case 'check-and-save-wallet-address': + WalletService.getInstance().checkAndGenerateAddress(message) + break case 'tx-db-changed': TxDbChangedSubject.getSubject().next(message) break @@ -139,7 +145,7 @@ export const createBlockSyncTask = async () => { case 'wallet-deleted': case 'address-created': case 'indexer-error': - resetSyncTask() + resetSyncTaskQueue.asyncPush(true) break default: break @@ -188,5 +194,5 @@ export const registerRequest = (c: ChildProcess, msg: Required) = }) }) -AddressCreatedSubject.getSubject().subscribe(() => resetSyncTask()) -WalletDeletedSubject.getSubject().subscribe(() => resetSyncTask()) +AddressCreatedSubject.getSubject().subscribe(() => resetSyncTaskQueue.asyncPush(true)) +WalletDeletedSubject.getSubject().subscribe(() => resetSyncTaskQueue.asyncPush(true)) diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts index 022ebf8352..462de283e1 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts @@ -1,6 +1,5 @@ import { queue, AsyncQueue } from 'async' import { TransactionPersistor } from 'services/tx' -import WalletService from 'services/wallets' import RpcService from 'services/rpc-service' import AssetAccountService from 'services/asset-account-service' import OutPoint from 'models/chain/out-point' @@ -193,10 +192,10 @@ export default class Queue { ) .map(addr => addr.walletId) ) - const walletService = WalletService.getInstance() - for (const walletId of walletIds) { - const wallet = walletService.get(walletId) - await wallet.checkAndGenerateAddresses() + if (process.send) { + process.send({ channel: 'check-and-save-wallet-address', message: [...walletIds] }) + } else { + throw new ShouldInChildProcess() } } diff --git a/packages/neuron-wallet/src/block-sync-renderer/task.ts b/packages/neuron-wallet/src/block-sync-renderer/task.ts index a8e23532a8..8a52a7764a 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/task.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/task.ts @@ -12,7 +12,7 @@ let syncQueue: SyncQueue | null export interface WorkerMessage { type: 'call' | 'response' | 'kill', id?: number, - channel: 'start' | 'queryIndexer' | 'unmount' | 'cache-tip-block-updated' | 'tx-db-changed' | 'wallet-deleted' | 'address-created' | 'indexer-error' + channel: 'start' | 'queryIndexer' | 'unmount' | 'cache-tip-block-updated' | 'tx-db-changed' | 'wallet-deleted' | 'address-created' | 'indexer-error' | 'check-and-save-wallet-address' message: T } diff --git a/packages/neuron-wallet/src/database/chain/migrations/1651133703535-RemoveDuplicateBlake160s.ts b/packages/neuron-wallet/src/database/chain/migrations/1656930265386-RemoveDuplicateBlake160s.ts similarity index 89% rename from packages/neuron-wallet/src/database/chain/migrations/1651133703535-RemoveDuplicateBlake160s.ts rename to packages/neuron-wallet/src/database/chain/migrations/1656930265386-RemoveDuplicateBlake160s.ts index bbaa621245..aab53964e7 100644 --- a/packages/neuron-wallet/src/database/chain/migrations/1651133703535-RemoveDuplicateBlake160s.ts +++ b/packages/neuron-wallet/src/database/chain/migrations/1656930265386-RemoveDuplicateBlake160s.ts @@ -1,8 +1,8 @@ import { In, MigrationInterface, QueryRunner } from "typeorm"; import HdPublicKeyInfo from "../entities/hd-public-key-info"; -export class RemoveDuplicateBlake160s1651133703535 implements MigrationInterface { - name = 'RemoveDuplicateBlake160s1651133703535' +export class RemoveDuplicateBlake160s1656930265386 implements MigrationInterface { + name = 'RemoveDuplicateBlake160s1656930265386' public async up(queryRunner: QueryRunner): Promise { const duplicateList = await queryRunner.connection diff --git a/packages/neuron-wallet/src/database/chain/ormconfig.ts b/packages/neuron-wallet/src/database/chain/ormconfig.ts index fd39a6f2c3..db85e17000 100644 --- a/packages/neuron-wallet/src/database/chain/ormconfig.ts +++ b/packages/neuron-wallet/src/database/chain/ormconfig.ts @@ -45,7 +45,7 @@ import { AddAddressDescription1602543179168 } from './migrations/1602543179168-A import { AddMultisigConfig1646817547204 } from './migrations/1646817547204-AddMultisigConfig' import { AddMultisigOutput1649729996969 } from './migrations/1649729996969-AddMultisigOutput' import { UpdateAddressDescription1650984779265 } from './migrations/1650984779265-UpdateAddressDescription' -import { RemoveDuplicateBlake160s1651133703535 } from './migrations/1651133703535-RemoveDuplicateBlake160s' +import { RemoveDuplicateBlake160s1656930265386 } from './migrations/1656930265386-RemoveDuplicateBlake160s' import { UpdateOutputChequeLockHash1652945662504 } from './migrations/1652945662504-UpdateOutputChequeLockHash' import { RemoveAddressesMultisigConfig1651820157100 } from './migrations/1651820157100-RemoveAddressesMultisigConfig' @@ -109,7 +109,7 @@ const connectOptions = async (genesisBlockHash: string): Promise v.walletId)) if (walletIds.size !== 1) { throw new Error('Addresses can only be created for one wallet at a time') @@ -69,7 +74,7 @@ export default class AddressService { ) const generatedAddresses: AddressInterface[] = [...addresses.receiving, ...addresses.change] - await this.create(generatedAddresses) + await AddressService.createQueue.asyncPush({ addresses: generatedAddresses }) return generatedAddresses } @@ -158,12 +163,17 @@ export default class AddressService { return generatedAddresses } - public static async generateAndSaveForPublicKey( - walletId: string, - publicKey: string, - addressType: AddressType, + private static async generateAndSaveForPublicKey({ + walletId, + publicKey, + addressType, + addressIndex + }: { + walletId: string + publicKey: string + addressType: AddressType addressIndex: number - ): Promise { + }): Promise { const isMainnet = NetworksService.getInstance().isMainnet() const address = publicKeyToAddress(publicKey, isMainnet) const publicKeyHash = AddressParser.toBlake160(address) diff --git a/packages/neuron-wallet/src/services/indexer.ts b/packages/neuron-wallet/src/services/indexer.ts index f2095a4661..4da269da95 100644 --- a/packages/neuron-wallet/src/services/indexer.ts +++ b/packages/neuron-wallet/src/services/indexer.ts @@ -10,7 +10,7 @@ import { Network } from 'models/network' import SyncedBlockNumber from 'models/synced-block-number' import NetworksService from './networks' import CommonUtils from 'utils/common' -import { resetSyncTask } from 'block-sync-renderer' +import { resetSyncTaskQueue } from 'block-sync-renderer' import { clean as cleanChain } from 'database/chain' import SettingsService from './settings' @@ -69,7 +69,7 @@ export default class IndexerService { } static clearCache = async (clearIndexerFolder = false) => { - await resetSyncTask(false) + await resetSyncTaskQueue.asyncPush(false) await cleanChain() if (clearIndexerFolder) { @@ -77,7 +77,7 @@ export default class IndexerService { await new SyncedBlockNumber().setNextBlock(BigInt(0)) } - await resetSyncTask(true) + await resetSyncTaskQueue.asyncPush(true) } static createFolder(dir: string) { diff --git a/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts index 6c4b433cfc..aef73165af 100644 --- a/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts +++ b/packages/neuron-wallet/src/services/monitor/ckb-indexer-monitor.ts @@ -1,4 +1,4 @@ -import { resetSyncTask } from 'block-sync-renderer/index' +import { resetSyncTaskQueue } from 'block-sync-renderer/index' import IndexerService from 'services/indexer' import { rpcRequest } from 'utils/rpc-request' import BaseMonitor from './base' @@ -17,11 +17,11 @@ export default class CkbIndexerMonitor extends BaseMonitor { } async restart(): Promise { - await resetSyncTask() + await resetSyncTaskQueue.asyncPush(true) } async stop(): Promise { - await resetSyncTask(false) + await resetSyncTaskQueue.asyncPush(false) } name: string = 'ckb-indexer' diff --git a/packages/neuron-wallet/src/services/wallets.ts b/packages/neuron-wallet/src/services/wallets.ts index 7449d1b2ac..1fa67d0362 100644 --- a/packages/neuron-wallet/src/services/wallets.ts +++ b/packages/neuron-wallet/src/services/wallets.ts @@ -223,7 +223,12 @@ export class HardwareWallet extends Wallet { public checkAndGenerateAddresses = async (): Promise => { const { addressType, addressIndex } = this.getDeviceInfo() const { publicKey } = AccountExtendedPublicKey.parse(this.extendedKey) - const address = await AddressService.generateAndSaveForPublicKey(this.id, publicKey, addressType, addressIndex) + const address = await AddressService.generateAndSaveForPublicKeyQueue.asyncPush({ + walletId: this.id, + publicKey, + addressType, + addressIndex + }) if (address) { return [address] @@ -336,6 +341,13 @@ export default class WalletService { await this.cleanupAddresses() } + public async checkAndGenerateAddress(walletIds: string[]) { + for (const walletId of new Set(walletIds)) { + const wallet = this.get(walletId) + await wallet.checkAndGenerateAddresses() + } + } + public create = (props: WalletProperties) => { if (!props) { throw new IsRequired('wallet property') diff --git a/packages/neuron-wallet/src/utils/queue.ts b/packages/neuron-wallet/src/utils/queue.ts new file mode 100644 index 0000000000..c0119f63ef --- /dev/null +++ b/packages/neuron-wallet/src/utils/queue.ts @@ -0,0 +1,49 @@ +import { AsyncQueue, AsyncResultIterator, queue } from 'async' + +export default function queueWrapper( + fn: (item: T) => Promise, + concurrency?: number, + ignoreSameItem?: boolean +): AsyncQueue & { asyncPush: (item: T) => Promise } { + const itemList: T[] = [] + const promiseList: Promise[] = [] + const queueFn: AsyncResultIterator = (item: T, callback: (err?: E | null, res?: R) => void) => { + try { + fn(item) + .then((v: R) => { + callback(null, v) + }) + .catch((err: E) => { + callback(err) + }) + } catch (error) { + callback(error) + } + } + const q = queue(queueFn, concurrency) + return Object.defineProperty(q, 'asyncPush', { + value: (item: T) => { + if (ignoreSameItem && itemList.length) { + if (item === itemList[itemList.length - 1] && q.length()) { + return promiseList[promiseList.length - 1] + } + } + itemList.push(item) + promiseList.push( + new Promise((resolve, reject) => { + q.push(item, (err, value) => { + itemList.shift() + promiseList.shift() + if (err) { + reject(err) + return + } + resolve(value) + }) + }) + ) + return promiseList[promiseList.length - 1] + }, + writable: false + }) as AsyncQueue & { asyncPush: (item: T) => Promise } +} diff --git a/packages/neuron-wallet/tests/block-sync-renderer/index/switchNetwork.test.ts b/packages/neuron-wallet/tests/block-sync-renderer/index/switchNetwork.test.ts index cd315ff9e7..7c0725d557 100644 --- a/packages/neuron-wallet/tests/block-sync-renderer/index/switchNetwork.test.ts +++ b/packages/neuron-wallet/tests/block-sync-renderer/index/switchNetwork.test.ts @@ -3,9 +3,13 @@ const { NetworkType } = require('models/network') describe(`switch to network`, () => { const STUB_NETWOKR = { id: '', name: '', remote: '', type: NetworkType.Normal, genesisHash: '0x', chain: 'ckb_dev' } const stubbedLoggerInfo = jest.fn() + const asyncPushMock = jest.fn() jest.doMock('utils/logger', () => ({ info: stubbedLoggerInfo })) + jest.doMock('utils/queue', () => ({ + __esModule: true, + default: jest.fn().mockReturnValue({ asyncPush: asyncPushMock }) + })) const blockSyncRenderer = require('block-sync-renderer') - const spyResetSyncTask = jest.spyOn(blockSyncRenderer, 'resetSyncTask').mockResolvedValue(0) afterEach(() => { stubbedLoggerInfo.mockClear() @@ -32,9 +36,9 @@ describe(`switch to network`, () => { expect(stubbedLoggerInfo).toHaveBeenCalled() }) - it(`should reset sync task`, () => { - blockSyncRenderer.switchToNetwork(STUB_NETWOKR, true) - expect(spyResetSyncTask).toHaveBeenCalledWith(true) + it(`should reset sync task`, async () => { + await blockSyncRenderer.switchToNetwork(STUB_NETWOKR, true) + expect(asyncPushMock).toHaveBeenCalledWith(true) }) }) diff --git a/packages/neuron-wallet/tests/block-sync-renderer/queue.test.ts b/packages/neuron-wallet/tests/block-sync-renderer/queue.test.ts index 1f8b6bb1e8..9076888921 100644 --- a/packages/neuron-wallet/tests/block-sync-renderer/queue.test.ts +++ b/packages/neuron-wallet/tests/block-sync-renderer/queue.test.ts @@ -220,7 +220,7 @@ describe('queue', () => { } }) it('checks and generate new addresses', () => { - expect(stubbedCheckAndGenerateAddressesFn).toHaveBeenCalledTimes(fakeTxs.length) + expect(stubbedProcessSend).toHaveBeenCalledWith({ channel: "check-and-save-wallet-address", message: [fakeWalletId]}) }) it('notify indexer connector of processed block number', () => { expect(stubbedNotifyCurrentBlockNumberProcessedFn).toHaveBeenCalledWith( diff --git a/packages/neuron-wallet/tests/services/address.test.ts b/packages/neuron-wallet/tests/services/address.test.ts index 1026bd800b..1ccbe876b0 100644 --- a/packages/neuron-wallet/tests/services/address.test.ts +++ b/packages/neuron-wallet/tests/services/address.test.ts @@ -317,10 +317,12 @@ describe('integration tests for AddressService', () => { const addressIndex = 0 beforeEach(async () => { await AddressService.generateAndSaveForPublicKey( - walletId, - publicKey, - addressType, - addressIndex + { + walletId, + publicKey, + addressType, + addressIndex + } ) generatedAddresses = await AddressService.getAddressesByWalletId(walletId) }); @@ -334,12 +336,13 @@ describe('integration tests for AddressService', () => { describe('when trying to generate for the same public key', () => { beforeEach(async () => { notifyAddressCreatedStub.mockReset() - await AddressService.generateAndSaveForPublicKey( + // @ts-ignore private-method + await AddressService.generateAndSaveForPublicKey({ walletId, publicKey, addressType, addressIndex - ) + }) generatedAddresses = await AddressService.getAddressesByWalletId(walletId) }); it('should not generate new address', () => { @@ -680,14 +683,16 @@ describe('integration tests for AddressService', () => { publicKeyInBlake160: addr.blake160 }) })) - await AddressService.create(receiving) + await AddressService.create({ addresses: receiving }) expect(stubbedAddressDbChangedSubjectNext).toHaveBeenCalledTimes(0) }) it('create with more than one wallet', async () => { - await expect(AddressService.create([ - { walletId: '1' }, - { walletId: '2' } - ])).rejects.toThrow(new Error('Addresses can only be created for one wallet at a time')) + await expect(AddressService.create({ + addresses: [ + { walletId: '1' }, + { walletId: '2' } + ] + })).rejects.toThrow(new Error('Addresses can only be created for one wallet at a time')) }) it('create with some exist', async () => { const { receiving, change } = await AddressService.generateAddresses( @@ -704,7 +709,9 @@ describe('integration tests for AddressService', () => { publicKeyInBlake160: addr.blake160 }) })) - await AddressService.create([...receiving, ...change]) + await AddressService.create({ + addresses: [...receiving, ...change] + }) expect(stubbedAddressDbChangedSubjectNext).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/neuron-wallet/tests/services/monitor.test.ts b/packages/neuron-wallet/tests/services/monitor.test.ts index 96003a3a26..f471f1f6bb 100644 --- a/packages/neuron-wallet/tests/services/monitor.test.ts +++ b/packages/neuron-wallet/tests/services/monitor.test.ts @@ -41,9 +41,11 @@ jest.mock('../../src/utils/rpc-request', () => ({ rpcRequest: () => rpcRequestMock() })) -const resetSyncTaskMock = jest.fn() +const asyncPushMock = jest.fn() jest.mock('../../src/block-sync-renderer/index', () => ({ - resetSyncTask: (v: boolean) => resetSyncTaskMock(v) + resetSyncTaskQueue: { + asyncPush: (v: boolean) => asyncPushMock(v) + } })) describe('ckb indexer monitor', () => { @@ -66,11 +68,11 @@ describe('ckb indexer monitor', () => { }) it('restart', async () => { await monitor.restart() - expect(resetSyncTaskMock).toHaveBeenCalled() + expect(asyncPushMock).toHaveBeenCalledWith(true) }) it('stop', async () => { await monitor.stop() - expect(resetSyncTaskMock).toHaveBeenCalledWith(false) + expect(asyncPushMock).toHaveBeenCalledWith(false) }) }) @@ -162,7 +164,7 @@ describe('monitor index', () => { beforeEach(() => { rpcRequestMock.mockReset() stopCkbNodeMock.mockReset() - resetSyncTaskMock.mockReset() + asyncPushMock.mockReset() }) it('start ckb monitor', () => { startMonitor('ckb', true) @@ -180,7 +182,7 @@ describe('monitor index', () => { expect(rpcRequestMock).toHaveBeenCalled() stopMonitor() expect(stopCkbNodeMock).toHaveBeenCalled() - expect(resetSyncTaskMock).toHaveBeenCalledWith(false) + expect(asyncPushMock).toHaveBeenCalledWith(false) }) it('stop-ckb-monitor', () => { stopMonitor('ckb') @@ -188,6 +190,6 @@ describe('monitor index', () => { }) it('stop-ckb-indexer-monitor', () => { stopMonitor('ckb-indexer') - expect(resetSyncTaskMock).toHaveBeenCalledWith(false) + expect(asyncPushMock).toHaveBeenCalledWith(false) }) }) diff --git a/packages/neuron-wallet/tests/services/wallets.test.ts b/packages/neuron-wallet/tests/services/wallets.test.ts index 774621e42b..4994d4ebb1 100644 --- a/packages/neuron-wallet/tests/services/wallets.test.ts +++ b/packages/neuron-wallet/tests/services/wallets.test.ts @@ -8,7 +8,7 @@ import { Manufacturer } from '../../src/services/hardware/common' const stubbedDeletedByWalletIdFn = jest.fn() const stubbedGenerateAndSaveForExtendedKeyFn = jest.fn() -const stubbedGenerateAndSaveForPublicKeyFn = jest.fn() +const stubbedGenerateAndSaveForPublicKeyQueueAsyncPush = jest.fn() const stubbedGetNextUnusedAddressByWalletIdFn = jest.fn() const stubbedGetNextUnusedChangeAddressByWalletIdFn = jest.fn() const stubbedGetUnusedReceivingAddressesByWalletIdFn = jest.fn() @@ -21,7 +21,9 @@ jest.doMock('../../src/services/addresses', () => { return { deleteByWalletId: stubbedDeleteByWalletId, generateAndSaveForExtendedKey: stubbedGenerateAndSaveForExtendedKeyFn, - generateAndSaveForPublicKey: stubbedGenerateAndSaveForPublicKeyFn, + generateAndSaveForPublicKeyQueue: { + asyncPush: stubbedGenerateAndSaveForPublicKeyQueueAsyncPush + }, getNextUnusedAddressByWalletId: stubbedGetNextUnusedAddressByWalletIdFn, getNextUnusedChangeAddressByWalletId: stubbedGetNextUnusedChangeAddressByWalletIdFn, getUnusedReceivingAddressesByWalletId: stubbedGetUnusedReceivingAddressesByWalletIdFn, @@ -36,7 +38,7 @@ import HdPublicKeyInfo from '../../src/database/chain/entities/hd-public-key-inf const resetMocks = () => { stubbedDeletedByWalletIdFn.mockReset() stubbedGenerateAndSaveForExtendedKeyFn.mockReset() - stubbedGenerateAndSaveForPublicKeyFn.mockReset() + stubbedGenerateAndSaveForPublicKeyQueueAsyncPush.mockReset() stubbedGetNextUnusedAddressByWalletIdFn.mockReset() stubbedGetNextUnusedChangeAddressByWalletIdFn.mockReset() stubbedGetUnusedReceivingAddressesByWalletIdFn.mockReset() @@ -274,12 +276,12 @@ describe('wallet service', () => { }) it('calls AddressService.generateAndSaveForExtendedKey', async () => { const { publicKey } = AccountExtendedPublicKey.parse(wallet4.extendedKey) - expect(stubbedGenerateAndSaveForPublicKeyFn).toHaveBeenCalledWith( - createdWallet.id, + expect(stubbedGenerateAndSaveForPublicKeyQueueAsyncPush).toHaveBeenCalledWith({ + walletId: createdWallet.id, publicKey, - 0, - 0 - ) + addressType: 0, + addressIndex: 0, + }) }) }); describe('#getNextAddressByWalletId', () => { @@ -462,4 +464,24 @@ describe('wallet service', () => { }) }); }); + + describe('checkAndGenerateAddress', () => { + const walletServiceMock = new WalletService() + const getMock = jest.spyOn(walletServiceMock, 'get') + const checkAndGenerateAddressesMock = jest.fn() + // @ts-ignore + getMock.mockReturnValue({ checkAndGenerateAddresses: () => checkAndGenerateAddressesMock() }) + + beforeEach(() => { + checkAndGenerateAddressesMock.mockReset() + }) + it('no duplicate walletId', async () => { + await walletServiceMock.checkAndGenerateAddress(['walletId1']) + expect(checkAndGenerateAddressesMock).toHaveBeenCalledTimes(1) + }) + it('with duplicate walletId', async () => { + await walletServiceMock.checkAndGenerateAddress(['walletId1', 'walletId1']) + expect(checkAndGenerateAddressesMock).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/packages/neuron-wallet/tests/utils/queue.test.ts b/packages/neuron-wallet/tests/utils/queue.test.ts new file mode 100644 index 0000000000..9b4bd34c05 --- /dev/null +++ b/packages/neuron-wallet/tests/utils/queue.test.ts @@ -0,0 +1,61 @@ +import queueWrapper from '../../src/utils/queue' + +const fnMock = jest.fn() +function createPromise(timer: number, result?: any, isReject: boolean = false) { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (isReject) { + reject(new Error(result)) + return + } + resolve(result | timer) + }, timer) + }) +} + +describe('queueWrapper', () => { + beforeEach(() => { + fnMock.mockReset() + }) + + it('asyncPush promise resolve', async () => { + const q = queueWrapper(fnMock) + fnMock.mockReturnValueOnce(createPromise(400)) + const res = await q.asyncPush(1) + expect(res).toBe(400) + }) + + it('asyncPush promise reject', async () => { + const q = queueWrapper(fnMock) + fnMock.mockReturnValueOnce(createPromise(400, 'reject error', true)) + await expect(q.asyncPush(1)).rejects.toThrow(new Error('reject error')) + }) + + it('asyncPush excute by order', async () => { + const q = queueWrapper(fnMock) + fnMock.mockReturnValueOnce(createPromise(400)) + .mockReturnValueOnce(createPromise(200)) + .mockReturnValueOnce(createPromise(100)) + q.asyncPush(1) + q.asyncPush(2) + q.asyncPush(3) + await q.drain() + expect(fnMock).toHaveBeenNthCalledWith(1, 1) + expect(fnMock).toHaveBeenNthCalledWith(2, 2) + expect(fnMock).toHaveBeenNthCalledWith(3, 3) + }) + + it('asyncPush excute by order and ignore same item', async () => { + const q = queueWrapper(fnMock, 1, true) + fnMock.mockReturnValueOnce(createPromise(400)) + .mockReturnValueOnce(createPromise(300)) + .mockReturnValueOnce(createPromise(300)) + q.asyncPush(1) + q.asyncPush(2) + q.asyncPush(2) + await q.drain() + expect(fnMock).toHaveBeenCalledTimes(2) + expect(fnMock).toHaveBeenNthCalledWith(1, 1) + expect(fnMock).toHaveBeenNthCalledWith(2, 2) + }) +}) From 1587ca20de6f0f53aa3d98fd6efea9e3cec56982 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Tue, 5 Jul 2022 23:17:02 +0800 Subject: [PATCH 08/13] fix: Adapt ledger nano s plus. (#2434) --- .../ImportHardware/detect-device.tsx | 12 +++++-- .../components/ImportHardware/name-wallet.tsx | 1 + .../ImportHardware/select-model.tsx | 35 +++++++++++++++---- packages/neuron-ui/src/locales/en.json | 3 +- packages/neuron-ui/src/locales/zh-tw.json | 3 +- packages/neuron-ui/src/locales/zh.json | 3 +- .../neuron-ui/src/services/remote/hardware.ts | 2 +- 7 files changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx index d78ea2c5e4..135441b078 100644 --- a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx @@ -44,7 +44,7 @@ const Info = ( ) } -const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch; model: Model }) => { +const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch; model: Model | null }) => { const [t] = useTranslation() const onBack = useCallback(() => { dispatch({ step: ImportStep.ImportHardware }) @@ -66,6 +66,14 @@ const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch diff --git a/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx b/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx index 354cf3dcf1..2dc89a5800 100644 --- a/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx @@ -44,6 +44,7 @@ const NameWallet = ({ const onInput = useCallback(e => { setWalletName(e.target.value) + setErrorMsg('') }, []) return ( diff --git a/packages/neuron-ui/src/components/ImportHardware/select-model.tsx b/packages/neuron-ui/src/components/ImportHardware/select-model.tsx index b714d0a878..1e5d053ec7 100644 --- a/packages/neuron-ui/src/components/ImportHardware/select-model.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/select-model.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' -import Select from 'widgets/Select' +import Select, { SelectOptions } from 'widgets/Select' import { Text } from 'office-ui-fabric-react' import { useHistory } from 'react-router-dom' import { useGoBack } from 'utils' @@ -9,6 +9,14 @@ import styles from './findDevice.module.scss' import { ActionType, Model, ImportStep } from './common' const supportedHardwareModels = [ + { + label: 'Ledger Nano S Plus', + value: 'Ledger Nano S Plus', + data: { + manufacturer: 'Ledger', + product: 'Nano S Plus', + }, + }, { label: 'Ledger Nano S', value: 'Ledger Nano S', @@ -25,6 +33,11 @@ const supportedHardwareModels = [ product: 'Nano X', }, }, + { + labelI18n: 'import-hardware.other-device', + value: 'Other Device', + data: null, + }, ] const SelectModel = ({ dispatch }: { dispatch: React.Dispatch }) => { @@ -42,16 +55,24 @@ const SelectModel = ({ dispatch }: { dispatch: React.Dispatch }) => const onDropDownChange = useCallback(({ data }) => { setModel(data) }, []) + const options = useMemo( + () => + supportedHardwareModels.map(v => + v.labelI18n + ? { + ...v, + label: t(v.labelI18n), + } + : v + ) as SelectOptions[], + [] + ) return (
{t('import-hardware.title.select-model')}
- {t('messages.experimental-message-hardware')}