diff --git a/packages/neuron-ui/src/components/CellManagement/hooks.ts b/packages/neuron-ui/src/components/CellManagement/hooks.ts index f0a746c5cb..1823cc98c0 100644 --- a/packages/neuron-ui/src/components/CellManagement/hooks.ts +++ b/packages/neuron-ui/src/components/CellManagement/hooks.ts @@ -398,7 +398,7 @@ export const usePassword = () => { } export const useHardWallet = ({ wallet, t }: { wallet: State.WalletIdentity; t: TFunction }) => { - const isWin32 = useMemo(() => { + const isWin32 = useMemo(() => { return getPlatform() === 'win32' }, []) const [error, setError] = useState() diff --git a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts index a1213dc389..fe2e933eec 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts +++ b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts @@ -71,7 +71,7 @@ export const useRepeatPassword = ({ password: string t: TFunction encryptedPassword?: string - onCancel: () => void + onCancel: (success: boolean) => void }) => { const dispatch = useDispatch() const [errMsg, setErrMsg] = useState('') @@ -89,7 +89,7 @@ export const useRepeatPassword = ({ updateLockWindowInfo( encryptedPassword ? { password: updatedRepeatPassword } : { password: updatedRepeatPassword, locked: true } )(dispatch) - onCancel() + onCancel(true) } } else { setErrMsg('') diff --git a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx index 629eda59f6..c9f0226413 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx @@ -13,7 +13,7 @@ const LockWindowDialog = ({ encryptedPassword, }: { show: boolean - onCancel: () => void + onCancel: (success?: boolean) => void encryptedPassword?: string }) => { const [t] = useTranslation() diff --git a/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss b/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss new file mode 100644 index 0000000000..1162951ae5 --- /dev/null +++ b/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss @@ -0,0 +1,23 @@ +@import '../../styles/mixin.scss'; + +.container { + width: 680px; + + .content { + max-width: 60%; + text-align: center; + margin: 0 auto; + font-size: 14px; + line-height: 24px; + color: var(--main-text-color); + + button { + border: none; + background: none; + color: var(--primary-color); + text-decoration: underline; + text-underline-offset: 4px; + cursor: pointer; + } + } +} diff --git a/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx b/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx new file mode 100644 index 0000000000..b67b984db5 --- /dev/null +++ b/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Dialog from 'widgets/Dialog' +import { openExternal } from 'services/remote' +import styles from './importFailureDialog.module.scss' + +const ImportFailureDialog = ({ show, onClose }: { show: boolean; onClose: () => void }) => { + const [t] = useTranslation() + + const onBtnClick = useCallback(() => { + openExternal( + 'https://github.com/nervosnetwork/ckb-cli/wiki/Import-ckb-cli-keystore-from%26to-Neuron-wallet#ckb-cli-and-neuron-use-the-keystore-in-different-way' + ) + }, []) + + return ( + +
+

+ {t('import-keystore.import-failure-msg')} + +

+
+
+ ) +} + +ImportFailureDialog.displayName = 'ImportFailureDialog' + +export default ImportFailureDialog diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index e3b71e74ac..3e7f211fd1 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -19,6 +19,7 @@ import ReplaceDuplicateWalletDialog, { useReplaceDuplicateWallet } from 'compone import { FinishCreateLoading, CreateFirstWalletNav } from 'components/WalletWizard' import TextField from 'widgets/TextField' import { importedWalletDialogShown } from 'services/localCache' +import ImportFailureDialog from '../ImportFailureDialog' import styles from './importKeystore.module.scss' const { MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } = CONSTANTS @@ -49,6 +50,7 @@ const ImportKeystore = () => { const navigate = useNavigate() const [fields, setFields] = useState(defaultFields) const [openingFile, setOpeningFile] = useState(false) + const [isImportFailureDialogShow, setIsImportFailureDialogShow] = useState(false) const { onImportingExitingWalletError, dialogProps } = useReplaceDuplicateWallet() const goBack = useGoBack() @@ -122,6 +124,11 @@ const ImportKeystore = () => { return } + if (res.status === ErrorCode.UnsupportedCkbCliKeystore) { + setIsImportFailureDialogShow(true) + return + } + if (res.message) { const msg = typeof res.message === 'string' ? res.message : res.message.content || '' if (msg) { @@ -140,7 +147,18 @@ const ImportKeystore = () => { closeDialog() }) }, - [fields.name, fields.password, fields.path, navigate, openDialog, closeDialog, disabled, setFields, t] + [ + fields.name, + fields.password, + fields.path, + navigate, + openDialog, + closeDialog, + disabled, + setFields, + t, + setIsImportFailureDialogShow, + ] ) const handleChange = useCallback( @@ -199,6 +217,10 @@ const ImportKeystore = () => { [setFields, wallets, t] ) + const onCloseImportFailureDialog = useCallback(() => { + setIsImportFailureDialogShow(false) + }, [setIsImportFailureDialogShow]) + return ( <>
@@ -248,6 +270,8 @@ const ImportKeystore = () => {
+ + ) } diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index d788190cf2..4f7dc2eb1a 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -492,10 +492,8 @@ const MultisigAddress = () => { confirmProps={{ type: 'cancel', className: styles.confirmBtn }} > Synchronization Abort -

Synchronization Abort

-

- Leaving the current window will cause the multisig synchronization to be aborted, so please confirm to leave. -

+

{t('multisig-address.synchronization-abort')}

+

{t('multisig-address.synchronization-abort-msg')}

{sendAction.sendFromMultisig && sendAction.isDialogOpen ? ( diff --git a/packages/neuron-ui/src/containers/LockWindow/index.tsx b/packages/neuron-ui/src/containers/LockWindow/index.tsx index 3afb787ed2..cc39bf31db 100644 --- a/packages/neuron-ui/src/containers/LockWindow/index.tsx +++ b/packages/neuron-ui/src/containers/LockWindow/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/media-has-caption */ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { AppActions, getLockWindowInfo, useDispatch, useState as useGlobalState } from 'states' import Spinner from 'widgets/Spinner' import Locked from 'widgets/Icons/Locked.png' @@ -8,10 +8,13 @@ import UnLockMp4 from 'widgets/Icons/unlock.mp4' import SplitPasswordInput from 'widgets/SplitPasswordInput' import { useTranslation } from 'react-i18next' import { clsx, isSuccessResponse } from 'utils' -import { isDark, unlockWindow } from 'services/remote' +import { isDark, signMessage, unlockWindow } from 'services/remote' import { retryUnlockWindow } from 'services/localCache' import { MILLISECS_PER_HOUR, MILLISECS_PER_MIN, MILLISECS_PER_SEC } from 'utils/getSyncLeftTime' +import { ControllerResponse } from 'services/remote/remoteApiWrapper' +import LockWindowDialog from 'components/GeneralSetting/LockWindowDialog' import styles from './lockWindow.module.scss' +import VerifyWallet from './verifyWallet' const passwordLen = 4 const wrongEnterTimes = 3 @@ -24,12 +27,6 @@ const formatterLockMillisecs = (lockMillisecs: number) => { const getWaitMillisecs = (retryTimes: number) => { if (retryTimes % wrongEnterTimes === 0) { - if (retryTimes >= 3 * wrongEnterTimes) { - return 24 * MILLISECS_PER_HOUR - } - if (retryTimes > wrongEnterTimes) { - return 30 * MILLISECS_PER_MIN - } return 5 * MILLISECS_PER_MIN } return undefined @@ -41,7 +38,7 @@ const LockWindow = ({ children }: { children: React.ReactNode }) => { useEffect(() => { getLockWindowInfo(dispatch) }, []) - const { app } = useGlobalState() + const { app, wallet } = useGlobalState() const [password, setPassword] = useState(new Array(passwordLen).fill('')) const [errMsg, setErrMsg] = useState('') const [retryUnlockInfo, setRetryUnlockInfo] = useState(retryUnlockWindow.get()) @@ -130,6 +127,25 @@ const LockWindow = ({ children }: { children: React.ReactNode }) => { } return () => clearInterval(interval) }, [retryUnlockInfo]) + const splitPasswordInputRef = useRef<{ focus: () => void } | null>(null) + const [isVerifyWalletDialogShow, setIsVerifyWalletDialogShow] = useState(false) + const [isResetPasswordDialogShow, setIsResetPasswordDialogShow] = useState(false) + const onVerifyWallet = useCallback( + async (walletPassword?: string) => { + const res: ControllerResponse = await signMessage({ + walletID: wallet?.id ?? '', + message: 'verify wallet for reset lock window password', + password: walletPassword ?? '', + }) + if (isSuccessResponse(res)) { + setIsVerifyWalletDialogShow(false) + setIsResetPasswordDialogShow(true) + } else { + throw new Error(typeof res.message === 'string' ? res.message : res.message.content) + } + }, + [setIsResetPasswordDialogShow, setIsVerifyWalletDialogShow, wallet] + ) if (!app.lockWindowInfo) { return (
@@ -156,11 +172,47 @@ const LockWindow = ({ children }: { children: React.ReactNode }) => { disabled={retryUnlockInfo.retryTimes % wrongEnterTimes === 0 && !!retryUnlockInfo.lastRetryTime} values={password} onChange={onUpdatePassword} + ref={splitPasswordInputRef} />
{errMsg || t('lock-window.enter-lock-password')} + {wallet.isWatchOnly ? null : ( + + )}
+ { + setIsVerifyWalletDialogShow(false) + setTimeout(() => { + // wait for dialog close + splitPasswordInputRef.current?.focus() + }, 10) + }} + onConfirm={onVerifyWallet} + /> + { + setIsResetPasswordDialogShow(false) + if (success) { + setPassword(new Array(passwordLen).fill('')) + } + setTimeout(() => { + // wait for dialog close + splitPasswordInputRef.current?.focus() + }, 10) + }} + /> ) } diff --git a/packages/neuron-ui/src/containers/LockWindow/lockWindow.module.scss b/packages/neuron-ui/src/containers/LockWindow/lockWindow.module.scss index a35391d16b..f6eb32bddb 100644 --- a/packages/neuron-ui/src/containers/LockWindow/lockWindow.module.scss +++ b/packages/neuron-ui/src/containers/LockWindow/lockWindow.module.scss @@ -20,6 +20,13 @@ &[data-has-err='true'] { color: var(--error-color); } + + & > button { + border: none; + color: var(--primary-color); + background-color: transparent; + cursor: pointer; + } } .passwordContainer { @@ -53,3 +60,12 @@ height: 88px; } } + +.verifyWallet { + min-width: 600px; + + .hardwalletErr { + justify-content: center; + margin-top: 12px; + } +} diff --git a/packages/neuron-ui/src/containers/LockWindow/verifyWallet.tsx b/packages/neuron-ui/src/containers/LockWindow/verifyWallet.tsx new file mode 100644 index 0000000000..f5822e8535 --- /dev/null +++ b/packages/neuron-ui/src/containers/LockWindow/verifyWallet.tsx @@ -0,0 +1,121 @@ +import React, { useCallback, useEffect, useState } from 'react' +import Dialog from 'widgets/Dialog' +import Hardware from 'widgets/Icons/Hardware.png' +import Button from 'widgets/Button' +import { useHardWallet, usePassword } from 'components/CellManagement/hooks' +import Alert from 'widgets/Alert' +import TextField from 'widgets/TextField' +import { useTranslation } from 'react-i18next' +import styles from './lockWindow.module.scss' + +const VerifyWallet = ({ + wallet, + show, + onCancel, + onConfirm, +}: { + wallet: State.Wallet + show: boolean + onCancel: () => void + onConfirm: (password?: string) => Promise +}) => { + const [t] = useTranslation() + const [loading, setLoading] = useState(false) + const { + isReconnecting, + isNotAvailable, + reconnect, + verifyDeviceStatus, + errorMessage: hardwalletError, + setError: setHardwalletError, + } = useHardWallet({ + wallet, + t, + }) + const { password, error, onPasswordChange, setError, resetPassword } = usePassword() + useEffect(() => { + if (show) { + resetPassword() + } + }, [show, resetPassword]) + useEffect(() => { + if (show && wallet.device) { + verifyDeviceStatus() + } + }, [show, wallet.device, verifyDeviceStatus]) + const onConfirmWrapper = useCallback(() => { + setLoading(true) + onConfirm(wallet.device ? undefined : password) + .catch(err => { + if (wallet.device) { + setHardwalletError(err.message) + } else { + setError(err.message) + } + }) + .finally(() => { + setLoading(false) + }) + }, [wallet.device, onConfirm, setLoading, setHardwalletError, setError, password]) + if (wallet.device) { + return ( + +
+ hard-wallet +
+
+ +
+ {hardwalletError ? ( + + {hardwalletError} + + ) : null} +
+ ) + } + return ( + + + + ) +} + +VerifyWallet.displayName = 'VerifyWallet' + +export default VerifyWallet diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index e37c643389..65b3341a08 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -202,6 +202,8 @@ "import-keystore": { "title": "Import Keystore File", "select-file": "Choose File", + "import-failure": "Import Failure", + "import-failure-msg": "Neuron does not support importing ckb-cli's keystore file due to different keystore file derivation rules.", "label": { "path": "Keystore File", "name": "Wallet Name", @@ -1137,6 +1139,8 @@ "export": "Export Tx" }, "multi-details": "Multisig address details", + "synchronization-abort": "Synchronization Abort", + "synchronization-abort-msg": "Leaving the current window will cause the multisig synchronization to be aborted, so please confirm to leave.", "create-dialog": { "title": "Create multisig address", "preview-title": "Multisig address has been generated", @@ -1301,8 +1305,10 @@ "lock-window": { "neuron-is-locked": "Neuron's window has been locked", "enter-lock-password": "Enter lock password", + "forget-password": "Forget password?", "lock-password-error": "Lock windows password error", - "failed-times": "Failed more than {{frequency}} times, please retry after {{time}}" + "failed-times": "Failed more than {{frequency}} times, please retry after {{time}}", + "verify-wallet": "Verify" } } } diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index 292de516e2..b8561c404f 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -194,6 +194,8 @@ "import-keystore": { "title": "Importar Keystore", "select-file": "Seleccionar Archivo", + "import-failure": "Error de importación", + "import-failure-msg": "Neuron no soporta la importación del archivo keystore de ckb-cli debido a diferentes reglas de derivación del archivo keystore.", "label": { "path": "Archivo de Claves", "name": "Nombre de la Billetera", @@ -1120,6 +1122,8 @@ "export": "Exportar Tx" }, "multi-details": "Detalles de la dirección multifirma", + "synchronization-abort": "Abortar sincronización", + "synchronization-abort-msg": "Salir de la ventana actual provocará que se aborte la sincronización de multisig, por favor confirme si desea salir.", "create-dialog": { "title": "Crear dirección multifirma", "preview-title": "La dirección multifirma se ha generado", @@ -1281,8 +1285,10 @@ "lock-window": { "neuron-is-locked": "La ventana de Neuron está bloqueada", "enter-lock-password": "Ingresar contraseña de bloqueo de pantalla", + "forget-password": "Olvidé mi contraseña?", "lock-password-error": "Contraseña de bloqueo de pantalla incorrecta", - "failed-times": "Fallo más de {{frequency}} veces, por favor inténtalo de nuevo después de {{time}}" + "failed-times": "Fallo más de {{frequency}} veces, por favor inténtalo de nuevo después de {{time}}", + "verify-wallet": "Verificar" } } } diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 680b411a03..985df1b7c9 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -201,6 +201,8 @@ "import-keystore": { "title": "Importer le fichier Keystore", "select-file": "Choisir le fichier", + "import-failure": "Échec de l'importation", + "import-failure-msg": "Neuron ne prend pas en charge l'importation du fichier Keystore de ckb-cli en raison de différentes règles de dérivation du fichier Keystore.", "label": { "path": "Fichier Keystore", "name": "Nom du Wallet", @@ -1127,6 +1129,8 @@ "export": "Exporter Tx" }, "multi-details": "Détails de l'adresse multisig", + "synchronization-abort": "Abandon de la synchronisation", + "synchronization-abort-msg": "Quitter la fenêtre actuelle entraînera l'abandon de la synchronisation multisig, veuillez confirmer si vous souhaitez quitter.", "create-dialog": { "title": "Créer une adresse multisig", "preview-title": "L'adresse multisig a été générée", @@ -1291,8 +1295,10 @@ "lock-window": { "neuron-is-locked": "a fenêtre de Neuron est verrouillée", "enter-lock-password": "Entrer le mot de passe de verrouillage d'écran", + "forget-password": "Mot de passe oublié?", "lock-password-error": "Mot de passe de verrouillage d'écran incorrect", - "failed-times": "Échec plus de {{frequency}} fois, veuillez réessayer après {{time}}" + "failed-times": "Échec plus de {{frequency}} fois, veuillez réessayer après {{time}}", + "verify-wallet": "Vérifier" } } } diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 899bfc41e1..d12dd36228 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -196,6 +196,8 @@ "import-keystore": { "title": "導入 Keystore 文件", "select-file": "選擇文件", + "import-failure": "導入失敗", + "import-failure-msg": "由於不同的 Keystore 文件衍生規則,Neuron 不支持導入 ckb-cli 的 Keystore 文件。", "label": { "path": "Keystore 檔案", "name": "錢包名稱", @@ -1130,6 +1132,8 @@ "export": "導出交易" }, "multi-details": "多簽地址詳情", + "synchronization-abort": "同步中止", + "synchronization-abort-msg": "離開當前窗口將導致多簽同步中止,請確認是否離開。", "create-dialog": { "title": "創建多重簽名地址", "preview-title": "簽名地址已生成", @@ -1290,8 +1294,10 @@ "lock-window": { "neuron-is-locked": "Neuron 窗口已鎖定", "enter-lock-password": "輸入鎖屏密碼", + "forget-password": "忘記密碼?", "lock-password-error": "鎖屏密碼錯誤", - "failed-times": "失敗超過 {{frequency}} 次, 請在 {{time}} 後重試" + "failed-times": "失敗超過 {{frequency}} 次, 請在 {{time}} 後重試", + "verify-wallet": "驗證" } } } diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 28d1944f35..0d5bba3a30 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -195,6 +195,8 @@ "import-keystore": { "title": "导入 Keystore 文件", "select-file": "选择文件", + "import-failure": "导入失败", + "import-failure-msg": "由于不同的 Keystore 文件派生规则,Neuron 不支持导入 ckb-cli 的 Keystore 文件。", "label": { "path": "Keystore 文件", "name": "钱包名称", @@ -1129,6 +1131,8 @@ "export": "导出交易" }, "multi-details": "多签地址详情", + "synchronization-abort": "同步中止", + "synchronization-abort-msg": "离开当前窗口将导致多签同步中止,请确认是否离开。", "create-dialog": { "title": "创建多重签名地址", "preview-title": "多签地址已生成", @@ -1293,8 +1297,10 @@ "lock-window": { "neuron-is-locked": "Neuron 窗口已锁定", "enter-lock-password": "输入锁屏密码", + "forget-password": "忘记密码?", "lock-password-error": "锁屏密码错误", - "failed-times": "失败超过 {{frequency}} 次, 请在 {{time}} 后重试" + "failed-times": "失败超过 {{frequency}} 次, 请在 {{time}} 后重试", + "verify-wallet": "验证" } } } diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 3f3d1a1940..83d7cab827 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -167,7 +167,7 @@ declare namespace Controller { interface SignMessageParams { walletID: string - address: string + address?: string password: string message: string } diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts index 747c8398a5..23c0b96b6f 100644 --- a/packages/neuron-ui/src/utils/enums.ts +++ b/packages/neuron-ui/src/utils/enums.ts @@ -112,6 +112,7 @@ export enum ErrorCode { // active warning WaitForFullySynced = 600, DuplicateImportWallet = 118, + UnsupportedCkbCliKeystore = 119, } export enum SyncStatus { diff --git a/packages/neuron-ui/src/widgets/Dialog/index.tsx b/packages/neuron-ui/src/widgets/Dialog/index.tsx index 607bad0d91..7a0056c25e 100644 --- a/packages/neuron-ui/src/widgets/Dialog/index.tsx +++ b/packages/neuron-ui/src/widgets/Dialog/index.tsx @@ -77,7 +77,13 @@ const Dialog = ({ (e.key === 'Escape' && enableCloseWithEsc ? onCancel : undefined)} + onKeyDown={e => { + if (e.key === 'Escape' && enableCloseWithEsc) { + onCancel?.() + } else if (e.key === 'Enter' && showFooter && showConfirm) { + handleConfirm(e) + } + }} role="none" > {showHeader ? ( diff --git a/packages/neuron-ui/src/widgets/SplitPasswordInput/index.tsx b/packages/neuron-ui/src/widgets/SplitPasswordInput/index.tsx index 71a1e56c76..64637d2a1f 100644 --- a/packages/neuron-ui/src/widgets/SplitPasswordInput/index.tsx +++ b/packages/neuron-ui/src/widgets/SplitPasswordInput/index.tsx @@ -1,65 +1,96 @@ -import React, { useCallback, useEffect, useRef } from 'react' +import React, { + type ForwardedRef, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' import styles from './splitPasswordInput.module.scss' -const SplitPasswordInput = ({ - values, - inputCount, - onChange, - disabled, -}: { - values: (string | undefined)[] - inputCount?: number - onChange: (value: string, idx: number) => void - disabled?: boolean -}) => { - const ref = useRef(null) - const onChangeInput = useCallback>( - e => { - const { dataset, value } = e.currentTarget - if (value.length > 1 || !dataset.idx) return - onChange(value, +dataset.idx) - if (ref.current && value.length > 0) { - const nextInput = ref.current.querySelector(`input:nth-child(${+dataset.idx + 2})`) as HTMLInputElement - nextInput?.focus() - } +const SplitPasswordInput = forwardRef( + ( + { + values, + inputCount, + onChange, + disabled, + }: { + values: (string | undefined)[] + inputCount?: number + onChange: (value: string, idx: number) => void + disabled?: boolean }, - [ref, values] - ) - const onKeyDown = useCallback>( - e => { - const { dataset, value } = e.currentTarget - if (e.key === 'Backspace' && dataset.idx && +dataset.idx > 0 && ref.current && value.length === 0) { - const lastInput = ref.current.querySelector(`input:nth-child(${+dataset.idx})`) as HTMLInputElement - lastInput?.focus() - onChange('', +dataset.idx - 1) + handlerRef: ForwardedRef<{ focus: () => void }> + ) => { + const ref = useRef(null) + const [focusIdx, setFocusIdx] = useState() + useImperativeHandle( + handlerRef, + () => { + return { + focus() { + if (ref.current && focusIdx !== undefined) { + const currentInput = ref.current.querySelector(`input:nth-child(${focusIdx})`) as HTMLInputElement + currentInput?.focus() + } + }, + } + }, + [focusIdx, ref] + ) + const onChangeInput = useCallback>( + e => { + const { dataset, value } = e.currentTarget + if (value.length > 1 || !dataset.idx) return + onChange(value, +dataset.idx) + if (ref.current && value.length > 0) { + const nextInput = ref.current.querySelector(`input:nth-child(${+dataset.idx + 2})`) as HTMLInputElement + nextInput?.focus() + setFocusIdx(+dataset.idx + 2) + } + }, + [ref, values] + ) + const onKeyDown = useCallback>( + e => { + const { dataset, value } = e.currentTarget + if (e.key === 'Backspace' && dataset.idx && +dataset.idx > 0 && ref.current && value.length === 0) { + const lastInput = ref.current.querySelector(`input:nth-child(${+dataset.idx})`) as HTMLInputElement + lastInput?.focus() + onChange('', +dataset.idx - 1) + setFocusIdx(+dataset.idx) + } + }, + [ref, values, setFocusIdx] + ) + useEffect(() => { + if (ref.current && values.join('').length === 0) { + const firstInput = ref.current.querySelector(`input:nth-child(1)`) as HTMLInputElement + firstInput?.focus() + setFocusIdx(1) } - }, - [ref, values] - ) - useEffect(() => { - if (ref.current && values.join('').length === 0) { - const firstInput = ref.current.querySelector(`input:nth-child(1)`) as HTMLInputElement - firstInput?.focus() - } - }, [values]) - return ( -
- {Array.from({ length: inputCount ?? values.length }).map((_, idx) => ( - - ))} -
- ) -} + }, [values, setFocusIdx]) + return ( +
+ {Array.from({ length: inputCount ?? values.length }).map((_, idx) => ( + + ))} +
+ ) + } +) SplitPasswordInput.displayName = 'SplitPasswordInput' export default SplitPasswordInput diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 911baa4d96..7f5b8b6aed 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -65,6 +65,7 @@ import CellManagement from './cell-management' import { UpdateCellLocalInfo } from '../database/chain/entities/cell-local-info' import { CKBLightRunner } from '../services/light-runner' import { OutPoint } from '@ckb-lumos/base' +import { updateApplicationMenu } from './app/menu' export type Command = 'export-xpubkey' | 'import-xpubkey' | 'delete-wallet' | 'backup-wallet' // Handle channel messages from renderer process and user actions. @@ -317,6 +318,7 @@ export default class ApiController { handle('update-lock-window-info', async (_, params: { locked?: boolean; password?: string }) => { SettingsService.getInstance().updateLockWindowInfo(params) + updateApplicationMenu(BrowserWindow.getFocusedWindow()) return { status: ResponseCode.Success, result: SettingsService.getInstance().lockWindowInfo, diff --git a/packages/neuron-wallet/src/controllers/cell-management.ts b/packages/neuron-wallet/src/controllers/cell-management.ts index 4e0ee8ce20..9bf158a65f 100644 --- a/packages/neuron-wallet/src/controllers/cell-management.ts +++ b/packages/neuron-wallet/src/controllers/cell-management.ts @@ -69,7 +69,12 @@ export default class CellManagement { const addresses = new Set((await AddressService.getAddressesByWalletId(currentWallet.id)).map(v => v.address)) const isMainnet = NetworksService.getInstance().isMainnet() if (!lockScripts.every(v => addresses.has(scriptToAddress(v, isMainnet)))) throw new AddressNotFound() - await SignMessage.sign(currentWallet.id, scriptToAddress(lockScripts[0], isMainnet), password, 'verify cell owner') + await SignMessage.sign({ + walletID: currentWallet.id, + password, + message: 'verify cell owner', + address: scriptToAddress(lockScripts[0], isMainnet), + }) return CellLocalInfoService.updateLiveCellLockStatus(outPoints, locked) } diff --git a/packages/neuron-wallet/src/controllers/sign-message.ts b/packages/neuron-wallet/src/controllers/sign-message.ts index a81916b7f6..cd2d9d0233 100644 --- a/packages/neuron-wallet/src/controllers/sign-message.ts +++ b/packages/neuron-wallet/src/controllers/sign-message.ts @@ -4,12 +4,10 @@ import { ResponseCode } from '../utils/const' export default class SignMessageController { public async sign(params: Controller.Params.SignParams): Promise> { - const signature: string = await SignMessage.sign( - params.walletID, - params.address.trim(), - params.password, - params.message - ) + const signature: string = await SignMessage.sign({ + ...params, + address: params.address?.trim(), + }) if (!signature) { throw new ServiceHasNoResponse('Sign') } diff --git a/packages/neuron-wallet/src/controllers/wallets.ts b/packages/neuron-wallet/src/controllers/wallets.ts index db66030bfa..98566937b4 100644 --- a/packages/neuron-wallet/src/controllers/wallets.ts +++ b/packages/neuron-wallet/src/controllers/wallets.ts @@ -22,6 +22,7 @@ import { UsedName, MainnetAddressRequired, TestnetAddressRequired, + UnsupportedCkbCliKeystore, } from '../exceptions' import AddressService from '../services/addresses' import TransactionSender from '../services/transaction-sender' @@ -166,6 +167,21 @@ export default class WalletsController { throw new InvalidJSON() } const keystoreObject = Keystore.fromJson(keystore) + + if (keystoreObject.isFromCkbCli()) { + throw new UnsupportedCkbCliKeystore() + } + + try { + keystoreObject.extendedPrivateKey(password) + } catch (error) { + if (error.message === 'Incorrect password!') { + throw new IncorrectPassword() + } else { + throw error + } + } + const masterPrivateKey = keystoreObject.extendedPrivateKey(password) const masterKeychain = new Keychain( Buffer.from(bytes.bytify(masterPrivateKey.privateKey)), diff --git a/packages/neuron-wallet/src/exceptions/wallet.ts b/packages/neuron-wallet/src/exceptions/wallet.ts index 25962d77a3..82af7d255f 100644 --- a/packages/neuron-wallet/src/exceptions/wallet.ts +++ b/packages/neuron-wallet/src/exceptions/wallet.ts @@ -85,6 +85,13 @@ export class DuplicateImportWallet extends Error { } } +export class UnsupportedCkbCliKeystore extends Error { + public code = 119 + constructor() { + super(t('messages.unsupported-ckb-cli-keystore')) + } +} + export default { WalletNotFound, CurrentWalletNotSet, @@ -96,4 +103,5 @@ export default { CapacityNotEnoughForChange, InvalidKeystore, DuplicateImportWallet, + UnsupportedCkbCliKeystore, } diff --git a/packages/neuron-wallet/src/locales/en.ts b/packages/neuron-wallet/src/locales/en.ts index 5c87164364..6a7dad241b 100644 --- a/packages/neuron-wallet/src/locales/en.ts +++ b/packages/neuron-wallet/src/locales/en.ts @@ -122,6 +122,7 @@ export default { 'connect-device-failed': 'The device cannot be connected, please check your connection.', 'unsupported-manufacturer': 'Devices from {{manufacturer}} are not yet supported.', 'wallet-not-supported-function': 'This wallet does not support {name} function.', + 'unsupported-ckb-cli-keystore': "Neuron does not support importing ckb-cli's keystore file.", 'invalid-transaction-file': 'Invalid transaction file.', 'offline-sign-failed': 'Signing failed, please check if you are signing with the correct wallet.', 'multisig-script-prefix-error': 'The multisig config is error', diff --git a/packages/neuron-wallet/src/locales/es.ts b/packages/neuron-wallet/src/locales/es.ts index 60134165fd..001e571013 100644 --- a/packages/neuron-wallet/src/locales/es.ts +++ b/packages/neuron-wallet/src/locales/es.ts @@ -121,6 +121,8 @@ export default { 'connect-device-failed': 'No se puede conectar el dispositivo, por favor, verifica tu conexión.', 'unsupported-manufacturer': 'Los dispositivos de {{manufacturer}} aún no son compatibles.', 'wallet-not-supported-function': 'Esta billetera no admite la función {name}.', + 'unsupported-ckb-cli-keystore': + 'Neuron no soporta la importación del archivo de almacenamiento de claves de ckb-cli.', 'invalid-transaction-file': 'Archivo de transacción no válido.', 'offline-sign-failed': 'Firma fallida, por favor, verifica si estás firmando con la billetera correcta.', 'multisig-script-prefix-error': 'Error en la configuración multifirma', diff --git a/packages/neuron-wallet/src/locales/fr.ts b/packages/neuron-wallet/src/locales/fr.ts index 12c9cf30a9..bcc1a30316 100644 --- a/packages/neuron-wallet/src/locales/fr.ts +++ b/packages/neuron-wallet/src/locales/fr.ts @@ -124,6 +124,8 @@ export default { 'connect-device-failed': "L'appareil ne peut pas être connecté, veuillez vérifier votre connexion.", 'unsupported-manufacturer': 'Les appareils de {{manufacturer}} ne sont pas encore pris en charge.', 'wallet-not-supported-function': 'Ce Wallet ne prend pas en charge la fonction {name}.', + 'unsupported-ckb-cli-keystore': + "Neuron ne prend pas en charge l'importation du fichier de stockage de clés de ckb-cli.", 'invalid-transaction-file': 'Fichier de transaction non valide.', 'offline-sign-failed': 'Échec de la signature, veuillez vérifier si vous signez avec le bon Wallet.', 'multisig-script-prefix-error': 'La configuration multisig est erronée', diff --git a/packages/neuron-wallet/src/services/sign-message.ts b/packages/neuron-wallet/src/services/sign-message.ts index aeaf32dca7..31882eba15 100644 --- a/packages/neuron-wallet/src/services/sign-message.ts +++ b/packages/neuron-wallet/src/services/sign-message.ts @@ -13,10 +13,20 @@ export default class SignMessage { private static ec = new EC('secp256k1') private static magicString = 'Nervos Message:' - public static async sign(walletID: string, address: string, password: string, message: string): Promise { + public static async sign({ + walletID, + password, + message, + address, + }: { + walletID: string + password: string + message: string + address?: string + }): Promise { const wallet = WalletService.getInstance().get(walletID) const addresses = await AddressService.getAddressesByWalletId(walletID) - let addr = addresses.find(addr => addr.address === address) + let addr = address ? addresses.find(addr => addr.address === address) : addresses[0] if (!addr) { throw new AddressNotFound() } diff --git a/packages/neuron-wallet/src/types/controller.d.ts b/packages/neuron-wallet/src/types/controller.d.ts index a59ea9fa0a..09ec01d15d 100644 --- a/packages/neuron-wallet/src/types/controller.d.ts +++ b/packages/neuron-wallet/src/types/controller.d.ts @@ -65,7 +65,7 @@ declare namespace Controller { interface SignParams { walletID: string - address: string + address?: string password: string message: string } diff --git a/packages/neuron-wallet/tests/controllers/cell-management.test.ts b/packages/neuron-wallet/tests/controllers/cell-management.test.ts index eb5a357e29..c41d19326f 100644 --- a/packages/neuron-wallet/tests/controllers/cell-management.test.ts +++ b/packages/neuron-wallet/tests/controllers/cell-management.test.ts @@ -57,8 +57,7 @@ jest.mock('../../src/services/addresses', () => ({ })) jest.mock('../../src/services/sign-message', () => ({ - sign: (walletID: string, address: string, password: string, message: string) => - signMock(walletID, address, password, message), + sign: (params: { walletID: string; address?: string; password: string; message: string }) => signMock(params), })) jest.mock('../../src/services/cell-local-info', () => ({ @@ -155,7 +154,12 @@ describe('CellManage', () => { getAddressesByWalletIdMock.mockResolvedValueOnce([{ address }]) const outPoints = [new OutPoint(`0x${'00'.repeat(32)}`, '0')] await CellManagement.updateLiveCellsLockStatus(outPoints, true, [lockScript], 'password') - expect(signMock).toBeCalledWith('walletId1', address, 'password', 'verify cell owner') + expect(signMock).toBeCalledWith({ + walletID: 'walletId1', + password: 'password', + message: 'verify cell owner', + address, + }) expect(updateLiveCellLockStatusMock).toBeCalledWith(outPoints, true) }) }) diff --git a/packages/neuron-wallet/tests/services/sign-message.test.ts b/packages/neuron-wallet/tests/services/sign-message.test.ts index 50bc48c7e9..d093abab06 100644 --- a/packages/neuron-wallet/tests/services/sign-message.test.ts +++ b/packages/neuron-wallet/tests/services/sign-message.test.ts @@ -78,14 +78,24 @@ describe(`SignMessage`, () => { it('not match wallet address', async () => { getAddressesByWalletIdMock.mockReturnValueOnce([]) await expect( - SignMessage.sign('walletId', signInfo.address, extendedKeyInfo.password, signInfo.message) + SignMessage.sign({ + walletID: 'walletId', + password: extendedKeyInfo.password, + message: signInfo.message, + address: signInfo.address, + }) ).rejects.toThrow(new AddressNotFound()) }) it('with generate', async () => { getAddressesByWalletIdMock.mockReturnValueOnce([{ address: signInfo.address }]) getPrivateKeyMock.mockReturnValueOnce(signInfo.privateKey) - const res = await SignMessage.sign('walletId', signInfo.address, extendedKeyInfo.password, signInfo.message) + const res = await SignMessage.sign({ + walletID: 'walletId', + password: extendedKeyInfo.password, + message: signInfo.message, + address: signInfo.address, + }) expect(res).toEqual(signInfo.signature) }) @@ -94,7 +104,12 @@ describe(`SignMessage`, () => { walletMock.mockReturnValueOnce({ isHardware: () => true }) const signMessage = jest.fn() hardWalletMock.mockReturnValueOnce({ signMessage }) - await SignMessage.sign('walletId', signInfo.address, extendedKeyInfo.password, signInfo.message) + await SignMessage.sign({ + walletID: 'walletId', + password: extendedKeyInfo.password, + message: signInfo.message, + address: signInfo.address, + }) expect(signMessage).toHaveBeenCalled() }) })