From 84692c740042d8f046e21555c6b3902c10187663 Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 22 Aug 2019 17:00:03 +0800 Subject: [PATCH 01/29] feat(neuron-ui): show confirmations on the History View --- .../src/components/History/index.tsx | 8 ++ .../src/components/TransactionList/index.tsx | 93 ++++++++++++++++--- packages/neuron-ui/src/locales/en.json | 8 +- packages/neuron-ui/src/locales/zh.json | 8 +- .../src/stories/TransactionList.stories.tsx | 21 ++++- .../src/stories/data/transactions.ts | 22 +++++ 6 files changed, 144 insertions(+), 16 deletions(-) diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 96adbb60e6..3a000f3884 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -13,10 +13,12 @@ import { useSearch } from './hooks' const History = ({ app: { + tipBlockNumber: chainBlockNumber, loadings: { transactionList: isLoading, updateDescription: isUpdatingDescription }, }, wallet: { id }, chain: { + tipBlockNumber: syncedBlockNumber, transactions: { pageNo = 1, pageSize = 15, totalCount = 0, items = [] }, }, history, @@ -33,6 +35,10 @@ const History = ({ }, [id, history]) const onSearch = useCallback(() => history.push(`${Routes.History}?keywords=${keywords}`), [history, keywords]) + const tipBlockNumber = useMemo(() => { + return Math.max(+syncedBlockNumber, +chainBlockNumber).toString() + }, [syncedBlockNumber, chainBlockNumber]) + const List = useMemo(() => { return ( @@ -51,6 +57,7 @@ const History = ({ isUpdatingDescription={isUpdatingDescription} walletID={id} items={items} + tipBlockNumber={tipBlockNumber} dispatch={dispatch} /> { const [t] = useTranslation() @@ -71,7 +79,20 @@ const TransactionList = ({ const transactionColumns: IColumn[] = useMemo( (): IColumn[] => [ - { name: t('history.type'), key: 'type', fieldName: 'type', minWidth: MIN_CELL_WIDTH, maxWidth: 50 }, + { + name: t('history.type'), + key: 'type', + fieldName: 'type', + minWidth: MIN_CELL_WIDTH, + maxWidth: 50, + onRender: (item?: FormatTransaction) => { + if (!item) { + return null + } + const type = t(`history.${item.type}`) + return {type} + }, + }, { name: t('history.timestamp'), key: 'timestamp', @@ -79,27 +100,74 @@ const TransactionList = ({ minWidth: 80, maxWidth: 80, onRender: (item?: FormatTransaction) => { - return item ? {uniformTimeFormatter(item.timestamp || item.createdAt).split(' ')[1]} : null + if (!item) { + return null + } + const time = uniformTimeFormatter(item.timestamp || item.createdAt).split(' ')[1] + return {time} }, }, { name: t('history.transaction-hash'), key: 'hash', fieldName: 'hash', + minWidth: 150, + maxWidth: 150, + onRender: (item?: FormatTransaction) => { + if (!item) { + return '-' + } + return ( + + {`${item.hash.slice(0, 8)}...${item.hash.slice(-6)}`} + + ) + }, + }, + { + name: t('history.confirmations'), + key: 'confirmation', minWidth: 100, - maxWidth: 600, + maxWidth: +tipBlockNumber > 1e12 ? undefined : 150, onRender: (item?: FormatTransaction) => { - if (item) { - return ( - - {item.hash} - - ) + if (!item || item.status !== 'success') { + return null } - return '-' + const confirmationCount = 1 + +tipBlockNumber - +item.blockNumber + if (confirmationCount < CONFIRMATION_THRESHOLD) { + return t(`history.confirming-with-count`, { + confirmations: `${confirmationCount} / ${CONFIRMATION_THRESHOLD}`, + }) + } + const confirmations = localNumberFormatter(confirmationCount) + return ( + + {confirmations} + + ) + }, + }, + { + name: t('history.status'), + key: 'status', + fieldName: 'status', + minWidth: 80, + maxWidth: 80, + onRender: (item?: FormatTransaction) => { + if (!item) { + return null + } + if (item.status !== 'success') { + const status = t(`history.${item.status}`) + return {status} + } + const confirmationCount = 1 + +tipBlockNumber - +item.blockNumber + if (confirmationCount < CONFIRMATION_THRESHOLD) { + return t(`history.confirming`) + } + return t(`history.success`) }, }, - { name: t('history.status'), key: 'status', fieldName: 'status', minWidth: 50, maxWidth: 50 }, { name: t('history.description'), key: 'description', @@ -151,6 +219,7 @@ const TransactionList = ({ }, ].map((col): IColumn => ({ fieldName: col.key, ariaLabel: col.name, ...col })), [ + tipBlockNumber, localDescription, onDescriptionChange, onDescriptionFieldBlur, diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index ce243c4eb6..99eccc9f81 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -126,11 +126,17 @@ "last": "Last", "description": "Description", "status": "Status", + "pending": "Pending", + "success": "Success", + "failed": "Failed", + "confirming": "Confirming", "blockNumber": "Block Number", "basic-information": "Basic Information", "search": { "placeholder": "Search tx hash, address or date (yyyy-mm-dd)" - } + }, + "confirmations": "Confirmations", + "confirming-with-count": "{{confirmations}} confirmations" }, "transaction": { "goBack": "Go back" diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 73808b1c64..b37a6e78bf 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -126,11 +126,17 @@ "last": "最后一页", "description": "标签", "status": "状态", + "pending": "已提交", + "success": "成功", + "failed": "失败", + "confirming": "确认中", "blockNumber": "区块高度", "basic-information": "基本信息", "search": { "placeholder": "使用交易哈希、地址或日期(yyyy-mm-dd)进行搜索" - } + }, + "confirmations": "确认次数", + "confirming-with-count": " 已确认 {{confirmations}} 次" }, "transaction": { "goBack": "返回" diff --git a/packages/neuron-ui/src/stories/TransactionList.stories.tsx b/packages/neuron-ui/src/stories/TransactionList.stories.tsx index 71d7ecb78c..999c698dfe 100644 --- a/packages/neuron-ui/src/stories/TransactionList.stories.tsx +++ b/packages/neuron-ui/src/stories/TransactionList.stories.tsx @@ -6,13 +6,21 @@ import transactions from './data/transactions' const stories = storiesOf('TransactionList', module) Object.entries(transactions).forEach(([title, list]) => { stories.add(title, () => ( - {}} /> + {}} + /> )) }) stories.add('Wtih empty pending list', () => ( item.status !== 'pending')} @@ -21,5 +29,14 @@ stories.add('Wtih empty pending list', () => ( )) stories.add('Shimmered List', () => { - return {}} /> + return ( + {}} + /> + ) }) diff --git a/packages/neuron-ui/src/stories/data/transactions.ts b/packages/neuron-ui/src/stories/data/transactions.ts index 60f063901c..33202023bf 100644 --- a/packages/neuron-ui/src/stories/data/transactions.ts +++ b/packages/neuron-ui/src/stories/data/transactions.ts @@ -25,6 +25,28 @@ const transactions: { blockNumber: '120', status: 'pending', }, + { + type: 'send', + createdAt: (new Date(1565240655845).getTime() - 100000).toString(), + updatedAt: '', + timestamp: '', + value: '-10000', + hash: '0x70abeeaa2ed08b7d7659341a122b9a2f2ede99bb6bd0df7398d7ffe488beab11', + description: 'description of send transaction', + blockNumber: '120', + status: 'success', + }, + { + type: 'receive', + createdAt: (new Date(1565240655845).getTime() - 200000).toString(), + updatedAt: '', + timestamp: '', + value: '10000', + hash: '0x70abeeaa2ed08b7d7659341a122b9a2f2ede99bb6bd0df7398d7ffe488beab22', + description: 'description of receive transaction', + blockNumber: '120', + status: 'success', + }, { type: 'send', createdAt: '', From 4d0cedef2f858fb4316a9158756c184baa1c050b Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 22 Aug 2019 18:00:17 +0800 Subject: [PATCH 02/29] refactor(neuron-ui): refactor the Overview View 1. extract unnecessary callback hooks outside the component 2. memoize event handler in a memo hook --- .../src/components/Overview/index.tsx | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index b948fc2b25..f004c09610 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -64,6 +64,45 @@ const genTypeLabel = ( } } +const onTransactionActivityRender = (item?: ActivityItem) => { + if (!item) { + return null + } + return ( + <> + + {`${item.typeLabel} ${shannonToCKBFormatter(item.value)} CKB`} + + + {item.confirmations} + + + ) +} + +const onTransactionRowRender = (props?: IDetailsRowProps) => { + if (!props) { + return null + } + const customStyles: Partial = { + root: { + animationDuration: '0!important', + }, + } + return +} + +const onTimestampRender = (item?: any) => { + if (!item) { + return null + } + return ( + + {timeFormatter(item.timestamp || item.createdAt)} + + ) +} + const ActivityList = ({ columns, items, @@ -114,6 +153,7 @@ const ActivityList = ({ ) : null} ) + const Overview = ({ dispatch, app: { tipBlockNumber, chain, epoch, difficulty }, @@ -150,45 +190,6 @@ const Overview = ({ history.push(Routes.History) }, [history]) - const onTransactionRowRender = useCallback((props?: IDetailsRowProps) => { - if (props) { - const customStyles: Partial = { - root: { - animationDuration: '0!important', - }, - } - return - } - return null - }, []) - - const onTransactionActivityRender = useCallback((item?: ActivityItem) => { - if (item) { - return ( - <> - - {`${item.typeLabel} ${shannonToCKBFormatter(item.value)} CKB`} - - - {item.confirmations} - - - ) - } - return null - }, []) - - const onTimestampRender = useCallback((item?: any) => { - if (item) { - return ( - - {timeFormatter(item.timestamp || item.createdAt)} - - ) - } - return null - }, []) - const activityColumns: IColumn[] = useMemo(() => { return [ { @@ -230,7 +231,7 @@ const Overview = ({ ...col, }) ) - }, [t, onTimestampRender, onTransactionActivityRender]) + }, [t]) const balanceProperties: Property[] = useMemo( () => [ @@ -263,18 +264,15 @@ const Overview = ({ [t, chain, epoch, difficulty, tipBlockNumber] ) - const showBlockchainStatus = useCallback(() => { - setDisplayBlockchainInfo(true) - }, [setDisplayBlockchainInfo]) - const hideBlockchainStatus = useCallback(() => { - setDisplayBlockchainInfo(false) - }, [setDisplayBlockchainInfo]) - const showMinerInfo = useCallback(() => { - setDisplayMinerInfo(true) - }, [setDisplayMinerInfo]) - const hideMinerInfo = useCallback(() => { - setDisplayMinerInfo(false) - }, [setDisplayMinerInfo]) + const [showBlockchainStatus, hideBlockchainStatus, showMinerInfo, hideMinerInfo] = useMemo( + () => [ + () => setDisplayBlockchainInfo(true), + () => setDisplayBlockchainInfo(false), + () => setDisplayMinerInfo(true), + () => setDisplayMinerInfo(false), + ], + [setDisplayBlockchainInfo, setDisplayMinerInfo] + ) const defaultAddress = useMemo(() => { return addresses.find(addr => addr.type === 0 && addr.index === 0) From 64166cc156e23478757948f3c69dd7008eeda94f Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 22 Aug 2019 11:52:34 +0800 Subject: [PATCH 03/29] feat(neuron-ui): set the cycles to empty if the transaction is invalid --- packages/neuron-ui/src/components/Send/hooks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index f77829c74b..0d14c1955e 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -126,6 +126,11 @@ const useOnTransactionChange = (walletID: string, items: TransactionOutput[], di payload: '0', }) }) + } else { + dispatch({ + type: AppActions.UpdateSendCycles, + payload: '0', + }) } }, 300) }, [walletID, items, dispatch]) From 6c2c618f5d0ff27bf9fc9ee11f561b4333c81b29 Mon Sep 17 00:00:00 2001 From: Chen Yu Date: Tue, 27 Aug 2019 13:34:37 +0800 Subject: [PATCH 04/29] Use error code for i18n (#895) * feat(neuron-ui): add error codes * refactor(neuron-ui): use error codes instead of error messages * feat(neuron-ui): add error messages on the Send View * feat(neuron-ui): add more Chinese for the i18n * refactor(neuron-ui): remove useless texts for the i18n * refactor(neuron-ui): simplify verify transaction outputs * test(neuron-ui): add tests of verify amount range * test(neuron-ui): add tests of verify amount * test(neuron-ui): add tests of the verify network name method * test(neuron-ui): add tests of the verify url method * feat(neuron-ui): add total amount verification on the send view * refactor(neuron-ui): update i18n text --- .../src/components/ImportKeystore/index.tsx | 26 ++- .../src/components/NetworkEditor/hooks.ts | 140 +++++++++------ .../src/components/NetworkEditor/index.tsx | 18 +- .../src/components/Overview/index.tsx | 9 +- .../neuron-ui/src/components/Send/hooks.ts | 165 ++++++++---------- .../neuron-ui/src/components/Send/index.tsx | 90 +++++++--- .../src/components/Transaction/index.tsx | 13 +- .../src/components/WalletEditor/hooks.ts | 17 +- .../src/components/WalletEditor/index.tsx | 17 +- .../src/components/WalletWizard/index.tsx | 23 ++- .../src/containers/Notification/index.tsx | 12 +- packages/neuron-ui/src/locales/en.json | 67 +++---- packages/neuron-ui/src/locales/zh.json | 69 ++++---- .../remote/controllerMethodWrapper.ts | 35 ++-- .../stateProvider/actionCreators/app.ts | 12 +- .../stateProvider/actionCreators/settings.ts | 13 +- .../actionCreators/transactions.ts | 5 +- .../stateProvider/actionCreators/wallets.ts | 71 +++++--- .../tests/{ => formatters}/formatters.test.ts | 0 .../tests/validators/verifyAmount/fixtures.ts | 37 ++++ .../validators/verifyAmount/index.test.ts | 11 ++ .../validators/verifyAmountRange/fixtures.ts | 22 +++ .../verifyAmountRange/index.test.ts | 10 ++ .../validators/verifyNetworkName/fixtures.ts | 43 +++++ .../verifyNetworkName/index.test.ts | 19 ++ .../validators/verifyTotalAmount/fixtures.ts | 35 ++++ .../verifyTotalAmount/index.test.ts | 19 ++ .../verifyTransactionOutputs/fixture.ts | 63 +++++++ .../verifyTransactionOutputs/index.test.ts | 10 ++ .../tests/validators/verifyURL/fixtures.ts | 49 ++++++ .../tests/validators/verifyURL/index.test.ts | 11 ++ packages/neuron-ui/src/types/App/index.d.ts | 10 +- packages/neuron-ui/src/utils/const.ts | 38 ++-- packages/neuron-ui/src/utils/formatters.ts | 11 ++ packages/neuron-ui/src/utils/validators.ts | 90 +++++++++- .../neuron-ui/src/widgets/QRScanner/index.tsx | 3 +- .../src/controllers/wallets/index.ts | 4 +- .../neuron-wallet/src/decorators/errors.ts | 2 +- .../src/types/controller/index.d.ts | 7 +- 39 files changed, 938 insertions(+), 358 deletions(-) rename packages/neuron-ui/src/tests/{ => formatters}/formatters.test.ts (100%) create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index 8361bb9ff4..bf094012f6 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -7,10 +7,17 @@ import { importWalletWithKeystore } from 'states/stateProvider/actionCreators' import { StateWithDispatch } from 'states/stateProvider/reducer' import { useGoBack } from 'utils/hooks' import generateWalletName from 'utils/generateWalletName' +import { ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' -const defaultFields = { +interface KeystoreFields { + path: string + name: string | undefined + password: string +} + +const defaultFields: KeystoreFields = { path: '', - name: '', + name: undefined, password: '', } @@ -25,7 +32,7 @@ const ImportKeystore = (props: React.PropsWithoutRef { - if (fields.name === '') { + if (fields.name === undefined) { const name = generateWalletName(wallets, wallets.length + 1, t) setFields({ ...fields, @@ -60,7 +67,7 @@ const ImportKeystore = (props: React.PropsWithoutRef { importWalletWithKeystore({ - name: fields.name, + name: fields.name || '', keystorePath: fields.path, password: fields.password, })(dispatch, history) @@ -70,6 +77,12 @@ const ImportKeystore = (props: React.PropsWithoutRef {Object.entries(fields).map(([key, value]) => { + let maxLength: number | undefined + if (key === 'name') { + maxLength = MAX_WALLET_NAME_LENGTH + } else if (key === 'password') { + maxLength = MAX_PASSWORD_LENGTH + } return ( { if (text === '') { - return t('messages.is-required', { field: t(`import-keystore.label.${key}`) }) + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: `keystore-${key}` }) } if (key === 'name' && isNameUsed) { - return t('messages.is-used', { field: t(`import-keystore.label.${key}`) }) + return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: `name`, fieldValue: text }) } return '' }} diff --git a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts index f65f54ee02..172926852c 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts +++ b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts @@ -3,9 +3,10 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import { StateDispatch } from 'states/stateProvider/reducer' import { createNetwork, updateNetwork, addNotification } from 'states/stateProvider/actionCreators' -import { Message, MAX_NETWORK_NAME_LENGTH } from 'utils/const' +import { MAX_NETWORK_NAME_LENGTH, ErrorCode } from 'utils/const' import i18n from 'utils/i18n' +import { verifyNetworkName, verifyURL } from 'utils/validators' enum PlaceHolder { Name = 'My Custom Node', @@ -69,8 +70,12 @@ export const useInitialize = ( initialize(network) } else { addNotification({ - type: 'warning', - content: i18n.t('messages.network-is-not-found'), + type: 'warning' as State.MessageType, + timestamp: +new Date(), + code: ErrorCode.FieldNotFound, + meta: { + fieldName: 'network', + }, }) } } @@ -86,14 +91,9 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any tooltip: TooltipText.URL, placeholder: PlaceHolder.URL, onGetErrorMessage: (url: string) => { - if (!url) { - return t('messages.url-required') - } - if (!/^https?:\/\//.test(url)) { - return t('messages.rpc-url-should-have-protocol') - } - if (/\s/.test(url)) { - return t('messages.rpc-url-should-have-no-whitespaces') + const res = verifyURL(url) + if (typeof res === 'object') { + return t(`messages.codes.${res.code}`, { fieldName: 'remote', fieldValue: url }) } return '' }, @@ -104,11 +104,13 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any tooltip: TooltipText.Name, placeholder: PlaceHolder.Name, onGetErrorMessage: (name: string) => { - if (!name) { - return t('messages.name-required') - } - if (usedNetworkNames.includes(name)) { - return t('messages.network-name-used') + const res = verifyNetworkName(name, usedNetworkNames) + if (typeof res === 'object') { + return t(`messages.codes.${res.code}`, { + fieldName: 'name', + fieldValue: name, + length: MAX_NETWORK_NAME_LENGTH, + }) } return '' }, @@ -118,13 +120,21 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any ) } -export const useIsInputsValid = (editor: EditorType, cachedNetwork: State.Network | undefined) => { - const [errors, setErrors] = useState([!cachedNetwork && !editor.name.value, !cachedNetwork && !editor.remote.value]) +export const useIsInputsValid = ( + editor: EditorType, + usedNetworkNames: string[], + cachedNetwork: State.Network | undefined +) => { + const hasError = useMemo(() => { + const nameRes = verifyNetworkName(editor.name.value, usedNetworkNames) + const URLRes = verifyURL(editor.remote.value) + return !(nameRes === true && URLRes === true) + }, [editor.name.value, editor.remote.value, usedNetworkNames]) const notModified = useMemo( () => cachedNetwork && (cachedNetwork.name === editor.name.value && cachedNetwork.remote === editor.remote.value), [cachedNetwork, editor.name.value, editor.remote.value] ) - return { errors, setErrors, notModified } + return { hasError, notModified } } export const useHandleSubmit = ( @@ -136,55 +146,85 @@ export const useHandleSubmit = ( dispatch: StateDispatch ) => useCallback(async () => { - const warning = { - type: 'warning' as 'warning', - timestamp: Date.now(), - content: '', - } + let errorMessage: State.Message | undefined if (!name) { - return addNotification({ - ...warning, - content: i18n.t(Message.NameRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldRequired, + meta: { + fieldName: 'name', + }, + } + return addNotification(errorMessage)(dispatch) } if (name.length > MAX_NETWORK_NAME_LENGTH) { - return addNotification({ - ...warning, - content: i18n.t(Message.LengthOfNameShouldBeLessThanOrEqualTo, { - length: MAX_NETWORK_NAME_LENGTH, - }), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldTooLong, + meta: { + fieldName: 'name', + fieldValue: name, + length: `${MAX_NETWORK_NAME_LENGTH}`, + }, + } + return addNotification(errorMessage)(dispatch) } if (!remote) { - return addNotification({ - ...warning, - content: i18n.t(Message.URLRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldRequired, + meta: { + fieldName: 'remote', + }, + } + return addNotification(errorMessage)(dispatch) } if (!remote.startsWith('http')) { - return addNotification({ - ...warning, - content: i18n.t(Message.ProtocolRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.ProtocolRequired, + meta: { + fieldName: 'remote', + fieldValue: remote, + }, + } + return addNotification(errorMessage)(dispatch) } // verification, for now, only name is unique if (id === 'new') { if (networks.some(network => network.name === name)) { - return addNotification({ - ...warning, - content: i18n.t(Message.NetworkNameUsed), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldUsed, + meta: { + fieldName: 'name', + fieldValue: name, + }, + } + return addNotification(errorMessage)(dispatch) } return createNetwork({ name, remote, })(dispatch, history) } + if (networks.some(network => network.name === name && network.id !== id)) { - return addNotification({ - ...warning, - content: i18n.t(Message.NetworkNameUsed), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldUsed, + meta: { + fieldName: 'name', + fieldValue: name, + }, + } + return addNotification(errorMessage)(dispatch) } return updateNetwork({ networkID: id!, diff --git a/packages/neuron-ui/src/components/NetworkEditor/index.tsx b/packages/neuron-ui/src/components/NetworkEditor/index.tsx index d7b0db0889..6b84cead1d 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/index.tsx +++ b/packages/neuron-ui/src/components/NetworkEditor/index.tsx @@ -27,32 +27,22 @@ const NetworkEditor = ({ const goBack = useGoBack(history) useInitialize(id, networks, editor.initialize, dispatch) - const { errors, setErrors, notModified } = useIsInputsValid(editor, cachedNetwork) + const { hasError, notModified } = useIsInputsValid(editor, usedNetworkNames, cachedNetwork) const handleSubmit = useHandleSubmit(id, editor.name.value, editor.remote.value, networks, history, dispatch) return (

{t('settings.network.edit-network.title')}

- {inputs.map((inputProps, idx) => ( + {inputs.map(inputProps => ( - { - const errs = [...errors] - errs.splice(idx, 1, msg !== '') - setErrors(errs) - }} - /> + ))} - +
) diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index f004c09610..cc7eca0a0d 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -28,7 +28,7 @@ import { updateTransactionList, addPopup } from 'states/stateProvider/actionCrea import { showTransactionDetails, showErrorMessage } from 'services/remote' import { localNumberFormatter, shannonToCKBFormatter, uniformTimeFormatter as timeFormatter } from 'utils/formatters' -import { PAGE_SIZE, Routes, CONFIRMATION_THRESHOLD } from 'utils/const' +import { PAGE_SIZE, Routes, CONFIRMATION_THRESHOLD, ErrorCode } from 'utils/const' import { backToTop } from 'utils/animations' const TITLE_FONT_SIZE = 'xxLarge' @@ -284,7 +284,10 @@ const Overview = ({ hideMinerInfo() addPopup('lock-arg-copied')(dispatch) } else { - showErrorMessage(t('messages.error'), t('messages.can-not-find-the-default-address')) + showErrorMessage( + t(`messages.error`), + t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: `default-address` }) + ) } }, [defaultAddress, t, hideMinerInfo, dispatch]) @@ -409,7 +412,7 @@ const Overview = ({
) : ( - {t('messages.can-not-find-the-default-address')} + {t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: `default-address` })} )} diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 0d14c1955e..6bcb392f5a 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -1,68 +1,16 @@ -import React, { useCallback, useEffect } from 'react' -import { IDropdownOption } from 'office-ui-fabric-react' +import React, { useState, useCallback, useEffect, useMemo } from 'react' import { AppActions, StateDispatch } from 'states/stateProvider/reducer' import { calculateCycles } from 'services/remote/wallets' -import { Message, MAX_DECIMAL_DIGITS } from 'utils/const' -import { verifyAddress, verifyAmountRange } from 'utils/validators' -import { outputsToTotalCapacity } from 'utils/formatters' +import { outputsToTotalCapacity, priceToFee } from 'utils/formatters' +import { verifyAddress, verifyAmount, verifyAmountRange, verifyTransactionOutputs } from 'utils/validators' +import { ErrorCode } from 'utils/const' +import { MAX_DECIMAL_DIGITS } from '../../utils/const' import { TransactionOutput } from '.' let cyclesTimer: ReturnType -const validateTransactionParams = ({ items, dispatch }: { items: TransactionOutput[]; dispatch?: StateDispatch }) => { - const errorAction = { - type: AppActions.AddNotification, - payload: { - type: 'warning', - timestamp: Date.now(), - content: Message.AtLeastOneAddressNeeded, - meta: {}, - }, - } - if (!items.length || !items[0].address) { - if (dispatch) { - dispatch(errorAction) - } - return false - } - const invalid = items.some( - (item): boolean => { - const isAddressValid = verifyAddress(item.address) - if (typeof isAddressValid === 'string') { - errorAction.payload.content = Message.InvalidAddress - errorAction.payload.meta = { address: item.address } - return true - } - if (Number.isNaN(+item.amount) || +item.amount < 0) { - errorAction.payload.content = Message.InvalidAmount - errorAction.payload.meta = { amount: item.amount } - return true - } - const [, decimal = ''] = item.amount.split('.') - if (decimal.length > MAX_DECIMAL_DIGITS) { - errorAction.payload.content = Message.DecimalExceed - errorAction.payload.meta = { amount: item.amount } - return true - } - if (!verifyAmountRange(item.amount)) { - errorAction.payload.content = Message.AmountTooSmall - errorAction.payload.meta = { amount: item.amount } - return true - } - return false - } - ) - if (invalid) { - if (dispatch) { - dispatch(errorAction) - } - return false - } - return true -} - const useUpdateTransactionOutput = (dispatch: StateDispatch) => useCallback( (field: string) => (idx: number) => (value: string) => { @@ -71,7 +19,7 @@ const useUpdateTransactionOutput = (dispatch: StateDispatch) => payload: { idx, item: { - [field]: value.trim(), + [field]: value.replace(/\s/, ''), }, }, }) @@ -98,14 +46,23 @@ const useRemoveTransactionOutput = (dispatch: StateDispatch) => [dispatch] ) -const useOnTransactionChange = (walletID: string, items: TransactionOutput[], dispatch: StateDispatch) => { +const useOnTransactionChange = ( + walletID: string, + items: TransactionOutput[], + dispatch: StateDispatch, + setIsTransactionValid: Function, + setTotalAmount: Function +) => { useEffect(() => { clearTimeout(cyclesTimer) cyclesTimer = setTimeout(() => { - if (validateTransactionParams({ items })) { + if (verifyTransactionOutputs(items)) { + setIsTransactionValid(true) + const totalAmount = outputsToTotalCapacity(items) + setTotalAmount(totalAmount) calculateCycles({ walletID, - capacities: outputsToTotalCapacity(items), + capacities: totalAmount, }) .then(response => { if (response.status) { @@ -127,19 +84,20 @@ const useOnTransactionChange = (walletID: string, items: TransactionOutput[], di }) }) } else { + setIsTransactionValid(false) dispatch({ type: AppActions.UpdateSendCycles, payload: '0', }) } }, 300) - }, [walletID, items, dispatch]) + }, [walletID, items, dispatch, setIsTransactionValid, setTotalAmount]) } const useOnSubmit = (items: TransactionOutput[], dispatch: StateDispatch) => useCallback( (walletID: string = '') => () => { - if (validateTransactionParams({ items, dispatch })) { + if (verifyTransactionOutputs(items)) { dispatch({ type: AppActions.UpdateTransactionID, payload: null, @@ -164,7 +122,7 @@ const useOnItemChange = (updateTransactionOutput: Function) => ) => { if (undefined !== value) { if (field === 'amount') { - if (Number.isNaN(+value) || /[^\d.]/.test(value)) { + if (Number.isNaN(+value) || /[^\d.]/.test(value) || +value < 0) { return } updateTransactionOutput(field)(idx)(value) @@ -176,16 +134,6 @@ const useOnItemChange = (updateTransactionOutput: Function) => [updateTransactionOutput] ) -const useCapacityUnitChange = (updateTransactionOutput: Function) => - useCallback( - (idx: number = -1) => (_e: React.FormEvent, option?: IDropdownOption) => { - if (option) { - updateTransactionOutput('unit')(idx)(option.key) - } - }, - [updateTransactionOutput] - ) - const useUpdateTransactionPrice = (dispatch: StateDispatch) => useCallback( (_e: React.FormEvent, value?: string) => { @@ -223,40 +171,79 @@ const clear = (dispatch: StateDispatch) => { const useClear = (dispatch: StateDispatch) => useCallback(() => clear(dispatch), [dispatch]) export const useInitialize = ( - address: string, items: TransactionOutput[], + price: string, + cycles: string, dispatch: React.Dispatch, - history: any + t: any ) => { + const fee = useMemo(() => priceToFee(price, cycles), [price, cycles]) // in shannon + const [isTransactionValid, setIsTransactionValid] = useState(false) + const [totalAmount, setTotalAmount] = useState('0') + const updateTransactionOutput = useUpdateTransactionOutput(dispatch) const onItemChange = useOnItemChange(updateTransactionOutput) - const onCapacityUnitChange = useCapacityUnitChange(updateTransactionOutput) - const onSubmit = useOnSubmit(items, dispatch) const addTransactionOutput = useAddTransactionOutput(dispatch) const removeTransactionOutput = useRemoveTransactionOutput(dispatch) const updateTransactionPrice = useUpdateTransactionPrice(dispatch) const onDescriptionChange = useSendDescriptionChange(dispatch) + const onSubmit = useOnSubmit(items, dispatch) const onClear = useClear(dispatch) - useEffect(() => { - if (address) { - updateTransactionOutput('address')(0)(address) - } - return () => { - clear(dispatch) - } - }, [address, dispatch, history, updateTransactionOutput]) + const onGetAddressErrorMessage = useCallback( + (addr: string) => { + if (addr === '') { + return t(`messages.codes.${ErrorCode.AddressIsEmpty}`) + } + if (!verifyAddress(addr)) { + return t(`messages.codes.${ErrorCode.FieldInvalid}`, { + fieldName: 'address', + fieldValue: addr, + }) + } + return '' + }, + [t] + ) + + const onGetAmountErrorMessage = useCallback( + (text: string) => { + const amount = text || '0' + + const msg = verifyAmount(amount) + if (typeof msg === 'object') { + return t(`messages.codes.${msg.code}`, { + fieldName: 'amount', + fieldValue: amount, + length: MAX_DECIMAL_DIGITS, + }) + } + if (!verifyAmountRange(amount)) { + return t(`messages.codes.${ErrorCode.AmountTooSmall}`, { + amount, + }) + } + + return undefined + }, + [t] + ) return { + fee, + totalAmount, + setTotalAmount, + isTransactionValid, + setIsTransactionValid, useOnTransactionChange, - updateTransactionOutput, onItemChange, - onCapacityUnitChange, - onSubmit, addTransactionOutput, removeTransactionOutput, updateTransactionPrice, onDescriptionChange, + onGetAddressErrorMessage, + onGetAmountErrorMessage, + onSubmit, onClear, } } diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index 6f77bee3ee..12e4fc5f9f 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -20,9 +20,10 @@ import QRScanner from 'widgets/QRScanner' import { StateWithDispatch } from 'states/stateProvider/reducer' import appState from 'states/initStates/app' -import { PlaceHolders, CapacityUnit } from 'utils/const' -import { shannonToCKBFormatter, priceToFee } from 'utils/formatters' +import { PlaceHolders, CapacityUnit, ErrorCode } from 'utils/const' +import { shannonToCKBFormatter } from 'utils/formatters' +import { verifyTotalAmount } from 'utils/validators' import { useInitialize } from './hooks' export interface TransactionOutput { @@ -38,31 +39,30 @@ const Send = ({ }, wallet: { id: walletID = '', balance = '' }, dispatch, - history, - match: { - params: { address = '' }, - }, }: React.PropsWithoutRef>) => { const { t } = useTranslation() const { + fee, + totalAmount, + setTotalAmount, + isTransactionValid, + setIsTransactionValid, useOnTransactionChange, - updateTransactionOutput, onItemChange, onSubmit, addTransactionOutput, removeTransactionOutput, updateTransactionPrice, onDescriptionChange, + onGetAddressErrorMessage, + onGetAmountErrorMessage, onClear, - } = useInitialize(address, send.outputs, dispatch, history) - useOnTransactionChange(walletID, send.outputs, dispatch) + } = useInitialize(send.outputs, send.price, send.cycles, dispatch, t) + useOnTransactionChange(walletID, send.outputs, dispatch, setIsTransactionValid, setTotalAmount) const leftStackWidth = '70%' const labelWidth = '140px' - const actionSpacer = ( - - - - ) + + const isAffordable = verifyTotalAmount(totalAmount, fee, balance) return ( @@ -78,7 +78,7 @@ const Send = ({ @@ -91,15 +91,17 @@ const Send = ({ value={item.address || ''} onChange={onItemChange('address', idx)} required + validateOnLoad={false} + onGetErrorMessage={onGetAddressErrorMessage} /> - + updateTransactionOutput('address')(idx)(data)} + onConfirm={(data: string) => onItemChange('address', idx)(undefined as any, data)} /> - + @@ -116,7 +118,7 @@ const Send = ({ @@ -130,6 +132,8 @@ const Send = ({ onChange={onItemChange('amount', idx)} disabled={sending} required + validateOnLoad={false} + onGetErrorMessage={onGetAmountErrorMessage} /> @@ -155,20 +159,53 @@ const Send = ({ /> - + + 1 || !isAffordable ? 'flex' : 'none', + }, + }} + tokens={{ childrenGap: 20 }} + > + + + + + + + - + - {actionSpacer} ) : ( - + )} diff --git a/packages/neuron-ui/src/components/Transaction/index.tsx b/packages/neuron-ui/src/components/Transaction/index.tsx index 1655afa835..ecd72826d1 100644 --- a/packages/neuron-ui/src/components/Transaction/index.tsx +++ b/packages/neuron-ui/src/components/Transaction/index.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Stack, DetailsList, Text, CheckboxVisibility, IColumn } from 'office-ui-fabric-react' import { currentWallet as currentWalletCache } from 'utils/localCache' -import { getTransaction } from 'services/remote' +import { getTransaction, showErrorMessage } from 'services/remote' import { transactionState } from 'states/initStates/chain' import { localNumberFormatter, uniformTimeFormatter, shannonToCKBFormatter } from 'utils/formatters' +import { ErrorCode } from 'utils/const' const MIN_CELL_WIDTH = 70 @@ -103,7 +104,11 @@ const Transaction = () => { if (res.status) { setTransaction(res.result) } else { - throw new Error(res.message.title) + showErrorMessage( + t(`messages.error`), + t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' }) + ) + window.close() } }) .catch((err: Error) => { @@ -113,7 +118,7 @@ const Transaction = () => { }) }) } - }, []) + }, [t]) useEffect(() => { window.addEventListener('storage', (e: StorageEvent) => { @@ -147,7 +152,7 @@ const Transaction = () => { if (error.code) { return ( - {error.message || t('messages.transaction-not-found')} + {error.message || t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' })} ) } diff --git a/packages/neuron-ui/src/components/WalletEditor/hooks.ts b/packages/neuron-ui/src/components/WalletEditor/hooks.ts index 38190028cb..8dd4bce01d 100644 --- a/packages/neuron-ui/src/components/WalletEditor/hooks.ts +++ b/packages/neuron-ui/src/components/WalletEditor/hooks.ts @@ -1,6 +1,7 @@ import { useState, useMemo, useCallback } from 'react' import { updateWalletProperty } from 'states/stateProvider/actionCreators' import { StateDispatch } from 'states/stateProvider/reducer' +import { ErrorCode, MAX_WALLET_NAME_LENGTH } from 'utils/const' import i18n from 'utils/i18n' export const useWalletEditor = () => { @@ -28,7 +29,7 @@ export const useInputs = ({ name }: ReturnType) => { ...name, label: i18n.t('settings.wallet-manager.edit-wallet.wallet-name'), placeholder: i18n.t('settings.wallet-manager.edit-wallet.wallet-name'), - maxLength: 20, + maxLength: MAX_WALLET_NAME_LENGTH, }, ], [name] @@ -44,15 +45,21 @@ export const useOnConfirm = (name: string = '', id: string = '', history: any, d }, [name, id, history, dispatch]) } -export const useAreParamsValid = (name: string) => { +export const useHint = (name: string, usedNames: string[], t: Function): string | null => { return useMemo(() => { - return !(name === '') - }, [name]) + if (name === '') { + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'name' }) + } + if (usedNames.includes(name)) { + return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: 'name', fieldValue: name }) + } + return null + }, [name, usedNames, t]) } export default { useWalletEditor, useInputs, useOnConfirm, - useAreParamsValid, + useHint, } diff --git a/packages/neuron-ui/src/components/WalletEditor/index.tsx b/packages/neuron-ui/src/components/WalletEditor/index.tsx index ecc6844f07..09c71077c1 100644 --- a/packages/neuron-ui/src/components/WalletEditor/index.tsx +++ b/packages/neuron-ui/src/components/WalletEditor/index.tsx @@ -5,16 +5,16 @@ import { Stack, TextField, PrimaryButton, DefaultButton } from 'office-ui-fabric import { StateWithDispatch } from 'states/stateProvider/reducer' -import { Routes } from 'utils/const' +import { Routes, ErrorCode } from 'utils/const' import { useGoBack } from 'utils/hooks' -import { useAreParamsValid, useOnConfirm, useInputs, useWalletEditor } from './hooks' +import { useHint, useOnConfirm, useInputs, useWalletEditor } from './hooks' const WalletNotFound = () => { const [t] = useTranslation() return (
-

{t('messages.wallet-is-not-found')}

+

{t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'wallet' })}

{`${t('navbar.settings')}-${t('settings.setting-tabs.wallets')}`} @@ -33,6 +33,7 @@ const WalletEditor = ({ const [t] = useTranslation() const wallet = useMemo(() => wallets.find(w => w.id === id), [id, wallets]) || { id: '', name: '' } + const usedNames = wallets.map(w => w.name).filter(n => n !== wallet.name) const editor = useWalletEditor() const { initialize } = editor @@ -42,7 +43,7 @@ const WalletEditor = ({ }, [id, initialize, wallet.name]) const inputs = useInputs(editor) - const areParamsValid = useAreParamsValid(editor.name.value) + const hint = useHint(editor.name.value, usedNames, t) const onConfirm = useOnConfirm(editor.name.value, wallet.id, history, dispatch) const goBack = useGoBack(history) @@ -56,13 +57,17 @@ const WalletEditor = ({ {inputs.map(inputProps => ( - + ))} - + ) diff --git a/packages/neuron-ui/src/components/WalletWizard/index.tsx b/packages/neuron-ui/src/components/WalletWizard/index.tsx index e8119349b5..4de577f8de 100644 --- a/packages/neuron-ui/src/components/WalletWizard/index.tsx +++ b/packages/neuron-ui/src/components/WalletWizard/index.tsx @@ -16,7 +16,7 @@ import withWizard, { WizardElementProps, WithWizardState } from 'components/with import { generateMnemonic, validateMnemonic, showErrorMessage } from 'services/remote' import { createWalletWithMnemonic, importWalletWithMnemonic } from 'states/stateProvider/actionCreators' -import { Routes, MnemonicAction } from 'utils/const' +import { Routes, MnemonicAction, ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' import { buttonGrommetIconStyles } from 'utils/icons' import { verifyPasswordComplexity } from 'utils/validators' import generateWalletName from 'utils/generateWalletName' @@ -36,15 +36,29 @@ const initState: WithWizardState = { } const submissionInputs = [ - { label: 'name', key: 'name', type: 'text', hint: 'wizard.set-wallet-name', autoFocus: false }, + { + label: 'name', + key: 'name', + type: 'text', + hint: 'wizard.set-wallet-name', + autoFocus: false, + maxLength: MAX_WALLET_NAME_LENGTH, + }, { label: 'password', key: 'password', type: 'password', hint: 'wizard.set-a-strong-password-to-protect-your-wallet', autoFocus: true, + maxLength: MAX_PASSWORD_LENGTH, + }, + { + label: 'confirm-password', + key: 'confirmPassword', + type: 'password', + autoFocus: false, + maxLength: MAX_PASSWORD_LENGTH, }, - { label: 'confirm-password', key: 'confirmPassword', type: 'password', autoFocus: false }, ] const Welcome = ({ rootPath = '/wizard', wallets = [], history }: WizardElementProps<{ rootPath: string }>) => { @@ -160,7 +174,7 @@ const Mnemonic = ({ }` ) } else { - showErrorMessage(t('messages.error'), t('messages.invalid-mnemonic')) + showErrorMessage(t(`messages.error`), t(`messages.codes.${ErrorCode.FieldInvalid}`, { fieldName: 'mnemonic' })) } } }, [isCreate, history, rootPath, type, imported, t, dispatch]) @@ -271,6 +285,7 @@ const Submission = ({ value={state[input.key]} onChange={onChange(input.key)} description={t(input.hint || '')} + maxLength={input.maxLength} />
))} diff --git a/packages/neuron-ui/src/containers/Notification/index.tsx b/packages/neuron-ui/src/containers/Notification/index.tsx index e7e5c83341..cce63b155e 100644 --- a/packages/neuron-ui/src/containers/Notification/index.tsx +++ b/packages/neuron-ui/src/containers/Notification/index.tsx @@ -83,7 +83,7 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef - {showTopAlert && notificationsInDesc.length ? ( + {showTopAlert && notification ? ( } > - {t(notification.content, notification.meta)} + {notification.code + ? t(`messages.codes.${notification.code}`, notification.meta) + : notification.content || t('messages.unknown-error')} ) : null} @@ -154,7 +156,11 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef - {t(n.content, n.meta)} + + {notification.code + ? t(`messages.codes.${notification.code}`, notification.meta) + : notification.content || t('messages.unknown-error')} + ) })} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 434e14f877..54a6da2105 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -90,6 +90,7 @@ "input-password-to-confirm": "Input password to confirm", "this-transaction-will-send": "This transaction will send", "scan-to-get-address": "Scan QR code to read the address to send to", + "total-amount": "Total Amount", "description": "Description", "description-optional": "Description (optional)", "balance": "Balance", @@ -240,45 +241,49 @@ "success": "success" }, "messages": { - "at-least-one-address-needed": "At least one address needed", - "name-required": "Name is required", - "url-required": "URL is required", - "protocol-required": "Protocol is required", - "length-of-name-should-be-less-than-or-equal-to": "Length of name should be less than or equal to {{length}}", - "network-name-used": "Network name is used", - "is-unremovable": "{{target}} is unremovable", - "create-wallet-success": "You have created wallet '{{name}}' successfully", - "network-is-not-found": "Network is not found", - "failed-to-initiate,-please-reopen-Neuron": "Failed to initiate, please reopen Neuron", - "no-wallet": "No Wallet", - "wallet-imported-successfully": "{{name}} imported successfully", - "wallet-created-successfully": "{{name}} created successfully", - "wallet-updated-successfully": "{{name}} updated successfully", - "wallet-not-found": "Wallet not found", - "no-transactions": "No transactions", "error": "Error", - "invalid-mnemonic": "Invalid mnemonic words", - "camera-not-available-or-disabled": "Camera is unavailable or disabled", - "can-not-find-the-default-address": "Cannot find the default address", - "create-wallet-successfully": "Create a wallet successfully", - "import-wallet-successfully": "Import a wallet successfully", + "unknown-error": "Unknown error", "update-wallet-successfully": "Update the wallet successfully", "delete-wallet-successfully": "Delete the wallet successfully", "create-network-successfully": "Create a network successfully", "update-network-successfully": "Update the network successfully", - "delete-network-successfully": "Delete the network successfully", "addr-copied": "Address has been copied to the clipboard", "qrcode-copied": "QR Code has been copied to the clipboard", "lock-arg-copied": "Lock Arg has been copied to the clipboard", - "transaction-not-found": "The transaction is not found", - "rpc-url-should-have-protocol": "The RPC URL should start with http(s)://", - "rpc-url-should-have-no-whitespaces": "The RPC URL should have no whitespaces", - "is-required": "{{field}} is required", - "is-used": "{{field}} is used", - "invalid-address": "{{address}} is an invalid address", - "amount-decimal-exceed": "The amount {{amount}} CKB is invalid, please enter an amount with no more than 8 decimal places", - "invalid-amount": "The amount {{amount}} CKB is invalid", - "amount-too-small": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB" + "fields": { + "wallet": "Wallet", + "name": "Name", + "remote": "RPC URL", + "network": "Network", + "address": "Address", + "amount": "Amount", + "transaction": "Transaction", + "default-address": "Default Address", + "mnemonic": "Mnemonic", + "keystore-path": "Keystore File", + "keystore-name": "Wallet name", + "keystore-password": "Password" + }, + "codes": { + "-3": "", + "100": "Amount is not enough", + "101": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB", + "102": "$t(messages.fields.{{fieldName}}) is invalid", + "201": "$t(messages.fields.{{fieldName}}) is required", + "202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is used", + "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is too long, it should be shorter than or equal to {{length}}", + "204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is too short, it should be longer than or equal to {{length}}", + "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid", + "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, please enter the $t(messages.fields.{{fieldName}}) with no more than {{length}} decimal places", + "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, it cannot be negative", + "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, it should start with http(s)://", + "209": "$t(messages.fields.{{fieldName}}) should have no whitespaces", + "301": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is irremovable", + "302": "Fail to launch the app", + "303": "$t(messages.fields.{{fieldName}}) is not found", + "304": "Camera is unavailable or disabled", + "305": "$t(messages.fields.address) cannot be empty" + } }, "sync": { "syncing": "Syncing", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 507f6ead45..c20c3828dd 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -90,6 +90,7 @@ "input-password-to-confirm": "输入密码以确认本次交易", "this-transaction-will-send": "本次交易将发送", "scan-to-get-address": "扫描二维码以获取地址", + "total-amount": "总金额", "description": "备注", "description-optional": "备注 (选填)", "balance": "余额", @@ -185,7 +186,7 @@ "edit-network": { "title": "添加或编辑网络", "rpc-url": "RPC地址", - "name": "名字" + "name": "名称" } } }, @@ -240,45 +241,49 @@ "success": "成功" }, "messages": { - "at-least-one-address-needed": "需要至少一个地址", - "name-required": "缺少名称", - "url-required": "缺少 URL", - "protocol-required": "请指定 URL 协议", - "length-of-name-should-be-less-than-or-equal-to": "名称长度应不大于 {{length}}", - "network-name-used": "节点名称已存在", - "is-unremovable": "{{target}}不可删除", - "create-wallet-success": "您已成功创建钱包 '{{name}}'", - "network-is-not-found": "未找到节点信息", - "failed-to-initiate,-please-reopen-Neuron": "初始化失败, 请新打开 Neuron", - "no-wallet": "没有钱包", - "wallet-imported-successfully": "{{name}} 导入成功", - "wallet-created-successfully": "{{name}} 创建成功", - "wallet-updated-successfully": "{{name}} 更新成功", - "wallet-not-found": "未找到钱包", - "no-transactions": "没有交易", "error": "错误", - "invalid-mnemonic": "助记词不合法", - "camera-not-available-or-disabled": "摄像头不可用或被禁用", - "can-not-find-the-default-address": "未获得默认地址", - "create-wallet-successfully": "新建钱包成功", - "import-wallet-successfully": "导入钱包成功", + "unknown-error": "未知错误", "update-wallet-successfully": "已更新钱包信息", "delete-wallet-successfully": "已删除钱包", "create-network-successfully": "新节点已添加", "update-network-successfully": "已更新节点信息", - "delete-network-successfully": "节点已删除", "addr-copied": "地址已复制到剪贴板", "qrcode-copied": "二维码已复制到剪贴板", "lock-arg-copied": "Lock Arg 已复制到剪贴板", - "transaction-not-found": "未找到交易", - "network-address-should-have-protocol": "RPC 地址应以 http(s)//: 开始", - "network-address-should-have-no-whitespaces": "RPC 地址不能包含空格", - "is-required": "{{field}}是必须的", - "is-used": "{{field}}已使用", - "invalid-address": "{{address}} 是无效的地址", - "amount-decimal-exceed": "金额 {{amount}} 是个无效的值, 其小数位应不超过 8 位", - "invalid-amount": "金额 {{amount} 是无效的数字", - "amount-too-small": "金额 {{amount}} 太小, 请输入一个不小于 61 CKB 的值" + "fields": { + "wallet": "钱包", + "name": "名称", + "remote": "RPC URL", + "network": "网络", + "address": "地址", + "amount": "金额", + "transaction": "交易", + "default-address": "默认地址", + "mnemonic": "助记词", + "keystore-path": "Keystore 文件", + "keystore-name": "钱包名称", + "keystore-password": "密码" + }, + "codes": { + "-3": "", + "100": "余额不足", + "101": "金额 {{amount}} CKB 太小, 请输入一个不小于 61 CKB 的值", + "102": "$t(messages.fields.{{fieldName}})无效", + "201": "缺少$t(messages.fields.{{fieldName}})", + "202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 已被使用", + "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太长, 其长度应不超过 {{length}}", + "204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太短, 其长度应不小于 {{length}}", + "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效", + "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其小数位应不超过 {{length}} 位", + "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值不能为负数", + "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值应以 http(s):// 开始", + "209": "$t(messages.fields.{{fieldName}})不能包含空格", + "301": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 不可删除", + "302": "无法启动应用", + "303": "未找到$t(messages.fields.{{fieldName}})", + "304": "摄像头不可用或被禁用", + "305": "$t(messages.fields.address)不能为空" + } }, "sync": { "syncing": "同步中", diff --git a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts index 6bef71266c..b44a7f8a6b 100644 --- a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts +++ b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts @@ -5,29 +5,39 @@ interface SuccessFromController { } interface FailureFromController { status: 0 - message: { - title: string - content?: string - } + message: + | string + | { + content?: string + meta?: { [key: string]: string } + } } export type ControllerResponse = SuccessFromController | FailureFromController export const RemoteNotLoadError = { status: 0 as 0, message: { - title: 'remote is not supported', + content: 'remote is not supported', }, } export const controllerNotLoaded = (controllerName: string) => ({ status: 0 as 0, message: { - title: `${controllerName} controller not loaded`, + content: `${controllerName} controller not loaded`, }, }) export const controllerMethodWrapper = (controllerName: string) => ( - callControllerMethod: (controller: any) => (params: any) => Promise<{ status: any; result: any; msg: string }> + callControllerMethod: ( + controller: any + ) => ( + params: any + ) => Promise<{ + status: any + result: any + message: { code?: number; content?: string; meta?: { [key: string]: string } } + }> ) => async (realParams?: any): Promise => { if (!window.remote) { return RemoteNotLoadError @@ -44,12 +54,14 @@ export const controllerMethodWrapper = (controllerName: string) => ( console.groupEnd() /* eslint-enable no-console */ } + if (!res) { return { status: 1, result: null, } } + if (res.status) { return { status: 1, @@ -57,16 +69,9 @@ export const controllerMethodWrapper = (controllerName: string) => ( } } - let title = '' - - if (typeof res === 'string') { - title = res - } else if (typeof res.msg === 'string') { - title = res.msg - } return { status: 0, - message: { title }, + message: typeof res.message === 'string' ? { content: res.message } : res.message || '', } } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts index ff5aa97ce5..d7f6c4ffef 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts @@ -1,7 +1,7 @@ import { NeuronWalletActions, AppActions, StateDispatch } from 'states/stateProvider/reducer' import { getNeuronWalletState } from 'services/remote' import initStates from 'states/initStates' -import { Routes } from 'utils/const' +import { Routes, ErrorCode } from 'utils/const' import { WalletWizardPath } from 'components/WalletWizard' import { addressesToBalance } from 'utils/formatters' import { @@ -73,16 +73,10 @@ export const addPopup = (text: string) => (dispatch: StateDispatch) => { }, 8000) } -export const addNotification = ({ type, content }: { type: 'alert' | 'warning'; content: string }) => ( - dispatch: StateDispatch -) => { +export const addNotification = (message: State.Message) => (dispatch: StateDispatch) => { dispatch({ type: AppActions.AddNotification, - payload: { - type, - content, - timestamp: Date.now(), - }, + payload: message, }) } export const dismissNotification = (timestamp: number) => (dispatch: StateDispatch) => { diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts index 2fc4283507..6279aa3103 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts @@ -1,6 +1,7 @@ import { createNetwork as createRemoteNetwork, updateNetwork as updateRemoteNetwork } from 'services/remote' import { addressBook } from 'utils/localCache' import { Routes } from 'utils/const' +import { failureResToNotification } from 'utils/formatters' import { addNotification, addPopup } from './app' import { AppActions, StateDispatch } from '../reducer' @@ -15,26 +16,22 @@ export const toggleAddressBook = () => { export const createNetwork = (params: Controller.CreateNetworkParams) => (dispatch: StateDispatch, history: any) => { createRemoteNetwork(params).then(res => { - if (res.status) { - dispatch({ - type: AppActions.Ignore, - payload: null, - }) + if (res.status === 1) { addPopup('create-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } export const updateNetwork = (params: Controller.UpdateNetworkParams) => (dispatch: StateDispatch, history: any) => { updateRemoteNetwork(params).then(res => { - if (res.status) { + if (res.status === 1) { addPopup('update-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts index 8aed69669c..491e0c12e4 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts @@ -4,6 +4,7 @@ import { getTransactionList, updateTransactionDescription as updateRemoteTransactionDescription, } from 'services/remote' +import { failureResToNotification } from 'utils/formatters' import { addNotification } from './app' export const updateTransactionList = (params: GetTransactionListParams) => (dispatch: StateDispatch) => { @@ -14,7 +15,7 @@ export const updateTransactionList = (params: GetTransactionListParams) => (disp payload: res.result, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -39,7 +40,7 @@ export const updateTransactionDescription = (params: Controller.UpdateTransactio }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) .finally(() => { diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts index 3b5f3c6753..23febb7f63 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts @@ -19,13 +19,13 @@ import { WalletWizardPath } from 'components/WalletWizard' import i18n from 'utils/i18n' import { wallets as walletsCache, currentWallet as currentWalletCache } from 'utils/localCache' import { Routes } from 'utils/const' -import { addressesToBalance } from 'utils/formatters' +import { addressesToBalance, failureResToNotification } from 'utils/formatters' import { NeuronWalletActions } from '../reducer' import { addNotification, addPopup } from './app' export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) => { getCurrentWallet().then(res => { - if (res.status) { + if (res.status === 1) { const payload = res.result || initStates.wallet if (!payload || !payload.id) { history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) @@ -36,7 +36,7 @@ export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) }) currentWalletCache.save(payload) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -46,10 +46,15 @@ export const createWalletWithMnemonic = (params: Controller.ImportMnemonicParams history: any ) => { createWallet(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) + } else if (res.message) { + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) + } } }) } @@ -59,10 +64,15 @@ export const importWalletWithMnemonic = (params: Controller.ImportMnemonicParams history: any ) => { importMnemonic(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) + } else if (res.message) { + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) + } } }) } @@ -72,17 +82,22 @@ export const importWalletWithKeystore = (params: Controller.ImportKeystoreParams history: any ) => { importKeystore(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) + } else if (res.message) { + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) + } } }) } export const updateWalletList = () => (dispatch: StateDispatch, history: any) => { getWalletList().then(res => { - if (res.status) { + if (res.status === 1) { const payload = res.result || [] if (!payload.length) { history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) @@ -93,7 +108,7 @@ export const updateWalletList = () => (dispatch: StateDispatch, history: any) => }) walletsCache.save(payload) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -109,7 +124,7 @@ export const updateWalletProperty = (params: Controller.UpdateWalletParams) => ( history.push(Routes.SettingsWallets) } } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -121,7 +136,7 @@ export const setCurrentWallet = (id: string) => (dispatch: StateDispatch) => { payload: null, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -135,17 +150,29 @@ export const sendTransaction = (params: Controller.SendTransaction) => (dispatch }) sendCapacity(params) .then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.History) } else { // TODO: the pretreatment is unnecessary once the error code is implemented - addNotification({ type: 'alert', content: res.message.title.replace(/(\b"|"\b)/g, '') })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.status, + content: (typeof res.message === 'string' ? res.message : res.message.content || '').replace( + /(\b"|"\b)/g, + '' + ), + meta: typeof res.message === 'string' ? undefined : res.message.meta, + })(dispatch) } dispatch({ type: AppActions.DismissPasswordRequest, payload: null, }) }) + .catch(err => { + console.warn(err) + }) .finally(() => { dispatch({ type: AppActions.UpdateLoadings, @@ -168,7 +195,7 @@ export const updateAddressListAndBalance = (params: Controller.GetAddressesByWal payload: { addresses, balance }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -193,7 +220,7 @@ export const updateAddressDescription = (params: Controller.UpdateAddressDescrip }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) .finally(() => { @@ -215,7 +242,7 @@ export const deleteWallet = (params: Controller.DeleteWalletParams) => (dispatch if (res.status) { addPopup('delete-wallet-successfully')(dispatch) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -232,7 +259,7 @@ export const backupWallet = (params: Controller.BackupWalletParams) => (dispatch payload: null, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } diff --git a/packages/neuron-ui/src/tests/formatters.test.ts b/packages/neuron-ui/src/tests/formatters/formatters.test.ts similarity index 100% rename from packages/neuron-ui/src/tests/formatters.test.ts rename to packages/neuron-ui/src/tests/formatters/formatters.test.ts diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts new file mode 100644 index 0000000000..9dcd4ac08c --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts @@ -0,0 +1,37 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + amount: string + expected: + | boolean + | { + code: ErrorCode + } + } +} = { + 'Valid Amount of 100000000000000000000000000000000000000000000000.00000001': { + amount: '100000000000000000000000000000000000000000000000.00000001', + expected: true, + }, + 'Amount which is not a number should fail': { + amount: 'not a number', + expected: { + code: ErrorCode.FieldInvalid, + }, + }, + 'Negative amount should fail': { + amount: '-1', + expected: { + code: ErrorCode.NotNegative, + }, + }, + 'Amount has more than 8 decimal places should fail': { + amount: '0.000000001', + expected: { + code: ErrorCode.DecimalExceed, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts new file mode 100644 index 0000000000..9efb3336d9 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts @@ -0,0 +1,11 @@ +import { verifyAmount } from 'utils/validators' +import { ErrorCode } from '../../../utils/const' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) + +describe(`Verify amount`, () => { + test.each(fixtureTable)(`%s`, (_title: string, amount: string, expected: boolean | { code: ErrorCode }) => { + expect(verifyAmount(amount)).toEqual(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts new file mode 100644 index 0000000000..b7d6801a72 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts @@ -0,0 +1,22 @@ +export default { + 'Amount of 0 should fail': { + amount: '0', + expected: false, + }, + 'Amount of 60.99999999 should fail': { + amount: '60.99999999', + expected: false, + }, + 'Amount equals to 61 should pass': { + amount: '61', + expected: true, + }, + 'Amount close to 61.00000001 should pass': { + amount: '61.00000001', + expected: true, + }, + 'Amount far away from 61 should pass': { + amount: '6100000001', + expected: true, + }, +} diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts new file mode 100644 index 0000000000..1138435193 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts @@ -0,0 +1,10 @@ +import { verifyAmountRange } from 'utils/validators' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) + +describe('Verify amount range', () => { + test.each(fixtureTable)(`%s`, (_title: string, amount: string, expected: boolean) => { + expect(verifyAmountRange(amount)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts new file mode 100644 index 0000000000..ea59d674e6 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts @@ -0,0 +1,43 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + name: string + usedNames: string[] + expected: boolean | { code: ErrorCode } + } +} = { + 'Valid name': { + name: 'Testnet', + usedNames: ['Local'], + expected: true, + }, + 'Empty name should fail': { + name: '', + usedNames: ['Local'], + expected: { + code: ErrorCode.FieldRequired, + }, + }, + 'Name consists of 28 charcters': { + name: '1234567890123456789012345678', + usedNames: ['Local'], + expected: true, + }, + 'Name consists of more than 28 characters should fail': { + name: '12345678901234567890123456789', + usedNames: ['Local'], + expected: { + code: ErrorCode.FieldTooLong, + }, + }, + 'Name which is used should fail': { + name: 'Testnet', + usedNames: ['Testnet', 'Local'], + expected: { + code: ErrorCode.FieldUsed, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts new file mode 100644 index 0000000000..43b350166a --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts @@ -0,0 +1,19 @@ +import { verifyNetworkName } from 'utils/validators' +import { ErrorCode } from 'utils/const' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { name, usedNames, expected }]) => [ + title, + name, + usedNames, + expected, +]) + +describe(`Verify network name`, () => { + test.each(fixtureTable)( + `%s`, + (_title: string, name: string, usedNames: string[], expected: boolean | { code: ErrorCode }) => { + expect(verifyNetworkName(name, usedNames)).toEqual(expected) + } + ) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts new file mode 100644 index 0000000000..d44e0c1b75 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts @@ -0,0 +1,35 @@ +const fixtures: { + [title: string]: { + totalAmount: string + fee: string + balance: string + expected: boolean + } +} = { + 'Valid total amount': { + totalAmount: '10000000000000000000000', + fee: '1', + balance: '10000000000000000000001', + expected: true, + }, + 'Too large total amount should fail': { + totalAmount: '10000000000000000000001', + fee: '0', + balance: '10000000000000000000000', + expected: false, + }, + 'Too large fee should fail': { + totalAmount: '10000000000000000000000', + fee: '1', + balance: '10000000000000000000000', + expected: false, + }, + 'Negative balance should fail': { + totalAmount: '10000000000000000000000', + fee: '10000000000', + balance: '-10000000000010000000000', + expected: false, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts new file mode 100644 index 0000000000..3b18e5a712 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts @@ -0,0 +1,19 @@ +import { verifyTotalAmount } from 'utils/validators' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { totalAmount, fee, balance, expected }]) => [ + title, + totalAmount, + fee, + balance, + expected, +]) + +describe('Verify total amount', () => { + test.each(fixtureTable)( + `%s`, + (_title: string, totalAmount: string, fee: string, balance: string, expected: boolean) => { + expect(verifyTotalAmount(totalAmount, fee, balance)).toBe(expected) + } + ) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts new file mode 100644 index 0000000000..91f12e4987 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts @@ -0,0 +1,63 @@ +const fixtures: { + [title: string]: { + outputs: { address: string; amount: string }[] + expected: boolean + } +} = { + 'Valid address and valid amount': { + outputs: [ + { + address: 'ckt1qyqg5w7emdntvnnk7utzqkz3kx276um0j4qs525t0y', + amount: '100', + }, + ], + expected: true, + }, + 'Empty address and valid amount should fail': { + outputs: [ + { + address: '', + amount: '100', + }, + ], + expected: false, + }, + 'Invalid address and valid amount should fail': { + outputs: [ + { + address: 'abcdefg', + amount: '100', + }, + ], + expected: false, + }, + 'Valid address and amount of invalid number should fail': { + outputs: [ + { + address: 'abcdefg', + amount: 'invalid number', + }, + ], + expected: false, + }, + 'Valid address and negative amount should fail': { + outputs: [ + { + address: 'abcdefg', + amount: '-1', + }, + ], + expected: false, + }, + 'Valid address and amount less than 61 should fail': { + outputs: [ + { + address: 'abcdefg', + amount: '60', + }, + ], + expected: false, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts new file mode 100644 index 0000000000..b4f07c7b7f --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts @@ -0,0 +1,10 @@ +import { verifyTransactionOutputs } from 'utils/validators' +import fixtures from './fixture' + +const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) + +describe(`Verify transaction outputs`, () => { + test.each(fixtureTable)(`%s`, (_title: string, outputs: { address: string; amount: string }[], expected: boolean) => { + expect(verifyTransactionOutputs(outputs)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts new file mode 100644 index 0000000000..7a2b71fea9 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts @@ -0,0 +1,49 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + url: string + expected: boolean | { code: ErrorCode } + } +} = { + 'URL starts with http://': { + url: 'http://localhost', + expected: true, + }, + 'URL starts with https://': { + url: 'https://localhost', + expected: true, + }, + 'URL starts with http should fail': { + url: 'http hello', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL starts with https should fail': { + url: 'https hello', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL start with ws:// should fail': { + url: 'ws://localhost', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL contains whitespaces should fail': { + url: 'http:// localhost', + expected: { + code: ErrorCode.NoWhiteSpaces, + }, + }, + 'Empty URL should fail': { + url: '', + expected: { + code: ErrorCode.FieldRequired, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts new file mode 100644 index 0000000000..04e325918c --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts @@ -0,0 +1,11 @@ +import { verifyURL } from 'utils/validators' +import { ErrorCode } from 'utils/const' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { url, expected }]) => [title, url, expected]) + +describe('Verify URL', () => { + test.each(fixtureTable)(`%s`, (_title: string, url: string, expected: boolean | { code: ErrorCode }) => { + expect(verifyURL(url)).toEqual(expected) + }) +}) diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 2cfb486745..35964b6440 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -47,11 +47,13 @@ declare namespace State { amount: string unit: any } - interface Message { - type: 'success' | 'warning' | 'alert' + type MessageType = 'success' | 'warning' | 'alert' + interface Message { + type: MessageType timestamp: number - content: string - meta?: { [key: string]: string } + code?: Code + content?: string + meta?: Meta } interface Send { txID: string diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 1dfa352b08..ad02db9781 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -1,4 +1,5 @@ export const MAX_NETWORK_NAME_LENGTH = 28 +export const MAX_WALLET_NAME_LENGTH = 20 export const ADDRESS_LENGTH = 46 export const MIN_PASSWORD_LENGTH = 8 export const MAX_PASSWORD_LENGTH = 50 @@ -49,20 +50,6 @@ export const PlaceHolders = { }, } -export enum Message { - NameRequired = 'messages.name-required', - URLRequired = 'messages.url-required', - LengthOfNameShouldBeLessThanOrEqualTo = 'messages.length-of-name-should-be-less-than-or-equal-to', - NetworkNameUsed = 'messages.network-name-used', - AtLeastOneAddressNeeded = 'messages.at-least-one-address-needed', - InvalidAddress = 'messages.invalid-address', - InvalidAmount = 'messages.invalid-amount', - DecimalExceed = 'messages.amount-decimal-exceed', - IsUnremovable = 'messages.is-unremovable', - ProtocolRequired = 'messages.protocol-required', - AmountTooSmall = 'messages.amount-too-small', -} - export enum MnemonicAction { Create = 'create', Verify = 'verify', @@ -77,3 +64,26 @@ export const FULL_SCREENS = [ `${Routes.WalletEditor}/`, `${Routes.NetworkEditor}/`, ] + +export enum ErrorCode { + // Errors from RPC + ErrorFromRPC = -3, + // Errors from neuron-wallet + AmountNotEnough = 100, + AmountTooSmall = 101, + // Parameter validation errors from neuron-ui + FieldRequired = 201, + FieldUsed = 202, + FieldTooLong = 203, + FieldTooShort = 204, + FieldInvalid = 205, + DecimalExceed = 206, + NotNegative = 207, + ProtocolRequired = 208, + NoWhiteSpaces = 209, + FieldIrremovable = 301, + FailToLaunch = 302, + FieldNotFound = 303, + CameraUnavailable = 304, + AddressIsEmpty = 305, +} diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index e178b94cd5..bc4dfaadcc 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -173,6 +173,16 @@ export const outputsToTotalCapacity = (outputs: { amount: string; unit: Capacity return totalCapacity.toString() } +export const failureResToNotification = (res: any): State.Message => { + return { + type: 'alert', + timestamp: +new Date(), + code: res.status, + content: typeof res.message !== 'string' ? res.message.content : res.message, + meta: typeof res.message !== 'string' ? res.message.meta : undefined, + } +} + export default { queryFormatter, currencyFormatter, @@ -183,4 +193,5 @@ export default { priceToFee, addressesToBalance, outputsToTotalCapacity, + failureResToNotification, } diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index 0f9667d913..cccd918f54 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -1,23 +1,44 @@ +import { MAX_NETWORK_NAME_LENGTH } from 'utils/const' +/* global BigInt */ import { ckbCore } from 'services/chain' -import { ADDRESS_LENGTH, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, MIN_AMOUNT } from './const' +import { MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, MIN_AMOUNT, MAX_DECIMAL_DIGITS, ErrorCode } from './const' -export const verifyAddress = (address: string): boolean | string => { - // TODO: verify address, prd required +export const verifyAddress = (address: string): boolean => { try { - if (address.length !== ADDRESS_LENGTH) { - throw new Error('Address length is incorrect') - } ckbCore.utils.parseAddress(address) return true } catch (err) { - return err.message + return false } } -export const verifyAmountRange = (amount: string) => { +export const verifyAmountRange = (amount: string = '') => { return +amount >= MIN_AMOUNT } +export const verifyAmount = (amount: string = '0') => { + if (Number.isNaN(+amount)) { + return { code: ErrorCode.FieldInvalid } + } + if (+amount < 0) { + return { code: ErrorCode.NotNegative } + } + const [, decimal = ''] = amount.split('.') + if (decimal.length > MAX_DECIMAL_DIGITS) { + return { + code: ErrorCode.DecimalExceed, + } + } + return true +} + +export const verifyTotalAmount = (totalAmount: string, fee: string, balance: string) => { + if (+balance < 0) { + return false + } + return BigInt(totalAmount) + BigInt(fee) <= BigInt(balance) +} + export const verifyPasswordComplexity = (password: string) => { if (!password) { return 'password-is-empty' @@ -51,8 +72,61 @@ export const verifyPasswordComplexity = (password: string) => { return true } +export const verifyTransactionOutputs = (items: { address: string; amount: string }[] = []) => { + return !items.some(item => { + if (item.address === '' || verifyAddress(item.address) !== true) { + return true + } + if (verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { + return true + } + return false + }) +} + +export const verifyNetworkName = (name: string, usedNames: string[]) => { + if (!name) { + return { + code: ErrorCode.FieldRequired, + } + } + if (usedNames.includes(name)) { + return { + code: ErrorCode.FieldUsed, + } + } + if (name.length > MAX_NETWORK_NAME_LENGTH) { + return { + code: ErrorCode.FieldTooLong, + } + } + return true +} + +export const verifyURL = (url: string) => { + if (!url) { + return { + code: ErrorCode.FieldRequired, + } + } + if (!/^https?:\/\//.test(url)) { + return { + code: ErrorCode.ProtocolRequired, + } + } + if (/\s/.test(url)) { + return { + code: ErrorCode.NoWhiteSpaces, + } + } + return true +} + export default { verifyAddress, verifyAmountRange, + verifyTotalAmount, verifyPasswordComplexity, + verifyTransactionOutputs, + verifyNetworkName, } diff --git a/packages/neuron-ui/src/widgets/QRScanner/index.tsx b/packages/neuron-ui/src/widgets/QRScanner/index.tsx index e2b3fcc942..067963af32 100644 --- a/packages/neuron-ui/src/widgets/QRScanner/index.tsx +++ b/packages/neuron-ui/src/widgets/QRScanner/index.tsx @@ -14,6 +14,7 @@ import jsQR from 'jsqr' import { showErrorMessage } from 'services/remote' import { drawPolygon } from 'utils/canvasActions' import { verifyAddress } from 'utils/validators' +import { ErrorCode } from 'utils/const' interface QRScannerProps { title: string @@ -100,7 +101,7 @@ const QRScanner = ({ title, label, onConfirm, styles }: QRScannerProps) => { requestAnimationFrame(tick) }) .catch((err: Error) => { - showErrorMessage(t('messages.camera-not-available-or-disabled'), err.message) + showErrorMessage(t(`messages.codes.${ErrorCode.CameraUnavailable}`), err.message) setOpen(false) }) }, [video, t, onConfirm, onDismiss]) diff --git a/packages/neuron-wallet/src/controllers/wallets/index.ts b/packages/neuron-wallet/src/controllers/wallets/index.ts index 6fc3c95328..0f98884eb9 100644 --- a/packages/neuron-wallet/src/controllers/wallets/index.ts +++ b/packages/neuron-wallet/src/controllers/wallets/index.ts @@ -371,7 +371,7 @@ export default class WalletsController { } catch (err) { return { status: ResponseCode.Fail, - msg: `Error: "${err.message}"`, + message: `Error: "${err.message}"`, } } } @@ -391,7 +391,7 @@ export default class WalletsController { } catch (err) { return { status: ResponseCode.Fail, - msg: `Error: "${err.message}"`, + message: `Error: "${err.message}"`, } } } diff --git a/packages/neuron-wallet/src/decorators/errors.ts b/packages/neuron-wallet/src/decorators/errors.ts index 93df8d62b0..efaa1b3ffe 100644 --- a/packages/neuron-wallet/src/decorators/errors.ts +++ b/packages/neuron-wallet/src/decorators/errors.ts @@ -10,7 +10,7 @@ export const CatchControllerError = (_target: any, _name: string, descriptor: Pr } catch (err) { return { status: ResponseCode.Fail, - msg: err.message, + message: typeof err.message === 'string' ? { content: err.message } : err.message, } } }, diff --git a/packages/neuron-wallet/src/types/controller/index.d.ts b/packages/neuron-wallet/src/types/controller/index.d.ts index c6d318a9c0..43b39ce24a 100644 --- a/packages/neuron-wallet/src/types/controller/index.d.ts +++ b/packages/neuron-wallet/src/types/controller/index.d.ts @@ -1,7 +1,12 @@ declare module Controller { interface Response { status: number - msg?: string + message?: + | string + | { + content?: string + meta?: { [key: string]: string } + } result?: T } From d45bd7b9632b49495e06c789b785a28cb4264e1b Mon Sep 17 00:00:00 2001 From: Keith Date: Tue, 27 Aug 2019 17:48:45 +0800 Subject: [PATCH 05/29] refactor(neuron-ui): rename outputsToTotalCapacity to outputsToTotalAmount and add more tests of formatter methods --- .../neuron-ui/src/components/Send/hooks.ts | 4 +- .../CKBToShannonFormatter/fixtures.ts | 69 +++++ .../CKBToShannonFormatter/index.test.ts | 11 + .../formatters/addressesToBalance/fixtures.ts | 79 +++++ .../addressesToBalance/index.test.ts | 10 + .../formatters/currencyFormatter/fixtures.ts | 43 +++ .../currencyFormatter/index.test.ts | 10 + .../src/tests/formatters/formatters.test.ts | 279 ------------------ .../outputsToTotalAmount/fixtures.ts | 53 ++++ .../outputsToTotalAmount/index.test.ts | 10 + .../shannonToCKBFormatter/fixtures.ts | 72 +++++ .../shannonToCKBFormatter/index.test.ts | 14 + packages/neuron-ui/src/utils/formatters.ts | 4 +- 13 files changed, 375 insertions(+), 283 deletions(-) create mode 100644 packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts create mode 100644 packages/neuron-ui/src/tests/formatters/addressesToBalance/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/formatters/addressesToBalance/index.test.ts create mode 100644 packages/neuron-ui/src/tests/formatters/currencyFormatter/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/formatters/currencyFormatter/index.test.ts delete mode 100644 packages/neuron-ui/src/tests/formatters/formatters.test.ts create mode 100644 packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/index.test.ts create mode 100644 packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/index.test.ts diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 6bcb392f5a..6eb2bafcb5 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -3,7 +3,7 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react' import { AppActions, StateDispatch } from 'states/stateProvider/reducer' import { calculateCycles } from 'services/remote/wallets' -import { outputsToTotalCapacity, priceToFee } from 'utils/formatters' +import { outputsToTotalAmount, priceToFee } from 'utils/formatters' import { verifyAddress, verifyAmount, verifyAmountRange, verifyTransactionOutputs } from 'utils/validators' import { ErrorCode } from 'utils/const' import { MAX_DECIMAL_DIGITS } from '../../utils/const' @@ -58,7 +58,7 @@ const useOnTransactionChange = ( cyclesTimer = setTimeout(() => { if (verifyTransactionOutputs(items)) { setIsTransactionValid(true) - const totalAmount = outputsToTotalCapacity(items) + const totalAmount = outputsToTotalAmount(items) setTotalAmount(totalAmount) calculateCycles({ walletID, diff --git a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts new file mode 100644 index 0000000000..76d9fd68c3 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts @@ -0,0 +1,69 @@ +import { CapacityUnit } from 'utils/const' + +const fixtures = [ + { + ckb: { + amount: `1.234`, + unit: CapacityUnit.CKB, + }, + expected: `123400000`, + }, + { + ckb: { + amount: `1.23456789`, + unit: CapacityUnit.CKB, + }, + expected: `123456789`, + }, + { + ckb: { + amount: `1.0`, + unit: CapacityUnit.CKB, + }, + expected: `100000000`, + }, + { + ckb: { + amount: `1.`, + unit: CapacityUnit.CKB, + }, + expected: `100000000`, + }, + { + ckb: { + amount: `0.123`, + unit: CapacityUnit.CKB, + }, + expected: `12300000`, + }, + { + ckb: { + amount: `.123`, + unit: CapacityUnit.CKB, + }, + expected: `12300000`, + }, + { + ckb: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + unit: CapacityUnit.CKB, + }, + expected: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, + }, + { + ckb: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + unit: CapacityUnit.CKKB, + }, + expected: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000`, + }, + { + ckb: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + unit: CapacityUnit.CKGB, + }, + expected: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000000000`, + }, +] + +export default fixtures diff --git a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts new file mode 100644 index 0000000000..0e5565f06f --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts @@ -0,0 +1,11 @@ +import { CapacityUnit } from 'utils/const' +import { CKBToShannonFormatter } from 'utils/formatters' +import fixtures from './fixtures' + +const fixtureTable = fixtures.map(({ ckb: { amount, unit }, expected }) => [amount, unit, expected]) + +describe(`Verify CKB to Shannons formatter`, () => { + test.each(fixtureTable)(`%s %s => %s shannons`, (amount: string, unit: CapacityUnit, expected: string) => { + expect(CKBToShannonFormatter(amount, unit)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/formatters/addressesToBalance/fixtures.ts b/packages/neuron-ui/src/tests/formatters/addressesToBalance/fixtures.ts new file mode 100644 index 0000000000..8e9ef9b4e9 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/addressesToBalance/fixtures.ts @@ -0,0 +1,79 @@ +const fixtures: { + [title: string]: { + addresses: { + balance: string | undefined + }[] + expected: string + } +} = { + basic: { + addresses: [ + { + balance: '100', + }, + { + balance: '10000', + }, + { + balance: '200', + }, + { + balance: '10000', + }, + ], + expected: '20300', + }, + 'number large than MAX SAFE INTEGER': { + addresses: [ + { + balance: '100', + }, + { + balance: '10000', + }, + { + balance: '200', + }, + { + balance: '100000000000000000000000000000000000000000000000000000000000', + }, + ], + expected: '100000000000000000000000000000000000000000000000000000010300', + }, + 'address has negative balance': { + addresses: [ + { + balance: '-100', + }, + { + balance: '10000', + }, + { + balance: '200', + }, + { + balance: '100000000000000000000000000000000000000000000000000000000000', + }, + ], + expected: '100000000000000000000000000000000000000000000000000000010100', + }, + 'address has undefined balance': { + addresses: [ + { + balance: undefined, + }, + { + balance: '10000', + }, + { + balance: '200', + }, + { + balance: '100000000000000000000000000000000000000000000000000000000000', + }, + ], + expected: '100000000000000000000000000000000000000000000000000000010200', + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/formatters/addressesToBalance/index.test.ts b/packages/neuron-ui/src/tests/formatters/addressesToBalance/index.test.ts new file mode 100644 index 0000000000..57ee59d3a2 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/addressesToBalance/index.test.ts @@ -0,0 +1,10 @@ +import { addressesToBalance } from 'utils/formatters' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { addresses, expected }]) => [title, addresses, expected]) + +describe(`Verify addresses to balance`, () => { + test.each(fixtureTable)(`%s`, (_title: string, addresses: any, expected: string) => { + expect(addressesToBalance(addresses)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/formatters/currencyFormatter/fixtures.ts b/packages/neuron-ui/src/tests/formatters/currencyFormatter/fixtures.ts new file mode 100644 index 0000000000..c90cb8f371 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/currencyFormatter/fixtures.ts @@ -0,0 +1,43 @@ +const fixtures = [ + { + value: { + shannons: `1234567890`, + unit: `CKB`, + exchange: `0.000000001`, + }, + expected: `1.23456789 CKB`, + }, + { + value: { + shannons: `1234567890`, + unit: `CKB`, + exchange: `0.00065`, + }, + expected: `802,469.1285 CKB`, + }, + { + value: { + shannons: `1234567890`, + unit: `CNY`, + exchange: `0.00065`, + }, + expected: `802,469.1285 CNY`, + }, + { + value: { + shannons: `1234567890123456789012345678901234567890123456789012345678901234567890`, + unit: `CNY`, + exchange: `0.65`, + }, + expected: `802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128.5 CNY`, + }, + { + value: { + shannons: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + unit: `CNY`, + exchange: `0.65`, + }, + expected: `8,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802.1 CNY`, + }, +] +export default fixtures diff --git a/packages/neuron-ui/src/tests/formatters/currencyFormatter/index.test.ts b/packages/neuron-ui/src/tests/formatters/currencyFormatter/index.test.ts new file mode 100644 index 0000000000..a8e804ef62 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/currencyFormatter/index.test.ts @@ -0,0 +1,10 @@ +import { currencyFormatter } from 'utils/formatters' +import fixtures from './fixtures' + +const fixtureTable = fixtures.map(({ value, expected }) => [value, expected]) + +describe(`Verify currency formatter`, () => { + test.each(fixtureTable)(`%j => %s`, (value: any, expected: string) => { + expect(currencyFormatter(value.shannons, value.unit, value.exchange)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/formatters/formatters.test.ts b/packages/neuron-ui/src/tests/formatters/formatters.test.ts deleted file mode 100644 index 17bcde6644..0000000000 --- a/packages/neuron-ui/src/tests/formatters/formatters.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { CapacityUnit } from 'utils/const' -import { - currencyFormatter, - currencyCode, - CKBToShannonFormatter, - shannonToCKBFormatter, - addressesToBalance, - outputsToTotalCapacity, -} from 'utils/formatters' - -describe(`formatters`, () => { - it(`currencyFormatter`, () => { - const fixtures = [ - { - source: { - shannons: `1234567890`, - unit: `CKB` as currencyCode, - exchange: `0.000000001`, - }, - target: `1.23456789 CKB`, - }, - { - source: { - shannons: `1234567890`, - unit: `CKB` as currencyCode, - exchange: `0.00065`, - }, - target: `802,469.1285 CKB`, - }, - { - source: { - shannons: `1234567890`, - unit: `CNY` as currencyCode, - exchange: `0.00065`, - }, - target: `802,469.1285 CNY`, - }, - { - source: { - shannons: `1234567890123456789012345678901234567890123456789012345678901234567890`, - unit: `CNY` as currencyCode, - exchange: `0.65`, - }, - target: `802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128.5 CNY`, - }, - { - source: { - shannons: `12345678901234567890123456789012345678901234567890123456789012345678901234`, - unit: `CNY` as currencyCode, - exchange: `0.65`, - }, - target: `8,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802.1 CNY`, - }, - ] - fixtures.forEach(fixture => { - const result = currencyFormatter(fixture.source.shannons, fixture.source.unit, fixture.source.exchange) - expect(result).toBe(fixture.target) - }) - }) - - describe(`CKB Formatter`, () => { - it(`CKB to Shannon`, () => { - const fixtures = [ - { - source: { - amount: `1.234`, - uint: CapacityUnit.CKB, - }, - target: `123400000`, - }, - { - source: { - amount: `1.23456789`, - uint: CapacityUnit.CKB, - }, - target: `123456789`, - }, - { - source: { - amount: `1.0`, - uint: CapacityUnit.CKB, - }, - target: `100000000`, - }, - { - source: { - amount: `1.`, - uint: CapacityUnit.CKB, - }, - target: `100000000`, - }, - { - source: { - amount: `0.123`, - uint: CapacityUnit.CKB, - }, - target: `12300000`, - }, - { - source: { - amount: `.123`, - uint: CapacityUnit.CKB, - }, - target: `12300000`, - }, - { - source: { - amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, - uint: CapacityUnit.CKB, - }, - target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, - }, - { - source: { - amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, - uint: CapacityUnit.CKKB, - }, - target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000`, - }, - { - source: { - amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, - uint: CapacityUnit.CKGB, - }, - target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000000000`, - }, - ] - - fixtures.forEach(fixture => { - expect(CKBToShannonFormatter(fixture.source.amount, fixture.source.uint)).toBe(fixture.target) - }) - }) - - it(`shannon to CKB`, () => { - const fixtures = [ - { - source: `123`, - target: `0.00000123`, - }, - { - source: `12300000`, - target: `0.123`, - }, - { - source: `123000000`, - target: `1.23`, - }, - { - source: `000123000000`, - target: `1.23`, - }, - { - source: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, - target: `12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234`, - }, - { - source: `12345678901234567890123456789012345678901234567890123456789012345678901234`, - target: `123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234`, - }, - { - source: `1234567890123456789012345678901234567890123456789012345678901234567890123400`, - target: `12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234`, - }, - - { - source: `-123`, - target: `-0.00000123`, - }, - { - source: `-12300000`, - target: `-0.123`, - }, - { - source: `-123000000`, - target: `-1.23`, - }, - { - source: `-000123000000`, - target: `-1.23`, - }, - { - source: `-1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, - target: `-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234`, - }, - { - source: `-12345678901234567890123456789012345678901234567890123456789012345678901234`, - target: `-123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234`, - }, - { - source: `-1234567890123456789012345678901234567890123456789012345678901234567890123400`, - target: `-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234`, - }, - { - source: `0`, - target: `0`, - }, - { - source: `-0`, - target: `0`, - }, - { - source: ``, - target: `0`, - }, - ] - - fixtures.forEach(fixture => { - expect(shannonToCKBFormatter(fixture.source)).toBe(fixture.target) - expect(shannonToCKBFormatter(fixture.source, true)).toBe( - +fixture.source > 0 ? `+${fixture.target}` : fixture.target - ) - }) - }) - - it('addresses to balance', () => { - const fixture = [ - { - address: 'ckt1q9gry5zg8stq8ruq5wfz3lm5wn2k7qw3ulsfmdhe98f2j1', - identifier: '4040ba0ed8a361c59c30bb92f46128f95eaa9bcb', - description: 'description', - type: 0 as 0 | 1, - txCount: 0, - balance: '100', - index: 0, - }, - { - address: 'ckt1q9gry5zg8stq8ruq5wfz3lm5wn2k7qw3ulsfmdhe98f2j3', - identifier: '4040ba0ed8a361c59c30bb92f46128f95eaa9bcb', - description: 'description', - type: 0 as 0 | 1, - txCount: 123, - balance: '10000', - index: 1, - }, - { - address: 'ckt1q9gry5zg8stq8ruq5wfz3lm5wn2k7qw3ulsfmdhe98f2j2', - identifier: '4040ba0ed8a361c59c30bb92f46128f95eaa9bcd', - description: 'description', - type: 1 as 0 | 1, - txCount: 0, - balance: '200', - index: 2, - }, - { - address: 'ckt1q9gry5zg8stq8ruq5wfz3lm5wn2k7qw3ulsfmdhe98f2jd', - identifier: '4040ba0ed8a361c59c30bb92f46128f95eaa9bcd', - description: 'description', - type: 1 as 0 | 1, - txCount: 123, - balance: '10000', - index: 3, - }, - ] - expect(addressesToBalance(fixture)).toBe('20300') - }) - - it('outputsToTotalCapacity', () => { - const fixture: any = [ - { - amount: '100', - unit: 'CKB', - }, - { - amount: '10000', - unit: 'CKB', - }, - { - amount: '200', - unit: 'CKB', - }, - { - amount: '10000', - unit: 'CKB', - }, - ] - expect(outputsToTotalCapacity(fixture)).toBe('20300') - }) - }) -}) diff --git a/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/fixtures.ts b/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/fixtures.ts new file mode 100644 index 0000000000..efff213700 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/fixtures.ts @@ -0,0 +1,53 @@ +const fixtures: { + [title: string]: { + outputs: { + amount: string + unit: 'CKB' + }[] + expected: string + } +} = { + basic: { + outputs: [ + { + amount: '100', + unit: 'CKB', + }, + { + amount: '10000', + unit: 'CKB', + }, + { + amount: '200', + unit: 'CKB', + }, + { + amount: '10000', + unit: 'CKB', + }, + ], + expected: '20300', + }, + 'amount large than MAX SAFE INTEGER': { + outputs: [ + { + amount: '100', + unit: 'CKB', + }, + { + amount: '10000', + unit: 'CKB', + }, + { + amount: '200', + unit: 'CKB', + }, + { + amount: '10000000000000000000000000000000000000000000000000000000000000000000', + unit: 'CKB', + }, + ], + expected: '10000000000000000000000000000000000000000000000000000000000000010300', + }, +} +export default fixtures diff --git a/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/index.test.ts b/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/index.test.ts new file mode 100644 index 0000000000..6360373170 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/outputsToTotalAmount/index.test.ts @@ -0,0 +1,10 @@ +import { outputsToTotalAmount } from 'utils/formatters' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) + +describe(`Verify outputs to total amount`, () => { + test.each(fixtureTable)(`%s`, (_title: string, outputs: any, expected: string) => { + expect(outputsToTotalAmount(outputs)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/fixtures.ts b/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/fixtures.ts new file mode 100644 index 0000000000..bf693cf5f3 --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/fixtures.ts @@ -0,0 +1,72 @@ +const fixtures = [ + { + shannons: `123`, + expected: `0.00000123`, + }, + { + shannons: `12300000`, + expected: `0.123`, + }, + { + shannons: `123000000`, + expected: `1.23`, + }, + { + shannons: `000123000000`, + expected: `1.23`, + }, + { + shannons: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, + expected: `12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234`, + }, + { + shannons: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + expected: `123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234`, + }, + { + shannons: `1234567890123456789012345678901234567890123456789012345678901234567890123400`, + expected: `12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234`, + }, + + { + shannons: `-123`, + expected: `-0.00000123`, + }, + { + shannons: `-12300000`, + expected: `-0.123`, + }, + { + shannons: `-123000000`, + expected: `-1.23`, + }, + { + shannons: `-000123000000`, + expected: `-1.23`, + }, + { + shannons: `-1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, + expected: `-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234`, + }, + { + shannons: `-12345678901234567890123456789012345678901234567890123456789012345678901234`, + expected: `-123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234`, + }, + { + shannons: `-1234567890123456789012345678901234567890123456789012345678901234567890123400`, + expected: `-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234`, + }, + { + shannons: `0`, + expected: `0`, + }, + { + shannons: `-0`, + expected: `0`, + }, + { + shannons: ``, + expected: `0`, + }, +] +export default fixtures diff --git a/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/index.test.ts b/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/index.test.ts new file mode 100644 index 0000000000..3787ca813a --- /dev/null +++ b/packages/neuron-ui/src/tests/formatters/shannonToCKBFormatter/index.test.ts @@ -0,0 +1,14 @@ +import { shannonToCKBFormatter } from 'utils/formatters' +import fixtures from './fixtures' + +const fixtureTable = fixtures.map(({ shannons, expected }) => [shannons, expected]) + +describe(`Verify shannon to CKB formatter`, () => { + test.each(fixtureTable)(`%s shannons => %s CKB without sign`, (shannons: string, expected: string) => { + expect(shannonToCKBFormatter(shannons)).toBe(expected) + }) + + test.each(fixtureTable)(`%s shannons => %s CKB with sign`, (shannons: string, expected: string) => { + expect(shannonToCKBFormatter(shannons, true)).toBe(+shannons > 0 ? `+${expected}` : expected) + }) +}) diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index bc4dfaadcc..a1baa6241a 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -163,7 +163,7 @@ export const addressesToBalance = (addresses: State.Address[] = []) => { .toString() } -export const outputsToTotalCapacity = (outputs: { amount: string; unit: CapacityUnit }[]) => { +export const outputsToTotalAmount = (outputs: { amount: string; unit: CapacityUnit }[]) => { const totalCapacity = outputs.reduce((total, cur) => { if (Number.isNaN(+cur.amount)) { return total @@ -192,6 +192,6 @@ export default { uniformTimeFormatter, priceToFee, addressesToBalance, - outputsToTotalCapacity, + outputsToTotalAmount, failureResToNotification, } From 6ca820058aa6c0c34e79c0a1b258ac47bdda005a Mon Sep 17 00:00:00 2001 From: classicalliu Date: Tue, 27 Aug 2019 18:34:43 +0800 Subject: [PATCH 06/29] feat: bump sdk to v0.19.0 in neuron-wallet --- packages/neuron-wallet/package.json | 6 +-- .../src/database/chain/entities/input.ts | 11 +--- .../src/database/chain/entities/output.ts | 11 +--- .../database/chain/entities/transaction.ts | 12 +++-- .../1566900661931-AlterDepsFromTransaction.ts | 32 ++++++++++++ .../src/database/chain/ormconfig.ts | 8 ++- .../neuron-wallet/src/models/lock-utils.ts | 50 ++++++++----------- packages/neuron-wallet/src/services/cells.ts | 5 +- .../services/sync/check-and-save/output.ts | 9 ++-- .../src/services/sync/check-and-save/tx.ts | 15 +++--- .../src/services/tx/transaction-generator.ts | 9 +++- .../src/services/tx/transaction-persistor.ts | 46 +++++++++-------- .../neuron-wallet/src/types/cell-types.ts | 25 ++++++---- .../neuron-wallet/src/types/convert-to.ts | 27 ++++++++-- .../neuron-wallet/src/types/type-convert.ts | 34 +++++++++---- yarn.lock | 40 +++++++++++++++ 16 files changed, 225 insertions(+), 115 deletions(-) create mode 100644 packages/neuron-wallet/src/database/chain/migrations/1566900661931-AlterDepsFromTransaction.ts diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index b5ee5b3a68..b898710641 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -34,8 +34,8 @@ ] }, "dependencies": { - "@nervosnetwork/ckb-sdk-core": "0.18.0", - "@nervosnetwork/ckb-sdk-utils": "0.18.0", + "@nervosnetwork/ckb-sdk-core": "0.19.0", + "@nervosnetwork/ckb-sdk-utils": "0.19.0", "bn.js": "4.11.8", "chalk": "2.4.2", "electron-log": "3.0.7", @@ -51,7 +51,7 @@ "uuid": "3.3.2" }, "devDependencies": { - "@nervosnetwork/ckb-types": "0.18.0", + "@nervosnetwork/ckb-types": "0.19.0", "@types/electron-devtools-installer": "2.2.0", "@types/elliptic": "6.4.8", "@types/sqlite3": "3.1.5", diff --git a/packages/neuron-wallet/src/database/chain/entities/input.ts b/packages/neuron-wallet/src/database/chain/entities/input.ts index f0c5995cb0..7b4ba19234 100644 --- a/packages/neuron-wallet/src/database/chain/entities/input.ts +++ b/packages/neuron-wallet/src/database/chain/entities/input.ts @@ -1,5 +1,5 @@ import { Entity, BaseEntity, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' -import { OutPoint, Input as InputInterface, CellOutPoint } from 'types/cell-types' +import { OutPoint, Input as InputInterface } from 'types/cell-types' import Transaction from './transaction' /* eslint @typescript-eslint/no-unused-vars: "warn" */ @@ -42,7 +42,7 @@ export default class Input extends BaseEntity { }) capacity: string | null = null - public cellOutPoint(): CellOutPoint | null { + public previousOutput(): OutPoint | null { if (!this.outPointTxHash || !this.outPointIndex) { return null } @@ -52,13 +52,6 @@ export default class Input extends BaseEntity { } } - public previousOutput(): OutPoint { - return { - blockHash: null, - cell: this.cellOutPoint(), - } - } - public toInterface(): InputInterface { return { previousOutput: this.previousOutput(), diff --git a/packages/neuron-wallet/src/database/chain/entities/output.ts b/packages/neuron-wallet/src/database/chain/entities/output.ts index 2fcf41d4a9..23e5606b53 100644 --- a/packages/neuron-wallet/src/database/chain/entities/output.ts +++ b/packages/neuron-wallet/src/database/chain/entities/output.ts @@ -1,5 +1,5 @@ import { Entity, BaseEntity, Column, PrimaryColumn, ManyToOne } from 'typeorm' -import { Script, OutPoint, Cell, CellOutPoint } from 'types/cell-types' +import { Script, OutPoint, Cell } from 'types/cell-types' import TransactionEntity from './transaction' /* eslint @typescript-eslint/no-unused-vars: "warn" */ @@ -35,20 +35,13 @@ export default class Output extends BaseEntity { }) status!: string - public cellOutPoint(): CellOutPoint { + public outPoint(): OutPoint { return { txHash: this.outPointTxHash, index: this.outPointIndex, } } - public outPoint(): OutPoint { - return { - blockHash: null, - cell: this.cellOutPoint(), - } - } - @ManyToOne(_type => TransactionEntity, transaction => transaction.outputs, { onDelete: 'CASCADE' }) transaction!: TransactionEntity diff --git a/packages/neuron-wallet/src/database/chain/entities/transaction.ts b/packages/neuron-wallet/src/database/chain/entities/transaction.ts index 865825c048..1184c997a8 100644 --- a/packages/neuron-wallet/src/database/chain/entities/transaction.ts +++ b/packages/neuron-wallet/src/database/chain/entities/transaction.ts @@ -11,7 +11,7 @@ import { AfterRemove, } from 'typeorm' import { remote } from 'electron' -import { Witness, OutPoint, Transaction as TransactionInterface, TransactionStatus } from 'types/cell-types' +import { Witness, Transaction as TransactionInterface, TransactionStatus, CellDep } from 'types/cell-types' import TxDbChangedSubject from 'models/subjects/tx-db-changed-subject' import InputEntity from './input' import OutputEntity from './output' @@ -37,7 +37,12 @@ export default class Transaction extends BaseEntity { @Column({ type: 'simple-json', }) - deps!: OutPoint[] + cellDeps!: CellDep[] + + @Column({ + type: 'simple-json', + }) + headerDeps!: string[] @Column({ type: 'simple-json', @@ -101,7 +106,8 @@ export default class Transaction extends BaseEntity { return { hash: this.hash, version: this.version, - deps: this.deps, + cellDeps: this.cellDeps, + headerDeps: this.headerDeps, inputs, outputs, timestamp: this.timestamp, diff --git a/packages/neuron-wallet/src/database/chain/migrations/1566900661931-AlterDepsFromTransaction.ts b/packages/neuron-wallet/src/database/chain/migrations/1566900661931-AlterDepsFromTransaction.ts new file mode 100644 index 0000000000..0b205d07ed --- /dev/null +++ b/packages/neuron-wallet/src/database/chain/migrations/1566900661931-AlterDepsFromTransaction.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AlterDepsFromTransaction1566900661931 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('transaction', new TableColumn({ + name: 'cellDeps', + type: 'simple-json', + default: [], + })) + + await queryRunner.addColumn('transaction', new TableColumn({ + name: 'headerDeps', + type: 'simple-json', + default: [], + })) + + await queryRunner.dropColumn('transaction', 'deps') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('transaction', new TableColumn({ + name: 'deps', + type: 'simple-json', + default: [], + })) + + await queryRunner.dropColumn('transaction', 'cellDeps') + await queryRunner.dropColumn('transaction', 'headerDeps') + } + +} diff --git a/packages/neuron-wallet/src/database/chain/ormconfig.ts b/packages/neuron-wallet/src/database/chain/ormconfig.ts index 98f2d75937..25fdbcd658 100644 --- a/packages/neuron-wallet/src/database/chain/ormconfig.ts +++ b/packages/neuron-wallet/src/database/chain/ormconfig.ts @@ -13,6 +13,7 @@ import SyncInfo from './entities/sync-info' import { InitMigration1561695143591 } from './migrations/1561695143591-InitMigration' import { AddStatusToTx1562038960990 } from './migrations/1562038960990-AddStatusToTx' import { AddConfirmed1565693320664 } from './migrations/1565693320664-AddConfirmed' +import { AlterDepsFromTransaction1566900661931 } from './migrations/1566900661931-AlterDepsFromTransaction' export const CONNECTION_NOT_FOUND_NAME = 'ConnectionNotFoundError' @@ -32,7 +33,12 @@ const connectOptions = async (genesisBlockHash: string): Promise { @@ -30,21 +32,16 @@ export default class LockUtils { return this.systemScriptInfo } - const systemCell = await core.loadSystemCell() + const systemCell = await core.loadSecp256k1Dep() let { codeHash } = systemCell - const { outPoint } = systemCell - let { blockHash } = outPoint - let { txHash } = outPoint.cell - const { index } = outPoint.cell + const { outPoint, hashType } = systemCell + let { txHash } = outPoint + const { index } = outPoint if (!codeHash.startsWith('0x')) { codeHash = `0x${codeHash}` } - if (!blockHash.startsWith('0x')) { - blockHash = `0x${blockHash}` - } - if (!txHash.startsWith('0x')) { txHash = `0x${txHash}` } @@ -52,12 +49,10 @@ export default class LockUtils { const systemScriptInfo = { codeHash, outPoint: { - blockHash, - cell: { - txHash, - index, - }, + txHash, + index, }, + hashType: hashType as ScriptHashType, } this.systemScriptInfo = systemScriptInfo @@ -70,23 +65,18 @@ export default class LockUtils { SystemScriptSubject.next({ codeHash: info.codeHash }) } - // use SDK lockScriptToHash - static lockScriptToHash = (lock: Script) => { - const codeHash: string = lock!.codeHash! - const args: string[] = lock.args! - const { hashType } = lock - // TODO: should support ScriptHashType.Type in the future - const lockHash: string = core.utils.lockScriptToHash({ - codeHash, - args, - hashType, - }) - - if (lockHash.startsWith('0x')) { - return lockHash + static computeScriptHash = async (script: Script): Promise => { + const ckbScript: CKBComponents.Script = ConvertTo.toSdkScript(script) + const hash: string = await (core.rpc as any).computeScriptHash(ckbScript) + if (!hash.startsWith('0x')) { + return `0x${hash}` } + return hash + } - return `0x${lockHash}` + // use SDK lockScriptToHash + static lockScriptToHash = async (lock: Script) => { + return LockUtils.computeScriptHash(lock) } static async addressToLockScript(address: string, hashType: ScriptHashType = ScriptHashType.Data): Promise