From 27f2a370cd0d99a99d2bf635624f283c9a420d66 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:26:07 +0900 Subject: [PATCH 1/8] Update ckb client versions (#3027) feat: update ckb client versions Co-authored-by: Keith-CY --- .ckb-light-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ckb-light-version b/.ckb-light-version index f54b243046..03f2afa1b4 100644 --- a/.ckb-light-version +++ b/.ckb-light-version @@ -1 +1 @@ -v0.3.4 +v0.3.5 From 7fe4b4d1610cc34fbdf2af3810d450d0fba2d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Wed, 17 Jan 2024 02:15:56 +0000 Subject: [PATCH 2/8] fix: Remove walletId for `get-multisig-config` API (#3010) --- .../src/components/MultisigAddress/hooks.ts | 4 ++-- .../src/components/MultisigAddress/index.tsx | 9 ++++++--- .../neuron-ui/src/services/remote/multisig.ts | 2 +- packages/neuron-wallet/src/controllers/api.ts | 4 ++-- .../neuron-wallet/src/controllers/multisig.ts | 4 ++-- packages/neuron-wallet/src/services/multisig.ts | 16 ++-------------- .../neuron-wallet/src/services/sync-progress.ts | 4 ++-- .../tests/controllers/multisig.test.ts | 4 ++-- .../tests/services/multisig.test.ts | 13 +++++-------- 9 files changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts index c2e0613d4b..a672164efd 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts +++ b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts @@ -73,12 +73,12 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM [walletId, setEntities] ) useEffect(() => { - getMultisigConfig(walletId).then(res => { + getMultisigConfig().then(res => { if (isSuccessResponse(res) && res.result) { setEntities(res.result) } }) - }, [setEntities, walletId]) + }, [setEntities]) const updateConfig = useCallback( (id: number) => (e: React.SyntheticEvent) => { const { value } = e.target as HTMLInputElement diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index fdfd91c33a..d6d38e702f 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -28,10 +28,12 @@ import { ReactComponent as Edit } from 'widgets/Icons/Edit.svg' import { Download, Search } from 'widgets/Icons/icon' import { HIDE_BALANCE, NetworkType } from 'utils/const' import { onEnter } from 'utils/inputDevice' +import getMultisigSignStatus from 'utils/getMultisigSignStatus' import { useSearch, useConfigManage, useExportConfig, useActions, useSubscription } from './hooks' import styles from './multisigAddress.module.scss' +const ApproveKey = 'approve' const tableActions = [ { key: 'info', @@ -46,7 +48,7 @@ const tableActions = [ icon: , }, { - key: 'approve', + key: ApproveKey, icon: , }, ] @@ -56,7 +58,7 @@ const MultisigAddress = () => { useOnLocaleChange(i18n) useExitOnWalletChange() const { - wallet: { id: walletId }, + wallet: { id: walletId, addresses }, chain: { networkID }, settings: { networks = [] }, } = useGlobalState() @@ -281,6 +283,7 @@ const MultisigAddress = () => { dataIndex: 'action', align: 'left', render(_, __, item) { + const { canSign } = getMultisigSignStatus({ multisigConfig: item, addresses }) return (
{ key={key} data-key={key} onClick={onClickItem(item)} - disabled={disabled} + disabled={key === ApproveKey ? !canSign || disabled : disabled} > {icon} {t(label)} diff --git a/packages/neuron-ui/src/services/remote/multisig.ts b/packages/neuron-ui/src/services/remote/multisig.ts index 79332443d4..5b5ce1579a 100644 --- a/packages/neuron-ui/src/services/remote/multisig.ts +++ b/packages/neuron-ui/src/services/remote/multisig.ts @@ -29,7 +29,7 @@ export type MultisigConfig = MultisigEntity & { } export const saveMultisigConfig = remoteApi, MultisigEntity>('save-multisig-config') -export const getMultisigConfig = remoteApi('get-multisig-config') +export const getMultisigConfig = remoteApi('get-multisig-config') export const importMultisigConfig = remoteApi('import-multisig-config') export const exportMultisigConfig = remoteApi('export-multisig-config') export const updateMultisigConfig = remoteApi, 'id'>, MultisigEntity>( diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 0d92bb8294..a9aa17f3a9 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -796,8 +796,8 @@ export default class ApiController { return this.#multisigController.deleteConfig(params) }) - handle('get-multisig-config', async (_, walletId: string) => { - return this.#multisigController.getConfig(walletId) + handle('get-multisig-config', async () => { + return this.#multisigController.getConfig() }) handle('import-multisig-config', async (_, walletId: string) => { diff --git a/packages/neuron-wallet/src/controllers/multisig.ts b/packages/neuron-wallet/src/controllers/multisig.ts index f77f4b52fc..ea41a69bb1 100644 --- a/packages/neuron-wallet/src/controllers/multisig.ts +++ b/packages/neuron-wallet/src/controllers/multisig.ts @@ -79,8 +79,8 @@ export default class MultisigController { } } - async getConfig(walletId: string) { - const result = await this.#multisigService.getMultisigConfig(walletId) + async getConfig() { + const result = await this.#multisigService.getMultisigConfig() return { status: ResponseCode.Success, result, diff --git a/packages/neuron-wallet/src/services/multisig.ts b/packages/neuron-wallet/src/services/multisig.ts index d26b678b17..9c28e92bba 100644 --- a/packages/neuron-wallet/src/services/multisig.ts +++ b/packages/neuron-wallet/src/services/multisig.ts @@ -11,7 +11,6 @@ import NetworksService from './networks' import Multisig from '../models/multisig' import SyncProgress, { SyncAddressType } from '../database/chain/entities/sync-progress' import { NetworkType } from '../models/network' -import WalletService from './wallets' import logger from '../utils/logger' const max64Int = '0x' + 'f'.repeat(16) @@ -21,7 +20,6 @@ export default class MultisigService { .getRepository(MultisigConfig) .createQueryBuilder() .where({ - walletId: multisigConfig.walletId, r: multisigConfig.r, m: multisigConfig.m, n: multisigConfig.n, @@ -69,13 +67,10 @@ export default class MultisigService { return { ...result, ...params } } - async getMultisigConfig(walletId: string) { + async getMultisigConfig() { const result = await getConnection() .getRepository(MultisigConfig) .createQueryBuilder() - .where({ - walletId, - }) .orderBy('id', 'DESC') .getMany() return result @@ -340,14 +335,7 @@ export default class MultisigService { } static async getMultisigConfigForLight() { - const currentWallet = WalletService.getInstance().getCurrent() - const multisigConfigs = await getConnection() - .getRepository(MultisigConfig) - .createQueryBuilder() - .where({ - walletId: currentWallet?.id, - }) - .getMany() + const multisigConfigs = await getConnection().getRepository(MultisigConfig).createQueryBuilder().getMany() return multisigConfigs.map(v => ({ walletId: v.walletId, script: Multisig.getMultisigScript(v.blake160s, v.r, v.m, v.n), diff --git a/packages/neuron-wallet/src/services/sync-progress.ts b/packages/neuron-wallet/src/services/sync-progress.ts index 2d42843bab..7bef4e3fb3 100644 --- a/packages/neuron-wallet/src/services/sync-progress.ts +++ b/packages/neuron-wallet/src/services/sync-progress.ts @@ -27,13 +27,13 @@ export default class SyncProgressService { .createQueryBuilder() .update(SyncProgress) .set({ delete: true }) - .where({ walletId: Not(In(existWalletIds)) }) + .where({ walletId: Not(In(existWalletIds)), addressType: SyncAddressType.Default }) .execute() await getConnection() .createQueryBuilder() .update(SyncProgress) .set({ delete: false }) - .where({ walletId: In(existWalletIds) }) + .where({ walletId: In(existWalletIds), addressType: SyncAddressType.Default }) .execute() } diff --git a/packages/neuron-wallet/tests/controllers/multisig.test.ts b/packages/neuron-wallet/tests/controllers/multisig.test.ts index cd8eedc507..f502c461d5 100644 --- a/packages/neuron-wallet/tests/controllers/multisig.test.ts +++ b/packages/neuron-wallet/tests/controllers/multisig.test.ts @@ -160,8 +160,8 @@ describe('test for multisig controller', () => { }) it('get config', async () => { - await multisigController.getConfig('abcd') - expect(MultiSigServiceMock.prototype.getMultisigConfig).toHaveBeenCalledWith('abcd') + await multisigController.getConfig() + expect(MultiSigServiceMock.prototype.getMultisigConfig).toHaveBeenCalledWith() }) describe('import config', () => { diff --git a/packages/neuron-wallet/tests/services/multisig.test.ts b/packages/neuron-wallet/tests/services/multisig.test.ts index 67c459c820..a661e72b56 100644 --- a/packages/neuron-wallet/tests/services/multisig.test.ts +++ b/packages/neuron-wallet/tests/services/multisig.test.ts @@ -87,8 +87,10 @@ describe('multisig service', () => { await expect(multisigService.saveMultisigConfig(defaultMultisigConfig)).rejects.toThrow() }) it('save success', async () => { - defaultMultisigConfig.walletId = 'walletId1' - const res = await multisigService.saveMultisigConfig(defaultMultisigConfig) + const anotherConfig = MultisigConfig.fromModel(multisigConfigModel) + anotherConfig.lastestBlockNumber = '0x0' + anotherConfig.r = 2 + const res = await multisigService.saveMultisigConfig(anotherConfig) const count = await getConnection() .getRepository(MultisigConfig) .createQueryBuilder() @@ -97,7 +99,6 @@ describe('multisig service', () => { }) .getCount() expect(count).toBe(1) - defaultMultisigConfig.walletId = 'walletId' }) }) @@ -127,12 +128,8 @@ describe('multisig service', () => { }) describe('test get config', () => { - it('no config', async () => { - const configs = await multisigService.getMultisigConfig('noconfigwallet') - expect(configs).toHaveLength(0) - }) it('has config wallet', async () => { - const configs = await multisigService.getMultisigConfig(multisigConfigModel.walletId) + const configs = await multisigService.getMultisigConfig() expect(configs).toHaveLength(1) }) }) From 9a875f24155d65a41e1d76b984c963c5ccd93dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Wed, 17 Jan 2024 04:17:19 +0000 Subject: [PATCH 3/8] fix: Disable unlock when there is no ckb. (#3020) --- .../src/components/NervosDAO/index.tsx | 1 + .../NervosDAORecord/daoRecordRow.module.scss | 5 +++ .../src/components/NervosDAORecord/index.tsx | 41 +++++++++++++++---- packages/neuron-ui/src/locales/en.json | 3 +- packages/neuron-ui/src/locales/fr.json | 3 +- packages/neuron-ui/src/locales/zh-tw.json | 3 +- packages/neuron-ui/src/locales/zh.json | 3 +- .../src/controllers/app/index.ts | 2 + 8 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/neuron-ui/src/components/NervosDAO/index.tsx b/packages/neuron-ui/src/components/NervosDAO/index.tsx index 0246117032..24fcef51da 100644 --- a/packages/neuron-ui/src/components/NervosDAO/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAO/index.tsx @@ -179,6 +179,7 @@ const NervosDAO = () => { genesisBlockTimestamp, connectionStatus, isPrivacyMode, + hasCkbBalance: +wallet.balance > 0, } return })} diff --git a/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss b/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss index a8ae72a98c..c533d52c3f 100644 --- a/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss +++ b/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss @@ -249,5 +249,10 @@ line-height: 56px; padding: 0; } + + .tip { + width: 220px; + white-space: normal; + } } } diff --git a/packages/neuron-ui/src/components/NervosDAORecord/index.tsx b/packages/neuron-ui/src/components/NervosDAORecord/index.tsx index 2dc5dca037..36256afcdb 100644 --- a/packages/neuron-ui/src/components/NervosDAORecord/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAORecord/index.tsx @@ -19,6 +19,7 @@ import { Clock } from 'widgets/Icons/icon' import { Link } from 'react-router-dom' import { HIDE_BALANCE } from 'utils/const' +import Tooltip from 'widgets/Tooltip' import styles from './daoRecordRow.module.scss' import hooks from './hooks' @@ -42,6 +43,7 @@ export interface DAORecordProps extends State.NervosDAORecord { tipBlockTimestamp: number // tip block timestamp, used to calculate apc, dovetails with current epoch genesisBlockTimestamp: number | undefined // genesis block timestamp, used to calculate apc isPrivacyMode?: boolean + hasCkbBalance?: boolean } export const DAORecord = ({ @@ -62,6 +64,7 @@ export const DAORecord = ({ withdrawInfo, unlockInfo, isPrivacyMode, + hasCkbBalance, }: DAORecordProps) => { const [t] = useTranslation() const [withdrawEpoch, setWithdrawEpoch] = useState('') @@ -143,8 +146,11 @@ export const DAORecord = ({ const compensatedPeriod = withdrawInfo?.timestamp && depositInfo?.timestamp ? +withdrawInfo.timestamp - +depositInfo.timestamp : undefined + const isWithdrawnDisabled = CellStatus.Deposited === cellStatus && !hasCkbBalance const isActionAvailable = - connectionStatus === 'online' && [CellStatus.Deposited, CellStatus.Unlockable].includes(cellStatus) + connectionStatus === 'online' && + [CellStatus.Deposited, CellStatus.Unlockable].includes(cellStatus) && + !isWithdrawnDisabled const depositOutPointKey = depositOutPoint ? `${depositOutPoint.txHash}-${depositOutPoint.index}` @@ -277,14 +283,31 @@ export const DAORecord = ({
-
} + placement="top" + tipClassName={styles.tip} + > + - ) : ( - - ) - } - /> +
+ + {t('set-start-block-number.warn')} +
+
+

{t('set-start-block-number.tip')}

+ + {t('set-start-block-number.view-block')} + + + ) : ( + + ) + } + /> + {isSetLessThanBefore ? ( + + {t('set-start-block-number.set-less-than-before')} + + ) : null} +
) diff --git a/packages/neuron-ui/src/components/PageContainer/pageContainer.module.scss b/packages/neuron-ui/src/components/PageContainer/pageContainer.module.scss index 9113944a90..8c6fb0278a 100755 --- a/packages/neuron-ui/src/components/PageContainer/pageContainer.module.scss +++ b/packages/neuron-ui/src/components/PageContainer/pageContainer.module.scss @@ -144,9 +144,32 @@ } } -.startBlockTip { - font-weight: 400; - color: var(--secondary-text-color); +.setBlockContent { + padding: 0 0 20px 0; + overflow-y: auto; + + .content { + padding: 0 16px 0 16px; + } + .startBlockTip { + font-weight: 400; + color: var(--secondary-text-color); + } + + .setBlockWarn { + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--warn-background-color); + color: var(--warn-text-color); + font-size: 12px; + font-weight: 500; + + & > svg { + margin-right: 4px; + } + } } .viewAction { diff --git a/packages/neuron-ui/src/components/SyncStatus/index.tsx b/packages/neuron-ui/src/components/SyncStatus/index.tsx index 6ff0fb9f70..01a7d4ccac 100644 --- a/packages/neuron-ui/src/components/SyncStatus/index.tsx +++ b/packages/neuron-ui/src/components/SyncStatus/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { SyncStatus as SyncStatusEnum, ConnectionStatus } from 'utils' +import { SyncStatus as SyncStatusEnum, ConnectionStatus, clsx, localNumberFormatter } from 'utils' import { Confirming, NewTab } from 'widgets/Icons/icon' import { ReactComponent as UnexpandStatus } from 'widgets/Icons/UnexpandStatus.svg' import { ReactComponent as StartBlock } from 'widgets/Icons/StartBlock.svg' @@ -13,12 +13,14 @@ const SyncDetail = ({ onOpenValidTarget, isLightClient, onOpenSetStartBlock, + startBlockNumber, }: { syncBlockNumbers: string isLookingValidTarget: boolean onOpenValidTarget: (e: React.SyntheticEvent) => void isLightClient: boolean onOpenSetStartBlock: () => void + startBlockNumber?: string }) => { const [t] = useTranslation() return ( @@ -42,10 +44,16 @@ const SyncDetail = ({ )} + {isLightClient && startBlockNumber ? ( +
+
{t('network-status.tooltip.start-block-number')}:
+
{localNumberFormatter(startBlockNumber)}
+
+ ) : null} {isLightClient && (
void + startBlockNumber?: string }>) => { const [t] = useTranslation() const [isOpen, setIsOpen] = useState(false) @@ -116,6 +126,7 @@ const SyncStatus = ({ onOpenValidTarget={onOpenValidTarget} isLightClient={isLightClient} onOpenSetStartBlock={onOpenSetStartBlock} + startBlockNumber={startBlockNumber} /> } trigger="click" diff --git a/packages/neuron-ui/src/components/SyncStatus/syncStatus.module.scss b/packages/neuron-ui/src/components/SyncStatus/syncStatus.module.scss index 94451a610e..9afd0e19e6 100755 --- a/packages/neuron-ui/src/components/SyncStatus/syncStatus.module.scss +++ b/packages/neuron-ui/src/components/SyncStatus/syncStatus.module.scss @@ -63,7 +63,7 @@ font-size: 12px; font-weight: 400; - &:nth-child(2) { + &.setStartBlockNumber { &::before { content: ''; display: block; @@ -74,10 +74,6 @@ } } - &:nth-child(3) { - margin-top: 12px; - } - & > div { display: flex; align-items: center; @@ -95,6 +91,19 @@ } } } + + .startBlockNumber { + margin-top: 8px; + .title { + font-size: 12px; + font-weight: 400; + } + .number { + color: var(--third-text-color); + font-family: D-DIN-PRO; + font-weight: 700; + } + } } .redDot { diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 10f74f7f6c..52d9861916 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -31,6 +31,7 @@ "tooltip": { "block-synced": "Block Synced", "looking-valid-target": "Looking for assumed valid target", + "start-block-number": "Start block number", "set-start-block-number": "Set start block number" }, "migrating": "migrating..." @@ -1125,10 +1126,13 @@ }, "set-start-block-number": { "title": "Set the start block number", + "warn": "Warning: Transactions before the start block of synchronization will not be synchronized", "tip": "The start sync block number is configuration when use light client ckb node, then you can set it and quick the sync", "input-place-holder": "Please enter the start sync block number, eg: 10,100,101", "locate-first-tx": "Locate the first transaction", - "view-block": "View the block" + "view-block": "View the block", + "set-less-than-before": "The block number you set is smaller than the previous set, the synchronization will restart, please proceed with caution.", + "reset-to-header-tip-number": "Unable to set a block number greater than the header tip block number, reset to the header tip block number." }, "main": { "external-node-detected-dialog": { diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 7a2189d3e2..9bf5bafb66 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -31,6 +31,7 @@ "tooltip": { "block-synced": "Bloc synchronisé", "looking-valid-target": "Recherche de la cible valide présumée", + "start-block-number": "bloc de départ", "set-start-block-number": "Définir le numéro de bloc de départ" }, "migrating": "migration en cours..." @@ -1125,10 +1126,13 @@ }, "set-start-block-number": { "title": "Définir le numéro de bloc de départ", + "warn": "Veuillez noter que les transactions antérieures à la synchronisation du bloc initial ne seront pas synchronisées !", "tip": "Le numéro de bloc de départ est une configuration lorsque vous utilisez le noeud CKB en mode client léger. Vous pouvez le définir et accélérer la synchronisation.", "input-place-holder": "Veuillez entrer le numéro de bloc de départ pour la synchronisation, par exemple : 10, 100, 101", "locate-first-tx": "Localiser la première transaction", - "view-block": "Voir le bloc" + "view-block": "Voir le bloc", + "set-less-than-before": "Le numéro de bloc que vous avez défini est inférieur au bloc précédent. La synchronisation recommencera, veuillez procéder avec prudence.", + "reset-to-header-tip-number": "Impossible de définir un numéro de bloc supérieur au bloc le plus récent. Réinitialisation au numéro de bloc le plus récent." }, "main": { "external-node-detected-dialog": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index c6ef72e6ab..fa8d5b9868 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -31,6 +31,7 @@ "tooltip": { "block-synced": "已同步區塊", "looking-valid-target": "尋找預設可信區塊", + "start-block-number": "起始區塊", "set-start-block-number": "設置同步起始區塊" }, "migrating": "正在數據遷移..." @@ -1096,10 +1097,13 @@ }, "set-start-block-number": { "title": "設置同步起始區塊", + "warn": "請註意,同步起始區塊之前的交易不會被同步!", "tip": "輕客戶端模式下的訂閱起始點是可配置的,因此您可以設置同步起始區塊以加快同步速度。", "input-place-holder": "輸入同步起始區塊高度,例如:10,100,101", "locate-first-tx": "定位第一筆交易", - "view-block": "查看區塊" + "view-block": "查看區塊", + "set-less-than-before": "您設置的區塊號小於之前區塊,同步將重新開始,請謹慎操作。", + "reset-to-header-tip-number": "無法設置比最新區塊更大的區塊號,已重置為最新區塊號。" }, "main": { "external-node-detected-dialog": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 7c68e910aa..6d31935a58 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -31,6 +31,7 @@ "tooltip": { "block-synced": "已同步区块", "looking-valid-target": "寻找预设可信区块", + "start-block-number": "起始区块", "set-start-block-number": "设置同步起始区块" }, "migrating": "正在数据迁移..." @@ -1117,10 +1118,13 @@ }, "set-start-block-number": { "title": "设置同步起始区块", + "warn": "请注意,同步起始区块之前的交易不会被同步!", "tip": "轻客户端模式下的订阅起始点是可配置的,因此您可以设置同步起始区块以加快同步速度。", "input-place-holder": "输入同步起始区块高度,例如:10,100,101", "locate-first-tx": "定位第一笔交易", - "view-block": "查看区块" + "view-block": "查看区块", + "set-less-than-before": "您设置的区块号小于之前的设定,同步将重新开始,请谨慎操作。", + "reset-to-header-tip-number": "无法设置比最新区块更大的区块号,已重置为最新区块号。" }, "main": { "external-node-detected-dialog": { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index 3ca2ffdff6..c9ab2831e6 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -60,6 +60,7 @@ type Action = | 'update-wallet' | 'delete-wallet' | 'backup-wallet' + | 'update-wallet-start-block-number' | 'get-all-addresses' | 'update-address-description' | 'request-password' diff --git a/packages/neuron-ui/src/services/remote/wallets.ts b/packages/neuron-ui/src/services/remote/wallets.ts index 89da4ef168..9c1e496c66 100644 --- a/packages/neuron-ui/src/services/remote/wallets.ts +++ b/packages/neuron-ui/src/services/remote/wallets.ts @@ -9,6 +9,9 @@ export const createWallet = remoteApi('create-wal export const updateWallet = remoteApi('update-wallet') export const deleteWallet = remoteApi('delete-wallet') export const backupWallet = remoteApi('backup-wallet') +export const updateWalletStartBlockNumber = remoteApi( + 'update-wallet-start-block-number' +) export const getAddressesByWalletID = remoteApi('get-all-addresses') export const updateAddressDescription = remoteApi('update-address-description') diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 243fd982de..9c1ac509e2 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -202,6 +202,7 @@ declare namespace State { device?: DeviceInfo isHD?: boolean isWatchOnly?: boolean + startBlockNumber?: string } interface DeviceInfo { diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 35ecc4f87b..a0a743e90f 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -35,7 +35,11 @@ declare namespace Controller { newPassword?: string name?: string device?: any - startBlockNumber?: string + } + + interface UpdateWalletStartBlockNumberParams { + id: string + startBlockNumber: string } interface RequestPasswordParams { diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index a9aa17f3a9..024eee26ff 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -64,6 +64,7 @@ import { resetSyncTaskQueue } from '../block-sync-renderer' import DataUpdateSubject from '../models/subjects/data-update' import CellManagement from './cell-management' import { UpdateCellLocalInfo } from '../database/chain/entities/cell-local-info' +import { CKBLightRunner } from '../services/light-runner' export type Command = 'export-xpubkey' | 'import-xpubkey' | 'delete-wallet' | 'backup-wallet' | 'migrate-acp' // Handle channel messages from renderer process and user actions. @@ -332,19 +333,45 @@ export default class ApiController { }) handle( - 'update-wallet', + 'update-wallet-start-block-number', async ( _, - params: { id: string; password?: string; name?: string; newPassword?: string; startBlockNumber?: string } + params: { + id: string + startBlockNumber: string + } ) => { const res = this.#walletsController.update(params) - if (params.startBlockNumber) { + const network = NetworksService.getInstance().getCurrent() + if (network.type !== NetworkType.Light) { + throw new Error('Only Light client can set start block number') + } + const lastSetStartBlockNumber = WalletsService.getInstance().getCurrent()?.toJSON().startBlockNumber + if (lastSetStartBlockNumber && +params.startBlockNumber < +lastSetStartBlockNumber) { + await CKBLightRunner.getInstance().clearNodeCache() + } else { resetSyncTaskQueue.asyncPush(true) } return res } ) + handle( + 'update-wallet', + async ( + _, + params: { + id: string + password?: string + name?: string + newPassword?: string + } + ) => { + const res = this.#walletsController.update(params) + return res + } + ) + handle('delete-wallet', async (_, { id = '', password = '' }) => { return this.#walletsController.delete({ id, password }) }) From adf593dd86844faee76c4ec2032b5e615e6a1cff Mon Sep 17 00:00:00 2001 From: Natixe <49234582+Natixe@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:16:32 +0100 Subject: [PATCH 5/8] Adds Spanish 'es' translation for wallet and UI functions (#3024) Co-authored-by: alejandroRbit <94946624+alejandroRbit@users.noreply.github.com> Co-authored-by: Chen Yu --- _typos.toml | 2 +- packages/neuron-ui/.storybook/electron.js | 2 +- packages/neuron-ui/src/locales/en.json | 3 +- packages/neuron-ui/src/locales/es.json | 1192 +++++++++++++++++ packages/neuron-ui/src/locales/fr.json | 3 +- packages/neuron-ui/src/locales/zh-tw.json | 3 +- packages/neuron-ui/src/locales/zh.json | 3 +- .../src/tests/calendar/index.test.ts | 29 +- packages/neuron-ui/src/utils/const.ts | 2 +- packages/neuron-ui/src/utils/i18n.ts | 2 + packages/neuron-wallet/src/locales/es.ts | 302 +++++ packages/neuron-wallet/src/locales/i18n.ts | 2 + .../neuron-wallet/src/services/settings.ts | 2 +- .../tests/services/setting.test.ts | 2 + 14 files changed, 1540 insertions(+), 9 deletions(-) create mode 100644 packages/neuron-ui/src/locales/es.json create mode 100644 packages/neuron-wallet/src/locales/es.ts diff --git a/_typos.toml b/_typos.toml index 78683d1960..ae82742a0c 100644 --- a/_typos.toml +++ b/_typos.toml @@ -6,4 +6,4 @@ numer = "numer" lastest = "lastest" [files] -extend-exclude = ["CHANGELOG.md", "**/migrations/*.ts", "**/fr.{ts,json}"] +extend-exclude = ["CHANGELOG.md", "**/migrations/*.ts", "**/fr.{ts,json}", "**/es.{ts,json}"] diff --git a/packages/neuron-ui/.storybook/electron.js b/packages/neuron-ui/.storybook/electron.js index 9978d578ce..492a24c539 100644 --- a/packages/neuron-ui/.storybook/electron.js +++ b/packages/neuron-ui/.storybook/electron.js @@ -1,5 +1,5 @@ const sendSyncValues = { - 'get-locale': ('zh', 'fr'), + 'get-locale': ('zh', 'fr', 'es'), 'get-version': '0.103.1', } diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 52d9861916..ebb212ab95 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -413,7 +413,8 @@ "en-US": "English(United States)", "zh": "中文(简体)", "zh-TW": "中文(繁體)", - "fr": "Français" + "fr": "Français", + "es": "Español" }, "data": { "ckb-node-data": "CKB Node Config & Storage", diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json new file mode 100644 index 0000000000..651e7d00e0 --- /dev/null +++ b/packages/neuron-ui/src/locales/es.json @@ -0,0 +1,1192 @@ +{ + "translation": { + "launch-screen": { + "loading-wallets": "Cargando billeteras" + }, + "contextmenu": { + "cut": "Cortar", + "copy": "Copiar", + "paste": "Pegar", + "selectall": "Seleccionar todo" + }, + "navbar": { + "overview": "Resumen", + "wallet": "Billetera", + "send": "Enviar", + "receive": "Recibir", + "history": "Historial", + "addresses": "Direcciones", + "nervos-dao": "Nervos DAO", + "settings": "Configuración", + "special-assets": "Activos personalizados", + "sync-not-start": "Sincronización no iniciada aún", + "connecting": "Conectando", + "experimental-functions": "Experimental", + "s-udt": "Cuentas de activos", + "update-neuron-with-ckb": "La versión del nodo de CKB no coincide con Neuron (v{{ version }}), lo que puede causar problemas de compatibilidad. Por favor actualice a la última versión de Neuron.", + "ckb-node-compatible": "El nodo CKB no es compatible con Neuron (v{{ version }}), por favor compruébelo antes de continuar.", + "ckb-without-indexer": "Por favor agregue la opción '--indexer' para iniciar el nodo local" + }, + "network-status": { + "tooltip": { + "block-synced": "Bloque sincronizado", + "looking-valid-target": "Buscando objetivo válido asumido", + "set-start-block-number": "Establecer el número de bloque inicial" + }, + "migrating": "migrando..." + }, + "import-hardware": { + "title": { + "select-model": "Por favor conecte su dispositivo y seleccione el modelo", + "detect-device": "Detectando dispositivo", + "name-wallet": "Nombre de la billetera", + "choose-address": "Elige una dirección del dispositivo" + }, + "actions": { + "confirm": "Por favor confirme en su dispositivo...", + "success": "¡Importación exitosa!", + "next": "Siguiente", + "cancel": "Cancelar", + "back": "Regresar", + "rescan": "Volver a escanear", + "close": "Cerrar", + "finish": "Finalizar creación" + }, + "errors": { + "multi-device": "Se han detectado varios dispositivos. Solo se puede conectar un dispositivo del mismo modelo.", + "device-not-found": "No se ha detectado ningún dispositivo. Por favor conecte su dispositivo.", + "ckb-app-not-found": "No se abre la aplicación CKB. Por favor abra la aplicación CKB en su dispositivo." + }, + "waiting": "Esperando a la aplicación CKB...", + "abort": "Esperando a la aplicación CKB... Acción cancelada", + "app-version": "Aplicación CKB v{{version}} en ejecución.", + "firmware-version": "Firmware v{{version}} detectado.", + "wallet-name": "Nombre de la billetera", + "select-model": "Selecciona Modelo", + "other-device": "Otro" + }, + "hardware-sign": { + "cancel": "Cancelar", + "title": "Firma a través de billetera de hardware", + "device": "Dispositivo:", + "status": { + "label": "Estado:", + "connect": "Conectado, listo para firmar.", + "user-input": "Conectado, esperando confirmación en el dispositivo...", + "disconnect": "Desconectado, por favor confirme que el dispositivo esté conectado y que la aplicación Nervos esté abierta." + }, + "inputs": "Entradas (se está firmando la {{index}} de {{length}})", + "outputs": "Salidas ({{length}})", + "actions": { + "close": "Cerrar", + "success": "¡Transacción exitosa!", + "rescan": "Volver a escanear" + } + }, + "hardware-verify-address": { + "title": "Verificar dirección a través de billetera de hardware", + "device": "Dispositivo:", + "address": "Dirección:", + "verified": "Verificado", + "invalid": "Inválido", + "status": { + "label": "Estado:", + "connect": "Conectado, listo para verificar.", + "user-input": "Conectado, esperando confirmación en el dispositivo...", + "disconnect": "Desconectado, por favor confirme que el dispositivo esté conectado y que la aplicación Nervos esté abierta." + }, + "actions": { + "close": "Cerrar", + "reconnect": "Reconectar", + "copy-address": "Copiar Dirección", + "verify": "Verificar Dirección", + "finish": "Ok" + } + }, + "offline-sign": { + "title": "Firma sin conexión", + "json-file": "Archivo JSON:", + "status": { + "label": "Estado:", + "unsigned": "Sin firmar", + "partially-signed": "Parcialmente firmado", + "signed": "Firmado" + }, + "wallet": "Billetera:", + "content": "Contenido:", + "export": "Exportar Tx", + "sign-and-export": "Firmar y exportar", + "sign-and-broadcast": "Firmar y transmitir", + "actions": { + "broadcast": "Transmitir", + "sign": "Firmar y exportar", + "cancel": "Cancelar" + } + }, + "overview": { + "date": "Fecha", + "type": "Tipo", + "balance": "Saldo", + "recent-activities": "Actividades Recientes", + "no-recent-activities": "No hay actividades recientes", + "activity": "Actividad", + "datetime": "Fecha y Hora", + "status": "Estado", + "amount": "Cantidad", + "address": "Dirección", + "sent": "Enviado", + "sending": "Enviando", + "received": "Recibido", + "receiving": "Recibiendo", + "more": "Más", + "copy-balance": "Copiar Saldo", + "create": "Crear cuenta de activo(s) {{name}}", + "destroy": "Destruir cuenta de activo(s) {{name}}", + "createSUDT": "Crear cuenta de activo <0>", + "destroySUDT": "Destruir cuenta de activo <0>", + "wallet-ready": "La billetera está lista, bienvenido", + "send": "Enviar", + "receive": "Recibir", + "locked": "Cantidad bloqueada", + "locked-balance": "Saldo bloqueado" + }, + "wizard": { + "welcome-to-nervos-neuron": "Bienvenido a Neuron", + "create-or-import-your-first-wallet": "Crea o importa tu primera billetera", + "create-new-wallet": "Crear una Billetera", + "import-mnemonic": "Importar Semilla de Billetera", + "import-keystore": "Importar desde Keystore", + "import-wallet": "Importar una Billetera", + "import-hardware-wallet": "Importar una Billetera de Hardware", + "new-wallet": "Nueva Billetera", + "wallet-seed": "Semilla de Billetera", + "keystore": "Keystore", + "hardware-wallet": "Billetera de Hardware", + "next": "Siguiente", + "back": "Regresar", + "name": "Nombre", + "your-wallet-seed-is": "Se ha generado la nueva frase semilla de tu billetera", + "input-your-seed": "Por favor introduce la frase semilla de tu billetera", + "password": "Contraseña", + "confirm-password": "Confirmar Contraseña", + "set-wallet-name-and-password": "Nombra tu nueva billetera y elige una contraseña segura para protegerla", + "set-wallet-name": "Dale un nombre a tu nueva billetera", + "set-a-strong-password-to-protect-your-wallet": "Crea una contraseña segura para proteger tu billetera", + "wallet-suffix": "Billetera {{suffix}}", + "write-down-seed": "Escribe la frase semilla de tu billetera y guárdala en un lugar seguro", + "new-name": "Usa un nombre no usado previamente para la nueva billetera", + "complex-password": "La contraseña debe tener entre 8 y 50 caracteres y contener al menos tres categorías de caracteres entre las siguientes: letras en mayúscula, letras en minúscula, números y símbolos especiales.", + "same-password": "La contraseña y la confirmación de contraseña deben coincidir", + "input-seed-verify": "Ingresa la frase semilla creada para verificar", + "no-wallet": "¿No tienes una billetera?", + "create-wallet": "Crear Billetera", + "repeat-password": "Repetir Contraseña", + "finish-create": "Finalizar Creación", + "creating-wallet": "Se está preparando la billetera, por favor espera", + "add-one": "Añadir una más" + }, + "import-keystore": { + "title": "Importar Keystore", + "select-file": "Seleccionar Archivo", + "label": { + "path": "Archivo de Claves", + "name": "Nombre de la Billetera", + "password": "Contraseña" + }, + "placeholder": { + "path": "Haz clic para seleccionar el archivo de claves", + "name": "Nombre para la nueva billetera", + "password": "Contraseña para verificar el archivo de claves" + }, + "button": { + "back": "Regresar", + "submit": "Enviar" + } + }, + "detail": { + "more-transactions": "Más" + }, + "send": { + "address": "Enviar a", + "input-address": "Por favor ingresa la dirección de destino", + "amount": "Cantidad (CKB)", + "input-amount": "Por favor ingresa la cantidad", + "send": "Enviar", + "reset": "Reiniciar", + "confirm-password": "Confirmar Contraseña", + "to": "Para", + "add-receiving-address": "Añadir Dirección de Recepción", + "input-password-to-confirm": "Ingresa la contraseña para confirmar", + "scan-to-get-address": "Escanea el código QR para leer la dirección a la que deseas enviar", + "total-amount": "Cantidad Total (CKB)", + "description": "Descripción", + "description-optional": "Por favor ingresa una descripción, opcional", + "balance": "Saldo", + "add-one": "Añadir uno más", + "remove-this": "Eliminar esta", + "fee": "Tarifa de Transacción (CKB)", + "advanced-fee-settings": "Configuración Avanzada de Tarifa", + "price": "Precio", + "pick-price": "Seleccionar Precio Rápido", + "custom-price": "Precio Personalizado", + "slow": "Lento", + "standard": "Estándar", + "fast": "Rápido", + "total-cycles": "Ciclos RISC-V Totales", + "scan-screen-qr-code": "Escanea el código QR en la pantalla", + "set-locktime": "Fijar hora de bloqueo", + "remove-receiving-address": "Eliminar Dirección de Recepción", + "remove-receiving-address-msg": "¿Estás seguro de que quieres eliminar la dirección de recepción seleccionada?", + "locktime-notice-content": "De acuerdo al número de bloque actual, puede haber algunas variaciones de tiempo en en el tiempo de bloqueo.", + "release-on": "Liberar en", + "locktime-warning": "Asegúrese de que la billetera del receptor pueda soportar el desbloqueo por vencimiento. (Nota: 1. Generalmente, las casas de cambio no admiten el desbloqueo por vencimiento {{extraNote}})", + "allow-use-sent-cell": "Se permite el uso de salidas no confirmadas en esta transacción.", + "submit-transaction": "Enviar la transacción" + }, + "receive": { + "title": "Recibir", + "address-book": "Libro de Direcciones", + "click-to-copy": "Haz clic para copiar la dirección", + "copy-address": "Copiar Dirección", + "address-not-found": "Dirección no encontrada", + "prompt": "Neuron elige una nueva dirección de recepción para una mejor privacidad. Por favor, ve al Libro de Direcciones si deseas utilizar una dirección de recepción utilizada anteriormente.", + "address-qrcode": "Código QR de la Dirección", + "address": "Dirección {{network}}", + "verify-address": "Verificar Dirección", + "turn-into-full-version-format": "Convertir en formato de versión completa", + "turn-into-deprecated-format": "Convertir en formato obsoleto", + "save-qr-code": "Descargar", + "copy-qr-code": "Copiar" + }, + "transaction-status": { + "pending": "Pendiente", + "success": "Éxito", + "failed": "Fallido", + "confirming": "Confirmación" + }, + "confirmationsCount_one": "{{count}} Confirmación", + "confirmationsCount_other": "{{count}} Confirmaciones", + "history": { + "title": "Historial", + "title-detail": "Detalle del Historial", + "meta": "Meta", + "type": "Tipo", + "date": "Fecha", + "timestamp": "Hora", + "amount": "Cantidad", + "transaction-hash": "Hash de Transacción", + "send": "Enviar", + "receive": "Recibir", + "other": "Otro", + "more-actions": "Más Acciones", + "detail": "Detalle", + "explorer": "Explorador", + "first": "Primero", + "previous": "Anterior", + "next": "Siguiente", + "last": "Último", + "description": "Descripción", + "status": "Estado", + "blockNumber": "Número de Bloque", + "basic-information": "Información Básica", + "search": { + "button": "Buscar", + "placeholder": "Buscar hash de transacción, dirección o fecha (aaaa-mm-dd)" + }, + "table": { + "name": "Nombre de la Billetera", + "type": "Tipo", + "amount": "Cantidad", + "timestamp": "Tiempo", + "status": "Estado", + "operation": "Operación" + }, + "export-history": "Exportar Historial de Transacciones", + "confirmationTimes": "Confirmaciones", + "confirming-with-count": "{{confirmations}} Confirmaciones", + "view-on-explorer": "Ver en el Explorador", + "no-txs": "No Historial de Transacciones", + "view-in-explorer": "Ver en el explorador", + "view-in-explorer-button-title": "Ver en el explorador", + "view-detail": "Detalle", + "view-detail-button-title": "Ver Detalle", + "opening": "Abriendo", + "copy-tx-hash": "Copiar Hash de Transacción", + "copy-balance": "Copiar Saldo", + "copy-address": "Copiar Dirección", + "create": "Crear Cuenta de Activos {{name}}", + "destroy": "Destruir Cuenta de Activos {{name}}" + }, + "transaction": { + "window-title": "Transacción: {{hash}}", + "date": "Fecha", + "transaction-hash": "Hash de Transacción", + "block-number": "Número de Bloque", + "goBack": "Volver", + "index": "Índice", + "address": "Dirección", + "income": "Ingreso", + "amount": "Cantidad", + "inputs": "Entradas", + "outputs": "Salidas", + "cell-from-cellbase": "Desde cellbase", + "lock-script": "Script de Bloqueo", + "lock-script-title": "Información de Dirección", + "deprecated-address-format": "Dirección en formato obsoleto" + }, + "addresses": { + "title": "Libro de Direcciones", + "addresses": "Direcciones", + "type": "Tipo", + "address": "Dirección", + "identifier": "Identificador", + "description": "Descripción", + "balance": "Saldo", + "transactions": "TXs", + "receiving-address": "Recepción", + "change-address": "Cambiar", + "copy-address": "Copiar Dirección", + "request-payment": "Solicitar Pago", + "view-on-explorer": "Ver en el Explorador", + "default-description": "Ninguna", + "all-address": "Todas" + }, + "settings": { + "title": { + "normal": "Ajustes", + "mac": "Preferencias" + }, + "go-to-overview": "Ir a Resumen", + "setting-tabs": { + "general": "General", + "wallets": "Billeteras", + "network": "Red", + "data": "Datos" + }, + "general": { + "show": "Mostrar", + "hide": "Ocultar", + "version": "Versión", + "language": "Idioma", + "select-language": "Seleccionar Idioma", + "apply": "Aplicar" + }, + "wallet-manager": { + "edit-wallet": { + "wallet-name": "Nombre de la Billetera", + "password": "Contraseña", + "new-password": "Nueva Contraseña", + "confirm-password": "Confirmar Contraseña", + "edit-wallet": "Editar Billetera" + }, + "edit-success": "Información de la billetera actualizada", + "delete-wallet-title": "Por Favor ingrese la {{name}} Contraseña", + "password": "Contraseña", + "wallet-detail": { + "balance": "Saldo" + } + }, + "network": { + "online": "En Línea", + "offline": "Desconectado", + "add-network": "Agregar Red", + "remove-network": "Eliminar Red", + "remove-network-msg": "¿Está seguro de que desea eliminar la red seleccionada?", + "edit-network": { + "title": "Editar Red", + "rpc-url": "URL RPC", + "name": "Nombre de la Red", + "input-rpc": "Por favor ingrese el rpc", + "input-network": "Por favor ingrese el nombre" + }, + "edit-success": "Edición de la red exitosa", + "mainnet": "Mainnet", + "testnet": "Testnet", + "lightTestnet": "Light Testnet", + "lightMainnet": "Light Mainnet", + "devnet": "Devnet", + "switch-network-type": "Cambiar a {{type}}" + }, + "locale": { + "en": "Inglés", + "en-US": "Inglés(Estados Unidos)", + "zh": "中文(简体)", + "zh-TW": "中文(繁體)", + "fr": "Francés", + "es": "Español" + }, + "data": { + "ckb-node-data": "Configuración de Datos del Nodo CKB", + "set": "Establecer", + "cache": "Caché", + "clear-cache-description": "Borre la memoria caché si encuentra problemas de sincronización de datos o visualización de saldo. Neuron volverá a escanear los datos de los bloques.", + "cache-cleared-on": "Caché actualizada en {{date}}", + "refresh": "Actualizar", + "clearing-cache": "Borrando Caché, por favor espere un momento", + "clear-success": "Caché Eliminada", + "set-path": "Establecer Ruta", + "remove-ckb-data-tip": "Por favor mueva los Datos del Nodo CKB de 【{{prevPath}}】 a 【{{currentPath}}】 y luego haga clic en \"Se han movido los datos\" para comenzar desde el bloque previamente sincronizado; O haga clic en \"Sincronizar desde el principio\" para empezar a sincronizar de nuevo.", + "resync-ckb-node-describe": "Atención: Sincronizar desde cero no eliminará los datos del Nodo CKB descargados previamente", + "cancel": "Cancelar", + "confirm-clear": "Confirmar", + "move-data-finish": "Se han movido los datos", + "re-sync": "Sincronizar desde cero", + "disabled-set-path": "Cuando el nodo CKB se inicia manualmente, la configuración de la ruta de configuración y el almacenamiento del nodo CKB no tienen efecto", + "ckb-node-storage": "Almacenamiento del Nodo CKB" + } + }, + "password-request": { + "wallet-not-found": "No se encontró la billetera", + "password": "Ingresar Contraseña", + "placeholder": "Por favor ingrese la contraseña de la billetera", + "send": { + "title": "Enviar la transacción" + }, + "send-nft": { + "title": "Enviar NFT" + }, + "delete": { + "title": "Eliminar la billetera" + }, + "backup": { + "title": "Respaldar la Billetera" + }, + "unlock": { + "title": "Por favor ingrese la contraseña de la billetera para reclamar el activo" + }, + "create-sudt-account": { + "title": "Crear Cuenta de Activos" + }, + "send-sudt": { + "title": "Enviar sUDT" + }, + "transfer-to-sudt": { + "title": "Enviar a cuenta sUDT" + }, + "send-ckb-asset": { + "title": "Enviar CKB" + }, + "send-acp": { + "title": "Enviar CKB" + }, + "send-acp-sudt-to-new-cell": { + "title": "Enviar sUDT" + }, + "send-acp-ckb-to-new-cell": { + "title": "Enviar CKB" + }, + "migrate-acp": { + "title": "Actualizar Cuentas de Activos" + }, + "send-cheque": { + "title": "Enviar Cheque" + }, + "withdraw-cheque": { + "title": "Retirar Cheque" + }, + "claim-cheque": { + "title": "Reclamar Cheque" + }, + "create-account-to-claim-cheque": { + "title": "Reclamar Cheque con Nueva Cuenta" + }, + "destroy-asset-account": { + "title": "Destruir Cuenta de Activos" + }, + "send-from-multisig": { + "title": "Enviar CKB desde dirección multifirma" + }, + "send-from-multisig-need-one": { + "title": "Enviar CKB desde dirección multifirma" + }, + "xpub-notice": "Para billetera de solo lectura, sólo es posible exportar transacciones" + }, + "qrcode": { + "copy": "Copiar imagen", + "save": "Guardar imagen" + }, + "common": { + "or": "o", + "ok": "Ok", + "dismiss": "Descartar", + "confirm": "Confirmar", + "open": "Abrir", + "cancel": "Cancelar", + "save": "Guardar", + "toggle": { + "on": "Encendido", + "off": "Apagado" + }, + "copy-tx-hash": "Copiar Hash de Transacción", + "copy-address": "Copiar Dirección", + "select": "Seleccionar", + "backup": "Respaldar", + "edit": "Editar", + "delete": "Borrar", + "click-to-edit": "Hacer clic para editar", + "notice": "Nota", + "experimental": "Experimental", + "close": "Cerrar", + "copy": "Copiar", + "copied": "Copiado", + "verification-failure": "Falló la Verificación", + "back": "Atrás", + "switch-to-light": "Cambiar a modo claro", + "switch-to-dark": "Cambiar a modo oscuro" + }, + "notification-panel": { + "title": "Notificaciones" + }, + "message-types": { + "warning": "advertencia", + "alert": "alerta", + "success": "éxito" + }, + "messages": { + "error": "Error", + "unknown-error": "Error desconocido", + "update-wallet-successfully": "La billetera fue actualizada", + "delete-wallet-successfully": "La billetera fue eliminada", + "create-network-successfully": "La red fue creada", + "update-network-successfully": "La red fue actualizada", + "clear-cache-successfully": "El caché fue borrado", + "addr-copied": "La dirección ha sido copiada en el portapapeles", + "qrcode-copied": "El Código QR ha sido copiado en el portapapeles", + "view-the-run-node-doc": "Ver la guía en el navegador", + "remain-ckb-for-withdraw": "Es recomendable reservar algunos CKBytes para operaciones de retiro", + "no-valid-addresses-found": "No se encontraron direcciones válidas", + "decimal-range": "El decimal debe ser un número entero de {{range}}(incluido)", + "experimental-message-hardware": "Esta es una función experimental. Preste atención al riesgo y use con precaución.", + "experimental-message": "Esta es una función experimental, puede cambiar en cualquier momento. Por favor, use con precaución.", + "rebuild-sync": "Para una mejor experiencia de usuario, Neuron ha adoptado un nuevo almacenamiento, lo que requiere una migración de datos (estimado de 20 a 60 minutos).\nLo sentimos por la inconveniencia.", + "migrate-warning": "Advertencia: el proceso de migración puede fallar por razones desconocidas, lo que resulta en resincronización, ¡haga una copia de seguridad manualmente y comience la migración!", + "migrate-ckb-data": "Migrar", + "migrate": "Migrar", + "secp256k1/blake160-address-required": "Se requiere la dirección Secp256k1/Blake160", + "light-client-locktime-warning": "2. El modo de cliente ligero no admite mostrar CKBytes en tiempo de bloqueo.", + "light-client-cheque-warning": "Advertencia: el modo de cliente ligero no admite mostrar activos de Cheque.", + "fields": { + "wallet": "Billetera", + "name": "Nombre", + "password": "Contraseña", + "remote": "URL de RPC", + "network": "Red", + "address": "Dirección", + "amount": "Cantidad", + "transaction": "Transacción", + "default-address": "Dirección predeterminada", + "mnemonic": "Mnemónico", + "keystore-path": "Archivo Keystore", + "keystore-name": "Nombre de la billetera", + "keystore-password": "Contraseña", + "deposit": "Depósito", + "account-name": "Nombre de la cuenta", + "token-name": "Nombre del token", + "token-id": "ID del token", + "symbol": "Símbolo", + "decimal": "Decimal" + }, + "codes": { + "-3": "", + "100": "La cantidad no es suficiente.", + "101": "La cantidad {{amount}} CKB es demasiado pequeña, por favor ingrese una cantidad no menor a {{required}} CKB.", + "102": "$t(messages.fields.{{fieldName}}) no es válido.", + "103": "$t(messages.fields.keystore-password) es incorrecta", + "104": "No se pudo conectar al nodo, por favor reinicie Neuron.", + "105": "Necesitas más CKBytes para el cambio (más de 61 CKBytes).", + "106": "Necesitas más CKBytes para el cambio (más de 61 CKBytes), o desliza hasta el final para enviar todo tu saldo.", + "107": "La cantidad {{amount}} CKB es demasiado pequeña, la transferencia con tiempo de bloqueo requiere una cantidad no menor a 69 CKB.", + "108": "La dirección proporcionada no pertenece a la billetera actual. Por favor revise su billetera o espere hasta que se complete la sincronización.", + "109": "Saldo insuficiente.", + "110": "Saldo disponible insuficiente, por favor intente de nuevo cuando la última transacción haya sido confirmada.", + "111": "La billetera actual no está configurada.", + "112": "Billetera {{id}} no encontrada.", + "113": "El archivo de almacenamiento es inválido, por favor revise la integridad del archivo.", + "114": "El balance mínimo de transferencia es {{bytes}} CKBytes.", + "115": "Necesitas más CKBytes para el cambio (más de 61 CKBytes), o haz clic en el botón 'Máx' para enviar todo tu saldo.", + "201": "$t(messages.fields.{{fieldName}}) es requerido.", + "202": "$t(messages.fields.{{fieldName}}) ya está en uso.", + "203": "$t(messages.fields.{{fieldName}}) debería ser más corto o igual a {{length}}.", + "204": "$t(messages.fields.{{fieldName}}) es demasiado corto, debe ser más largo o igual a {{length}}.", + "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} es inválido.", + "206": "$t(messages.fields.{{fieldName}}) no es válido, introduzca $t(messages.fields.{{fieldName}}) con no más de {{length}} decimales.", + "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} es inválido, no puede ser negativo.", + "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} no es válido, debería comenzar con http(s)://", + "209": "$t(messages.fields.{{fieldName}}) no debe tener espacios en blanco.", + "210": "\"{{value}}\" está reservado, por favor utilice otro.", + "211": "La cantidad no puede ser 0", + "212": "$t(messages.fields.{{fieldName}}) es demasiado simple", + "301": "$t(messages.fields.{{fieldName}}) {{fieldValue}} no se puede eliminar.", + "303": "$t(messages.fields.{{fieldName}}) no se encuentra.", + "304": "La cámara no está disponible o está deshabilitada.", + "305": "$t(messages.fields.address) no puede estar vacío.", + "306": "Por favor introduzca una dirección de red principal", + "307": "Por favor introduzca una dirección de testnet", + "308": "La cantidad no es suficiente", + "309": "El destinatario debe actualizar la dirección de su cuenta para aceptar más transferencias.", + "310": "Por favor introduzca una dirección {{tagName}}", + "402": "La aplicación CKB no se abre. Abra la aplicación CKB en su dispositivo.", + "403": "No se detectó ningún dispositivo. Por favor conecte su dispositivo", + "404": "Se detectaron varios dispositivos. Sólo se puede conectar un dispositivo del mismo modelo.", + "600": "Asegúrese de que la sincronización haya finalizado antes de realizar cualquier operación relacionada con transacciones." + } + }, + "sync": { + "synced": "Sincronizado 100%", + "syncing": "Sincronizando {{ syncPercents }}", + "block-number": "número de bloque", + "syncing-balance": "El saldo se está actualizando", + "slow": "La sincronización es lenta", + "sync-failed": "Error de sincronización, por favor compruebe la red", + "sync-not-start": "La sincronización no se ha iniciado todavía, por favor intente reiniciar Neuron Wallet", + "connecting": "Conectando" + }, + "pagination": { + "previous-page": "Página anterior", + "next-page": "Página siguiente", + "first-page": "Primera página", + "last-page": "Última página", + "page": "Página", + "selected": "Página actual", + "range": "{{start}} - {{end}} de {{count}}", + "page-no": "Página {{pageNo}}" + }, + "nervos-dao": { + "free": "Disponible", + "locked": "Bloqueado", + "deposit": "Depósito", + "deposit-rules": "Reglas de Depósito", + "copy-balance": "Copiar Saldo", + "deposit-records": "Depósitos", + "completed-records": "Completados", + "apc": "APC Actual", + "apc-tooltip": "Porcentaje de Compensación Anual Actual", + "fee": "Tarifa de Transacción: ", + "deposit-to-nervos-dao": "Depositar en Nervos DAO", + "withdraw-from-nervos-dao": "Retirar de Nervos DAO", + "cancel": "Cancelar", + "proceed": "Continuar", + "compensation": "Compensación", + "notice-wait-time": "Aviso: Está a punto de iniciar un retiro, el cual tardará {{epochs}} epochs {{blocks}} bloques (aproximadamente {{days}} días) para completarse.", + "deposit-dialog-title": "Depósito", + "deposit-terms": "Nervos DAO requiere 102 CKBytes para el almacenamiento de Cells de depósito, las cuales no reciben compensación. Por favor visite Nervos DAO RFC para obtener más información sobre Nervos DAO", + "minimal-fee-required": "La capacidad mínima de depósito es de {{minimal}} CKBytes", + "compensation-accumulated": "Compensación acumulada en {{blockNumber}} bloques", + "withdraw-alert": "Advertencia: solo quedan {{epochs}} epochs ({{hours}} horas) hasta el final de su periodo de bloqueo actual. Si desea retirar en el periodo de bloqueo actual, envíe la solicitud de retiro a tiempo. Solo quedan {{nextLeftEpochs}} epochs ({{days}} días) hasta el final de su próximo periodo de bloqueo.", + "balance-not-reserved": "No reservar ningún CKBytes para futuras operaciones de DAO (no se recomienda)", + "deposit-amount": "Cantidad a Depositar (CKB)", + "deposit-record": { + "deposited-at": "Depositado", + "completed-at": "Completado", + "deposit-pending": "Depósito Pendiente", + "withdraw-action-label": "Retirar", + "unlock-action-label": "Desbloquear", + "record": "Registro", + "deposited": "Depositado", + "withdrawn": "Retirado", + "unlocked": "Desbloqueado", + "view-tx-detail": "Ver detalles de la transacción", + "locked-period": "Período de Bloqueo", + "compensated-period": "Período de Compensación", + "days-hours": "{{days}} días {{hours}} horas", + "no-deposit": "Sin Depósito", + "no-completed": "Sin Completar" + }, + "compensation-period": { + "tooltip": { + "compensated-period": "Período de Compensación", + "days-hours": "{{days}} días {{hours}} horas", + "immature-for-withdraw": "Se tarda al menos 16 horas (4epochs) para realizar la siguiente acción", + "normal": "La cantidad de compensación es baja, no se recomienda la acción de retiro", + "suggested": "Período de retiro sugerido para una compensación maximizada", + "ending": "Es posible que el retiro solicitado se posponga y se bloquee durante un ciclo adicional de 30 días", + "withdrawn": "Retirado" + }, + "stage-messages": { + "pending": "Pendiente...", + "immature-for-withdraw": "No se puede retirar CKB durante las próximas {{ horas }} horas", + "immature-for-unlock": "No se puede desbloquear CKB durante las próximas {{ horas }} horas", + "next-compensation-cycle": "El siguiente ciclo de compensación comenzará en aproximadamente {{días}} días", + "withdrawing": "Retirando...", + "compensation-cycle-will-end": "El ciclo de compensación termina en aproximadamente {{días}} días", + "compensation-cycle-has-ended": "El ciclo ha terminado, CKB está listo para ser desbloqueado", + "unlocking": "Desbloqueando..." + } + } + }, + "nervos-dao-detail": { + "tx-detail": "Detalles de la transacción", + "deposited": "Depositado", + "withdrawn": "Retirado", + "unlocked": "Desbloqueado", + "basic-information": "Información básica", + "transaction-hash": "Hash de transacción", + "blockNumber": "Número de bloque", + "datetime": "Fecha y hora", + "income": "Ingreso", + "index": "Índice", + "address": "Dirección", + "amount": "Cantidad", + "cell-from-cellbase": "Desde cellbase", + "cancel": "Cancelar", + "next": "Siguiente" + }, + "lock-info-dialog": { + "address-info": "Información de dirección", + "deprecated-address": "Dirección obsoleta" + }, + "updates": { + "check-updates": "Buscar actualizaciones", + "checking-updates": "Buscando...", + "update-available": "Actualizaciones disponibles actualmente.", + "downloading-update": "Descargando actualización...", + "update-not-available": "Actualmente no hay actualizaciones disponibles.", + "updates-found-do-you-want-to-update": "Hay una actualización ({{versión}}) disponible", + "install-update": "Actualización inmediata", + "updates-downloaded-about-to-quit-and-install": "Actualización descargada. Listo para instalar y reiniciar.", + "quit-and-install": "Instalar y reiniciar" + }, + "datetime": { + "mon": { + "full": "Lunes", + "short": "Lun.", + "tag": "L" + }, + "tue": { + "full": "Martes", + "short": "Mar.", + "tag": "Ma" + }, + "wed": { + "full": "Miércoles", + "short": "Mié.", + "tag": "X" + }, + "thur": { + "full": "Jueves", + "short": "Jue.", + "tag": "J" + }, + "fri": { + "full": "Viernes", + "short": "Vie.", + "tag": "V" + }, + "sat": { + "full": "Sábado", + "short": "Sáb.", + "tag": "Sá" + }, + "sun": { + "full": "Domingo", + "short": "Dom.", + "tag": "D" + }, + "jan": { + "full": "Enero", + "short": "Ene.", + "tag": "E" + }, + "feb": { + "full": "Febrero", + "short": "Feb.", + "tag": "F" + }, + "mar": { + "full": "Marzo", + "short": "Mar.", + "tag": "M" + }, + "apr": { + "full": "Abril", + "short": "Abr.", + "tag": "A" + }, + "may": { + "full": "Mayo", + "short": "May.", + "tag": "M" + }, + "june": { + "full": "Junio", + "short": "Jun.", + "tag": "J" + }, + "july": { + "full": "Julio", + "short": "Jul.", + "tag": "J" + }, + "aug": { + "full": "Agosto", + "short": "Ago.", + "tag": "Ago" + }, + "sept": { + "full": "Septiembre", + "short": "Sept.", + "tag": "S" + }, + "oct": { + "full": "Octubre", + "short": "Oct.", + "tag": "O" + }, + "nov": { + "full": "Noviembre", + "short": "Nov.", + "tag": "N" + }, + "dec": { + "full": "Diciembre", + "short": "Dic.", + "tag": "D" + }, + "timezone": "Zona horaria", + "previous-month": "mes anterior", + "next-month": "próximo mes", + "start-tomorrow": "El tiempo seleccionado debe comenzar a partir de mañana." + }, + "sign-and-verify": { + "window-title": "Firmar o verificar mensaje", + "sign-or-verify-message": "Firmar o verificar mensaje", + "message": "Mensaje", + "address": "Dirección", + "signature": "Firma", + "cancel": "Cancelar", + "sign": "Firmar", + "verify": "Verificar", + "password": "Contraseña", + "confirm": "Confirmar", + "verification-success": "Verificación exitosa", + "verification-failure": "Error de verificación", + "address-not-found": "La dirección proporcionada no pertenece a la billetera actual. Revise su billetera o espere hasta que se complete la sincronización.", + "sign-with-magic-byte": "El mensaje se firmará con bytes mágicos 'Mensaje de Nervos:'", + "verify-tip": "Se puede verificar con Neuron a partir de v0.33.1", + "verify-old-sign-success": "El mensaje fue firmado por Neuron antes de v0.33.1", + "input-message": "Por favor ingrese mensaje", + "input-choose-address": "Por favor ingrese o elija una dirección", + "input-password": "Por favor ingrese su contraseña" + }, + "special-assets": { + "title": "Activos personalizados", + "date": "Fecha", + "assets": "Activos", + "type-script-tooltip": "Type Script", + "data-tooltip": "Datos", + "user-defined-asset": "Activo no catalogado", + "user-defined-asset-tooltip": "Este es un activo no catalogado. Neuron no puede procesarlo actualmente. Utilice un software de terceros para verificarlo.", + "locked-asset": "Bloqueado", + "locked-asset-tooltip": "El parámetro de bloqueo es de {{epochs}} épocas, la fecha de lanzamiento estimada es {{year}}-{{month}}-{{day}} (según la altura del bloque en ejecución real, pueden haber algunas variaciones de tiempo en el tiempo de bloqueo).", + "withdraw-asset-tooltip": "Hora estimada de lanzamiento es {{year}}-{{month}}-{{day}} {{hour}}:{{minute}} (según la altura del bloque en ejecución real).", + "user-defined-token-tooltip": "Migre el activo sUDT a una cuenta de activos sUDT", + "claim-asset": "Reclamar", + "withdraw-asset": "Retirar", + "view-details": "Ver detalles", + "release-success": "El activo fue liberado, vea los detalles en Historial.", + "no-special-assets": "No hay activos personalizados", + "experimental": "Experimental", + "unknown-asset": "Activo desconocido", + "transfer-nft": "Enviar", + "user-defined-token": "Migrar", + "transfer-nft-success": "Transferencia de activos exitosa", + "migrate-sudt-success": "Conversión a nueva cuenta de activo sUDT exitosa", + "send-sudt-success": "Transferencia exitosa a la cuenta de activo sUDT", + "unlock-success": "Activo reclamado. Vaya al historial de transacciones para obtener más detalles.", + "withdraw-cheque-success": "El activo fue retirado. Vaya al historial de transacciones para obtener más detalles.", + "claim-cheque-success": "Activo reclamado. Vaya al historial de transacciones para obtener más detalles." + }, + "migrate-sudt": { + "title": "Migrar a una cuenta de activo sUDT", + "choose-title": "Modo de migración", + "next": "Siguiente", + "back": "Atrás", + "input-token": "Por favor ingrese el nombre del token", + "input-symbol": "Por favor ingrese el símbolo", + "input-decimal": "Por favor ingrese el decimal", + "turn-into-new-account": { + "title": "Conviértalo en una nueva cuenta de activo sUDT", + "sub-title": "Convierta el activo sUDT en una nueva cuenta sUDT, ocupa al menos 142 CKbytes.", + "cancel": "Cancelar", + "confirm": "Confirmar" + }, + "transfer-to-exist-account": { + "title": "Transferir a una cuenta de activo sUDT", + "sub-title": "Transfiera todo el saldo sUDT a una cuenta sUDT existente. Asegúrese de que la cuenta objetivo esté activa." + }, + "cancel": "Cancelar", + "confirm": "Confirmar", + "balance": "Balance", + "amount": "Monto", + "address": "Dirección" + }, + "s-udt": { + "edit-account-success": "La información de la cuenta ha sido editada con éxito", + "create-account-success": "La cuenta de activos se ha creado correctamente", + "account-list": { + "title": "Cuentas de activos", + "no-asset-accounts": "No se ha detectado ningún activo sUDT", + "search": "Nombre de cuenta, nombre de token o símbolo", + "syncing": "Sincronizando, los datos actuales pueden ser inexactos", + "send": "Enviar", + "receive": "Recibir", + "set-account-info": "Establecer información de la cuenta" + }, + "create-dialog": { + "create-asset-account": "Crear cuenta de activos", + "input-account-name": "Por favor ingrese el nombre de la cuenta", + "select-account-type": "Seleccione el tipo de cuenta", + "account-name": "Nombre de la cuenta", + "sudt-account": "Cuenta de activos sUDT", + "delete-failed": "No se pudo eliminar la configuración multifirma, el motivo del fallo: {{reason}}", + "ckb-account": "Cuenta CKB", + "set-token-info": "Establecer información del token", + "token-id": "ID del token", + "token-name": "Nombre del token", + "symbol": "Símbolo", + "decimal": "Decimal", + "confirm": "Confirmar", + "cancel": "Cancelar", + "next": "Siguiente", + "back": "Atrás", + "occupy-142-ckb": "Ocupa al menos 142 CKbytes.", + "occupy-61-ckb": "Ocupa al menos 61 CKbytes.", + "input": { + "account-name": "Por favor ingrese el nombre de la cuenta", + "token-id": "Ingrese el ID del token", + "token-name": "Ingrese el nombre del token", + "symbol": "Ingrese el símbolo", + "decimal": "Ingrese el decimal" + }, + "placeholder": { + "token-id": "El ID del token es el Args del sUDT Type Script, que es el mismo que el hash de bloqueo del emisor del token." + } + }, + "send": { + "title": "Enviar", + "address": "Enviar a", + "address-placeholder": "Ingrese la dirección", + "input-address": "Ingrese la dirección", + "input-description": "Ingrese la descripción, opcional", + "amount": "Monto", + "amount-placeholder": "Ingrese la cantidad", + "description": "Descripción", + "description-placeholder": "Ingrese la descripción, opcional", + "submit": "Enviar", + "click-to-edit": "Haga clic para editar", + "cheque-address-hint": { + "label": "Bloquee temporalmente 162 CKBytes", + "tooltip": "La capacidad de 162 CKBytes se bloqueará temporalmente para inicializar la transferencia del token, y se desbloqueará automáticamente una vez que el receptor reclame el token." + }, + "destroy": "Destruir", + "destroy-ckb-desc": "Devolverá todo el CKB de esta cuenta de activo CKB a su dirección de cambio.", + "destroy-sudt-desc": "Devolverá el CKB ocupado de la cuenta de activos a su dirección de cambio.", + "extra-ckb-send-to-secp256": { + "label": "Enviar 142 CKBytes adicionales junto con {{assetName}}", + "tooltip": "Se enviarán 142 CKBytes adicionales junto con {{assetName}} para mantener el activo" + }, + "extra-ckb-send-to-acp": { + "label": "Enviar {{extraCKB}} CKBytes adicionales para crear una cuenta de activo para el receptor" + }, + "extra-ckb-send-to-unknow": { + "label": "La dirección de recepción tiene un script de bloqueo desconocido, asegúrese de que el receptor sepa cómo manejarlo, y se enviarán {{extraCKB}} CKBytes adicionales junto con {{assetName}} para mantener el activo" + }, + "select-option": "Confirme la opción anterior antes de enviar" + }, + "receive": { + "notation": "Solo acepta {{symbol}}" + }, + "update-dialog": { + "update-asset-account": "Detalle de cuenta", + "account-name": "Nombre de la cuenta", + "token-id": "ID del token", + "token-name": "Nombre del token", + "symbol": "Símbolo", + "decimal": "Decimal", + "confirm": "Confirmar", + "cancel": "Cancelar" + } + }, + "multisig-address": { + "window-title": "Direcciones de Multifirma", + "search": { + "placeholder": "buscar por dirección de multifirma, alias" + }, + "add": { + "label": "Crear" + }, + "import": { + "label": "Importar" + }, + "export": { + "label": "Exportar" + }, + "ok": "Aceptar", + "no-data": "No hay dirección multifirma", + "table": { + "address": "Dirección del firmante", + "alias": "Alias", + "type": "Tipo", + "balance": "Balance", + "copy-address": "Copiar dirección", + "action": "Acciones", + "more": "Más", + "sync-block": "Bloque sincronizado", + "actions": { + "info": "Información", + "send": "Enviar", + "approve": "Aprobar", + "delete": "Eliminar" + } + }, + "import-dialog": { + "actions": { + "cancel": "Cancelar", + "confirm": "Confirmar" + }, + "notice": "Solo se puede importar una configuración a la vez. Si el contenido del archivo es una matriz, se importa el primero de forma predeterminada." + }, + "delete-failed": "Error al eliminar la configuración multifirma", + "remove-multisig-address": "Eliminar dirección multifirma", + "remove-multisig-address-msg": "¿Está seguro de que desea eliminar la dirección multifirma seleccionada?", + "send-ckb": { + "title": "Enviar CKB desde una dirección multifirma", + "detail": "Enviar desde la dirección multifirma {{m}}-de-{{n}}: <0>", + "balance": "Balance", + "address": "Dirección", + "amount": "Monto", + "send": "Enviar", + "cancel": "Cancelar", + "export": "Exportar Tx" + }, + "multi-details": "Detalles de la dirección multifirma", + "create-dialog": { + "title": "Crear dirección multifirma", + "preview-title": "La dirección multifirma se ha generado", + "index": "Índice", + "required": "Requerido", + "signer-address": "Dirección del firmante", + "copy-address": "Copiar dirección", + "placeholder": "Ingrese {{type}} (1-255)", + "m-n": { + "title": "Ingrese la configuración de dirección multifirma en el tipo de m-de-n donde se requiere cualquier m llaves privadas de un posible n para mover el activo", + "m-less-equal-n": "Para la dirección multifirma m-de-n, m no debe ser mayor que n", + "m-n-required": "Ingrese m/n", + "m-n-between-0-256": "m/n debe estar entre 0 y 256" + }, + "multi-address-info": { + "title": "Ingrese los firmantes para la dirección multifirma {{m}}-de-{{n}}", + "view-title": "La dirección multifirma {{m}}-de-{{n}}", + "ckb-address-placeholder": "Ingrese la dirección de ckb" + }, + "multi-list": "Lista de firmantes", + "actions": { + "cancel": "Cancelar", + "back": "Atrás", + "next": "Siguiente", + "generate-address": "Generar dirección", + "confirm": "Confirmar" + }, + "duplicate-address-forbid": "La dirección no puede ser duplicada" + }, + "approve-dialog": { + "title": "Aprobar con dirección multifirma", + "detail": "Aprobar con la dirección multifirma {{m}}-de-{{n}} <0>", + "transaction": "Transacción", + "cancel": "Cancelar", + "signAndExport": "Firmar y exportar", + "export": "Exportar Tx", + "signAndBroadcast": "Firmar y enviar", + "broadcast": "Enviar", + "content": "Contenido:", + "status": "Estado", + "signerApprove": "Falta la aprobación de {{m}} firmante(s), incluidos {{r}} de firmante(s) especificado(s)", + "noRSignerApprove": "Falta la aprobación de {{m}} firmante(s)", + "signed": "Firmado", + "view-concise-data": "Datos concisos", + "view-raw-data": "Datos sin procesar" + } + }, + "dropdown": { + "placeholder": "Por favor seleccione..." + }, + "price-switch": { + "price": "Precio", + "customPrice": "Precio personalizado", + "errorTip": "El precio no puede ser menor que {{minPrice}} shannons/KB, vuelva a ingresar", + "hintTip": "El precio sugerido es {{suggestFeeRate}} shannons/KB", + "priceColumn": "Precio {{priceValue}} shannons/KB | Tarifa {{feeValue}} CKB", + "fast": "Rápido", + "standard": "Estándar", + "slow": "Lento", + "countDownTip": "El precio se actualizará después de {{countDown}} segundos", + "switchToCustomPrice": "Cambio a precio personalizado", + "switchToPrice": "Cambio a precio" + }, + "set-start-block-number": { + "title": "Configurar el número de bloque inicial", + "tip": "El número de bloque de sincronización inicial se configura cuando se utiliza el nodo CKB de cliente ligero, por tanto puede configurarlo y acelerar la sincronización", + "input-place-holder": "Ingrese el número de bloque de sincronización inicial, por ejemplo: 10,100,101", + "locate-first-tx": "Ubicar la primera transacción", + "view-block": "Ver bloque" + }, + "main": { + "external-node-detected-dialog": { + "title": "Nodo externo detectado", + "body-tips-without-network": "\"Internal Node\" está reservado para el nodo CKB incorporado, agregue una nueva opción de red para el nodo externo.", + "body-tips-with-network": "\"Internal Node\" está reservado para el nodo CKB incorporado, seleccione una de las siguientes opciones de red o agregue una nueva para el nodo externo.", + "add-network": "Agregar red", + "ignore-external-node": "Ignorar nodo externo" + } + }, + "cell-manage": { + "title": "Gestionar Cell", + "wallet-balance": "Balance de la billetera", + "table": { + "head": { + "date": "Fecha", + "type": "Tipo", + "balance": "Balance", + "status": "Estado", + "description": "Descripción", + "action": "Acción" + }, + "locked": "Bloqueado", + "unlocked": "Desbloqueado", + "default-description": "Ninguno" + }, + "cell-detail-dialog": { + "title": "Detalles de la Cell", + "capacity-used": "Capacidad utilizada", + "data": "Datos", + "total": "Total", + "used": "Usado" + }, + "locked-reason": { + "multi-locktime": "La Cell tiene un bloqueo de tiempo y no se puede usar hasta {{time}}.", + "multi-locktime-reached": "El bloqueo de tiempo para esta Cell ha expirado y debe reclamarla en Activos personalizados antes de usarla.", + "cheque-acp-multisig": "Esta es una Cell {{type}} y no admite desbloqueo.", + "NFT-SUDT-DAO": "Esta es un activo {{type}} y no admite desbloqueo.", + "Unknown": "Este es un activo no catalogado. Revise y confirme antes de continuar." + }, + "cell-lock-dialog": { + "title": "Bloquear Cell", + "capacity": "Bloquear selección de Cells de {{capacity}} CKB", + "locked-cell-can-not-use": "Advertencia, las Cells bloqueadas no se pueden utilizar." + }, + "cell-unlock-dialog": { + "title": "Desbloquear Cell", + "capacity": "Desbloquear Cell seleccionada {{capacity}} CKB" + }, + "cell-consume-dialog": { + "title": "Consumir Cell", + "warn-consume": "Al consumir la Cell, no se retendrá ningún dato y se utilizará todo el ckb, asegúrese de que no haya datos importantes que necesiten ser retenidos" + }, + "enter-password": "Ingresar Contraseña", + "password-placeholder": "Ingrese la contraseña de la billetera", + "lock": "Bloquear", + "unlock": "Desbloquear", + "consume": "Consumir" + } + } +} \ No newline at end of file diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 9bf5bafb66..98e95bd3e6 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -413,7 +413,8 @@ "en-US": "Anglais (États-Unis)", "zh": "中文(简体)", "zh-TW": "中文(繁體)", - "fr": "Français" + "fr": "Français", + "es": "Español" }, "data": { "ckb-node-data": "Configuration & stockage du noeud CKB", diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index fa8d5b9868..ed8a420952 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -407,7 +407,8 @@ "en-US": "English(United States)", "zh": "中文(简体)", "zh-TW": "中文(繁體)", - "fr": "Français" + "fr": "Français", + "es": "Español" }, "data": { "ckb-node-data": "節點配置和數據", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 6d31935a58..4edaf921b0 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -406,7 +406,8 @@ "en-US": "English(United States)", "zh": "中文(简体)", "zh-TW": "中文(繁體)", - "fr": "Français" + "fr": "Français", + "es": "Español" }, "data": { "ckb-node-data": "节点配置和数据", diff --git a/packages/neuron-ui/src/tests/calendar/index.test.ts b/packages/neuron-ui/src/tests/calendar/index.test.ts index cf7e24dea2..bfac169ca6 100644 --- a/packages/neuron-ui/src/tests/calendar/index.test.ts +++ b/packages/neuron-ui/src/tests/calendar/index.test.ts @@ -110,8 +110,12 @@ describe('Get Local Month Short Names', () => { const names = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'] expect(getLocalMonthShortNames('fr')).toEqual(names) }) -}) + it('Spanish', () => { + const names = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sept', 'oct', 'nov', 'dic'] + expect(getLocalMonthShortNames('es')).toEqual(names) + }) +}) describe('Get Local Month Names', () => { it('Chinese', () => { const names = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] @@ -152,6 +156,24 @@ describe('Get Local Month Names', () => { ] expect(getLocalMonthNames('fr')).toEqual(names) }) + + it('Spanish', () => { + const names = [ + 'enero', + 'febrero', + 'marzo', + 'abril', + 'mayo', + 'junio', + 'julio', + 'agosto', + 'septiembre', + 'octubre', + 'noviembre', + 'diciembre', + ] + expect(getLocalMonthNames('es')).toEqual(names) + }) }) describe('Get Local Week Names', () => { @@ -165,6 +187,11 @@ describe('Get Local Week Names', () => { expect(getLocalWeekNames('en')).toEqual(names) }) + it('Spanish', () => { + const names = ['D', 'L', 'M', 'X', 'J', 'V', 'S'] + expect(getLocalWeekNames('es')).toEqual(names) + }) + it('French', () => { const names = ['D', 'L', 'M', 'M', 'J', 'V', 'S'] expect(getLocalWeekNames('fr')).toEqual(names) diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 624cf75931..7442b63012 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -49,7 +49,7 @@ export const DEFAULT_SUDT_FIELDS = { CKBSymbol: 'CKB', CKBDecimal: '8', } -export const LOCALES = ['zh', 'zh-TW', 'en', 'en-US', 'fr'] as const +export const LOCALES = ['zh', 'zh-TW', 'en', 'en-US', 'fr', 'es'] as const // address property export const SHORT_ADDR_LENGTH = 46 diff --git a/packages/neuron-ui/src/utils/i18n.ts b/packages/neuron-ui/src/utils/i18n.ts index e422f4364d..7202152a5e 100644 --- a/packages/neuron-ui/src/utils/i18n.ts +++ b/packages/neuron-ui/src/utils/i18n.ts @@ -6,11 +6,13 @@ import zh from 'locales/zh.json' import en from 'locales/en.json' import zhTW from 'locales/zh-tw.json' import fr from 'locales/fr.json' +import es from 'locales/es.json' i18n.use(initReactI18next).init({ resources: { en, fr, + es, zh, 'zh-TW': zhTW, }, diff --git a/packages/neuron-wallet/src/locales/es.ts b/packages/neuron-wallet/src/locales/es.ts new file mode 100644 index 0000000000..259a23ee13 --- /dev/null +++ b/packages/neuron-wallet/src/locales/es.ts @@ -0,0 +1,302 @@ +export default { + translation: { + keywords: { + wallet: 'Billetera', + password: 'Contraseña', + 'wallet-name': 'Nombre de la Billetera', + }, + 'application-menu': { + neuron: { + about: 'Acerca de {{app}}', + preferences: 'Preferencias...', + 'check-updates': 'Buscar Actualizaciones...', + quit: 'Salir de {{app}}', + }, + wallet: { + label: 'Billetera', + select: 'Seleccionar Billetera', + 'create-new': 'Crear Nueva Billetera', + import: 'Importar Billetera', + backup: 'Respaldar Billetera Actual', + 'export-xpubkey': 'Exportar Clave Pública Extendida', + delete: 'Eliminar Billetera Actual', + 'change-password': 'Cambiar Contraseña', + 'import-mnemonic': 'Importar Semilla de Billetera', + 'import-keystore': 'Importar desde Keystore', + 'import-xpubkey': 'Importar Clave Pública Extendida', + 'import-hardware': 'Importar Billetera de Hardware', + }, + edit: { + label: 'Editar', + cut: 'Cortar', + copy: 'Copiar', + paste: 'Pegar', + selectall: 'Seleccionar Todo', + }, + tools: { + label: 'Herramientas', + 'sign-and-verify': 'Firmar/Verificar Mensaje', + 'multisig-address': 'Direcciones Multifirma', + 'offline-sign': 'Firma sin Conexión', + 'clear-sync-data': 'Limpiar todos los datos sincronizados', + }, + window: { + label: 'Ventana', + minimize: 'Minimizar', + close: 'Cerrar Ventana', + }, + help: { + label: 'Ayuda', + 'nervos-website': 'Sitio web de Nervos', + 'source-code': 'Código Fuente', + 'report-issue': 'Informar Problema', + 'contact-us': 'Contáctenos', + 'contact-us-message': + '> Por favor, adjunte la información de depuración exportada a través de "Menú" -> "Ayuda" -> "Exportar Información de Depuración".', + documentation: 'Documentación', + settings: 'Configuración', + 'export-debug-info': 'Exportar Información de Depuración', + }, + develop: { + develop: 'Desarrollar', + 'force-reload': 'Forzar Recarga', + reload: 'Recargar', + 'toggle-dev-tools': 'Alternar Herramientas de Desarrollo', + }, + }, + services: { + transactions: 'Transacciones', + wallets: 'Billeteras', + }, + messages: { + 'failed-to-load-networks': 'Error al cargar las redes.', + 'Networks-will-be-reset': 'Las redes se reiniciarán.', + 'wallet-password-less-than-min-length': 'La contraseña debe tener al menos {{minPasswordLength}} caracteres.', + 'wallet-password-more-than-max-length': 'La contraseña debe tener hasta {{maxPasswordLength}} caracteres.', + 'wallet-password-letter-complexity': + 'La contraseña debe contener una combinación de letras mayúsculas y minúsculas, números y símbolos especiales.', + 'current-wallet-not-set': 'La billetera actual no está configurada.', + 'incorrect-password': 'Contraseña incorrecta', + 'invalid-address': 'La dirección {{address}} no es válida.', + 'codehash-not-loaded': 'codehash no está cargado.', + 'wallet-not-found': 'Billetera {{id}} no encontrada.', + 'failed-to-create-mnemonic': 'Error al crear mnemónico.', + 'network-not-found': 'No se encuentra la red con ID {{id}}.', + 'invalid-name': 'El nombre de {{field}} no es válido.', + 'default-network-unremovable': 'La red predeterminada no se puede quitar.', + 'lack-of-default-network': 'Falta la red predeterminada.', + 'current-network-not-set': 'La RPC del nodo CKB actual no ha sido configurada.', + 'transaction-not-found': 'No se encuentra la transacción {{hash}}.', + 'is-required': '{{field}} es obligatorio.', + 'invalid-format': '{{field}} tiene un formato inválido.', + 'used-name': 'El nombre de {{field}} ya está en uso, elija otro.', + 'missing-required-argument': 'Falta el argumento obligatorio.', + 'save-keystore': 'Guardar Keystore.', + 'save-extended-public-key': 'Guardar Clave Pública Extendida.', + 'import-extended-public-key': 'Importar Clave Pública Extendida.', + 'invalid-mnemonic': 'La semilla de la billetera no es válida, por favor, revísela nuevamente.', + 'unsupported-cipher': 'Cifrado no compatible.', + 'capacity-not-enough': 'Saldo insuficiente.', + 'capacity-not-enough-for-change': 'Necesitas más capacidades para el cambio (más de 61 CKBytes).', + 'capacity-not-enough-for-change-by-transfer': + "Necesitas más capacidades para el cambio (más de 61 CKBytes), o haz clic en el botón 'Max' para enviar todo tu saldo.", + 'live-capacity-not-enough': + 'Saldo disponible insuficiente, por favor, inténtalo nuevamente cuando la última transacción haya sido confirmada.', + 'capacity-too-small': 'El saldo mínimo de transferencia es de {{bytes}} CKBytes.', + 'should-be-type-of': '{{field}} debe ser de tipo {{type}}.', + 'invalid-keystore': 'Keystore no válido, por favor, verifica la integridad de tu archivo.', + 'invalid-json': 'Archivo JSON no válido, por favor, verifica la integridad de tu archivo.', + 'cell-is-not-yet-live': 'Por favor, espera hasta que la última transacción sea confirmada por la cadena.', + 'transaction-is-not-committed-yet': + 'No se pueden encontrar Cells requeridas en la cadena, por favor, asegúrate de que las transacciones relacionadas hayan sido confirmadas.', + 'mainnet-address-required': '{{address}} no es una dirección de mainnet.', + 'testnet-address-required': '{{address}} no es una dirección de testnet.', + 'address-not-found': + 'La dirección proporcionada no pertenece a la billetera actual. Por favor, verifica tu billetera o espera a que se complete la sincronización.', + 'target-output-not-found': 'No hay una billetera de cuenta asociada con esta dirección.', + 'acp-same-account': 'La cuenta de pago y la cuenta receptora no deben ser iguales.', + 'device-sign-canceled': + "Has cancelado la solicitud de firma. De lo contrario, asegúrate de que la aplicación Nervos en tu dispositivo tenga la configuración 'permitir datos de contrato' habilitada.", + '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}.', + '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', + 'multisig-config-not-exist': 'La configuración multifirma no existe', + 'multisig-config-exist': 'La configuración multifirma ya existe', + 'multisig-config-address-error': 'La configuración de dirección de la configuración multifirma es incorrecta', + 'multisig-config-need-error': 'La generación de transacciones multifirma requiere configuración multifirma', + 'transaction-no-input-parameter': 'Falta un parámetro requerido en la Cell de entrada de la consulta', + 'migrate-sudt-no-type': 'La Cell de migración no tiene un type script', + 'multisig-not-signed': 'Faltan firmas parciales para transacciones multifirma', + 'multisig-lock-hash-mismatch': + 'La dirección multifirma actual no coincide con la transacción que se va a aprobar', + 'sudt-acp-have-data': 'La cuenta ACP de sUDT destruida tiene saldo', + 'no-match-address-for-sign': 'No se encontró una dirección coincidente', + 'target-lock-error': 'La cuenta de activos CKB solo puede transferirse a una dirección sepe256k1 o acp', + 'no-exist-ckb-node-data': + '{{path}} no tiene configuración y almacenamiento de nodo CKB, presiona confirmar para sincronizar desde cero', + 'light-client-sudt-acp-error': + 'El modo cliente ligero no admite el envío de activos a la cuenta de activos de otra persona', + 'could-not-connect-service': 'No se pudo conectar al servicio, por favor, inténtalo nuevamente más tarde.', + }, + messageBox: { + button: { + confirm: 'Aceptar', + discard: 'Cancelar', + }, + 'clear-sync-data': { + title: 'Borrar todos los datos sincronizados', + message: + 'Borrar todos los datos sincronizados eliminará todos los datos sincronizados locales y volverá a sincronizar los datos en la cadena, la sincronización completa puede llevar mucho tiempo.', + }, + 'send-capacity': { + title: 'Enviar transacción', + }, + 'remove-network': { + title: 'Eliminar red', + message: 'La red {{name}} (dirección: {{address}}) será eliminada.', + alert: 'Esta es la red actual, al eliminarla, la conexión cambiará a la red predeterminada', + }, + 'remove-wallet': { + title: 'Eliminar la billetera', + password: 'Contraseña', + }, + 'backup-keystore': { + title: 'Respaldar el Keystore', + password: 'Contraseña', + }, + transaction: { + title: 'Transacción: {{hash}}', + }, + 'sign-and-verify': { + title: 'Firmar/verificar mensaje', + }, + 'multisig-address': { + title: 'Direcciones Multifirma', + }, + 'ckb-dependency': { + title: 'Nodo CKB empaquetado', + message: 'Se requiere dependencia', + detail: `Los nodos de red en Neuron dependen de componentes C++, así que instale la última versión de Microsoft Visual C++ Redistributable for x64 para garantizar que el software funcione correctamente.`, + buttons: { + 'install-and-exit': 'Instalar y salir', + }, + }, + 'acp-migration': { + title: 'Actualizar cuenta de activos', + message: 'Actualizar cuenta de activos', + detail: + 'Recientemente, nuestro equipo de seguridad identificó una posible vulnerabilidad en el script experimental de la cuenta de activos. Hemos implementado un nuevo script de cuenta de activos con una solución en la red principal y todas las cuentas de activos futuras usarán la nueva versión. Le sugerimos que las actualice para utilizar el nuevo script.', + buttons: { + migrate: 'Actualizar de forma segura ahora', + skip: 'Conozco el riesgo, actualizaré más tarde', + }, + }, + 'acp-migration-completed': { + title: '¡Felicidades! Ha completado la actualización segura.', + message: '¡Felicidades! Ha completado la actualización segura.', + buttons: { + ok: 'Aceptar', + }, + }, + 'hard-fork-migrate': { + message: + 'Para adaptarse a la última versión de CKB, Neuron volverá a sincronizar los datos en la cadena, y la sincronización completa puede llevar mucho tiempo.', + }, + 'mail-us': { + message: + 'Por favor, envíenos un correo con la información de depuración exportada a través de "Menú" -> "Ayuda" -> "Exportar información de depuración".', + 'open-client': 'Abrir cliente de correo', + 'fail-message': + 'No se puede iniciar el cliente de correo, copie la dirección de correo, agregue la información de depuración exportada a través de "Menú" -> "Ayuda" -> "Exportar información de depuración" y envíenosla.', + 'copy-mail-addr': 'Copiar dirección de correo', + }, + 'migrate-failed': { + title: 'Falló la migración', + message: + 'La migración falló, presione Aceptar para eliminar los datos antiguos y sincronizar desde cero, o haga clic en Cancelar para migrar más tarde al reiniciar Neuron. Razón del fallo de la migración: {{ reason }}', + buttons: { + ok: 'Aceptar', + cancel: 'Cancelar', + }, + }, + }, + prompt: { + password: { + label: 'Ingrese su contraseña', + submit: 'Enviar', + cancel: 'Cancelar', + }, + }, + updater: { + 'update-not-available': 'Actualmente no hay actualizaciones disponibles.', + }, + common: { + yes: 'Sí', + no: 'No', + ok: 'OK', + cancel: 'Cancelar', + error: 'Error', + }, + 'export-debug-info': { + 'export-debug-info': 'Exportar Información de Depuración', + 'debug-info-exported': 'La información de depuración se ha exportado a {{ file }}', + }, + about: { + 'app-version': '{{name}} Versión: {{version}}', + 'ckb-client-version': 'Versión del Cliente CKB: {{version}}', + 'ckb-light-client-version': 'Versión del Cliente Ligero CKB: {{version}}', + }, + settings: { + title: { + normal: 'Configuraciones', + mac: 'Preferencias', + }, + }, + 'export-transactions': { + 'export-transactions': 'Exportar Historial de Transacciones', + 'export-success': 'Las transacciones se han exportado', + 'transactions-exported': '{{total}} registros de transacciones se han exportado a {{file}}', + column: { + time: 'Tiempo', + 'block-number': 'Número de Bloque', + 'tx-hash': 'Hash de Transacción', + 'tx-type': 'Tipo de Transacción', + amount: 'Cantidad de CKB', + 'udt-amount': 'Cantidad de UDT', + description: 'Descripción', + }, + 'tx-type': { + send: 'Enviar', + receive: 'Recibir', + 'create-asset-account': 'Crear Cuenta de Activos {{name}}', + 'destroy-asset-account': 'Destruir Cuenta de Activos {{name}}', + }, + }, + 'offline-signature': { + 'export-transaction': 'Exportar Transacción como JSON', + 'transaction-exported': 'La transacción se ha exportado a {{filePath}}.', + 'load-transaction': 'Cargar archivo de transacción', + }, + 'multisig-config': { + 'import-config': 'Importar configuración multifirma', + 'export-config': 'Exportar configuración multifirma', + 'config-exported': 'Las configuraciones multifirma se han exportado a {{filePath}}.', + 'import-duplicate': 'Por favor, verifique configuraciones duplicadas', + 'import-result': 'Importación exitosa {{success}}, fallida {{fail}}.{{failCheck}}', + 'confirm-delete': '¿Confirmar eliminar la configuración multifirma?', + 'approve-tx': 'Confirmar transacción multifirma', + 'delete-actions': { + ok: 'Confirmar', + cancel: 'Cancelar', + }, + }, + 'open-in-explorer': { + title: 'Ver en CKB Explorer', + transaction: 'transacción', + message: 'Ver {{type}} {{key}} en CKB Explorer', + }, + }, +} diff --git a/packages/neuron-wallet/src/locales/i18n.ts b/packages/neuron-wallet/src/locales/i18n.ts index 882ba31cfd..703b1f042c 100644 --- a/packages/neuron-wallet/src/locales/i18n.ts +++ b/packages/neuron-wallet/src/locales/i18n.ts @@ -3,11 +3,13 @@ import zh from './zh' import en from './en' import zhTW from './zh-tw' import fr from './fr' +import es from './es' i18n.init({ resources: { en, fr, + es, zh, 'zh-TW': zhTW, }, diff --git a/packages/neuron-wallet/src/services/settings.ts b/packages/neuron-wallet/src/services/settings.ts index 89f82f35d6..719552dc60 100644 --- a/packages/neuron-wallet/src/services/settings.ts +++ b/packages/neuron-wallet/src/services/settings.ts @@ -9,7 +9,7 @@ import { LIGHT_CLIENT_MAINNET, LIGHT_CLIENT_TESTNET } from '../utils/const' const { app } = env -export const locales = ['zh', 'zh-TW', 'en', 'en-US', 'fr'] as const +export const locales = ['zh', 'zh-TW', 'en', 'en-US', 'fr', 'es'] as const export type Locale = (typeof locales)[number] const settingKeys = { ckbDataPath: 'ckbDataPath', diff --git a/packages/neuron-wallet/tests/services/setting.test.ts b/packages/neuron-wallet/tests/services/setting.test.ts index 96f760ddac..609c5dfe8e 100644 --- a/packages/neuron-wallet/tests/services/setting.test.ts +++ b/packages/neuron-wallet/tests/services/setting.test.ts @@ -73,8 +73,10 @@ describe('SettingsService', () => { it('set', () => { SettingsService.getInstance().locale = 'zh' SettingsService.getInstance().locale = 'fr' + SettingsService.getInstance().locale = 'es' expect(writeSyncMock).toBeCalledWith('locale', 'zh') expect(writeSyncMock).toBeCalledWith('locale', 'fr') + expect(writeSyncMock).toBeCalledWith('locale', 'es') expect(updateApplicationMenuMock).toHaveBeenCalled() }) it('set exception', () => { From f555d5bceedcbff72066e7e96102d0a08e3af5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Thu, 18 Jan 2024 07:56:32 +0000 Subject: [PATCH 6/8] fix: Add learn more link for compatible table (#3023) --- .../neuron-ui/src/containers/Navbar/index.tsx | 27 ++++++++++++++++--- .../src/containers/Navbar/navbar.module.scss | 8 ++++++ packages/neuron-ui/src/locales/en.json | 3 ++- packages/neuron-ui/src/locales/es.json | 3 ++- packages/neuron-ui/src/locales/fr.json | 3 ++- packages/neuron-ui/src/locales/zh-tw.json | 3 ++- packages/neuron-ui/src/locales/zh.json | 3 ++- packages/neuron-ui/src/types/App/index.d.ts | 2 +- .../AlertDialog/alertDialog.module.scss | 2 +- .../src/widgets/AlertDialog/index.tsx | 2 +- 10 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx index 6c38bdbfe7..e7d2a9d14c 100644 --- a/packages/neuron-ui/src/containers/Navbar/index.tsx +++ b/packages/neuron-ui/src/containers/Navbar/index.tsx @@ -1,16 +1,21 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { useLocation, NavLink, useNavigate } from 'react-router-dom' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { NeuronWalletActions, showGlobalAlertDialog, useDispatch, useState as useGlobalState } from 'states' -import { VerifyExternalCkbNodeRes, checkForUpdates, getVersion, verifyExternalCkbNode } from 'services/remote' +import { + VerifyExternalCkbNodeRes, + checkForUpdates, + getVersion, + openExternal, + verifyExternalCkbNode, +} from 'services/remote' import { AppUpdater as AppUpdaterSubject } from 'services/subjects' import Badge from 'widgets/Badge' import Logo from 'widgets/Icons/Logo.png' import { Overview, History, NervosDAO, Settings, Experimental, MenuExpand, ArrowNext } from 'widgets/Icons/icon' import { RoutePath, clsx, isSuccessResponse, useOnLocaleChange } from 'utils' import Tooltip from 'widgets/Tooltip' - import styles from './navbar.module.scss' export const FULL_SCREENS = [`/wizard/`, `/keystore/`, RoutePath.ImportHardware] @@ -107,6 +112,10 @@ const Navbar = () => { } }, [network?.readonly]) + const gotoCompatile = useCallback(() => { + openExternal(`https://neuron.magickbase.com${i18n.language.startsWith('zh') ? '/zh' : ''}/download`) + }, [i18n.language]) + useEffect(() => { // isUpdated is true or version is not empty means check update has return if (!verifyCkbResult || (isUpdated !== true && !version)) { @@ -121,7 +130,17 @@ const Navbar = () => { } else if (!verifyCkbResult.ckbIsCompatible) { showGlobalAlertDialog({ type: 'warning', - message: t('navbar.ckb-node-compatible', { version: getVersion() }), + message: ( + + {t('navbar.learn-more')} + , + ]} + /> + ), action: 'ok', })(dispatch) } else if (!verifyCkbResult.withIndexer) { diff --git a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss index c2ee9f8021..269bd4c26a 100644 --- a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss +++ b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss @@ -198,3 +198,11 @@ $hover-bg-color: #3cc68a4d; top: 0; right: 0; } + +.learnMore { + border: none; + background-color: transparent; + color: var(--primary-color); + padding: 0; + cursor: pointer; +} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index ebb212ab95..338f714db6 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -24,7 +24,8 @@ "experimental-functions": "Experimental", "s-udt": "Asset Accounts", "update-neuron-with-ckb": "The version of the CKB node does not match Neuron (v{{ version }}), which may cause compatibility issues. Please update to the latest version of Neuron.", - "ckb-node-compatible": "CKB node is not compatible with Neuron (v{{ version }}), please check before further operation.", + "learn-more": "Learn More", + "ckb-node-compatible": "CKB node is not compatible with Neuron (v{{ version }}), please check before further operation. (<0>{{btnText}})", "ckb-without-indexer": "Please add '--indexer' option to start local node" }, "network-status": { diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index 651e7d00e0..9909598ea0 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -24,7 +24,8 @@ "experimental-functions": "Experimental", "s-udt": "Cuentas de activos", "update-neuron-with-ckb": "La versión del nodo de CKB no coincide con Neuron (v{{ version }}), lo que puede causar problemas de compatibilidad. Por favor actualice a la última versión de Neuron.", - "ckb-node-compatible": "El nodo CKB no es compatible con Neuron (v{{ version }}), por favor compruébelo antes de continuar.", + "learn-more": "Saber más", + "ckb-node-compatible": "El nodo CKB no es compatible con Neuron (v{{ version }}), por favor compruébelo antes de continuar. (<0>{{btnText}})", "ckb-without-indexer": "Por favor agregue la opción '--indexer' para iniciar el nodo local" }, "network-status": { diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 98e95bd3e6..a4fcb7e2c9 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -24,7 +24,8 @@ "experimental-functions": "Expérimental", "s-udt": "Comptes d'actifs", "update-neuron-with-ckb": "La version du noeud CKB ne correspond pas à Neuron (v{{ version }}), ce qui peut entraîner des problèmes de compatibilité. Veuillez mettre à jour vers la dernière version de Neuron.", - "ckb-node-compatible": "Le noeud CKB n'est pas compatible avec Neuron (v{{ version }}), veuillez vérifier avant toute opération ultérieure.", + "learn-more": "En savoir plus", + "ckb-node-compatible": "Le noeud CKB n'est pas compatible avec Neuron (v{{ version }}), veuillez vérifier avant toute opération ultérieure. (<0>{{btnText}})", "ckb-without-indexer": "Veuillez ajouter l'option '--indexer' pour démarrer le noeud local" }, "network-status": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index ed8a420952..151d07d7dd 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -24,7 +24,8 @@ "experimental-functions": "實驗性功能", "s-udt": "資產賬戶", "update-neuron-with-ckb": "CKB 節點版本與 Neuron (v{{ version }}) 不匹配,可能導致兼容性問題。請更新到最新版 Neuron。", - "ckb-node-compatible": "CKB 節點版本與 Neuron (v{{ version }}) 不兼容,請謹慎使用。", + "learn-more": "了解更多", + "ckb-node-compatible": "CKB 節點版本與 Neuron (v{{ version }}) 不兼容,請謹慎使用。(<0>{{btnText}})", "ckb-without-indexer": "請添加 '--indexer' 參數來啟動本地節點" }, "network-status": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 4edaf921b0..05fdaf61bd 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -24,7 +24,8 @@ "experimental-functions": "实验性功能", "s-udt": "资产账户", "update-neuron-with-ckb": "CKB 节点版本与 Neuron (v{{ version }}) 不匹配,可能导致兼容性问题。请更新到最新版 Neuron。", - "ckb-node-compatible": "CKB 节点版本与 Neuron (v{{ version }}) 不兼容,请谨慎使用。", + "learn-more": "了解更多", + "ckb-node-compatible": "CKB 节点版本与 Neuron (v{{ version }}) 不兼容,请谨慎使用。(<0>{{btnText}})", "ckb-without-indexer": "请添加 '--indexer' 参数来启动本地节点" }, "network-status": { diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 9c1ac509e2..dd3e99b739 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -138,7 +138,7 @@ declare namespace State { type GlobalAlertDialog = { show?: boolean title?: string - message?: string + message?: React.ReactNode type: 'success' | 'failed' | 'warning' action?: 'ok' | 'cancel' | 'all' onClose?: () => void diff --git a/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss b/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss index 28a879e2b2..b89b270757 100644 --- a/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss +++ b/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss @@ -4,7 +4,7 @@ @include dialog-container; padding: 28px 24px 24px; text-align: center; - width: 456px; + width: 480px; &::backdrop { @include overlay; diff --git a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx index eaab451223..4955d0fac2 100644 --- a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx +++ b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx @@ -21,7 +21,7 @@ const AlertDialog = ({ }: { show?: boolean title?: string - message?: string + message?: React.ReactNode type: AlertType onOk?: () => void onCancel?: () => void From e1894de3fa7be8ab16184f5d3358b57e94448509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Fri, 19 Jan 2024 02:23:40 +0000 Subject: [PATCH 7/8] Feat: view chain transaction detail (#2986) * feat: Support view the chain tx detai * chore: Add es and fr transaction * chore: Change `send` to `receive` --- .../HardwareSign/HardwareSignOnPage.tsx | 111 +++++ .../HardwareSign/hardwareSign.module.scss | 12 + .../src/components/HardwareSign/hooks.ts | 425 +++++++++++++++++ .../src/components/HardwareSign/index.tsx | 427 ++---------------- .../src/components/MultisigAddress/index.tsx | 2 - .../components/OfflineSignDialog/index.tsx | 8 +- .../PasswordRequest/PasswordRequestInPage.tsx | 124 +++++ .../src/components/PasswordRequest/hooks.ts | 366 +++++++++++++++ .../src/components/PasswordRequest/index.tsx | 378 ++-------------- .../passwordRequest.module.scss | 9 + .../neuron-ui/src/components/Send/hooks.ts | 41 +- .../components/SendTxDetail/TxTopology.tsx | 129 ++++++ .../src/components/SendTxDetail/hooks.ts | 13 + .../src/components/SendTxDetail/index.tsx | 118 +++++ .../SendTxDetail/sendTxDetail.module.scss | 92 ++++ .../SendTxDetail/txTopology.module.scss | 274 +++++++++++ packages/neuron-ui/src/locales/en.json | 12 + packages/neuron-ui/src/locales/es.json | 14 +- packages/neuron-ui/src/locales/fr.json | 12 + packages/neuron-ui/src/locales/zh-tw.json | 12 + packages/neuron-ui/src/locales/zh.json | 12 + packages/neuron-ui/src/router.tsx | 11 + packages/neuron-ui/src/states/init/chain.ts | 1 - packages/neuron-ui/src/types/App/index.d.ts | 6 +- packages/neuron-ui/src/utils/enums.ts | 1 + .../src/widgets/Tooltip/tooltip.module.scss | 7 + .../neuron-wallet/src/models/chain/input.ts | 11 +- .../neuron-wallet/src/models/chain/output.ts | 1 + packages/neuron-wallet/src/services/cells.ts | 8 +- .../src/services/tx/transaction-generator.ts | 2 + .../tests/services/cells.test.ts | 40 +- 31 files changed, 1908 insertions(+), 771 deletions(-) create mode 100644 packages/neuron-ui/src/components/HardwareSign/HardwareSignOnPage.tsx create mode 100644 packages/neuron-ui/src/components/HardwareSign/hooks.ts create mode 100644 packages/neuron-ui/src/components/PasswordRequest/PasswordRequestInPage.tsx create mode 100644 packages/neuron-ui/src/components/PasswordRequest/hooks.ts create mode 100644 packages/neuron-ui/src/components/SendTxDetail/TxTopology.tsx create mode 100644 packages/neuron-ui/src/components/SendTxDetail/hooks.ts create mode 100644 packages/neuron-ui/src/components/SendTxDetail/index.tsx create mode 100644 packages/neuron-ui/src/components/SendTxDetail/sendTxDetail.module.scss create mode 100644 packages/neuron-ui/src/components/SendTxDetail/txTopology.module.scss diff --git a/packages/neuron-ui/src/components/HardwareSign/HardwareSignOnPage.tsx b/packages/neuron-ui/src/components/HardwareSign/HardwareSignOnPage.tsx new file mode 100644 index 0000000000..50793cdb1d --- /dev/null +++ b/packages/neuron-ui/src/components/HardwareSign/HardwareSignOnPage.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useState as useGlobalState } from 'states' +import { errorFormatter, useGoBack } from 'utils' +import AlertDialog from 'widgets/AlertDialog' +import Button from 'widgets/Button' +import { Export, Sign } from 'widgets/Icons/icon' +import HDWalletSign from '../HDWalletSign' +import useHardwareSign, { HardwareSignProps } from './hooks' +import styles from './hardwareSign.module.scss' + +const HardwareSignOnPage = ({ + signType, + signMessage, + wallet, + onDismiss, + offlineSignJSON, + offlineSignType, + walletID, + actionType, + multisigConfig, + onSuccess, +}: HardwareSignProps & State.PasswordRequest) => { + const { + app: { + send: { description, generatedTx }, + loadings: { sending: isSending = false }, + }, + experimental, + } = useGlobalState() + const [t] = useTranslation() + const onGoBack = useGoBack() + const { + offlineSignActionType, + status, + error, + isLoading, + isNotAvailableToSign, + productName, + signAndExportFromGenerateTx, + sign, + reconnect, + exportTransaction, + } = useHardwareSign({ + signType, + signMessage, + wallet, + offlineSignJSON, + offlineSignType, + description, + generatedTx, + isSending, + passwordRequest: { walletID, actionType, multisigConfig, onSuccess }, + experimental, + }) + + if (error) { + return + } + + return ( +
+
+

+ {t('hardware-sign.device')} + {productName} +

+

+ {t('hardware-sign.status.label')} + {status} +

+ +
{wallet.isHD && generatedTx ? : null}
+ + {offlineSignJSON === undefined && signType === 'transaction' ? ( +
+ +
+ +
+ ) : null} +
+
+
+
+ ) +} + +HardwareSignOnPage.displayName = 'HardwareSignOnPage' + +export default HardwareSignOnPage diff --git a/packages/neuron-ui/src/components/HardwareSign/hardwareSign.module.scss b/packages/neuron-ui/src/components/HardwareSign/hardwareSign.module.scss index 1fe600f938..09baaec176 100644 --- a/packages/neuron-ui/src/components/HardwareSign/hardwareSign.module.scss +++ b/packages/neuron-ui/src/components/HardwareSign/hardwareSign.module.scss @@ -49,3 +49,15 @@ .warning { color: var(--error-color); } + +.hardwareSignInPage { + .container { + width: 100%; + } + .actions { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 24px; + } +} diff --git a/packages/neuron-ui/src/components/HardwareSign/hooks.ts b/packages/neuron-ui/src/components/HardwareSign/hooks.ts new file mode 100644 index 0000000000..e3cdf418c8 --- /dev/null +++ b/packages/neuron-ui/src/components/HardwareSign/hooks.ts @@ -0,0 +1,425 @@ +import { CkbAppNotFoundException, DeviceNotFoundException } from 'exceptions' +import { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { + DeviceInfo, + OfflineSignJSON, + OfflineSignStatus, + OfflineSignType, + connectDevice, + exportTransactionAsJSON, + getDeviceCkbAppVersion, + getDevices, + getPlatform, + signAndExportTransaction, + updateWallet, +} from 'services/remote' +import { ControllerResponse } from 'services/remote/remoteApiWrapper' +import { + AppActions, + migrateAcp, + sendCreateSUDTAccountTransaction, + sendSUDTTransaction, + sendTransaction, + useDispatch, +} from 'states' +import { ErrorCode, RoutePath, errorFormatter, isSuccessResponse, useDidMount } from 'utils' + +export type SignType = 'message' | 'transaction' + +export interface HardwareSignProps { + signType: SignType + wallet: State.WalletIdentity + offlineSignJSON?: OfflineSignJSON + offlineSignType?: OfflineSignType + signMessage?: (password: string) => Promise + onDismiss?: () => void +} + +export default ({ + signType, + signMessage, + wallet, + offlineSignJSON, + offlineSignType, + description, + generatedTx, + isSending, + passwordRequest, + experimental, + onCancel, +}: { + description: string + generatedTx: State.GeneratedTx | null + isSending: boolean + passwordRequest: State.PasswordRequest + experimental: State.Experimental | null + onCancel?: (dismiss: boolean) => void +} & Omit) => { + const { actionType = null, multisigConfig } = passwordRequest + const [t] = useTranslation() + const dispatch = useDispatch() + const navigate = useNavigate() + const isWin32 = useMemo(() => { + return getPlatform() === 'win32' + }, []) + const [status, setStatus] = useState('') + const connectStatus = t('hardware-sign.status.connect') + const userInputStatus = t('hardware-sign.status.user-input') + const disconnectStatus = t('hardware-sign.status.disconnect') + const ckbAppNotFoundStatus = t(CkbAppNotFoundException.message) + const isNotAvailableToSign = useMemo(() => { + return status === disconnectStatus || status === ckbAppNotFoundStatus + }, [status, disconnectStatus, ckbAppNotFoundStatus]) + const [error, setError] = useState('') + const [deviceInfo, setDeviceInfo] = useState(wallet.device!) + const [isReconnecting, setIsReconnecting] = useState(false) + const isLoading = useMemo(() => { + return status === userInputStatus || isReconnecting || isSending + }, [status, userInputStatus, isReconnecting]) + + const productName = `${wallet.device!.manufacturer} ${wallet.device!.product}` + + const offlineSignActionType = useMemo(() => { + switch (offlineSignJSON?.type) { + case OfflineSignType.CreateSUDTAccount: + return 'create-sudt-account' + case OfflineSignType.SendSUDT: + return 'send-sudt' + case OfflineSignType.UnlockDAO: + return 'unlock' + case OfflineSignType.SendFromMultisigOnlySig: + return 'send-from-multisig' + default: + return 'send' + } + }, [offlineSignJSON]) + + const signAndExportFromJSON = useCallback(async () => { + const res = await signAndExportTransaction({ + ...offlineSignJSON!, + walletID: wallet.id, + password: '', + }) + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + } + onCancel?.(!!res.result) + }, [offlineSignJSON, dispatch, onCancel, t, wallet.id]) + + const signAndExportFromGenerateTx = useCallback(async () => { + setStatus(userInputStatus) + const json: OfflineSignJSON = { + transaction: generatedTx || experimental?.tx, + status: OfflineSignStatus.Signed, + type: offlineSignType!, + description, + asset_account: experimental?.assetAccount, + } + const res = await signAndExportTransaction({ + ...json, + walletID: wallet.id, + password: '', + multisigConfig, + }) + setStatus(connectStatus) + if (!isSuccessResponse(res)) { + setStatus(connectStatus) + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + } + onCancel?.(!!res.result) + }, [ + dispatch, + onCancel, + t, + wallet.id, + generatedTx, + userInputStatus, + description, + experimental, + offlineSignType, + connectStatus, + multisigConfig, + ]) + + const ensureDeviceAvailable = useCallback( + async (device: DeviceInfo) => { + try { + const connectionRes = await connectDevice(device) + let { descriptor } = device + if (!isSuccessResponse(connectionRes)) { + // for win32, opening or closing the ckb app changes the HID descriptor(deviceInfo), + // so if we can't connect to the device, we need to re-search device automatically. + // for unix, the descriptor never changes unless user plugs the device into another USB port, + // in that case, mannauly re-search device one time will do. + if (isWin32) { + setIsReconnecting(true) + const devicesRes = await getDevices(device) + setIsReconnecting(false) + if (isSuccessResponse(devicesRes) && Array.isArray(devicesRes.result) && devicesRes.result.length > 0) { + const [updatedDeviceInfo] = devicesRes.result + descriptor = updatedDeviceInfo.descriptor + setDeviceInfo(updatedDeviceInfo) + } else { + throw new DeviceNotFoundException() + } + } else { + throw new DeviceNotFoundException() + } + } + + // getDeviceCkbAppVersion will halt forever while in win32 sleep mode. + const ckbVersionRes = await Promise.race([ + getDeviceCkbAppVersion(descriptor), + new Promise((_, reject) => { + setTimeout(() => reject(), 1000) + }), + ]).catch(() => { + return { status: ErrorCode.DeviceInSleep } + }) + + if (!isSuccessResponse(ckbVersionRes)) { + if (ckbVersionRes.status !== ErrorCode.DeviceInSleep) { + throw new CkbAppNotFoundException() + } else { + throw new DeviceNotFoundException() + } + } + setStatus(connectStatus) + } catch (err) { + if (err instanceof CkbAppNotFoundException) { + setStatus(ckbAppNotFoundStatus) + } else { + setStatus(disconnectStatus) + } + } + }, + [connectStatus, disconnectStatus, ckbAppNotFoundStatus, isWin32] + ) + + const signTx = useCallback(async () => { + try { + await ensureDeviceAvailable(deviceInfo) + setStatus(userInputStatus) + const type = actionType || offlineSignActionType + const tx = offlineSignJSON?.transaction || generatedTx + // eslint-disable-next-line camelcase + const assetAccount = offlineSignJSON?.asset_account ?? experimental?.assetAccount + if (offlineSignJSON !== undefined) { + await signAndExportFromJSON() + return + } + switch (type) { + case 'send': + case 'send-nft': + case 'destroy-asset-account': + case 'send-cheque': + case 'claim-cheque': { + if (isSending) { + break + } + sendTransaction({ + walletID: wallet.id, + tx: tx || experimental?.tx, + description, + })(dispatch).then(res => { + if (isSuccessResponse(res)) { + navigate?.(RoutePath.History) + } else { + setError(res.message) + } + }) + break + } + case 'unlock': { + if (isSending) { + break + } + sendTransaction({ walletID: wallet.id, tx, description })(dispatch).then(res => { + if (isSuccessResponse(res)) { + navigate?.(RoutePath.History) + } else { + setError(res.message) + } + }) + break + } + case 'create-sudt-account': + case 'create-account-to-claim-cheque': { + const params: Controller.SendCreateSUDTAccountTransaction.Params = { + walletID: wallet.id, + assetAccount, + tx: tx || experimental?.tx, + } + sendCreateSUDTAccountTransaction(params)(dispatch).then(res => { + if (isSuccessResponse(res)) { + navigate?.(RoutePath.History) + } else { + setError(res.message) + } + }) + break + } + case 'send-ckb-asset': + case 'send-acp-sudt-to-new-cell': + case 'send-acp-ckb-to-new-cell': + case 'send-sudt': { + let skipLastInputs = true + if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { + skipLastInputs = false + } + const params: Controller.SendSUDTTransaction.Params = { + walletID: wallet.id, + tx: tx || experimental?.tx, + skipLastInputs, + } + sendSUDTTransaction(params)(dispatch).then(res => { + if (isSuccessResponse(res)) { + navigate?.(RoutePath.History) + } else { + setError(res.message) + } + }) + break + } + case 'migrate-acp': { + await migrateAcp({ id: wallet.id })(dispatch).then(res => { + if (isSuccessResponse(res)) { + navigate?.(RoutePath.History) + } else { + setError(typeof res.message === 'string' ? res.message : res.message.content ?? 'migrate-acp error') + } + }) + break + } + case 'send-from-multisig-need-one': { + if (isSending) { + break + } + await sendTransaction({ walletID: wallet.id, tx: generatedTx, description, multisigConfig })(dispatch).then( + res => { + if (!isSuccessResponse(res)) { + setError(res.message.content) + } + } + ) + break + } + default: { + break + } + } + } catch (err) { + setStatus(disconnectStatus) + } + }, [ + actionType, + offlineSignActionType, + userInputStatus, + disconnectStatus, + experimental, + generatedTx, + offlineSignJSON, + isSending, + deviceInfo, + wallet.id, + description, + dispatch, + navigate, + signAndExportFromJSON, + ensureDeviceAvailable, + multisigConfig, + ]) + + const signMsg = useCallback(async () => { + await ensureDeviceAvailable(deviceInfo) + setStatus(userInputStatus) + await signMessage?.('') + }, [ensureDeviceAvailable, signMessage, deviceInfo, userInputStatus]) + + const sign = useCallback( + async (e?: React.FormEvent) => { + if (e) { + e.preventDefault() + } + if (signType === 'message') { + await signMsg() + } else { + await signTx() + } + }, + [signType, signTx, signMsg] + ) + + const reconnect = useCallback(async () => { + setIsReconnecting(true) + try { + const res = await getDevices(deviceInfo) + if (isSuccessResponse(res) && Array.isArray(res.result) && res.result.length > 0) { + const [device] = res.result + setDeviceInfo(device) + if (device.descriptor !== deviceInfo.descriptor) { + await updateWallet({ + id: wallet.id, + device, + }) + } + await ensureDeviceAvailable(device) + } + } catch (err) { + setStatus(disconnectStatus) + } finally { + setIsReconnecting(false) + } + }, [deviceInfo, disconnectStatus, ensureDeviceAvailable, wallet.id]) + + const exportTransaction = useCallback(async () => { + const res = await exportTransactionAsJSON({ + transaction: generatedTx || experimental?.tx, + status: OfflineSignStatus.Unsigned, + type: offlineSignType!, + description, + asset_account: experimental?.assetAccount, + }) + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + onCancel?.(!!res.result) + }, [offlineSignType, generatedTx, onCancel, description, experimental]) + + useDidMount(() => { + ensureDeviceAvailable(deviceInfo) + }) + + return { + offlineSignActionType, + status, + error, + isLoading, + isNotAvailableToSign, + productName, + signAndExportFromJSON, + signAndExportFromGenerateTx, + signTx, + signMsg, + sign, + reconnect, + exportTransaction, + } +} diff --git a/packages/neuron-ui/src/components/HardwareSign/index.tsx b/packages/neuron-ui/src/components/HardwareSign/index.tsx index 49c8e7264e..937d9b084f 100644 --- a/packages/neuron-ui/src/components/HardwareSign/index.tsx +++ b/packages/neuron-ui/src/components/HardwareSign/index.tsx @@ -1,59 +1,31 @@ -import React, { useCallback, useState, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { - useState as useGlobalState, - useDispatch, - sendTransaction, - sendCreateSUDTAccountTransaction, - sendSUDTTransaction, - AppActions, - migrateAcp, -} from 'states' -import { ControllerResponse } from 'services/remote/remoteApiWrapper' -import { NavigateFunction } from 'react-router-dom' -import { - connectDevice, - getDevices, - exportTransactionAsJSON, - OfflineSignStatus, - OfflineSignType, - OfflineSignJSON, - signAndExportTransaction, - getDeviceCkbAppVersion, - DeviceInfo, - updateWallet, - getPlatform, -} from 'services/remote' -import { ErrorCode, errorFormatter, isSuccessResponse, RoutePath, useDidMount } from 'utils' +import React, { useCallback } from 'react' +import { AppActions, useDispatch, useState as useGlobalState } from 'states' +import { errorFormatter } from 'utils' import Dialog from 'widgets/Dialog' import AlertDialog from 'widgets/AlertDialog' import Button from 'widgets/Button' import { Export, Sign } from 'widgets/Icons/icon' -import { CkbAppNotFoundException, DeviceNotFoundException } from 'exceptions' +import { useTranslation } from 'react-i18next' import HDWalletSign from '../HDWalletSign' import styles from './hardwareSign.module.scss' - -export type SignType = 'message' | 'transaction' - -export interface HardwareSignProps { - signType: SignType - wallet: State.WalletIdentity - offlineSignJSON?: OfflineSignJSON - offlineSignType?: OfflineSignType - onDismiss: () => void - signMessage?: (password: string) => Promise - navigate?: NavigateFunction -} +import useHardwareSign, { HardwareSignProps } from './hooks' const HardwareSign = ({ signType, signMessage, - navigate, wallet, onDismiss, offlineSignJSON, offlineSignType, }: HardwareSignProps) => { + const { + app: { + send: { description, generatedTx }, + loadings: { sending: isSending = false }, + passwordRequest, + }, + experimental, + } = useGlobalState() const [t] = useTranslation() const dispatch = useDispatch() const onCancel = useCallback( @@ -65,366 +37,35 @@ const HardwareSign = ({ }) } if (dismiss) { - onDismiss() + onDismiss?.() } }, [dispatch, signType, onDismiss] ) - const isWin32 = useMemo(() => { - return getPlatform() === 'win32' - }, []) - const [status, setStatus] = useState('') - const connectStatus = t('hardware-sign.status.connect') - const userInputStatus = t('hardware-sign.status.user-input') - const disconnectStatus = t('hardware-sign.status.disconnect') - const ckbAppNotFoundStatus = t(CkbAppNotFoundException.message) - const isNotAvailableToSign = useMemo(() => { - return status === disconnectStatus || status === ckbAppNotFoundStatus - }, [status, disconnectStatus, ckbAppNotFoundStatus]) - const { - app: { - send: { description, generatedTx }, - loadings: { sending: isSending = false }, - passwordRequest: { actionType = null, multisigConfig }, - }, - experimental, - } = useGlobalState() - const [error, setError] = useState('') - const [deviceInfo, setDeviceInfo] = useState(wallet.device!) - const [isReconnecting, setIsReconnecting] = useState(false) - const isLoading = useMemo(() => { - return status === userInputStatus || isReconnecting || isSending - }, [status, userInputStatus, isReconnecting]) - - const productName = `${wallet.device!.manufacturer} ${wallet.device!.product}` - - const offlineSignActionType = useMemo(() => { - switch (offlineSignJSON?.type) { - case OfflineSignType.CreateSUDTAccount: - return 'create-sudt-account' - case OfflineSignType.SendSUDT: - return 'send-sudt' - case OfflineSignType.UnlockDAO: - return 'unlock' - case OfflineSignType.SendFromMultisigOnlySig: - return 'send-from-multisig' - default: - return 'send' - } - }, [offlineSignJSON]) - - const signAndExportFromJSON = useCallback(async () => { - const res = await signAndExportTransaction({ - ...offlineSignJSON!, - walletID: wallet.id, - password: '', - }) - if (!isSuccessResponse(res)) { - setError(errorFormatter(res.message, t)) - return - } - if (res.result) { - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) - } - onCancel(!!res.result) - }, [offlineSignJSON, dispatch, onCancel, t, wallet.id]) - - const signAndExportFromGenerateTx = useCallback(async () => { - setStatus(userInputStatus) - const json: OfflineSignJSON = { - transaction: generatedTx || experimental?.tx, - status: OfflineSignStatus.Signed, - type: offlineSignType!, - description, - asset_account: experimental?.assetAccount, - } - const res = await signAndExportTransaction({ - ...json, - walletID: wallet.id, - password: '', - multisigConfig, - }) - setStatus(connectStatus) - if (!isSuccessResponse(res)) { - setStatus(connectStatus) - setError(errorFormatter(res.message, t)) - return - } - if (res.result) { - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) - } - onCancel(!!res.result) - }, [ - dispatch, - onCancel, - t, - wallet.id, - generatedTx, - userInputStatus, - description, - experimental, - offlineSignType, - connectStatus, - multisigConfig, - ]) - - const ensureDeviceAvailable = useCallback( - async (device: DeviceInfo) => { - try { - const connectionRes = await connectDevice(device) - let { descriptor } = device - if (!isSuccessResponse(connectionRes)) { - // for win32, opening or closing the ckb app changes the HID descriptor(deviceInfo), - // so if we can't connect to the device, we need to re-search device automatically. - // for unix, the descriptor never changes unless user plugs the device into another USB port, - // in that case, mannauly re-search device one time will do. - if (isWin32) { - setIsReconnecting(true) - const devicesRes = await getDevices(device) - setIsReconnecting(false) - if (isSuccessResponse(devicesRes) && Array.isArray(devicesRes.result) && devicesRes.result.length > 0) { - const [updatedDeviceInfo] = devicesRes.result - descriptor = updatedDeviceInfo.descriptor - setDeviceInfo(updatedDeviceInfo) - } else { - throw new DeviceNotFoundException() - } - } else { - throw new DeviceNotFoundException() - } - } - - // getDeviceCkbAppVersion will halt forever while in win32 sleep mode. - const ckbVersionRes = await Promise.race([ - getDeviceCkbAppVersion(descriptor), - new Promise((_, reject) => { - setTimeout(() => reject(), 1000) - }), - ]).catch(() => { - return { status: ErrorCode.DeviceInSleep } - }) - - if (!isSuccessResponse(ckbVersionRes)) { - if (ckbVersionRes.status !== ErrorCode.DeviceInSleep) { - throw new CkbAppNotFoundException() - } else { - throw new DeviceNotFoundException() - } - } - setStatus(connectStatus) - } catch (err) { - if (err instanceof CkbAppNotFoundException) { - setStatus(ckbAppNotFoundStatus) - } else { - setStatus(disconnectStatus) - } - } - }, - [connectStatus, disconnectStatus, ckbAppNotFoundStatus, isWin32] - ) - - const signTx = useCallback(async () => { - try { - await ensureDeviceAvailable(deviceInfo) - setStatus(userInputStatus) - const type = actionType || offlineSignActionType - const tx = offlineSignJSON?.transaction || generatedTx - // eslint-disable-next-line camelcase - const assetAccount = offlineSignJSON?.asset_account ?? experimental?.assetAccount - if (offlineSignJSON !== undefined) { - await signAndExportFromJSON() - return - } - switch (type) { - case 'send': - case 'send-nft': - case 'destroy-asset-account': - case 'send-cheque': - case 'claim-cheque': { - if (isSending) { - break - } - sendTransaction({ - walletID: wallet.id, - tx: tx || experimental?.tx, - description, - })(dispatch).then(res => { - if (isSuccessResponse(res)) { - navigate?.(RoutePath.History) - } else { - setError(res.message) - } - }) - break - } - case 'unlock': { - if (isSending) { - break - } - sendTransaction({ walletID: wallet.id, tx, description })(dispatch).then(res => { - if (isSuccessResponse(res)) { - navigate?.(RoutePath.History) - } else { - setError(res.message) - } - }) - break - } - case 'create-sudt-account': - case 'create-account-to-claim-cheque': { - const params: Controller.SendCreateSUDTAccountTransaction.Params = { - walletID: wallet.id, - assetAccount, - tx: tx || experimental?.tx, - } - sendCreateSUDTAccountTransaction(params)(dispatch).then(res => { - if (isSuccessResponse(res)) { - navigate?.(RoutePath.History) - } else { - setError(res.message) - } - }) - break - } - case 'send-ckb-asset': - case 'send-acp-sudt-to-new-cell': - case 'send-acp-ckb-to-new-cell': - case 'send-sudt': { - let skipLastInputs = true - if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { - skipLastInputs = false - } - const params: Controller.SendSUDTTransaction.Params = { - walletID: wallet.id, - tx: tx || experimental?.tx, - skipLastInputs, - } - sendSUDTTransaction(params)(dispatch).then(res => { - if (isSuccessResponse(res)) { - navigate?.(RoutePath.History) - } else { - setError(res.message) - } - }) - break - } - case 'migrate-acp': { - await migrateAcp({ id: wallet.id })(dispatch).then(res => { - if (isSuccessResponse(res)) { - navigate?.(RoutePath.History) - } else { - setError(typeof res.message === 'string' ? res.message : res.message.content ?? 'migrate-acp error') - } - }) - break - } - case 'send-from-multisig-need-one': { - if (isSending) { - break - } - await sendTransaction({ walletID: wallet.id, tx: generatedTx, description, multisigConfig })(dispatch).then( - res => { - if (!isSuccessResponse(res)) { - setError(res.message.content) - } - } - ) - break - } - default: { - break - } - } - } catch (err) { - setStatus(disconnectStatus) - } - }, [ - actionType, offlineSignActionType, - userInputStatus, - disconnectStatus, - experimental, - generatedTx, + status, + error, + isLoading, + isNotAvailableToSign, + productName, + signAndExportFromGenerateTx, + sign, + reconnect, + exportTransaction, + } = useHardwareSign({ + signType, + signMessage, + wallet, offlineSignJSON, - isSending, - deviceInfo, - wallet.id, + offlineSignType, description, - dispatch, - navigate, - signAndExportFromJSON, - ensureDeviceAvailable, - multisigConfig, - ]) - - const signMsg = useCallback(async () => { - await ensureDeviceAvailable(deviceInfo) - setStatus(userInputStatus) - await signMessage?.('') - }, [ensureDeviceAvailable, signMessage, deviceInfo, userInputStatus]) - - const sign = useCallback( - async (e?: React.FormEvent) => { - if (e) { - e.preventDefault() - } - if (signType === 'message') { - await signMsg() - } else { - await signTx() - } - }, - [signType, signTx, signMsg] - ) - - const reconnect = useCallback(async () => { - setIsReconnecting(true) - try { - const res = await getDevices(deviceInfo) - if (isSuccessResponse(res) && Array.isArray(res.result) && res.result.length > 0) { - const [device] = res.result - setDeviceInfo(device) - if (device.descriptor !== deviceInfo.descriptor) { - await updateWallet({ - id: wallet.id, - device, - }) - } - await ensureDeviceAvailable(device) - } - } catch (err) { - setStatus(disconnectStatus) - } finally { - setIsReconnecting(false) - } - }, [deviceInfo, disconnectStatus, ensureDeviceAvailable, wallet.id]) - - const exportTransaction = useCallback(async () => { - const res = await exportTransactionAsJSON({ - transaction: generatedTx || experimental?.tx, - status: OfflineSignStatus.Unsigned, - type: offlineSignType!, - description, - asset_account: experimental?.assetAccount, - }) - if (!isSuccessResponse(res)) { - setError(errorFormatter(res.message, t)) - return - } - onCancel(!!res.result) - }, [offlineSignType, generatedTx, onCancel, description, experimental]) - - useDidMount(() => { - ensureDeviceAvailable(deviceInfo) + generatedTx, + isSending, + passwordRequest, + experimental, + onCancel, }) - if (error) { return } @@ -434,7 +75,7 @@ const HardwareSign = ({ show title={t('hardware-sign.title')} onCancel={onCancel} - showConfirm={(actionType || offlineSignActionType) !== 'send-from-multisig'} + showConfirm={(passwordRequest.actionType || offlineSignActionType) !== 'send-from-multisig'} confirmText={isNotAvailableToSign ? t('hardware-sign.actions.rescan') : t('sign-and-verify.sign')} isLoading={isLoading} onConfirm={isNotAvailableToSign ? reconnect : sign} diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index d6d38e702f..262fbe8596 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -12,7 +12,6 @@ import MultisigAddressCreateDialog from 'components/MultisigAddressCreateDialog' import MultisigAddressInfo from 'components/MultisigAddressInfo' import SendFromMultisigDialog from 'components/SendFromMultisigDialog' import { MultisigConfig } from 'services/remote' -import PasswordRequest from 'components/PasswordRequest' import ApproveMultisigTxDialog from 'components/ApproveMultisigTxDialog' import Dialog from 'widgets/Dialog' import Table from 'widgets/Table' @@ -383,7 +382,6 @@ const MultisigAddress = () => { isMainnet={isMainnet} /> ) : null} -
) } diff --git a/packages/neuron-ui/src/components/OfflineSignDialog/index.tsx b/packages/neuron-ui/src/components/OfflineSignDialog/index.tsx index ee497aeb65..a1f3179c6e 100644 --- a/packages/neuron-ui/src/components/OfflineSignDialog/index.tsx +++ b/packages/neuron-ui/src/components/OfflineSignDialog/index.tsx @@ -188,13 +188,7 @@ const OfflineSignDialog = ({ isBroadcast, wallet, offlineSignJSON, onDismiss }: if (wallet.device) { return ( - + ) } diff --git a/packages/neuron-ui/src/components/PasswordRequest/PasswordRequestInPage.tsx b/packages/neuron-ui/src/components/PasswordRequest/PasswordRequestInPage.tsx new file mode 100644 index 0000000000..4f12d12088 --- /dev/null +++ b/packages/neuron-ui/src/components/PasswordRequest/PasswordRequestInPage.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import Button from 'widgets/Button' +import TextField from 'widgets/TextField' +import { ReactComponent as Attention } from 'widgets/Icons/ExperimentalAttention.svg' +import HardwareSignOnPage from 'components/HardwareSign/HardwareSignOnPage' +import { Export, Sign } from 'widgets/Icons/icon' +import { useState as useGlobalState } from 'states' +import { OfflineSignType } from 'services/remote' +import { useTranslation } from 'react-i18next' +import { useGoBack } from 'utils' +import usePasswordResuest from './hooks' +import styles from './passwordRequest.module.scss' + +const PasswordRequestInPage = ({ + walletID = '', + actionType = null, + multisigConfig, + onSuccess, + onCancel, +}: State.PasswordRequest & { onCancel?: () => void }) => { + const { + app: { + send: { description, generatedTx }, + loadings: { sending: isSending = false }, + }, + settings: { wallets = [] }, + experimental, + } = useGlobalState() + + const [t] = useTranslation() + const onGoBack = useGoBack() + + const { + error, + wallet, + isLoading, + signType, + disabled, + password, + onSubmit, + onChange, + exportTransaction, + signAndExportFromGenerateTx, + } = usePasswordResuest({ + description, + generatedTx, + isSending, + passwordRequest: { walletID, actionType, multisigConfig, onSuccess }, + wallets, + experimental, + }) + + if (!wallet) { + return null + } + + if (wallet.device) { + return ( + + ) + } + + return ( +
+ {wallet.isWatchOnly ? ( +
+ + {t('password-request.xpub-notice')} +
+ ) : ( + + )} + {signType !== OfflineSignType.Invalid ? ( +
+ + {!wallet.isWatchOnly && ( + <> +
+ + + )} +
+ ) : null} +
+
+
+ ) +} + +PasswordRequestInPage.displayName = 'PasswordRequestInPage' +export default PasswordRequestInPage diff --git a/packages/neuron-ui/src/components/PasswordRequest/hooks.ts b/packages/neuron-ui/src/components/PasswordRequest/hooks.ts new file mode 100644 index 0000000000..db51eb2835 --- /dev/null +++ b/packages/neuron-ui/src/components/PasswordRequest/hooks.ts @@ -0,0 +1,366 @@ +import { PasswordIncorrectException } from 'exceptions' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { + OfflineSignStatus, + OfflineSignType, + exportTransactionAsJSON, + invokeShowErrorMessage, + requestOpenInExplorer, + signAndExportTransaction, +} from 'services/remote' +import { + AppActions, + backupWallet, + deleteWallet, + migrateAcp, + sendCreateSUDTAccountTransaction, + sendSUDTTransaction, + sendTransaction, + useDispatch, +} from 'states' +import { ErrorCode, RoutePath, errorFormatter, isSuccessResponse } from 'utils' + +export default ({ + description, + generatedTx, + isSending, + passwordRequest, + wallets, + experimental, + onDismiss, +}: { + description: string + generatedTx: State.GeneratedTx | null + isSending: boolean + passwordRequest: State.PasswordRequest + wallets: State.WalletIdentity[] + experimental: State.Experimental | null + onDismiss?: () => void +}) => { + const dispatch = useDispatch() + const [t] = useTranslation() + const navigate = useNavigate() + + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const { walletID = '', actionType = null, multisigConfig, onSuccess } = passwordRequest + + useEffect(() => { + setPassword('') + setError('') + }, [actionType, setError, setPassword]) + + const signType = useMemo(() => { + switch (actionType) { + case 'create-sudt-account': + return OfflineSignType.CreateSUDTAccount + case 'send-ckb-asset': + case 'send-acp-ckb-to-new-cell': + case 'send-acp-sudt-to-new-cell': + case 'transfer-to-sudt': + case 'send-sudt': + return OfflineSignType.SendSUDT + case 'unlock': + return OfflineSignType.UnlockDAO + case 'send-nft': + case 'send-from-multisig-need-one': + case 'send': + return OfflineSignType.Regular + case 'send-from-multisig': + return OfflineSignType.SendFromMultisigOnlySig + default: + return OfflineSignType.Invalid + } + }, [actionType]) + + const exportTransaction = useCallback(async () => { + const res = await exportTransactionAsJSON({ + transaction: generatedTx || experimental?.tx, + status: OfflineSignStatus.Unsigned, + type: signType, + description, + asset_account: experimental?.assetAccount, + }) + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + onDismiss?.() + } + }, [signType, generatedTx, onDismiss, description, experimental]) + + const wallet = useMemo(() => wallets.find(w => w.id === walletID), [walletID, wallets]) + + const isLoading = + [ + 'send', + 'unlock', + 'create-sudt-account', + 'send-sudt', + 'transfer-to-sudt', + 'send-ckb-asset', + 'send-acp-ckb-to-new-cell', + 'send-acp-sudt-to-new-cell', + 'send-cheque', + 'withdraw-cheque', + 'claim-cheque', + 'create-account-to-claim-cheque', + 'send-from-multisig-need-one', + 'send-from-multisig', + 'destroy-asset-account', + ].includes(actionType || '') && isSending + const disabled = !password || isSending + + const onSubmit = useCallback( + async (e?: React.FormEvent) => { + if (e) { + e.preventDefault() + } + if (disabled) { + return + } + const handleSendTxRes = ({ status }: { status: number }) => { + if (isSuccessResponse({ status })) { + if (onSuccess) { + onSuccess() + return + } + navigate(RoutePath.History) + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + } + try { + switch (actionType) { + case 'send': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(handleSendTxRes) + break + } + case 'send-from-multisig-need-one': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: generatedTx, description, password, multisigConfig })(dispatch).then( + (res: { result?: string; status: number; message: string | { content: string } }) => { + if (isSuccessResponse(res)) { + requestOpenInExplorer({ type: 'transaction', key: res.result }) + } else if (res.status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } else { + invokeShowErrorMessage({ + title: t('messages.error'), + content: typeof res.message === 'string' ? res.message : res.message.content!, + }) + } + } + ) + break + } + case 'delete': { + await deleteWallet({ id: walletID, password })(dispatch).then(status => { + if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'backup': { + await backupWallet({ id: walletID, password })(dispatch).then(status => { + if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'migrate-acp': { + await migrateAcp({ id: walletID, password })(dispatch).then(({ status }) => { + if (isSuccessResponse({ status })) { + navigate(RoutePath.History) + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'unlock': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(({ status }) => { + if (isSuccessResponse({ status })) { + dispatch({ + type: AppActions.SetGlobalDialog, + payload: 'unlock-success', + }) + onSuccess?.() + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'create-sudt-account': { + const params: Controller.SendCreateSUDTAccountTransaction.Params = { + walletID, + assetAccount: experimental?.assetAccount, + tx: experimental?.tx, + password, + } + await sendCreateSUDTAccountTransaction(params)(dispatch).then(handleSendTxRes) + break + } + case 'send-ckb-asset': + case 'send-acp-ckb-to-new-cell': + case 'send-acp-sudt-to-new-cell': + case 'send-sudt': + case 'transfer-to-sudt': { + let skipLastInputs = true + if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { + skipLastInputs = false + } + const params: Controller.SendSUDTTransaction.Params = { + walletID, + tx: experimental?.tx, + password, + skipLastInputs, + } + await sendSUDTTransaction(params)(dispatch).then(handleSendTxRes) + break + } + case 'destroy-asset-account': + case 'send-nft': + case 'send-cheque': { + if (isSending) { + break + } + await sendTransaction({ + walletID, + tx: experimental?.tx, + description: experimental?.tx?.description, + password, + })(dispatch).then(handleSendTxRes) + break + } + case 'claim-cheque': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: experimental?.tx, password })(dispatch).then(handleSendTxRes) + break + } + case 'create-account-to-claim-cheque': { + if (isSending) { + break + } + await sendCreateSUDTAccountTransaction({ + walletID, + password, + tx: experimental?.tx, + assetAccount: { + ...experimental?.assetAccount, + tokenID: experimental?.assetAccount.tokenId, + balance: '0', + }, + })(dispatch).then(handleSendTxRes) + break + } + case 'withdraw-cheque': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: experimental?.tx, password })(dispatch).then(handleSendTxRes) + break + } + default: { + break + } + } + } catch (err) { + if (err instanceof PasswordIncorrectException) { + setError(t(err.message)) + } + } + }, + [ + dispatch, + walletID, + password, + actionType, + description, + navigate, + isSending, + generatedTx, + disabled, + experimental, + setError, + t, + multisigConfig, + ] + ) + + const onChange = useCallback( + (e: React.SyntheticEvent) => { + const { value } = e.target as HTMLInputElement + setPassword(value) + setError('') + }, + [setPassword, setError] + ) + + const signAndExportFromGenerateTx = useCallback(async () => { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { + sending: true, + }, + }) + const json = { + transaction: generatedTx || experimental?.tx, + status: OfflineSignStatus.Signed, + type: signType, + description, + asset_account: experimental?.assetAccount, + } + const res = await signAndExportTransaction({ + ...json, + walletID, + password, + multisigConfig, + }) + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + onDismiss?.() + } + }, [description, dispatch, experimental, generatedTx, onDismiss, password, signType, t, walletID, multisigConfig]) + + return { + error, + wallet, + isLoading, + signType, + actionType, + disabled, + password, + onSubmit, + onChange, + exportTransaction, + signAndExportFromGenerateTx, + } +} diff --git a/packages/neuron-ui/src/components/PasswordRequest/index.tsx b/packages/neuron-ui/src/components/PasswordRequest/index.tsx index e653f84423..1310ccc45f 100644 --- a/packages/neuron-ui/src/components/PasswordRequest/index.tsx +++ b/packages/neuron-ui/src/components/PasswordRequest/index.tsx @@ -1,34 +1,14 @@ -import React, { useState, useCallback, useMemo, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import TextField from 'widgets/TextField' import HardwareSign from 'components/HardwareSign' import { ReactComponent as Attention } from 'widgets/Icons/ExperimentalAttention.svg' -import { ErrorCode, RoutePath, isSuccessResponse, errorFormatter } from 'utils' import Dialog from 'widgets/Dialog' import { Export, Sign } from 'widgets/Icons/icon' - -import { - useState as useGlobalState, - useDispatch, - AppActions, - sendTransaction, - deleteWallet, - backupWallet, - migrateAcp, - sendCreateSUDTAccountTransaction, - sendSUDTTransaction, -} from 'states' -import { - exportTransactionAsJSON, - OfflineSignStatus, - OfflineSignType, - signAndExportTransaction, - requestOpenInExplorer, - invokeShowErrorMessage, -} from 'services/remote' -import { PasswordIncorrectException } from 'exceptions' +import { AppActions, useDispatch, useState as useGlobalState } from 'states' +import { OfflineSignType } from 'services/remote' +import usePasswordResuest from './hooks' import styles from './passwordRequest.module.scss' const PasswordRequest = () => { @@ -36,343 +16,47 @@ const PasswordRequest = () => { app: { send: { description, generatedTx }, loadings: { sending: isSending = false }, - passwordRequest: { walletID = '', actionType = null, multisigConfig, onSuccess }, + passwordRequest, }, settings: { wallets = [] }, experimental, - wallet: currentWallet, } = useGlobalState() - const dispatch = useDispatch() const [t] = useTranslation() - const navigate = useNavigate() - - const [password, setPassword] = useState('') - const [error, setError] = useState('') - - useEffect(() => { - setPassword('') - setError('') - }, [actionType, setError, setPassword]) - + const dispatch = useDispatch() const onDismiss = useCallback(() => { dispatch({ type: AppActions.DismissPasswordRequest, }) }, [dispatch]) - const signType = useMemo(() => { - switch (actionType) { - case 'create-sudt-account': - return OfflineSignType.CreateSUDTAccount - case 'send-ckb-asset': - case 'send-acp-ckb-to-new-cell': - case 'send-acp-sudt-to-new-cell': - case 'transfer-to-sudt': - case 'send-sudt': - return OfflineSignType.SendSUDT - case 'unlock': - return OfflineSignType.UnlockDAO - case 'send-nft': - case 'send-from-multisig-need-one': - case 'send': - return OfflineSignType.Regular - case 'send-from-multisig': - return OfflineSignType.SendFromMultisigOnlySig - default: - return OfflineSignType.Invalid - } - }, [actionType]) - - const exportTransaction = useCallback(async () => { - const res = await exportTransactionAsJSON({ - transaction: generatedTx || experimental?.tx, - status: OfflineSignStatus.Unsigned, - type: signType, - description, - asset_account: experimental?.assetAccount, - }) - if (!isSuccessResponse(res)) { - setError(errorFormatter(res.message, t)) - return - } - if (res.result) { - onDismiss() - } - }, [signType, generatedTx, onDismiss, description, experimental]) - - const wallet = useMemo(() => wallets.find(w => w.id === walletID), [walletID, wallets]) - - const isLoading = - [ - 'send', - 'unlock', - 'create-sudt-account', - 'send-sudt', - 'transfer-to-sudt', - 'send-ckb-asset', - 'send-acp-ckb-to-new-cell', - 'send-acp-sudt-to-new-cell', - 'send-cheque', - 'withdraw-cheque', - 'claim-cheque', - 'create-account-to-claim-cheque', - 'send-from-multisig-need-one', - 'send-from-multisig', - 'destroy-asset-account', - ].includes(actionType || '') && isSending - const disabled = !password || isSending - - const onSubmit = useCallback( - async (e?: React.FormEvent) => { - if (e) { - e.preventDefault() - } - if (disabled) { - return - } - const handleSendTxRes = ({ status }: { status: number }) => { - if (isSuccessResponse({ status })) { - if (onSuccess) { - onSuccess() - return - } - navigate(RoutePath.History) - } else if (status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } - } - try { - switch (actionType) { - case 'send': { - if (isSending) { - break - } - await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(handleSendTxRes) - break - } - case 'send-from-multisig-need-one': { - if (isSending) { - break - } - await sendTransaction({ walletID, tx: generatedTx, description, password, multisigConfig })(dispatch).then( - (res: { result?: string; status: number; message: string | { content: string } }) => { - if (isSuccessResponse(res)) { - requestOpenInExplorer({ type: 'transaction', key: res.result }) - } else if (res.status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } else { - invokeShowErrorMessage({ - title: t('messages.error'), - content: typeof res.message === 'string' ? res.message : res.message.content!, - }) - } - } - ) - break - } - case 'delete': { - await deleteWallet({ id: walletID, password })(dispatch).then(status => { - if (status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } - }) - break - } - case 'backup': { - await backupWallet({ id: walletID, password })(dispatch).then(status => { - if (status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } - }) - break - } - case 'migrate-acp': { - await migrateAcp({ id: walletID, password })(dispatch).then(({ status }) => { - if (isSuccessResponse({ status })) { - navigate(RoutePath.History) - } else if (status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } - }) - break - } - case 'unlock': { - if (isSending) { - break - } - await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(({ status }) => { - if (isSuccessResponse({ status })) { - dispatch({ - type: AppActions.SetGlobalDialog, - payload: 'unlock-success', - }) - onSuccess?.() - } else if (status === ErrorCode.PasswordIncorrect) { - throw new PasswordIncorrectException() - } - }) - break - } - case 'create-sudt-account': { - const params: Controller.SendCreateSUDTAccountTransaction.Params = { - walletID, - assetAccount: experimental?.assetAccount, - tx: experimental?.tx, - password, - } - await sendCreateSUDTAccountTransaction(params)(dispatch).then(handleSendTxRes) - break - } - case 'send-ckb-asset': - case 'send-acp-ckb-to-new-cell': - case 'send-acp-sudt-to-new-cell': - case 'send-sudt': - case 'transfer-to-sudt': { - let skipLastInputs = true - if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { - skipLastInputs = false - } - const params: Controller.SendSUDTTransaction.Params = { - walletID, - tx: experimental?.tx, - password, - skipLastInputs, - } - await sendSUDTTransaction(params)(dispatch).then(handleSendTxRes) - break - } - case 'destroy-asset-account': - case 'send-nft': - case 'send-cheque': { - if (isSending) { - break - } - await sendTransaction({ - walletID, - tx: experimental?.tx, - description: experimental?.tx?.description, - password, - })(dispatch).then(handleSendTxRes) - break - } - case 'claim-cheque': { - if (isSending) { - break - } - await sendTransaction({ walletID, tx: experimental?.tx, password })(dispatch).then(handleSendTxRes) - break - } - case 'create-account-to-claim-cheque': { - if (isSending) { - break - } - await sendCreateSUDTAccountTransaction({ - walletID, - password, - tx: experimental?.tx, - assetAccount: { - ...experimental?.assetAccount, - tokenID: experimental?.assetAccount.tokenId, - balance: '0', - }, - })(dispatch).then(handleSendTxRes) - break - } - case 'withdraw-cheque': { - if (isSending) { - break - } - await sendTransaction({ walletID, tx: experimental?.tx, password })(dispatch).then(handleSendTxRes) - break - } - default: { - break - } - } - } catch (err) { - if (err instanceof PasswordIncorrectException) { - setError(t(err.message)) - } - } - }, - [ - dispatch, - walletID, - password, - actionType, - description, - navigate, - isSending, - generatedTx, - disabled, - experimental, - setError, - t, - multisigConfig, - ] - ) - - const onChange = useCallback( - (e: React.SyntheticEvent) => { - const { value } = e.target as HTMLInputElement - setPassword(value) - setError('') - }, - [setPassword, setError] - ) - - const signAndExportFromGenerateTx = useCallback(async () => { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { - sending: true, - }, - }) - const json = { - transaction: generatedTx || experimental?.tx, - status: OfflineSignStatus.Signed, - type: signType, - description, - asset_account: experimental?.assetAccount, - } - const res = await signAndExportTransaction({ - ...json, - walletID, - password, - multisigConfig, - }) - dispatch({ - type: AppActions.UpdateLoadings, - payload: { sending: false }, - }) - if (!isSuccessResponse(res)) { - setError(errorFormatter(res.message, t)) - return - } - if (res.result) { - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) - onDismiss() - } - }, [description, dispatch, experimental, generatedTx, onDismiss, password, signType, t, walletID, multisigConfig]) - + const { + error, + wallet, + isLoading, + signType, + actionType, + disabled, + password, + onSubmit, + onChange, + exportTransaction, + signAndExportFromGenerateTx, + } = usePasswordResuest({ + description, + generatedTx, + isSending, + passwordRequest, + wallets, + experimental, + onDismiss, + }) if (!wallet) { return null } if (wallet.device) { - return ( - - ) + return } return ( @@ -406,13 +90,13 @@ const PasswordRequest = () => { ].includes(actionType ?? '') ? null : (
{wallet ? wallet.name : null}
)} - {currentWallet.isWatchOnly && ( + {wallet.isWatchOnly && (
{t('password-request.xpub-notice')}
)} - {currentWallet.isWatchOnly || ( + {wallet.isWatchOnly || ( { - {!currentWallet.isWatchOnly && ( + {!wallet.isWatchOnly && ( <>