diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e40b27121..e1d1593ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# [0.19.0-beta.0](https://github.com/nervosnetwork/neuron/compare/v0.18.0-beta.1...v0.19.0-beta.0) (2019-08-29) + +### BREAKING CHANGES + +* Code signing certification was updated. Manual installation of this version is required as automatic update won't work. +* There was a bug that caused the keystore mac to calculate incorrectly. Older exported keystore files might not be able to be imported. +* DB structure was changed per CKB transaction and other types' changes. + +### Bug Fixes + +* Fix keystore mac calculation +* add outputsData and headerDeps ([f2432eb](https://github.com/nervosnetwork/neuron/commit/f2432eb)) +* Error when handling crypto.scryptSync with N > 16384 ([eac4339](https://github.com/nervosnetwork/neuron/commit/eac4339)) +* fix filterOutputs for async filter ([c851963](https://github.com/nervosnetwork/neuron/commit/c851963)) +* hide some tests in lock utils tests ([19c6673](https://github.com/nervosnetwork/neuron/commit/19c6673)) +* remove default hash_type=data ([33cc4cf](https://github.com/nervosnetwork/neuron/commit/33cc4cf)) + + +### Features + +* bump sdk to v0.19.0 in neuron-ui ([3b6ede9](https://github.com/nervosnetwork/neuron/commit/3b6ede9)) +* bump sdk to v0.19.0 in neuron-wallet ([6ca8200](https://github.com/nervosnetwork/neuron/commit/6ca8200)) +* Increase KDF params N value to 2**18 to produce more secure keystore ([06f5ac6](https://github.com/nervosnetwork/neuron/commit/06f5ac6)) +* Remove MenuCommand enum ([#900](https://github.com/nervosnetwork/neuron/issues/900)) ([e40c5cf](https://github.com/nervosnetwork/neuron/commit/e40c5cf)) +* Update TypeScript to 3.6 ([ac61a5b](https://github.com/nervosnetwork/neuron/commit/ac61a5b)) +* **neuron-ui:** set the cycles to empty if the transaction is invalid ([64166cc](https://github.com/nervosnetwork/neuron/commit/64166cc)) +* **neuron-ui:** show confirmations on the History View ([84692c7](https://github.com/nervosnetwork/neuron/commit/84692c7)) + + + # [0.18.0-beta.1](https://github.com/nervosnetwork/neuron/compare/v0.18.0-beta.0...v0.18.0-beta.1) (2019-08-22) diff --git a/lerna.json b/lerna.json index c1a77c9df1..5a45b898d7 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.18.0-beta.1", + "version": "0.19.0-beta.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index b41f603fc6..d9bbc5eee6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.18.0-beta.1", + "version": "0.19.0-beta.0", "private": true, "author": { "name": "Nervos Core Dev", @@ -44,8 +44,8 @@ }, "devDependencies": { "@types/jest": "24.0.13", - "@typescript-eslint/eslint-plugin": "1.10.2", - "@typescript-eslint/parser": "1.10.2", + "@typescript-eslint/eslint-plugin": "2.0.0", + "@typescript-eslint/parser": "2.0.0", "concurrently": "4.1.1", "cross-env": "5.2.0", "eslint": "5.16.0", @@ -61,7 +61,7 @@ "nodemon": "1.19.1", "prettier": "1.17.1", "ts-jest": "24.0.2", - "typescript": "3.5.3", + "typescript": "3.6.2", "wait-on": "3.3.0" }, "dependencies": {} diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index c55d425d2b..c7ff47163a 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.18.0-beta.1", + "version": "0.19.0-beta.0", "private": true, "author": { "name": "Nervos Core Dev", @@ -43,7 +43,7 @@ "last 2 chrome versions" ], "dependencies": { - "@nervosnetwork/ckb-sdk-core": "0.18.0", + "@nervosnetwork/ckb-sdk-core": "0.19.0", "@uifabric/experiments": "7.10.0", "@uifabric/styling": "7.4.0", "canvg": "2.0.0", 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} /> { - 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 b948fc2b25..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' @@ -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) @@ -286,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]) @@ -411,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 f77829c74b..6eb2bafcb5 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 { 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' 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 = outputsToTotalAmount(items) + setTotalAmount(totalAmount) calculateCycles({ walletID, - capacities: outputsToTotalCapacity(items), + capacities: totalAmount, }) .then(response => { if (response.status) { @@ -126,15 +83,21 @@ const useOnTransactionChange = (walletID: string, items: TransactionOutput[], di payload: '0', }) }) + } 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, @@ -159,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) @@ -171,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) => { @@ -218,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/TransactionList/index.tsx b/packages/neuron-ui/src/components/TransactionList/index.tsx index e968129c0c..44508c2094 100644 --- a/packages/neuron-ui/src/components/TransactionList/index.tsx +++ b/packages/neuron-ui/src/components/TransactionList/index.tsx @@ -16,8 +16,14 @@ import { StateDispatch } from 'states/stateProvider/reducer' import { contextMenu, showTransactionDetails } from 'services/remote' import { useLocalDescription } from 'utils/hooks' -import { shannonToCKBFormatter, uniformTimeFormatter as timeFormatter, uniformTimeFormatter } from 'utils/formatters' +import { + shannonToCKBFormatter, + uniformTimeFormatter as timeFormatter, + uniformTimeFormatter, + localNumberFormatter, +} from 'utils/formatters' import { onRenderRow } from 'utils/fabricUIRender' +import { CONFIRMATION_THRESHOLD } from 'utils/const' const theme = getTheme() @@ -50,12 +56,14 @@ const TransactionList = ({ isUpdatingDescription = false, items = [], walletID, + tipBlockNumber, dispatch, }: { isLoading?: boolean isUpdatingDescription?: boolean walletID: string items: State.Transaction[] + tipBlockNumber: string dispatch: StateDispatch }) => { 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/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 edac456aa7..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", @@ -126,11 +127,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" @@ -234,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 12484622f7..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": "余额", @@ -126,11 +127,17 @@ "last": "最后一页", "description": "标签", "status": "状态", + "pending": "已提交", + "success": "成功", + "failed": "失败", + "confirming": "确认中", "blockNumber": "区块高度", "basic-information": "基本信息", "search": { "placeholder": "使用交易哈希、地址或日期(yyyy-mm-dd)进行搜索" - } + }, + "confirmations": "确认次数", + "confirming-with-count": " 已确认 {{confirmations}} 次" }, "transaction": { "goBack": "返回" @@ -179,7 +186,7 @@ "edit-network": { "title": "添加或编辑网络", "rpc-url": "RPC地址", - "name": "名字" + "name": "名称" } } }, @@ -234,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/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: '', diff --git a/packages/neuron-ui/src/tests/formatters.test.ts b/packages/neuron-ui/src/tests/formatters.test.ts deleted file mode 100644 index 17bcde6644..0000000000 --- a/packages/neuron-ui/src/tests/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/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/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/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..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 @@ -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, @@ -182,5 +192,6 @@ export default { uniformTimeFormatter, priceToFee, addressesToBalance, - outputsToTotalCapacity, + outputsToTotalAmount, + 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/package.json b/packages/neuron-wallet/package.json index b5ee5b3a68..cef26d0bec 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -3,7 +3,7 @@ "productName": "Neuron", "description": "CKB Neuron Wallet", "homepage": "https://www.nervos.org/", - "version": "0.18.0-beta.1", + "version": "0.19.0-beta.0", "private": true, "author": { "name": "Nervos Core Dev", @@ -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", @@ -64,7 +64,7 @@ "electron-devtools-installer": "2.2.4", "electron-notarize": "0.1.1", "lint-staged": "9.2.0", - "neuron-ui": "0.18.0-beta.1", + "neuron-ui": "0.19.0-beta.0", "rimraf": "2.6.3", "spectron": "8.0.0", "ts-transformer-imports": "0.4.3", diff --git a/packages/neuron-wallet/src/controllers/app/options.ts b/packages/neuron-wallet/src/controllers/app/options.ts index 5e4a57fe5b..1d337d6b57 100644 --- a/packages/neuron-wallet/src/controllers/app/options.ts +++ b/packages/neuron-wallet/src/controllers/app/options.ts @@ -7,14 +7,6 @@ import i18n from 'utils/i18n' import env from 'env' import AppController from '.' -export enum MenuCommand { - ShowAbout = 'show-about', - ShowPreferences = 'show-preferences', - OpenNervosWebsite = 'open-nervos-website', - OpenSourceCodeRepository = 'open-sourcecode-repository', - SetUILocale = 'set-ui-language', -} - export enum URL { Preference = '/settings/general', CreateWallet = '/wizard/mnemonic/create', @@ -186,4 +178,4 @@ export const contextMenuTemplate: { }, } -export default { MenuCommand, URL } +export default { URL } 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/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..ef3c763633 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/1562038960990-AddStatusToTx.ts b/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts deleted file mode 100644 index 233ab2c59c..0000000000 --- a/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {MigrationInterface, QueryRunner, TableColumn, getConnection, In} from "typeorm"; -import TransactionEntity from '../entities/transaction' -import { OutputStatus } from '../../../services/tx/params' -import { TransactionStatus } from '../../../types/cell-types' -import OutputEntity from 'database/chain/entities/output' - -export class AddStatusToTx1562038960990 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - // TransactionStatus.Success = 'success' - await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'status' varchar NOT NULL DEFAULT 'success';`) - - const pendingTxHashes: string[] = (await getConnection() - .getRepository(OutputEntity) - .createQueryBuilder('output') - .select(`output.outPointTxHash`, 'txHash') - .where({ - status: OutputStatus.Sent - }) - .getRawMany()) - .filter(output => output.txHash) - await getConnection() - .createQueryBuilder() - .update(TransactionEntity) - .set({ status: TransactionStatus.Pending }) - .where({ - hash: In(pendingTxHashes) - }) - .execute() - - await queryRunner.changeColumn('transaction', 'status', new TableColumn({ - name: 'status', - type: 'varchar', - })) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn('transaction', 'status') - } - -} diff --git a/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts b/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts deleted file mode 100644 index 69448a80d7..0000000000 --- a/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {MigrationInterface, QueryRunner} from "typeorm"; - -export class AddConfirmed1565693320664 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'confirmed' boolean NOT NULL DEFAULT false;`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn('transaction', 'confirmed') - } - -} diff --git a/packages/neuron-wallet/src/database/chain/migrations/1561695143591-InitMigration.ts b/packages/neuron-wallet/src/database/chain/migrations/1566959757554-InitMigration.ts similarity index 91% rename from packages/neuron-wallet/src/database/chain/migrations/1561695143591-InitMigration.ts rename to packages/neuron-wallet/src/database/chain/migrations/1566959757554-InitMigration.ts index 211327651b..af467315d0 100644 --- a/packages/neuron-wallet/src/database/chain/migrations/1561695143591-InitMigration.ts +++ b/packages/neuron-wallet/src/database/chain/migrations/1566959757554-InitMigration.ts @@ -1,10 +1,10 @@ import {MigrationInterface, QueryRunner} from "typeorm"; -export class InitMigration1561695143591 implements MigrationInterface { +export class InitMigration1566959757554 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE TABLE "output" ("outPointTxHash" varchar NOT NULL, "outPointIndex" varchar NOT NULL, "capacity" varchar NOT NULL, "lock" text NOT NULL, "lockHash" varchar NOT NULL, "status" varchar NOT NULL, "transactionHash" varchar, PRIMARY KEY ("outPointTxHash", "outPointIndex"))`); - await queryRunner.query(`CREATE TABLE "transaction" ("hash" varchar PRIMARY KEY NOT NULL, "version" varchar NOT NULL, "deps" text NOT NULL, "witnesses" text NOT NULL, "timestamp" varchar, "blockNumber" varchar, "blockHash" varchar, "description" varchar, "createdAt" varchar NOT NULL, "updatedAt" varchar NOT NULL)`); + await queryRunner.query(`CREATE TABLE "transaction" ("hash" varchar PRIMARY KEY NOT NULL, "version" varchar NOT NULL, "cellDeps" text NOT NULL, "headerDeps" text NOT NULL, "witnesses" text NOT NULL, "timestamp" varchar, "blockNumber" varchar, "blockHash" varchar, "description" varchar, "status" varchar NOT NULL, "createdAt" varchar NOT NULL, "updatedAt" varchar NOT NULL, "confirmed" boolean NOT NULL)`); await queryRunner.query(`CREATE TABLE "input" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "outPointTxHash" varchar, "outPointIndex" varchar, "since" varchar NOT NULL, "lockHash" varchar, "capacity" varchar, "transactionHash" varchar)`); await queryRunner.query(`CREATE TABLE "sync_info" ("name" varchar PRIMARY KEY NOT NULL, "value" varchar NOT NULL)`); await queryRunner.query(`CREATE TABLE "temporary_output" ("outPointTxHash" varchar NOT NULL, "outPointIndex" varchar NOT NULL, "capacity" varchar NOT NULL, "lock" text NOT NULL, "lockHash" varchar NOT NULL, "status" varchar NOT NULL, "transactionHash" varchar, CONSTRAINT "FK_29236a0eb11fac458990882f985" FOREIGN KEY ("transactionHash") REFERENCES "transaction" ("hash") ON DELETE CASCADE ON UPDATE NO ACTION, PRIMARY KEY ("outPointTxHash", "outPointIndex"))`); diff --git a/packages/neuron-wallet/src/database/chain/ormconfig.ts b/packages/neuron-wallet/src/database/chain/ormconfig.ts index 98f2d75937..d7732905f5 100644 --- a/packages/neuron-wallet/src/database/chain/ormconfig.ts +++ b/packages/neuron-wallet/src/database/chain/ormconfig.ts @@ -9,10 +9,7 @@ import Transaction from './entities/transaction' import Input from './entities/input' import Output from './entities/output' 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 { InitMigration1566959757554 } from './migrations/1566959757554-InitMigration' export const CONNECTION_NOT_FOUND_NAME = 'ConnectionNotFoundError' @@ -32,7 +29,7 @@ const connectOptions = async (genesisBlockHash: string): Promise { - const salt = crypto.randomBytes(32) - const iv = crypto.randomBytes(16) - const params = { - n: 8192, - r: 8, - p: 1, - } + static create = ( + extendedPrivateKey: ExtendedPrivateKey, + password: string, + options: { salt?: Buffer; iv?: Buffer } = {} + ) => { + const salt = options.salt || crypto.randomBytes(32) + const iv = options.iv || crypto.randomBytes(16) const kdfparams: KdfParams = { dklen: 32, salt: salt.toString('hex'), - ...params, + n: 2 ** 18, + r: 8, + p: 1, } - const derivedKey: Buffer = crypto.scryptSync(Buffer.from(password), salt, kdfparams.dklen, { - N: kdfparams.n, - r: kdfparams.r, - p: kdfparams.p, - }) + const derivedKey = crypto.scryptSync(password, salt, kdfparams.dklen, Keystore.scryptOptions(kdfparams)) const cipher = crypto.createCipheriv(CIPHER, derivedKey.slice(0, 16), iv) if (!cipher) { @@ -75,11 +72,6 @@ export default class Keystore { cipher.update(Buffer.from(extendedPrivateKey.serialize(), 'hex')), cipher.final(), ]) - const hash = new SHA3(256) - const mac = hash - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest() - .toString('hex') return new Keystore( { @@ -90,7 +82,7 @@ export default class Keystore { cipher: CIPHER, kdf: 'scrypt', kdfparams, - mac, + mac: Keystore.mac(derivedKey, ciphertext), }, uuid() ) @@ -98,23 +90,9 @@ export default class Keystore { // Decrypt and return serialized extended private key. decrypt(password: string): string { - const { kdfparams } = this.crypto - const derivedKey: Buffer = crypto.scryptSync( - Buffer.from(password), - Buffer.from(kdfparams.salt, 'hex'), - kdfparams.dklen, - { - N: kdfparams.n, - r: kdfparams.r, - p: kdfparams.p, - } - ) + const derivedKey = this.derivedKey(password) const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex') - const mac = new SHA3(256) - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest() - .toString('hex') - if (mac !== this.crypto.mac) { + if (Keystore.mac(derivedKey, ciphertext) !== this.crypto.mac) { throw new IncorrectPassword() } const decipher = crypto.createDecipheriv( @@ -130,22 +108,31 @@ export default class Keystore { } checkPassword = (password: string) => { + const derivedKey = this.derivedKey(password) + const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex') + return Keystore.mac(derivedKey, ciphertext) === this.crypto.mac + } + + derivedKey = (password: string) => { const { kdfparams } = this.crypto - const derivedKey: Buffer = crypto.scryptSync( - Buffer.from(password), + return crypto.scryptSync( + password, Buffer.from(kdfparams.salt, 'hex'), kdfparams.dklen, - { - N: kdfparams.n, - r: kdfparams.r, - p: kdfparams.p, - } + Keystore.scryptOptions(kdfparams) ) - const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex') - const mac = new SHA3(256) - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest() - .toString('hex') - return mac === this.crypto.mac + } + + static mac = (derivedKey: Buffer, ciphertext: Buffer) => { + return new Keccak(256).update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])).digest('hex') + } + + static scryptOptions = (kdfparams: KdfParams) => { + return { + N: kdfparams.n, + r: kdfparams.r, + p: kdfparams.p, + maxmem: 128 * (kdfparams.n + kdfparams.p + 2) * kdfparams.r, + } } } diff --git a/packages/neuron-wallet/src/models/lock-utils.ts b/packages/neuron-wallet/src/models/lock-utils.ts index 1c50517490..780eaf0dca 100644 --- a/packages/neuron-wallet/src/models/lock-utils.ts +++ b/packages/neuron-wallet/src/models/lock-utils.ts @@ -1,13 +1,15 @@ import NodeService from 'services/node' import { OutPoint, Script, ScriptHashType } from 'types/cell-types' import env from 'env' -import { SystemScriptSubject } from './subjects/system-script' +import ConvertTo from 'types/convert-to' +import { SystemScriptSubject } from 'models/subjects/system-script' const { core } = NodeService.getInstance() export interface SystemScript { codeHash: string outPoint: OutPoint + hashType: ScriptHashType } const subscribed = (target: any, propertyName: string) => { @@ -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,26 +65,21 @@ 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