diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 701f3773e7..3cb095a771 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: node: - - 16.4.0 + - 16.10.0 os: - macos-latest - ubuntu-20.04 @@ -23,6 +23,11 @@ jobs: name: ${{ matrix.os }}(Node.js ${{ matrix.node }}) steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/package_for_test.yml b/.github/workflows/package_for_test.yml index f01ab472db..917e613440 100644 --- a/.github/workflows/package_for_test.yml +++ b/.github/workflows/package_for_test.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: node: - - 16.4.0 + - 16.10.0 os: - macos-latest - ubuntu-20.04 @@ -18,6 +18,11 @@ jobs: name: ${{ matrix.os }}(Node.js ${{ matrix.node }}) steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 68f174ba3f..803d2f2cd2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: node: - - 16.4.0 + - 16.10.0 os: - macos-latest - ubuntu-20.04 @@ -20,6 +20,11 @@ jobs: name: ${{ matrix.os }}(Node.js ${{ matrix.node }}) steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index beb844a13b..f372c6687d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +# 0.103.1 (2022-06-11) + +### New features +* #2375: Adapt new testnet URL(@yanguoyu) +* #2400: Support send acp/sudt to any address.(@yanguoyu) +* #2406: Refine export tx(@yanguoyu) +* #2408: add address format toggle in receive tabs(@Keith-CY) + + +### Bug fixes +* #2344: Delete unnecessary conditions. Retry should await.(@yanguoyu) +* #2390: Fix unmatched cheque lockHash(@yanguoyu) +* #2405: Remove covering above `update` button(@yanguoyu) +* #2409: Remove scroll(@yanguoyu) +* #2410: style of release note(@Keith-CY) + + +### Refactor +* #2368: Use blake160 replace addresse. Rename 'multi-sign' to 'multisig'(@yanguoyu) +* #2373, #2412: improve neuron-ui typescript, tests, deps and bundle(@qiweiii) + + +**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.103.0...v0.103.1 + + + # 0.103.0 (2022-05-10) ### Hardfork diff --git a/lerna.json b/lerna.json index 40ae796c61..5e03949c2d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.103.0", + "version": "0.103.1", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index 773f2374af..9d8b788387 100644 --- a/package.json +++ b/package.json @@ -44,19 +44,22 @@ } }, "devDependencies": { - "@types/jest": "24.0.18", + "@babel/core": "7.17.10", + "@types/jest": "27.5.0", "@types/node": "16.11.7", - "@typescript-eslint/eslint-plugin": "2.19.0", - "@typescript-eslint/parser": "2.19.0", + "@typescript-eslint/eslint-plugin": "5.23.0", + "@typescript-eslint/parser": "5.23.0", "concurrently": "7.0.0", "cross-env": "7.0.3", - "eslint": "6.7.2", "husky": "3.0.5", "lerna": "4.0.0", "ncp": "2.0.0", - "ts-jest": "24.0.2", + "ts-jest": "27.1.4", "typescript": "3.8.2", "wait-on": "6.0.0" }, - "dependencies": {} + "dependencies": {}, + "resolutions": { + "@types/react": "16.9.15" + } } diff --git a/packages/neuron-ui/.eslintrc.js b/packages/neuron-ui/.eslintrc.js index bfb9d8ef29..e82516a083 100644 --- a/packages/neuron-ui/.eslintrc.js +++ b/packages/neuron-ui/.eslintrc.js @@ -1,86 +1,159 @@ module.exports = { - "extends": ["airbnb", "plugin:prettier/recommended"], - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "settings": { - "import/resolver": { - "node": { - "paths": ["src"], - "extensions": [".js", ".ts", ".jsx", ".tsx"] - } + extends: ['airbnb', 'plugin:prettier/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + settings: { + 'import/resolver': { + node: { + paths: ['src'], + extensions: ['.js', '.ts', '.jsx', '.tsx'], + }, + }, + react: { + version: 'detect', }, - "react": { - "version": "detect" - } }, - "rules": { - "prettier/prettier": [2, { - "printWidth": 120 - }], - "semi": [2, "never"], - "curly": [2, "all"], - "comma-dangle": [2, { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "always-multiline", - "exports": "always-multiline", - "functions": "ignore" - }], - "import/no-extraneous-dependencies": [2, { - "devDependencies": true - }], - "no-unused-vars": "off", - "implicit-arrow-linebreak": "off", - "@typescript-eslint/no-unused-vars": ["error", { - "vars": "local", - "args": "after-used", - "ignoreRestSiblings": false - }], - "arrow-parens": [2, "as-needed"], - "max-len": [2, { - "code": 120, - "ignoreComments": true, - "ignoreTrailingComments": true, - "ignoreUrls": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreRegExpLiterals": true, - }], - "object-curly-newline": ["error", { - "ObjectExpression": { - "consistent": true + rules: { + 'prettier/prettier': [ + 2, + { + printWidth: 120, + endOfLine: 'auto', + }, + ], + semi: [2, 'never'], + curly: [2, 'all'], + 'comma-dangle': [ + 2, + { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'ignore', + }, + ], + 'import/no-extraneous-dependencies': [ + 2, + { + devDependencies: true, + }, + ], + 'no-unused-vars': 'off', + 'implicit-arrow-linebreak': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'local', + args: 'after-used', + ignoreRestSiblings: false, + }, + ], + 'arrow-parens': [2, 'as-needed'], + 'max-len': [ + 2, + { + code: 120, + ignoreComments: true, + ignoreTrailingComments: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true, + }, + ], + 'object-curly-newline': [ + 'error', + { + ObjectExpression: { + consistent: true, + }, + ObjectPattern: { + consistent: true, + }, + ImportDeclaration: { + consistent: true, + }, + ExportDeclaration: { + multiline: true, + minProperties: 3, + }, }, - "ObjectPattern": { - "consistent": true + ], + 'no-plusplus': [0], + 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + 'max-classes-per-file': [0], + 'react/jsx-filename-extension': [ + 1, + { + extensions: ['.ts', '.tsx'], }, - "ImportDeclaration": { - "consistent": true, + ], + 'react/jsx-props-no-spreading': [0], + 'typescript-eslint/no-angle-bracket-type-assertion': [0], + 'no-alert': [0], + 'no-console': [ + 2, + { + allow: ['info', 'warn', 'error', 'group', 'groupEnd'], }, - "ExportDeclaration": { - "multiline": true, - "minProperties": 3 - } - }], - "no-plusplus": [0], - "lines-between-class-members": ["error", "always", { exceptAfterSingleLine: true }], - "max-classes-per-file": [0], - "react/jsx-filename-extension": [1, { - "extensions": [".ts", ".tsx"] - }], - "react/jsx-props-no-spreading": [0], - "typescript-eslint/no-angle-bracket-type-assertion": [0], - "no-alert": [0], - "no-console": [2, { - "allow": ["info", "warn", "error", "group", "groupEnd"] - }], - "no-bitwise": [0] + ], + 'no-bitwise': [0], + 'import/extensions': [ + 'error', + 'ignorePackages', + { + ts: 'never', + tsx: 'never', + }, + ], + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error'], + '@typescript-eslint/no-var-requires': 'off', + 'react/prop-types': 'off', + 'react/function-component-definition': [ + 2, + { + namedComponents: 'arrow-function', + unnamedComponents: 'arrow-function', + }, + ], + 'react/require-default-props': 'off', + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + '{}': false, + Function: false, + }, + }, + ], + 'no-unsafe-optional-chaining': ['warn', { disallowArithmeticOperators: false }], + 'react/no-unstable-nested-components': ['warn', { allowAsProps: true }], + 'default-param-last': 'off', + 'react/jsx-no-useless-fragment': 'off', }, - "env": { - "jest": true, - "node": true, - "browser": true + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-undef': 'off', + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: { + 'react/jsx-no-constructed-context-values': 0, + }, + }, + ], + env: { + jest: true, + node: true, + browser: true, }, - "globals": { - "BigInt": "readonly" + globals: { + BigInt: 'readonly', }, } diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index 4e56fc0306..b4c3f4909e 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.103.0", + "version": "0.103.1", "private": true, "author": { "name": "Nervos Core Dev", @@ -15,10 +15,10 @@ "main": "./build", "license": "MIT", "scripts": { - "start": "react-app-rewired start", + "start": "cross-env DISABLE_ESLINT_PLUGIN=true GENERATE_SOURCEMAP=false react-app-rewired start", "lint": "eslint --fix --ext .tsx,.ts,.js src", "test": "react-app-rewired test --env=jsdom --watchAll=false", - "build": "react-app-rewired build", + "build": "cross-env DISABLE_ESLINT_PLUGIN=true GENERATE_SOURCEMAP=false react-app-rewired build", "clean": "rimraf build/*", "precommit": "lint-staged", "storybook": "start-storybook -p 9009 -s public", @@ -27,9 +27,6 @@ "publishConfig": { "registry": "https://registry.npmjs.org/" }, - "eslintConfig": { - "extends": "react-app" - }, "lint-staged": { "src/**/*.{ts,tsx}": [ "eslint --fix", @@ -43,9 +40,8 @@ "last 2 chrome versions" ], "dependencies": { - "primereact": "7.1.0", - "@nervosnetwork/ckb-sdk-core": "0.102.2", - "@nervosnetwork/ckb-sdk-utils": "0.102.2", + "@nervosnetwork/ckb-sdk-core": "0.103.1", + "@nervosnetwork/ckb-sdk-utils": "0.103.1", "@uifabric/experiments": "7.42.4", "@uifabric/styling": "7.20.0", "canvg": "2.0.0", @@ -53,6 +49,7 @@ "immer": "9.0.12", "jsqr": "1.4.0", "office-ui-fabric-react": "7.180.3", + "primereact": "7.1.0", "qr.js": "0.0.0", "react": "16.12.0", "react-dom": "16.12.0", @@ -61,6 +58,7 @@ "sass": "1.47.0" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "7.17.12", "@storybook/addon-actions": "5.3.18", "@storybook/addon-knobs": "5.3.18", "@storybook/addon-links": "5.3.18", @@ -82,17 +80,17 @@ "electron": "16.0.6", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", - "eslint-config-airbnb": "18.0.1", - "eslint-config-prettier": "6.7.0", - "eslint-plugin-import": "2.18.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-prettier": "3.1.1", - "eslint-plugin-react": "7.17.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-import": "2.25.2", + "eslint-plugin-jsx-a11y": "6.5.1", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-react": "7.29.4", "jest-styled-components": "7.0.0-beta.1", "lint-staged": "9.5.0", "prettier": "1.19.1", - "react-app-rewired": "2.1.5", - "react-scripts": "3.4.1", + "react-app-rewired": "2.2.1", + "react-scripts": "5.0.1", "react-test-renderer": "16.12.0", "rimraf": "3.0.0", "storybook-react-router": "1.0.8" diff --git a/packages/neuron-ui/src/components/ApproveMultisigTx/index.tsx b/packages/neuron-ui/src/components/ApproveMultisigTx/index.tsx index 9d11f4f44c..3bc19fa7c1 100644 --- a/packages/neuron-ui/src/components/ApproveMultisigTx/index.tsx +++ b/packages/neuron-ui/src/components/ApproveMultisigTx/index.tsx @@ -125,11 +125,11 @@ const ApproveMultisigTx = ({ ) : ( <>
Inputs
- {offlineSignJson.transaction?.inputs?.map(v => ( + {offlineSignJson.transaction?.inputs?.map((v: any) => ( ))}
Outputs
- {offlineSignJson.transaction?.outputs?.map(v => ( + {offlineSignJson.transaction?.outputs?.map((v: any) => ( ))} diff --git a/packages/neuron-ui/src/components/CompensationProgressBar/compensationProgressBar.module.scss b/packages/neuron-ui/src/components/CompensationProgressBar/compensationProgressBar.module.scss index 68de604918..2a6bb4b925 100644 --- a/packages/neuron-ui/src/components/CompensationProgressBar/compensationProgressBar.module.scss +++ b/packages/neuron-ui/src/components/CompensationProgressBar/compensationProgressBar.module.scss @@ -1,3 +1,5 @@ +@use "sass:math"; + $progress-green: #3cc68a; $progress-yellow: #f7ae4d; $progress-grey: #e3e3e3; @@ -7,7 +9,7 @@ $progress-grey-hover: #888; $progress-indicator-color: #000; $progress-indicator-height: 20px; $progress-bar-height: 10px; -$progress-indicator-offet: ($progress-bar-height - $progress-indicator-height) / 2; +$progress-indicator-offet: math.div($progress-bar-height - $progress-indicator-height, 2); @mixin basic { display: block; @@ -27,7 +29,7 @@ $progress-indicator-offet: ($progress-bar-height - $progress-indicator-height) / background: repeating-linear-gradient(-45deg, #ccc, #ccc 6.8px, #e3e3e3 6.8px, #e3e3e3 13.6px); background-size: 200% 100%; - border-radius: $progress-bar-height/2; + border-radius: math.div($progress-bar-height, 2); animation: move 35s linear infinite; } @@ -38,7 +40,7 @@ $progress-indicator-offet: ($progress-bar-height - $progress-indicator-height) / height: $progress-bar-height; border-radius: 2px; background: linear-gradient(90deg, $progress-grey 0%, $progress-grey 76.7%, $progress-green 76.7%, $progress-green 96.7%, $progress-yellow 96.7%, $progress-yellow 100%); - border-radius: $progress-bar-height/2; + border-radius: math.div($progress-bar-height, 2); } @@ -59,8 +61,8 @@ $progress-indicator-offet: ($progress-bar-height - $progress-indicator-height) / } &::-webkit-progress-value { - border-top-left-radius: $progress-bar-height/2; - border-bottom-left-radius: $progress-bar-height/2; + border-top-left-radius: math.div($progress-bar-height, 2); + border-bottom-left-radius: math.div($progress-bar-height, 2); border-right: 1px solid #000; box-shadow: 0 0 0 1500px rgba(255, 255, 255, 0.4); } diff --git a/packages/neuron-ui/src/components/GeneralSetting/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/index.tsx index 679e6b37bd..133119882f 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/index.tsx @@ -96,7 +96,15 @@ const GeneralSetting = ({ updater, dispatch }: GeneralSettingProps) => { return (
{t('settings.general.version')}
- {showNewVersion ? null : ( + {showNewVersion ? ( +
+ +
+ ) : ( <>
{version}
@@ -119,15 +127,6 @@ const GeneralSetting = ({ updater, dispatch }: GeneralSettingProps) => {
)} -
- {showNewVersion ? ( - - ) : null} -
{t('settings.general.language')}
diff --git a/packages/neuron-ui/src/components/GeneralSetting/style.module.scss b/packages/neuron-ui/src/components/GeneralSetting/style.module.scss index 9008153480..2b14ae7c93 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/style.module.scss +++ b/packages/neuron-ui/src/components/GeneralSetting/style.module.scss @@ -100,6 +100,8 @@ $action-button-width: 11.25rem; .releaseNotesStyle { grid-area: release-note; + width: 100%; + box-sizing: border-box; overflow: scroll; min-height: 200px; height: calc(100vh - 400px); diff --git a/packages/neuron-ui/src/components/HardwareSign/index.tsx b/packages/neuron-ui/src/components/HardwareSign/index.tsx index ae06c09752..69a797f37d 100644 --- a/packages/neuron-ui/src/components/HardwareSign/index.tsx +++ b/packages/neuron-ui/src/components/HardwareSign/index.tsx @@ -58,15 +58,20 @@ const HardwareSign = ({ const [t] = useTranslation() const dialogRef = useRef(null) const dispatch = useDispatch() - const onCancel = useCallback(() => { - if (signType === 'transaction') { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { sending: false }, - }) - } - onDismiss() - }, [dispatch, signType, onDismiss]) + const onCancel = useCallback( + (dismiss: boolean = true) => { + if (signType === 'transaction') { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + } + if (dismiss) { + onDismiss() + } + }, + [dispatch, signType, onDismiss] + ) const isWin32 = useMemo(() => { return getPlatform() === 'win32' }, []) @@ -91,7 +96,7 @@ const HardwareSign = ({ const [deviceInfo, setDeviceInfo] = useState(wallet.device!) const [isReconnecting, setIsReconnecting] = useState(false) const isLoading = useMemo(() => { - return status === userInputStatus || isReconnecting + return status === userInputStatus || isReconnecting || isSending }, [status, userInputStatus, isReconnecting]) const productName = `${wallet.device!.manufacturer} ${wallet.device!.product}` @@ -121,11 +126,13 @@ const HardwareSign = ({ setError(errorFormatter(res.message, t)) return } - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) - onCancel() + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + } + onCancel(!!res.result) }, [offlineSignJSON, dispatch, onCancel, t, wallet.id]) const signAndExportFromGenerateTx = useCallback(async () => { @@ -143,16 +150,19 @@ const HardwareSign = ({ password: '', multisigConfig, }) + setStatus(connectStatus) if (!isSuccessResponse(res)) { setStatus(connectStatus) setError(errorFormatter(res.message, t)) return } - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) - onCancel() + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + } + onCancel(!!res.result) }, [ dispatch, onCancel, @@ -196,7 +206,9 @@ const HardwareSign = ({ // getDeviceCkbAppVersion will halt forever while in win32 sleep mode. const ckbVersionRes = await Promise.race([ getDeviceCkbAppVersion(descriptor), - new Promise((_, reject) => setTimeout(() => reject(), 1000)), + new Promise((_, reject) => { + setTimeout(() => reject(), 1000) + }), ]).catch(() => { return { status: ErrorCode.DeviceInSleep } }) @@ -210,7 +222,7 @@ const HardwareSign = ({ } setStatus(connectStatus) } catch (err) { - if (err.code === ErrorCode.CkbAppNotFound) { + if (err instanceof CkbAppNotFoundException) { setStatus(ckbAppNotFoundStatus) } else { setStatus(disconnectStatus) @@ -283,11 +295,12 @@ const HardwareSign = ({ }) break } - case 'send-acp': - case 'send-acp-to-default': + case 'send-ckb-asset': + case 'send-acp-sudt-to-new-cell': + case 'send-acp-ckb-to-new-cell': case 'send-sudt': { let skipLastInputs = true - if (actionType === 'send-acp-to-default') { + if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { skipLastInputs = false } const params: Controller.SendSUDTTransaction.Params = { @@ -309,6 +322,7 @@ const HardwareSign = ({ if (isSuccessResponse(res)) { history!.push(RoutePath.History) } else { + // @ts-ignore setError(res.message.content) } }) @@ -396,18 +410,22 @@ const HardwareSign = ({ }, [deviceInfo, disconnectStatus, ensureDeviceAvailable, wallet.id]) const exportTransaction = useCallback(async () => { - await exportTransactionAsJSON({ + const res = await exportTransactionAsJSON({ transaction: generatedTx || experimental?.tx, status: OfflineSignStatus.Unsigned, type: offlineSignType!, description, asset_account: experimental?.assetAccount, }) - onCancel() + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + onCancel(!!res.result) }, [offlineSignType, generatedTx, onCancel, description, experimental]) useDidMount(() => { - // eslint-disable-next-line no-unused-expressions + // @ts-ignore dialogRef.current?.showModal() ensureDeviceAvailable(deviceInfo) }) diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 20f5f3bee7..378799d76a 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -17,9 +17,6 @@ import styles from './history.module.scss' const History = () => { const { - app: { - loadings: { transactionList: isLoading }, - }, wallet: { id, name: walletName }, chain: { networkID, @@ -85,7 +82,6 @@ const History = () => {
{totalCount ? ( { keywords, onKeywordsChange, onSearch, - isLoading, id, walletName, items, diff --git a/packages/neuron-ui/src/components/ImportHardware/common.ts b/packages/neuron-ui/src/components/ImportHardware/common.ts index a42da79ade..605eca45a4 100644 --- a/packages/neuron-ui/src/components/ImportHardware/common.ts +++ b/packages/neuron-ui/src/components/ImportHardware/common.ts @@ -10,6 +10,11 @@ export enum RoutePath { ImportHardware = '/import-hardware', } +export interface Model { + manufacturer: string + product: string +} + export interface LocationState { entryPath: string model: Model @@ -19,8 +24,3 @@ export interface LocationState { } error?: FailureFromController['message'] } - -export interface Model { - manufacturer: string - product: string -} diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index 47a864c461..a83b5eabef 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -190,22 +190,20 @@ const ImportKeystore = () => { .filter(([key]) => !key.endsWith('Error')) .map(([key, value]) => { return ( - <> - - + ) })}
diff --git a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx index 1863ffdeb5..6c7537b77e 100644 --- a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx +++ b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx @@ -8,7 +8,7 @@ import { useDialog } from 'utils' import styles from './lockInfoDialog.module.scss' import getLockSupportShortAddress from '../../utils/getLockSupportShortAddress' -interface LockInfoDialog { +interface LockInfoDialogProps { lockInfo: CKBComponents.Script | null isMainnet: boolean onDismiss: () => void @@ -45,7 +45,7 @@ const ShortAddr = ({ lockScript, isMainnet }: { lockScript: CKBComponents.Script ) } -const LockInfoDialog = ({ lockInfo, isMainnet, onDismiss }: LockInfoDialog) => { +const LockInfoDialog = ({ lockInfo, isMainnet, onDismiss }: LockInfoDialogProps) => { const [t] = useTranslation() const [copied, setCopied] = useState(false) const dialogRef = useRef(null) diff --git a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts index 46462b16e7..ca64a4709b 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts +++ b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts @@ -1,8 +1,9 @@ -import React, { useCallback, useState, useEffect } from 'react' -import { useDialogWrapper, isSuccessResponse } from 'utils' +import React, { useCallback, useState, useEffect, useMemo } from 'react' +import { useDialogWrapper, isSuccessResponse, getMultisigAddress, DefaultLockInfo } from 'utils' import { MultisigOutputUpdate } from 'services/subjects' import { MultisigConfig, + MultisigEntity, saveMultisigConfig, getMultisigConfig, importMultisigConfig, @@ -13,10 +14,10 @@ import { loadMultisigTxJson, OfflineSignJSON, } from 'services/remote' +import { addressToScript, scriptToAddress } from '@nervosnetwork/ckb-sdk-utils' -export const useSearch = (clearSelected: () => void) => { +export const useSearch = (clearSelected: () => void, onFilterConfig: (searchKey: string) => void) => { const [keywords, setKeywords] = useState('') - const [searchKeywords, setSearchKeywords] = useState('') const onKeywordsChange = (_e?: React.FormEvent, newValue?: string) => { if (undefined !== newValue) { @@ -26,90 +27,111 @@ export const useSearch = (clearSelected: () => void) => { const onSearch = useCallback( value => { - setSearchKeywords(value) + onFilterConfig(value) clearSelected() }, - [setSearchKeywords, clearSelected] + [onFilterConfig, clearSelected] ) const onClear = useCallback(() => { onSearch('') }, [onSearch]) - return { keywords, onKeywordsChange, setKeywords, onSearch, searchKeywords, onClear } + return { keywords, onKeywordsChange, setKeywords, onSearch, onClear } } export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isMainnet: boolean }) => { - const [configs, setConfigs] = useState([]) + const [entities, setEntities] = useState([]) const saveConfig = useCallback( - ({ m, n, r, addresses, fullPayload }) => { + ({ m, n, r, addresses }: { m: number; n: number; r: number; addresses: string[] }) => { return saveMultisigConfig({ m, n, r, - addresses, - fullPayload, + blake160s: addresses.map(v => addressToScript(v).args), walletId, }).then(res => { if (isSuccessResponse(res)) { - if (res.result) { - setConfigs(v => [res.result!, ...v]) - } + setEntities(v => (res.result ? [res.result, ...v] : v)) } else { throw new Error(typeof res.message === 'string' ? res.message : res.message.content) } }) }, - [walletId, setConfigs] + [walletId, setEntities] ) useEffect(() => { - getMultisigConfig({ - walletId, - }).then(res => { - if (isSuccessResponse(res)) { - setConfigs(res.result) + getMultisigConfig(walletId).then(res => { + if (isSuccessResponse(res) && res.result) { + setEntities(res.result) } }) - }, [setConfigs, walletId]) + }, [setEntities, walletId]) const updateConfig = useCallback( (id: number) => (alias: string | undefined) => { updateMultisigConfig({ id, alias: alias || '' }).then(res => { if (isSuccessResponse(res)) { - setConfigs(v => v.map(config => (config.id === res.result?.id ? res.result : config))) + setEntities(v => v.map(config => (res.result && config.id === res.result?.id ? res.result : config))) } }) }, - [setConfigs] + [setEntities] ) - const filterConfig = useCallback((key: string) => { - setConfigs(v => - v.filter(config => { - return config.alias?.includes(key) || config.fullPayload === key - }) - ) - }, []) const deleteConfigById = useCallback( (id: number) => { - setConfigs(v => v.filter(config => config.id !== id)) + setEntities(v => v.filter(config => config.id !== id)) }, - [setConfigs] + [setEntities] ) const onImportConfig = useCallback(() => { - importMultisigConfig({ isMainnet, walletId }).then(res => { + importMultisigConfig(walletId).then(res => { if (isSuccessResponse(res) && res.result) { const { result } = res if (result) { - setConfigs(v => [...result, ...v]) + setEntities(v => [...result, ...v]) } } }) - }, [walletId, isMainnet]) + }, [walletId]) + const [searchKeywords, setSearchKeywords] = useState('') + const onFilterConfig = useCallback( + (v: string) => { + setSearchKeywords(v) + }, + [setSearchKeywords] + ) + const allConfigs = useMemo( + () => + entities.map(entity => ({ + ...entity, + addresses: entity.blake160s.map(args => + scriptToAddress( + { + args, + codeHash: DefaultLockInfo.CodeHash, + hashType: DefaultLockInfo.HashType, + }, + isMainnet + ) + ), + fullPayload: getMultisigAddress(entity.blake160s, entity.r, entity.m, entity.n, isMainnet), + })), + [entities, isMainnet] + ) + const configs = useMemo( + () => + searchKeywords + ? allConfigs.filter(v => v.alias?.includes(searchKeywords) || v.fullPayload === searchKeywords) + : allConfigs, + [allConfigs, searchKeywords] + ) return { saveConfig, - allConfigs: configs, + allConfigs, updateConfig, deleteConfigById, - filterConfig, onImportConfig, + configs, + onFilterConfig, } } diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index c803c1bf5b..68e95209ea 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -53,7 +53,15 @@ const MultisigAddress = () => { }, [i18n.language]) const isMainnet = isMainnetUtil(networks, networkID) const { openDialog, closeDialog, dialogRef, isDialogOpen } = useDialogWrapper() - const { allConfigs, saveConfig, updateConfig, deleteConfigById, onImportConfig } = useConfigManage({ + const { + allConfigs, + saveConfig, + updateConfig, + deleteConfigById, + onImportConfig, + configs, + onFilterConfig, + } = useConfigManage({ walletId, isMainnet, }) @@ -96,17 +104,8 @@ const MultisigAddress = () => { onChangeCheckedAll, exportConfig, clearSelected, - } = useExportConfig(allConfigs) - const { keywords, onKeywordsChange, onSearch, searchKeywords, onClear } = useSearch(clearSelected) - const configs = useMemo( - () => - searchKeywords - ? allConfigs.filter(v => { - return v.alias?.includes(searchKeywords) || v.fullPayload === searchKeywords - }) - : allConfigs, - [allConfigs, searchKeywords] - ) + } = useExportConfig(configs) + const { keywords, onKeywordsChange, onSearch, onClear } = useSearch(clearSelected, onFilterConfig) const sendTotalBalance = useMemo(() => { if (sendAction.sendFromMultisig?.fullPayload) { return multisigBanlances[sendAction.sendFromMultisig.fullPayload] @@ -117,7 +116,6 @@ const MultisigAddress = () => {
{ }, [setN] ) - const isError = useMemo(() => { - return !m || !n || Number(m) > Number(n) + const errorI18nKey: string | undefined = useMemo(() => { + if (!m || !n) { + return 'm-n-required' + } + const numM = Number(m) + const numN = Number(n) + if (numM > numN) { + return 'm-less-equal-n' + } + if (numM < 1 || numM > 255 || numN < 1 || numN > 255) { + return 'm-n-between-0-255' + } + return undefined }, [m, n]) return { m, n, setMBySelect, setNBySelect, - isError, + errorI18nKey, } } @@ -117,17 +128,18 @@ export const useViewMultisigAddress = ({ const [multisigAddress, changeMultisigAddress] = useState('') useEffect(() => { if (step === Step.viewMultiAddress) { - createMultisigAddress({ - r, - m, - n, - addresses, - isMainnet, - }).then(res => { - if (isSuccessResponse(res) && res.result) { - changeMultisigAddress(res.result) - } - }) + try { + const address = getMultisigAddress( + addresses.map(v => addressToScript(v).args), + r, + m, + n, + isMainnet + ) + changeMultisigAddress(address) + } catch (error) { + // ignore error. The ui ensures the correctness of the parameters + } } }, [step, changeMultisigAddress, m, n, r, addresses, isMainnet]) return multisigAddress diff --git a/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx b/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx index 09e3201315..53f9f5a302 100644 --- a/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx @@ -18,11 +18,13 @@ const SetMN = ({ n, changeM, changeN, + errorI18nKey, }: { m: string n: string changeM: (v: string) => void changeN: (v: string) => void + errorI18nKey?: string }) => { const [t] = useTranslation() return ( @@ -34,9 +36,7 @@ const SetMN = ({     of    
- {m && n && Number(m) > Number(n) && ( - {t('multisig-address.create-dialog.m-n.error')} - )} + {errorI18nKey && {t(`multisig-address.create-dialog.m-n.${errorI18nKey}`)}}
) } @@ -46,11 +46,11 @@ const MultisigAddressCreateDialog = ({ confirm: saveConfig, }: { closeDialog: () => void - confirm: (v: Omit) => Promise + confirm: (v: Omit) => Promise }) => { const [step, changeStep] = useState(Step.setMN) const [t] = useTranslation() - const { m, n, setMBySelect, setNBySelect, isError: mnErr } = useMAndN() + const { m, n, setMBySelect, setNBySelect, errorI18nKey: mnErr } = useMAndN() const next = useCallback(() => { changeStep(step + 1) }, [changeStep, step]) @@ -87,18 +87,17 @@ const MultisigAddressCreateDialog = ({ saveConfig({ m: Number(m), n: Number(n), - r, + r: Number(r), addresses, - fullPayload: multisigAddress, }).then(() => { closeDialog() }) - }, [m, n, r, addresses, multisigAddress, saveConfig, closeDialog]) + }, [m, n, r, addresses, saveConfig, closeDialog]) return ( <>

{t('multisig-address.create-dialog.title')}

- {step === Step.setMN && } + {step === Step.setMN && } {step === Step.setMultiAddress && ( <>

{t('multisig-address.create-dialog.multi-address-info.title', { m, n })}

@@ -126,7 +125,7 @@ const MultisigAddressCreateDialog = ({
diff --git a/packages/neuron-ui/src/components/MultisigAddressInfo/index.tsx b/packages/neuron-ui/src/components/MultisigAddressInfo/index.tsx index 9183ea2255..8e9b21e988 100644 --- a/packages/neuron-ui/src/components/MultisigAddressInfo/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddressInfo/index.tsx @@ -22,57 +22,56 @@ export const MultisigAddressTable = ({ }) => { const [t] = useTranslation() return ( - <> - - - - {['index', 'required', 'signer-address'].map(field => ( - - ))} - - - - {addresses.map((v, idx) => ( - - - + + {['index', 'required', 'signer-address'].map(field => ( + + ))} + + + + {addresses.map((v, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + + + - - - ))} - -
{t(`multisig-address.create-dialog.${field}`)}
{`#${idx + 1}`} - +
{t(`multisig-address.create-dialog.${field}`)}
{`#${idx + 1}`} + r || disabled} + /> + + {disabled ? ( + + {v.slice(0, -6)} + ... + {v.slice(-6)} + + ) : ( + r || disabled} + value={v} + className={styles.addressField} + onChange={changeAddress} + disabled={disabled} + placeholder={t('multisig-address.create-dialog.multi-address-info.ckb-address-placeholder')} + error={addressErrors?.[idx] ? t(addressErrors[idx]!.message, addressErrors[idx]!.i18n) : undefined} /> - - {disabled ? ( - - {v.slice(0, -6)} - ... - {v.slice(-6)} - - ) : ( - - )} -
- + )} + + + ))} + + ) } diff --git a/packages/neuron-ui/src/components/NetworkSetting/index.tsx b/packages/neuron-ui/src/components/NetworkSetting/index.tsx index e8b358dc9c..74174586e1 100644 --- a/packages/neuron-ui/src/components/NetworkSetting/index.tsx +++ b/packages/neuron-ui/src/components/NetworkSetting/index.tsx @@ -56,7 +56,7 @@ const NetworkSetting = ({ chain = chainState, settings: { networks = [] } }: Sta key: network.id, text: network.name, checked: chain.networkID === network.id, - onRenderLabel: ({ text }: IChoiceGroupOption) => { + onRenderLabel: props => { const isDefault = network.type === 0 return (
- {text} + {props?.text} {`(${network.remote}`} diff --git a/packages/neuron-ui/src/components/PasswordRequest/index.tsx b/packages/neuron-ui/src/components/PasswordRequest/index.tsx index ba83fb57a7..be548dc745 100644 --- a/packages/neuron-ui/src/components/PasswordRequest/index.tsx +++ b/packages/neuron-ui/src/components/PasswordRequest/index.tsx @@ -65,8 +65,9 @@ const PasswordRequest = () => { switch (actionType) { case 'create-sudt-account': return OfflineSignType.CreateSUDTAccount - case 'send-acp': - case 'send-acp-to-default': + case 'send-ckb-asset': + case 'send-acp-ckb-to-new-cell': + case 'send-acp-sudt-to-new-cell': case 'send-sudt': return OfflineSignType.SendSUDT case 'unlock': @@ -83,14 +84,20 @@ const PasswordRequest = () => { }, [actionType]) const exportTransaction = useCallback(async () => { - onDismiss() - await exportTransactionAsJSON({ + const res = await exportTransactionAsJSON({ transaction: generatedTx || experimental?.tx, status: OfflineSignStatus.Unsigned, type: signType, description, asset_account: experimental?.assetAccount, }) + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + onDismiss() + } }, [signType, generatedTx, onDismiss, description, experimental]) useDialog({ show: actionType, dialogRef, onClose: onDismiss }) @@ -103,8 +110,9 @@ const PasswordRequest = () => { 'unlock', 'create-sudt-account', 'send-sudt', - 'send-acp', - 'send-acp-to-default', + 'send-ckb-asset', + 'send-acp-ckb-to-new-cell', + 'send-acp-sudt-to-new-cell', 'send-cheque', 'withdraw-cheque', 'claim-cheque', @@ -206,11 +214,12 @@ const PasswordRequest = () => { await sendCreateSUDTAccountTransaction(params)(dispatch).then(handleSendTxRes) break } - case 'send-acp': - case 'send-acp-to-default': + case 'send-ckb-asset': + case 'send-acp-ckb-to-new-cell': + case 'send-acp-sudt-to-new-cell': case 'send-sudt': { let skipLastInputs = true - if (actionType === 'send-acp-to-default') { + if (actionType === 'send-acp-sudt-to-new-cell' || actionType === 'send-acp-ckb-to-new-cell') { skipLastInputs = false } const params: Controller.SendSUDTTransaction.Params = { @@ -322,29 +331,28 @@ const PasswordRequest = () => { password, multisigConfig, }) - if (!isSuccessResponse(res)) { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { sending: false }, - }) - setError(errorFormatter(res.message, t)) - return - } - dispatch({ - type: AppActions.UpdateLoadedTransaction, - payload: res.result!, - }) dispatch({ type: AppActions.UpdateLoadings, payload: { sending: false }, }) - onDismiss() + if (!isSuccessResponse(res)) { + setError(errorFormatter(res.message, t)) + return + } + if (res.result) { + dispatch({ + type: AppActions.UpdateLoadedTransaction, + payload: res.result!, + }) + onDismiss() + } }, [description, dispatch, experimental, generatedTx, onDismiss, password, signType, t, walletID, multisigConfig]) const dropdownList = [ { text: t('offline-sign.sign-and-export'), onClick: signAndExportFromGenerateTx, + disabled: !password, }, ] @@ -372,8 +380,8 @@ const PasswordRequest = () => { 'unlock', 'create-sudt-account', 'send-sudt', - 'send-acp', - 'send-acp-to-default', + 'send-acp-ckb-to-new-cell', + 'send-acp-sudt-to-new-cell', 'send-cheque', 'withdraw-cheque', 'claim-cheque', diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 75194c5e6e..bcdb3729af 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -1,14 +1,26 @@ import React, { useCallback, useMemo, useState } from 'react' import { useRouteMatch, useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' +import { addressToScript, bech32Address, AddressPrefix } from '@nervosnetwork/ckb-sdk-utils' import Button from 'widgets/Button' import QRCode from 'widgets/QRCode' import CopyZone from 'widgets/CopyZone' import { RoutePath } from 'utils' import { useState as useGlobalState, useDispatch } from 'states' +import { ReactComponent as AddressToggleIcon } from 'widgets/Icons/AddressTransform.svg' import VerifyHardwareAddress from 'components/VerifyHardwareAddress' import styles from './receive.module.scss' +const toShortAddr = (addr: string) => { + try { + const script = addressToScript(addr) + const isMainnet = addr.startsWith('ckb') + return bech32Address(script.args, { prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet }) + } catch { + return '' + } +} + const Receive = () => { const { wallet } = useGlobalState() const dispatch = useDispatch() @@ -18,15 +30,19 @@ const Receive = () => { } = useRouteMatch() const history = useHistory() const [displayVerifyDialog, setDisplayVerifyDialog] = useState(false) + const [isInShortFormat, setIsInShortFormat] = useState(false) const { addresses } = wallet const isSingleAddress = addresses.length === 1 const accountAddress = useMemo(() => { + let addr = '' if (isSingleAddress) { - return addresses[0].address + addr = addresses[0].address + } else { + addr = (address || addresses.find(a => a.type === 0 && a.txCount === 0)?.address) ?? '' } - return (address || addresses.find(addr => addr.type === 0 && addr.txCount === 0)?.address) ?? '' - }, [address, addresses, isSingleAddress]) + return isInShortFormat ? toShortAddr(addr) : addr + }, [address, addresses, isSingleAddress, isInShortFormat]) const onAddressBookClick = useCallback(() => { history.push(RoutePath.Addresses) @@ -53,6 +69,14 @@ const Receive = () => { {accountAddress} +
{isSingleAddress ? null :

{t('receive.prompt')}

} {isSingleAddress ? null : ( diff --git a/packages/neuron-ui/src/components/Receive/receive.module.scss b/packages/neuron-ui/src/components/Receive/receive.module.scss index 39cd5443fd..d6708e814b 100644 --- a/packages/neuron-ui/src/components/Receive/receive.module.scss +++ b/packages/neuron-ui/src/components/Receive/receive.module.scss @@ -13,6 +13,24 @@ justify-content: center; } +.addressToggle{ + display: flex; + align-items: center; + appearance: none; + border: none; + background: none; + svg { + pointer-events: none; + } + &:hover { + g, + path { + stroke: var(--nervos-green); + fill: var(--nervos-green); + } + } +} + .copyBtn { appearance: none; border: none; diff --git a/packages/neuron-ui/src/components/SUDTAccountPile/index.tsx b/packages/neuron-ui/src/components/SUDTAccountPile/index.tsx index 75317dd735..36d03f7b83 100644 --- a/packages/neuron-ui/src/components/SUDTAccountPile/index.tsx +++ b/packages/neuron-ui/src/components/SUDTAccountPile/index.tsx @@ -14,7 +14,6 @@ export interface SUDTAccountPileProps { symbol?: string balance: string tokenId: string - address: string decimal: string onClick: React.EventHandler> } diff --git a/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx b/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx index 6b708ac6c7..6760578e5e 100644 --- a/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx +++ b/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx @@ -224,7 +224,7 @@ const SUDTCreateDialog = ({ } } } - const openSUDTTokenUrl = useOpenSUDTTokenUrl(info.tokenId, isMainnet); + const openSUDTTokenUrl = useOpenSUDTTokenUrl(info.tokenId, isMainnet) return (
{step === 0 ? ( @@ -252,10 +252,10 @@ const SUDTCreateDialog = ({ text: t(accType.label), checked: accountType === accType.key, disabled: insufficient[accType.key], - onRenderLabel: ({ text }: IChoiceGroupOption) => { + onRenderLabel: props => { return ( -
{text}
+
{props?.text}
{t( `s-udt.create-dialog.${AccountType.CKB === accType.key ? 'occupy-61-ckb' : 'occupy-142-ckb'}` @@ -303,18 +303,16 @@ const SUDTCreateDialog = ({ } /> ))} - { - accountType === AccountType.SUDT && !tokenErrors.tokenId && info.tokenId && ( - - ) - } + {accountType === AccountType.SUDT && !tokenErrors.tokenId && info.tokenId && ( + + )}

diff --git a/packages/neuron-ui/src/components/SUDTReceive/sUDTReceive.module.scss b/packages/neuron-ui/src/components/SUDTReceive/sUDTReceive.module.scss index f711d786c8..368640fd97 100644 --- a/packages/neuron-ui/src/components/SUDTReceive/sUDTReceive.module.scss +++ b/packages/neuron-ui/src/components/SUDTReceive/sUDTReceive.module.scss @@ -32,8 +32,30 @@ } .address { + display: flex; + align-items: center; + width: max-content; margin: 14px auto 0; - text-align: center; +} + +.addressToggle{ + display: flex; + align-items: center; + appearance: none; + border: none; + background: none; + + svg { + pointer-events: none; + } + + &:hover { + g, + path { + stroke: var(--nervos-green); + fill: var(--nervos-green); + } + } } .notation { diff --git a/packages/neuron-ui/src/components/SUDTSend/hooks.ts b/packages/neuron-ui/src/components/SUDTSend/hooks.ts new file mode 100644 index 0000000000..ecc58d830c --- /dev/null +++ b/packages/neuron-ui/src/components/SUDTSend/hooks.ts @@ -0,0 +1,202 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { AccountType, isAnyoneCanPayAddress, isSecp256k1Address, isSuccessResponse, shannonToCKBFormatter } from 'utils' +import { SUDTAccount } from 'components/SUDTAccountList' +import { DEFAULT_SUDT_FIELDS } from 'utils/const' +import { generateChequeTransaction, generateSUDTTransaction, getHoldSUDTCellCapacity } from 'services/remote' +import { AppActions, useDispatch } from 'states' + +export enum SendType { + secp256Cheque = 'cheque', + secp256NewCell = 'secp256NewCell', + acpExistCell = 'acpExistCell', + acpNewCell = 'acpNewCell', + unknowNewCell = 'unknowNewCell', + sendCKB = 'sendCKB', +} + +export enum AddressLockType { + secp256 = 'secp256', + acp = 'acp', + unknow = 'unknow', +} + +export function useAddressLockType(address: string, isMainnet: boolean) { + return useMemo(() => { + if (isSecp256k1Address(address)) { + return AddressLockType.secp256 + } + if (isAnyoneCanPayAddress(address, isMainnet)) { + return AddressLockType.acp + } + return AddressLockType.unknow + }, [address]) +} + +type Option = { + tooltip?: string + label: string + key: SendType + params?: object +} +export function useOptions({ + address, + addressLockType, + accountInfo, + isAddressCorrect, +}: { + address: string + addressLockType: AddressLockType + accountInfo: SUDTAccount | null + isAddressCorrect: boolean +}) { + const [holdSUDTCellCapacity, setHoldSUDTCellCapacity] = useState() + useEffect(() => { + if ( + accountInfo?.tokenId && + accountInfo?.tokenId !== DEFAULT_SUDT_FIELDS.CKBTokenId && + isAddressCorrect && + address + ) { + getHoldSUDTCellCapacity({ + address, + tokenID: accountInfo.tokenId, + }) + .then(res => { + if (isSuccessResponse(res)) { + setHoldSUDTCellCapacity(res.result) + } + }) + .catch(() => { + setHoldSUDTCellCapacity(undefined) + }) + } else { + setHoldSUDTCellCapacity(undefined) + } + }, [accountInfo, address, isAddressCorrect]) + return useMemo(() => { + if (accountInfo?.tokenId === DEFAULT_SUDT_FIELDS.CKBTokenId) { + return undefined + } + if (addressLockType === AddressLockType.secp256) { + return [ + { + label: 'cheque-address-hint.label', + tooltip: 'cheque-address-hint.tooltip', + key: SendType.secp256Cheque, + }, + { + label: 'extra-ckb-send-to-secp256.label', + tooltip: 'extra-ckb-send-to-secp256.tooltip', + key: SendType.secp256NewCell, + params: { assetName: accountInfo?.accountName }, + }, + ] + } + if (holdSUDTCellCapacity) { + return [ + { + label: + addressLockType === AddressLockType.acp ? 'extra-ckb-send-to-acp.label' : 'extra-ckb-send-to-unknow.label', + key: addressLockType === AddressLockType.acp ? SendType.acpNewCell : SendType.unknowNewCell, + params: { assetName: accountInfo?.accountName, extraCKB: shannonToCKBFormatter(holdSUDTCellCapacity) }, + }, + ] + } + return undefined + }, [addressLockType, holdSUDTCellCapacity, accountInfo]) +} + +export function useSendType({ + addressLockType, + accountType, +}: { + addressLockType: AddressLockType + accountType: AccountType +}) { + const [sendType, setSendType] = useState() + useEffect(() => { + if (accountType === AccountType.CKB) { + setSendType(SendType.sendCKB) + return + } + switch (addressLockType) { + case AddressLockType.secp256: + setSendType(SendType.secp256Cheque) + break + case AddressLockType.acp: + setSendType(undefined) + break + case AddressLockType.unknow: + setSendType(undefined) + break + default: + break + } + }, [addressLockType, accountType]) + const onChange = useCallback( + (e: React.SyntheticEvent) => { + const { id, checked } = e.target as HTMLInputElement + setSendType(checked ? (id as SendType) : undefined) + }, + [setSendType] + ) + return { + sendType, + onChange, + } +} + +export function getGenerator(sendType?: SendType) { + if (sendType === SendType.secp256Cheque) { + return generateChequeTransaction + } + return generateSUDTTransaction +} + +export function useOnSumbit({ + isSubmittable, + accountType, + walletId, + addressLockType, + sendType, +}: { + isSubmittable: boolean + accountType: AccountType + walletId: string + addressLockType: AddressLockType + sendType?: SendType +}) { + const dispatch = useDispatch() + return useCallback( + (e: React.FormEvent) => { + e.preventDefault() + e.stopPropagation() + if (isSubmittable) { + let actionType: State.PasswordRequest['actionType'] = 'send-sudt' + switch (sendType) { + case SendType.sendCKB: + actionType = addressLockType === AddressLockType.secp256 ? 'send-acp-ckb-to-new-cell' : 'send-ckb-asset' + break + case SendType.secp256Cheque: + actionType = 'send-cheque' + break + case SendType.acpNewCell: + case SendType.secp256NewCell: + case SendType.unknowNewCell: + actionType = 'send-acp-sudt-to-new-cell' + break + default: + actionType = 'send-sudt' + } + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: walletId, + actionType, + }, + }) + } + }, + [isSubmittable, dispatch, walletId, accountType, addressLockType, sendType] + ) +} diff --git a/packages/neuron-ui/src/components/SUDTSend/index.tsx b/packages/neuron-ui/src/components/SUDTSend/index.tsx index d145801d2f..a72d9fa69b 100644 --- a/packages/neuron-ui/src/components/SUDTSend/index.tsx +++ b/packages/neuron-ui/src/components/SUDTSend/index.tsx @@ -9,14 +9,9 @@ import TextField from 'widgets/TextField' import Breadcrum from 'widgets/Breadcrum' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' +import { ReactComponent as TooltipIcon } from 'widgets/Icons/Tooltip.svg' import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg' -import { - getSUDTAccount, - generateSUDTTransaction, - generateSendAllSUDTTransaction, - generateChequeTransaction, - destoryAssetAccount, -} from 'services/remote' +import { getSUDTAccount, destoryAssetAccount } from 'services/remote' import { useState as useGlobalState, useDispatch, AppActions } from 'states' import { validateAssetAccountAddress as validateAddress, @@ -30,10 +25,10 @@ import { AccountType, isSuccessResponse, validateAmountRange, - isSecp256k1Address, CONSTANTS, } from 'utils' import { AmountNotEnoughException } from 'exceptions' +import { AddressLockType, getGenerator, useAddressLockType, useOnSumbit, useOptions, useSendType } from './hooks' import styles from './sUDTSend.module.scss' const { INIT_SEND_PRICE, DEFAULT_SUDT_FIELDS } = CONSTANTS @@ -94,17 +89,19 @@ const SUDTSend = () => { const [isLoaded, setIsLoaded] = useState(false) const [remoteError, setRemoteError] = useState('') - const isSecp256k1Addr = isSecp256k1Address(sendState.address) + const isMainnet = isMainnetUtil(networks, networkID) + const addressLockType = useAddressLockType(sendState.address, isMainnet) + const isSecp256k1Addr = addressLockType === AddressLockType.secp256 const [accountInfo, setAccountInfo] = useState, 'accountId' | 'accountName' | 'tokenName' | 'balance' | 'tokenId' | 'decimal' | 'symbol' > | null>(null) + const accountType = accountInfo?.tokenId === DEFAULT_SUDT_FIELDS.CKBTokenId ? AccountType.CKB : AccountType.SUDT + const { sendType, onChange: onChangeSendType } = useSendType({ addressLockType, accountType }) const timerRef = useRef(null) - const isMainnet = isMainnetUtil(networks, networkID) - const accountType = accountInfo?.tokenId === DEFAULT_SUDT_FIELDS.CKBTokenId ? AccountType.CKB : AccountType.SUDT const fee = experimental?.tx?.fee ? `${shannonToCKBFormatter(experimental.tx.fee)}` : '0' useEffect(() => { @@ -173,9 +170,14 @@ const SUDTSend = () => { !isSending && Object.values(errors).every(v => !v) && [Fields.Address, Fields.Amount].every(key => sendState[key as Fields.Address | Fields.Amount].trim()) - - const isSubmittable = isFormReady && experimental?.tx && !remoteError - + const options = useOptions({ + address: sendState.address, + addressLockType, + accountInfo, + isAddressCorrect: !errors.address, + }) + const isOptionCorrect = !!(!options?.length || sendType) + const isSubmittable = isFormReady && experimental?.tx && !remoteError && isOptionCorrect useEffect(() => { const clearTimer = () => { if (timerRef.current) { @@ -184,7 +186,7 @@ const SUDTSend = () => { } clearTimer() - if (!accountInfo) { + if (!accountInfo || !isOptionCorrect) { return clearTimer } const amount = sudtAmountToValue(sendState.amount, accountInfo?.decimal) @@ -209,19 +211,11 @@ const SUDTSend = () => { assetAccountID: accountInfo?.accountId, walletID: walletId, address: sendState.address, - amount, + amount: sendState.sendAll ? 'all' : amount, feeRate: sendState.price, description: sendState.description, } - let generator = generateSUDTTransaction - if (isSecp256k1Addr && accountType === AccountType.SUDT) { - generator = generateChequeTransaction - if (sendState.sendAll) { - params.amount = 'all' - } - } else if (sendState.sendAll) { - generator = generateSendAllSUDTTransaction - } + const generator = getGenerator(sendType) generator(params) .then(res => { if (isSuccessResponse(res)) { @@ -248,6 +242,8 @@ const SUDTSend = () => { setRemoteError, accountInfo, timerRef, + sendType, + isOptionCorrect, ]) useEffect(() => { @@ -287,31 +283,6 @@ const SUDTSend = () => { [dispatch] ) - const onSubmit = useCallback( - (e: React.FormEvent) => { - e.preventDefault() - e.stopPropagation() - if (isSubmittable) { - let actionType: 'send-sudt' | 'send-acp' | 'send-cheque' | 'send-acp-to-default' = 'send-sudt' - if (accountType === AccountType.CKB && isSecp256k1Addr) { - actionType = 'send-acp-to-default' - } else if (accountType === AccountType.CKB) { - actionType = 'send-acp' - } else if (isSecp256k1Addr) { - actionType = 'send-cheque' - } - globalDispatch({ - type: AppActions.RequestPassword, - payload: { - walletID: walletId as string, - actionType, - }, - }) - } - }, - [isSubmittable, globalDispatch, walletId, accountType, isSecp256k1Addr] - ) - const [isDestroying, setIsDestroying] = useState(false) const onDestroy = useCallback(() => { setIsDestroying(true) @@ -347,6 +318,7 @@ const SUDTSend = () => { () => accountType === AccountType.CKB || BigInt(accountInfo?.balance || 0) === BigInt(0), [accountType, accountInfo] ) + const onSubmit = useOnSumbit({ isSubmittable, accountType, walletId, addressLockType, sendType }) if (!isLoaded) { return ( @@ -391,14 +363,27 @@ const SUDTSend = () => { disabled={sendState.sendAll} error={errors[field.key]} className={styles[field.key]} - hint={ - field.key === Fields.Address && isSecp256k1Addr && accountType === AccountType.SUDT - ? t('s-udt.send.cheque-address-hint') - : undefined - } /> ) })} + {options?.length && + options.map(v => ( +

+ 1 ? 'radio' : 'checkbox'} + id={v.key} + checked={v.key === sendType} + onChange={onChangeSendType} + /> + + {v.tooltip ? ( + + + + ) : null} +
+ ))} + {!isOptionCorrect &&
{t('s-udt.send.select-option')}
}