diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx index 8db87ebf76..7cf9513d42 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx +++ b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx @@ -289,7 +289,7 @@ const HistoryDetailPage = () => { title: t('transaction.address'), dataIndex: 'type', align: 'left', - width: '560px', + width: '600px', render: (_, __, item) => { const { address } = handleListData(item) return ( diff --git a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx index 2242042484..5eac285eaf 100644 --- a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx +++ b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx @@ -35,19 +35,23 @@ const ShortAddr = ({ return null } - const shortAddr = scriptToAddress(lockScript, { isMainnet, deprecated: true }) + try { + const shortAddr = scriptToAddress(lockScript, { isMainnet, deprecated: true }) - return ( - <> -
- {t('transaction.deprecated-address-format')} -
-
- {shortAddr} - onCopy(shortAddr)} /> -
- - ) + return ( + <> +
+ {t('transaction.deprecated-address-format')} +
+
+ {shortAddr} + onCopy(shortAddr)} /> +
+ + ) + } catch (error) { + return null + } } const LockInfoDialog = ({ lockInfo, isMainnet, onDismiss }: LockInfoDialogProps) => { diff --git a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts index 55dcb2b2d6..967fe64370 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts +++ b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts @@ -1,5 +1,13 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react' -import { isSuccessResponse, getMultisigAddress, DefaultLockInfo, addressToScript, scriptToAddress } from 'utils' +import { + isSuccessResponse, + getMultisigAddress, + DefaultLockInfo, + addressToScript, + scriptToAddress, + LegacyMultiSigLockInfo, + MultiSigLockInfo, +} from 'utils' import { MultisigOutputUpdate } from 'services/subjects' import { MultisigConfig, @@ -11,11 +19,13 @@ import { exportMultisigConfig, deleteMultisigConfig, getMultisigBalances, + getMultisigDAOBalances, loadMultisigTxJson, OfflineSignJSON, getMultisigSyncProgress, } from 'services/remote' import { computeScriptHash } from '@ckb-lumos/lumos/utils' +import { remindRegenerateMultisigAddress } from 'services/localCache' export const useSearch = (clearSelected: () => void, onFilterConfig: (searchKey: string) => void) => { const [keywords, setKeywords] = useState('') @@ -55,13 +65,45 @@ export const useSearch = (clearSelected: () => void, onFilterConfig: (searchKey: export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isMainnet: boolean }) => { const [entities, setEntities] = useState([]) const saveConfig = useCallback( - ({ m, n, r, addresses }: { m: number; n: number; r: number; addresses: string[] }) => { + ({ + m, + n, + r, + addresses, + lockCodeHash = MultiSigLockInfo.CodeHash, + }: { + m: number + n: number + r: number + addresses: string[] + lockCodeHash?: string + }) => { return saveMultisigConfig({ m, n, r, blake160s: addresses.map(v => addressToScript(v).args), walletId, + lockCodeHash, + }).then(res => { + if (isSuccessResponse(res)) { + setEntities(v => (res.result ? [res.result, ...v] : v)) + } else { + throw new Error(typeof res.message === 'string' ? res.message : res.message.content) + } + }) + }, + [walletId, setEntities] + ) + const regenerateConfig = useCallback( + (config: MultisigConfig) => { + return saveMultisigConfig({ + m: config.m, + n: config.n, + r: config.r, + blake160s: config.blake160s, + walletId, + lockCodeHash: MultiSigLockInfo.CodeHash, }).then(res => { if (isSuccessResponse(res)) { setEntities(v => (res.result ? [res.result, ...v] : v)) @@ -132,7 +174,8 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM { isMainnet } ) ), - fullPayload: getMultisigAddress(entity.blake160s, entity.r, entity.m, entity.n, isMainnet), + fullPayload: getMultisigAddress(entity.blake160s, entity.r, entity.m, entity.n, isMainnet, entity.lockCodeHash), + isLegacy: entity.lockCodeHash === LegacyMultiSigLockInfo.CodeHash, })), [entities, isMainnet] ) @@ -145,6 +188,7 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM ) return { saveConfig, + regenerateConfig, allConfigs, onUpdateConfigAlias, onUpdateConfig, @@ -267,6 +311,42 @@ const useDeleteAction = (deleteConfigById: (id: number) => void) => { } } +const useRegenerateAction = (regenerateConfig: (config: MultisigConfig) => Promise) => { + const [config, setConfig] = useState() + const [isNoRemind, setIsNoRemind] = useState(false) + const [regenerateErrorMessage, setRegenerateErrorMessage] = useState() + + const handleCheckbox = useCallback( + (e: React.ChangeEvent) => { + const { checked } = e.target + setIsNoRemind(checked) + remindRegenerateMultisigAddress.save(checked) + }, + [setIsNoRemind] + ) + + const regenerate = useCallback( + async (multisigConfig: MultisigConfig) => { + try { + await regenerateConfig(multisigConfig) + } catch (error) { + setRegenerateErrorMessage(error instanceof Error ? error.message : '') + } + }, + [config, isNoRemind, setRegenerateErrorMessage] + ) + + return { + action: regenerate, + config, + setConfig, + isNoRemind, + handleCheckbox, + regenerateErrorMessage, + setRegenerateErrorMessage, + } +} + const useApproveAction = () => { const [isDialogOpen, setIsDialogOpen] = useState(false) const [multisigConfig, setMultisigConfig] = useState() @@ -342,7 +422,13 @@ const useDaoWithdrawAction = () => { } } -export const useActions = ({ deleteConfigById }: { deleteConfigById: (id: number) => void }) => { +export const useActions = ({ + deleteConfigById, + regenerateConfig, +}: { + deleteConfigById: (id: number) => void + regenerateConfig: (config: MultisigConfig) => Promise +}) => { return { deleteAction: useDeleteAction(deleteConfigById), infoAction: useInfoAction(), @@ -350,6 +436,7 @@ export const useActions = ({ deleteConfigById }: { deleteConfigById: (id: number approveAction: useApproveAction(), daoDepositAction: useDaoDepositAction(), daoWithdrawAction: useDaoWithdrawAction(), + regenerateAction: useRegenerateAction(regenerateConfig), } } @@ -365,6 +452,7 @@ export const useSubscription = ({ isLightClient: boolean }) => { const [multisigBanlances, setMultisigBanlances] = useState>({}) + const [multisigDaoBalances, setMultisigDaoBalances] = useState>({}) const [multisigSyncProgress, setMultisigSyncProgress] = useState>({}) const getAndSaveMultisigBalances = useCallback(() => { getMultisigBalances({ isMainnet, multisigAddresses: configs.map(v => v.fullPayload) }).then(res => { @@ -372,7 +460,12 @@ export const useSubscription = ({ setMultisigBanlances(res.result) } }) - }, [setMultisigBanlances, isMainnet, configs]) + getMultisigDAOBalances({ isMainnet, multisigAddresses: configs.map(v => v.fullPayload) }).then(res => { + if (isSuccessResponse(res) && res.result) { + setMultisigDaoBalances(res.result) + } + }) + }, [setMultisigBanlances, setMultisigDaoBalances, isMainnet, configs]) const hashToPayload = useMemo( () => configs.reduce>( @@ -415,7 +508,7 @@ export const useSubscription = ({ clearInterval(interval) } }, [isLightClient, getAndSaveMultisigSyncProgress]) - return { multisigBanlances, multisigSyncProgress } + return { multisigBanlances, multisigDaoBalances, multisigSyncProgress } } export const useCancelWithLightClient = () => { diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index ab9dbf0b8e..697a8394e6 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -7,6 +7,7 @@ import { useGoBack, useOnWindowResize, calculateFee, + clsx, } from 'utils' import appState from 'states/init/app' import { useState as useGlobalState } from 'states' @@ -22,6 +23,7 @@ import Table from 'widgets/Table' import Tooltip from 'widgets/Tooltip' import Toast from 'widgets/Toast' import AlertDialog from 'widgets/AlertDialog' +import CopyZone from 'widgets/CopyZone' import { Download, Search, @@ -37,6 +39,10 @@ import { LineDownArrow, DAODeposit, DAOWithdrawal, + Attention, + Regenerate, + LockCodeHash, + Copy, } from 'widgets/Icons/icon' import { getHeader } from 'services/chain' import AttentionCloseDialog from 'widgets/Icons/Attention.png' @@ -48,6 +54,7 @@ import Button from 'widgets/Button' import SetStartBlockNumberDialog from 'components/SetStartBlockNumberDialog' import { type TFunction } from 'i18next' import hooks from 'components/NervosDAO/hooks' +import { remindRegenerateMultisigAddress } from 'services/localCache' import { useSearch, useConfigManage, @@ -86,6 +93,10 @@ const tableActions = [ key: 'daoWithdraw', icon: , }, + { + key: 'regenerate', + icon: , + }, ] const LearnMore = React.memo(({ t }: { t: TFunction }) => ( @@ -133,20 +144,24 @@ const MultisigAddress = () => { onImportConfig, configs, onFilterConfig, + regenerateConfig, } = useConfigManage({ walletId, isMainnet, }) - const { multisigBanlances, multisigSyncProgress } = useSubscription({ + const { multisigBanlances, multisigDaoBalances, multisigSyncProgress } = useSubscription({ walletId, isMainnet, configs: allConfigs, isLightClient, }) - const { deleteAction, infoAction, sendAction, approveAction, daoDepositAction, daoWithdrawAction } = useActions({ - deleteConfigById, - }) + const { deleteAction, infoAction, sendAction, approveAction, daoDepositAction, daoWithdrawAction, regenerateAction } = + useActions({ + deleteConfigById, + regenerateConfig, + }) const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [showRegenerateDialog, setShowRegenerateDialog] = useState(false) const { suggestFeeRate } = useGetCountDownAndFeeRateStats() const [globalAPC, setGlobalAPC] = useState(0) const [genesisBlockTimestamp, setGenesisBlockTimestamp] = useState(undefined) @@ -177,6 +192,14 @@ const MultisigAddress = () => { case 'daoWithdraw': daoWithdrawAction.action(multisigConfig) break + case 'regenerate': + if (remindRegenerateMultisigAddress.get()) { + regenerateAction.action(multisigConfig) + } else { + regenerateAction.setConfig(multisigConfig) + setShowRegenerateDialog(true) + } + break default: break } @@ -195,13 +218,32 @@ const MultisigAddress = () => { })), [t, selectIds] ) - const listNoBalanceActionOptions = useMemo( - () => - listActionOptions.map(item => ({ - ...item, - disabled: item.disabled || item.key === 'send', - })), - [listActionOptions] + + const getListActionOptions = useCallback( + (config: MultisigConfig) => { + const options: typeof listActionOptions = [] + listActionOptions.forEach(item => { + if (config.isLegacy && ['daoDeposit', 'daoWithdraw'].includes(item.key)) { + return + } + if (!config.isLegacy && ['regenerate'].includes(item.key)) { + return + } + if ( + item.key === 'send' && + (!multisigBanlances[config.fullPayload] || multisigBanlances[config.fullPayload] === '0') + ) { + options.push({ + ...item, + disabled: true, + }) + return + } + options.push(item) + }) + return options + }, + [listActionOptions, multisigBanlances] ) const daoDisabledMessage = useMemo(() => { @@ -297,6 +339,10 @@ const MultisigAddress = () => { } }, [t, setNotice, daoDepositAction, daoWithdrawAction]) + const showDaoMultisigScriptNotice = useMemo(() => { + return allConfigs.some(config => config.isLegacy) + }, [allConfigs]) + return (
{ showFooter={false} >
+ {showDaoMultisigScriptNotice && ( +
+ +
+ openExternal('https://github.com/Magickbase/neuron-public-issues/issues/457')} + aria-label="Learn more" + />, + ]} + /> +
+
+ )}
@@ -377,6 +440,29 @@ const MultisigAddress = () => { render(_, __, item) { return (
+ +
+

Code Hash

+
+ {item.isLegacy ? 'Legacy' : 'Recommended'} +
+
+ + {item.lockCodeHash} + + +
+ } + isTriggerNextToChild + tipClassName={styles.lockCodeHashTip} + > +
+ + @{item.lockCodeHash.slice(2, 10)} +
+ {item.fullPayload.slice(0, 5)}...{item.fullPayload.slice(-5)}
) @@ -460,7 +546,15 @@ const MultisigAddress = () => { render(_, __, item, show) { return (
- {show ? shannonToCKBFormatter(multisigBanlances[item.fullPayload] || '0') : HIDE_BALANCE} CKB +
+ {show ? shannonToCKBFormatter(multisigBanlances[item.fullPayload] || '0') : HIDE_BALANCE} CKB +
+ {!item.isLegacy && multisigDaoBalances[item.fullPayload] && ( +
+ (Nervos DAO: + {show ? shannonToCKBFormatter(multisigDaoBalances[item.fullPayload]) : HIDE_BALANCE} CKB) +
+ )}
) }, @@ -478,10 +572,7 @@ const MultisigAddress = () => { className={styles.tipContent} tip={
- {(!multisigBanlances[item.fullPayload] || multisigBanlances[item.fullPayload] === '0' - ? listNoBalanceActionOptions - : listActionOptions - ).map(({ key, label, icon, disabled }) => ( + {getListActionOptions(item).map(({ key, label, icon, disabled }) => (
+ } + type="warning" + onCancel={() => setShowRegenerateDialog(false)} + okText={t('multisig-address.regenerate-dialog.title')} + onOk={() => { + regenerateAction.action(regenerateAction.config!) + setShowRegenerateDialog(false) + }} + /> + + regenerateAction.setRegenerateErrorMessage('')} + /> + svg { + width: 16px; + flex-shrink: 0; + } + & > div { + max-width: 90%; + } + button { + color: var(--warn-text-color); + font-size: 12px; + padding: 0; + font-weight: 500; + display: inline-block; + background: transparent; + border: none; + text-decoration: underline; + cursor: pointer; + } + } + .head { display: flex; align-items: center; @@ -81,7 +114,7 @@ .tableWrap { margin-top: 16px; min-height: 360px; - min-width: 648px; + min-width: 748px; border: 1px solid var(--table-head-border-color); border-radius: 16px; border-top: none; @@ -98,6 +131,60 @@ .address { font-family: 'JetBrains Mono'; + position: relative; + display: flex; + align-items: center; + column-gap: 4px; + + .titleWrap { + display: flex; + align-items: center; + column-gap: 4px; + color: var(--main-text-color); + font-weight: 500; + + .badge { + font-size: 12px; + line-height: 18px; + color: var(--tabs-active); + font-weight: 500; + background: var(--tag-background-color); + padding: 0 4px; + border-radius: 2px; + } + .legacy { + color: var(--third-text-color); + background: var(--input-border-color); + } + } + .lockCodeHash { + display: flex; + align-items: center; + font-size: 13px; + border: 1px solid var(--tabs-active); + border-radius: 4px; + padding: 0 4px; + line-height: 24px; + color: var(--tabs-active); + font-weight: 500; + &.legacy { + border: 1px solid var(--divide-line-color); + color: var(--input-second-color); + svg { + path { + stroke: var(--input-second-color); + } + } + } + } + + .copyLockCodeHash { + @include copyAddress; + } + + .lockCodeHashTip { + margin-left: 50px; + } } .descTipRoot { @@ -206,6 +293,15 @@ fill: var(--main-text-color); } } + + &[data-key='regenerate'] { + g, + path { + stroke: var(--main-text-color); + fill: none; + } + } + &:hover { border-radius: 8px; background-color: var(--tooltip-hover-background-color); @@ -220,6 +316,22 @@ text-align: center; } +.regenerateDialog { + width: 568px; + @include checkbox; + .regenerateDialogDetail { + color: var(--main-text-color); + margin-bottom: -24px; + & > div { + margin-bottom: 32px; + } + } + .checkboxWrap { + font-size: 14px; + color: var(--third-text-color); + } +} + .closeMultisigDialog { width: 568px; @@ -277,7 +389,7 @@ &:hover, &:active { path { - stroke: var(--primary-color); + stroke: var(--secondary-text-color); } } } diff --git a/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx b/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx index b6ad621098..ff2c938706 100644 --- a/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddressCreateDialog/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { useState as useGlobalState } from 'states' import { MultisigConfig } from 'services/remote' import MultisigAddressInfo, { MultisigAddressTable } from 'components/MultisigAddressInfo' -import { isMainnet as isMainnetUtil } from 'utils' +import { isMainnet as isMainnetUtil, MultiSigLockInfo } from 'utils' import Dialog from 'widgets/Dialog' import TextField from 'widgets/TextField' import { useMAndN, useMultiAddress, useViewMultisigAddress } from './hooks' @@ -90,6 +90,7 @@ const MultisigAddressCreateDialog = ({ n: Number(n), r: Number(r), addresses, + lockCodeHash: MultiSigLockInfo.CodeHash, }) .then(() => { closeDialog() diff --git a/packages/neuron-ui/src/components/MultisigAddressNervosDAODialog/index.tsx b/packages/neuron-ui/src/components/MultisigAddressNervosDAODialog/index.tsx index 0926fa9f56..edb3f6abc7 100644 --- a/packages/neuron-ui/src/components/MultisigAddressNervosDAODialog/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddressNervosDAODialog/index.tsx @@ -221,7 +221,7 @@ const MultisigAddressNervosDAODialog = ({ return ( > = isMainnet ? [...commonLockArray, AnyoneCanPayLockInfoOnLina, ChequeLockInfoOnLina] @@ -44,6 +48,36 @@ const ScriptTag = ({ return null } + if (LegacyMultiSigLockInfo.TagName === foundLock.TagName || MultiSigLockInfo.TagName === foundLock.TagName) { + const isLegacy = LegacyMultiSigLockInfo.TagName === foundLock.TagName + return ( +
+ +
+

Code Hash

+
+ {isLegacy ? 'Legacy' : 'Recommended'} +
+
+ + {foundLock.CodeHash} + + +
+ } + showTriangle + isTriggerNextToChild + > + + +
+ ) + } return (