diff --git a/.gitignore b/.gitignore index 9cdabea55..4476332cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .vscode .idea/ +.eslintcache node_modules/ build/ @@ -8,6 +9,7 @@ dist/ npm-debug.log *.log coverage/ +dist/ *.orig *.swp *.bak diff --git a/renovate.json b/renovate.json index 39a2b6e9a..4bd832f5f 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ] + "extends": ["config:base"] } diff --git a/src/components/Card/HashCard/index.tsx b/src/components/Card/HashCard/index.tsx index d1824c1d5..e2831e6f8 100644 --- a/src/components/Card/HashCard/index.tsx +++ b/src/components/Card/HashCard/index.tsx @@ -1,11 +1,12 @@ import type { FC, ReactNode } from 'react' import { Link } from 'react-router-dom' -import { Tooltip } from 'antd' +import { Radio, Tooltip } from 'antd' import { useTranslation } from 'react-i18next' +import { LayoutLiteProfessional } from '../../../constants/common' import CopyIcon from '../../../assets/copy.png' import { explorerService } from '../../../services/ExplorerService' import SmallLoading from '../../Loading/SmallLoading' -import { useIsMobile, useNewAddr, useDeprecatedAddr } from '../../../utils/hook' +import { useIsMobile, useNewAddr, useDeprecatedAddr, useSearchParams, useUpdateSearchParams } from '../../../utils/hook' import SimpleButton from '../../SimpleButton' import { ReactComponent as OpenInNew } from '../../../assets/open_in_new.svg' import { ReactComponent as DownloadIcon } from '../../../assets/download_tx.svg' @@ -48,6 +49,7 @@ export default ({ showDASInfoOnHeader?: boolean | string }) => { const isMobile = useIsMobile() + const { Professional, Lite } = LayoutLiteProfessional const setToast = useSetToast() const { t } = useTranslation() @@ -56,6 +58,19 @@ export default ({ const deprecatedAddr = useDeprecatedAddr(hash) const counterpartAddr = newAddr === hash ? deprecatedAddr : newAddr + const searchParams = useSearchParams('layout') + const defaultLayout = Professional + const updateSearchParams = useUpdateSearchParams<'layout'>() + const layout = searchParams.layout === Lite ? Lite : defaultLayout + + const onChangeLayout = (layoutType: LayoutLiteProfessional) => { + updateSearchParams(params => + layoutType === defaultLayout + ? Object.fromEntries(Object.entries(params).filter(entry => entry[0] !== 'layout')) + : { ...params, layout: layoutType }, + ) + } + const handleExportTxClick = async () => { const res = await explorerService.api.requesterV2(`transactions/${hash}/raw`).catch(error => { setToast({ message: error.message }) @@ -86,7 +101,6 @@ export default ({
{title}
)} -
{loading ? ( @@ -133,11 +147,26 @@ export default ({ ) : null}
+ {!isMobile && isTx && !loading ? ( +
+ onChangeLayout(value)} + value={layout} + optionType="button" + buttonStyle="solid" + /> +
+ ) : null} + {(showDASInfoOnHeader || showDASInfoOnHeader === '') && ( )}
- {specialAddress && ( @@ -149,6 +178,23 @@ export default ({ {hash} + + {isMobile && isTx && !loading ? ( +
+ onChangeLayout(value)} + value={layout} + optionType="button" + buttonStyle="solid" + /> +
+ ) : null} + {children} ) diff --git a/src/components/Card/HashCard/styles.module.scss b/src/components/Card/HashCard/styles.module.scss index 62b91cc6f..c7edcd4ef 100644 --- a/src/components/Card/HashCard/styles.module.scss +++ b/src/components/Card/HashCard/styles.module.scss @@ -85,3 +85,57 @@ color: #333; } } + +.professionalLiteBox { + margin-left: 10px; + + .layoutButtons { + > label { + height: 40px; + width: 120px; + text-align: center; + font-weight: 400; + font-size: 16px; + line-height: 38px; + color: #333; + border: 1px solid #e5e5e5; + box-shadow: none !important; + + &::before { + content: none !important; + } + + &:hover { + color: #333; + background: #fff; + } + + &:first-child { + border-radius: 4px 0 0 4px; + } + + &:last-child { + border-radius: 0 4px 4px 0; + } + + &:global(.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)) { + background: #333; + border-color: #333 !important; + + &:hover { + background: #333; + } + } + } + + @media screen and (width <= 750px) { + width: 100%; + margin: 6px 0 20px; + + > label { + height: 40px; + width: 50%; + } + } + } +} diff --git a/src/components/DecimalCapacity/index.tsx b/src/components/DecimalCapacity/index.tsx index 623bebb48..a45171cab 100644 --- a/src/components/DecimalCapacity/index.tsx +++ b/src/components/DecimalCapacity/index.tsx @@ -1,23 +1,27 @@ +import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { DecimalPanel, DecimalPartPanel, DecimalZerosPanel } from './styled' +import styles from './styles.module.scss' export default ({ value, fontSize, - color, + balanceChangeType = 'none', hideUnit, hideZero, marginBottom = '1px', }: { value: string + balanceChangeType?: 'payment' | 'income' | 'none' fontSize?: string - color?: string hideUnit?: boolean hideZero?: boolean marginBottom?: string }) => { const { t } = useTranslation() const integer = value.split('.')[0] || '0' + const isPayment = balanceChangeType === 'payment' + const balanceChangeTypeClass = isPayment ? 'subtraction' : 'addition' let decimal = value.split('.')[1] || '' let zeros = '' @@ -33,16 +37,24 @@ export default ({ return ( - {integer} - + {integer} + {decimal} {!hideZero && ( - + {zeros} )} - {!hideUnit &&
{t('common.ckb_unit')}
} + {!hideUnit &&
{t('common.ckb_unit')}
}
) } diff --git a/src/components/DecimalCapacity/styled.tsx b/src/components/DecimalCapacity/styled.tsx index 35ed1906c..abc4054bc 100644 --- a/src/components/DecimalCapacity/styled.tsx +++ b/src/components/DecimalCapacity/styled.tsx @@ -5,6 +5,14 @@ export const DecimalPanel = styled.div` justify-content: flex-end; align-items: flex-end; + .subtraction { + color: var(--accent-color); + } + + .addition { + color: var(--primary-color); + } + .decimalUnit { margin-left: 5px; @@ -17,7 +25,7 @@ export const DecimalPanel = styled.div` export const DecimalPartPanel = styled.div` margin-bottom: ${(props: { marginBottom: string }) => (props.marginBottom ? props.marginBottom : '1px')}; font-size: ${(props: { fontSize?: string; color?: string; marginBottom: string }) => - props.fontSize ? props.fontSize : '12px'}; + props.fontSize ? props.fontSize : '14px'}; color: ${(props: { color?: string }) => (props.color ? props.color : '#999999')}; @media (max-width: 1000px) { @@ -32,7 +40,7 @@ export const DecimalPartPanel = styled.div` export const DecimalZerosPanel = styled.div` margin-bottom: ${(props: { marginBottom: string }) => (props.marginBottom ? props.marginBottom : '1px')}; font-size: ${(props: { fontSize?: string; color?: string; marginBottom: string }) => - props.fontSize ? props.fontSize : '12px'}; + props.fontSize ? props.fontSize : '14px'}; color: ${(props: { color?: string }) => (props.color ? props.color : '#999999')}; @media (max-width: 1000px) { diff --git a/src/components/DecimalCapacity/styles.module.scss b/src/components/DecimalCapacity/styles.module.scss new file mode 100644 index 000000000..7d18ea543 --- /dev/null +++ b/src/components/DecimalCapacity/styles.module.scss @@ -0,0 +1,3 @@ +.intergerPart { + font-size: 16px; +} diff --git a/src/components/TransactionItem/TransactionIncome/index.tsx b/src/components/TransactionItem/TransactionIncome/index.tsx index f188b001c..eba38aa33 100644 --- a/src/components/TransactionItem/TransactionIncome/index.tsx +++ b/src/components/TransactionItem/TransactionIncome/index.tsx @@ -15,9 +15,10 @@ export default ({ income }: { income: string }) => { if (bigIncome.isNaN()) { bigIncome = new BigNumber(0) } + const isIncome = bigIncome.isGreaterThanOrEqualTo(0) return ( - + {isMobile && ( current Address @@ -25,7 +26,7 @@ export default ({ income }: { income: string }) => { )} {!isMobile && ( diff --git a/src/components/TransactionItem/TransactionItemCell/index.tsx b/src/components/TransactionItem/TransactionItemCell/index.tsx index 6a32695d0..0e1a11cf1 100644 --- a/src/components/TransactionItem/TransactionItemCell/index.tsx +++ b/src/components/TransactionItem/TransactionItemCell/index.tsx @@ -9,7 +9,7 @@ import CurrentAddressIcon from '../../../assets/current_address.svg' import UDTTokenIcon from '../../../assets/udt_token.png' import { useCurrentLanguage } from '../../../utils/i18n' import { localeNumberString, parseUDTAmount } from '../../../utils/number' -import { shannonToCkb, shannonToCkbDecimal } from '../../../utils/util' +import { isDaoCell, isDaoDepositCell, isDaoWithdrawCell, shannonToCkb, shannonToCkbDecimal } from '../../../utils/util' import { TransactionCellPanel, TransactionCellCapacityPanel, @@ -30,12 +30,6 @@ import { useBoolean, useIsMobile } from '../../../utils/hook' import CopyTooltipText from '../../Text/CopyTooltipText' import EllipsisMiddle from '../../EllipsisMiddle' -const isDaoDepositCell = (cellType: State.CellTypes) => cellType === 'nervos_dao_deposit' - -const isDaoWithdrawCell = (cellType: State.CellTypes) => cellType === 'nervos_dao_withdrawing' - -const isDaoCell = (cellType: State.CellTypes) => isDaoDepositCell(cellType) || isDaoWithdrawCell(cellType) - const AddressTextWithAlias: FC<{ address: string to?: string diff --git a/src/components/TransactionItem/TransactionLiteIncome/index.module.scss b/src/components/TransactionItem/TransactionLiteIncome/index.module.scss index a7a471803..3b4fabb5a 100644 --- a/src/components/TransactionItem/TransactionLiteIncome/index.module.scss +++ b/src/components/TransactionItem/TransactionLiteIncome/index.module.scss @@ -22,5 +22,5 @@ } .decreased { - color: #fa504f; + color: var(--accent-color); } diff --git a/src/components/TransactionItem/TransactionLiteIncome/index.tsx b/src/components/TransactionItem/TransactionLiteIncome/index.tsx index ee2e6d364..8f389f742 100644 --- a/src/components/TransactionItem/TransactionLiteIncome/index.tsx +++ b/src/components/TransactionItem/TransactionLiteIncome/index.tsx @@ -10,17 +10,13 @@ export default ({ income }: { income: string }) => { if (bigIncome.isNaN()) { bigIncome = new BigNumber(0) } + const isIncome = bigIncome.isGreaterThanOrEqualTo(0) return ( -
+
) diff --git a/src/constants/common.ts b/src/constants/common.ts index eb98f5f3f..c6bc2792b 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -110,5 +110,10 @@ export enum ChainName { Testnet = 'pudge', } +export enum LayoutLiteProfessional { + Lite = 'lite', + Professional = 'professional', +} + export const MAINNET_URL = `https://${config.BASE_URL}` export const TESTNET_URL = `https://${ChainName.Testnet}.${config.BASE_URL}` diff --git a/src/index.css b/src/index.css index 4b3e4603d..9c467eb24 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,8 @@ body { --primary-color: #00cc9b; + --accent-color: #FA504F; + --primary-text-color: #333; --primary-hover-bg-color: #e8fff1; --primary-chiffon-color: #e6fcf7; --navbar-height: 64px; diff --git a/src/locales/en.json b/src/locales/en.json index dd513713b..46c58145f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -453,7 +453,9 @@ } }, "professional": "Professional", - "lite": "Lite" + "lite": "Lite", + "unknown_assets": "Unknown Assets", + "mint": "Mint" }, "block": { "block": "Block", diff --git a/src/locales/zh.json b/src/locales/zh.json index df7482307..55bfe6acc 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -452,7 +452,9 @@ } }, "professional": "专业版", - "lite": "精简版" + "lite": "精简版", + "unknown_assets": "未知资产", + "mint": "铸造" }, "block": { "block": "区块", diff --git a/src/pages/Address/AddressComp.tsx b/src/pages/Address/AddressComp.tsx index b859c3c67..4cdf29964 100644 --- a/src/pages/Address/AddressComp.tsx +++ b/src/pages/Address/AddressComp.tsx @@ -43,6 +43,7 @@ import ArrowUpIcon from '../../assets/arrow_up.png' import ArrowUpBlueIcon from '../../assets/arrow_up_blue.png' import ArrowDownIcon from '../../assets/arrow_down.png' import ArrowDownBlueIcon from '../../assets/arrow_down_blue.png' +import { LayoutLiteProfessional } from '../../constants/common' import { omit } from '../../utils/object' import { CsvExport } from '../../components/CsvExport' import PaginationWithRear from '../../components/PaginationWithRear' @@ -341,17 +342,20 @@ export const AddressTransactions = ({ const isMobile = useIsMobile() const { t } = useTranslation() const { currentPage, pageSize, setPage } = usePaginationParamsInListPage() + const { Professional, Lite } = LayoutLiteProfessional const searchParams = useSearchParams('layout') - const defaultLayout = 'professional' + const defaultLayout = Professional const updateSearchParams = useUpdateSearchParams<'layout' | 'sort' | 'tx_type'>() - const layout = searchParams.layout === 'lite' ? 'lite' : defaultLayout + const layout = searchParams.layout === Lite ? Lite : defaultLayout const totalPages = Math.ceil(total / pageSize) - const onChangeLayout = (lo: 'professional' | 'lite') => { - updateSearchParams(params => (lo === defaultLayout ? omit(params, ['layout']) : { ...params, layout: lo })) + const onChangeLayout = (layoutType: LayoutLiteProfessional) => { + updateSearchParams(params => + layoutType === defaultLayout + ? Object.fromEntries(Object.entries(params).filter(entry => entry[0] !== 'layout')) + : { ...params, layout: layoutType }, + ) } - - // REFACTOR: could be an independent component const handleTimeSort = () => { updateSearchParams( params => @@ -394,8 +398,8 @@ export const AddressTransactions = ({ onChangeLayout(value)} value={layout} diff --git a/src/pages/Transaction/TransactionCell/index.tsx b/src/pages/Transaction/TransactionCell/index.tsx index c75492f7c..a85676598 100644 --- a/src/pages/Transaction/TransactionCell/index.tsx +++ b/src/pages/Transaction/TransactionCell/index.tsx @@ -43,7 +43,7 @@ import { useDASAccount } from '../../../contexts/providers/dasQuery' import styles from './styles.module.scss' import AddressText from '../../../components/AddressText' -const Addr: FC<{ address: string; isCellBase: boolean }> = ({ address, isCellBase }) => { +export const Addr: FC<{ address: string; isCellBase: boolean }> = ({ address, isCellBase }) => { const alias = useDASAccount(address) const { t } = useTranslation() diff --git a/src/pages/Transaction/TransactionComp.tsx b/src/pages/Transaction/TransactionComp.tsx deleted file mode 100644 index 9419bb5fe..000000000 --- a/src/pages/Transaction/TransactionComp.tsx +++ /dev/null @@ -1,422 +0,0 @@ -/* eslint-disable react/no-array-index-key */ -import { useState, ReactNode, FC } from 'react' -import { Link } from 'react-router-dom' -import BigNumber from 'bignumber.js' -import { Trans, useTranslation } from 'react-i18next' -import OverviewCard, { OverviewItemData } from '../../components/Card/OverviewCard' -import { parseSimpleDate } from '../../utils/date' -import { localeNumberString } from '../../utils/number' -import { useFormatConfirmation, shannonToCkb, matchTxHash } from '../../utils/util' -import { - TransactionBlockHeightPanel, - TransactionInfoContentPanel, - TransactionOverviewPanel, - TransactionInfoContentItem, - TransactionInfoItemPanel, -} from './styled' -import TransactionCellList from './TransactionCellList' -import DecimalCapacity from '../../components/DecimalCapacity' -import ArrowUpIcon from '../../assets/arrow_up.png' -import ArrowDownIcon from '../../assets/arrow_down.png' -import ArrowUpBlueIcon from '../../assets/arrow_up_blue.png' -import ArrowDownBlueIcon from '../../assets/arrow_down_blue.png' -import { isMainnet } from '../../utils/chain' -import SimpleButton from '../../components/SimpleButton' -import HashTag from '../../components/HashTag' -import { useAddrFormatToggle } from '../../utils/hook' -import ComparedToMaxTooltip from '../../components/Tooltip/ComparedToMaxTooltip' -import { HelpTip } from '../../components/HelpTip' -import { useLatestBlockNumber } from '../../services/ExplorerService' - -const showTxStatus = (txStatus: string) => txStatus?.replace(/^\S/, s => s.toUpperCase()) ?? '-' - -const TransactionBlockHeight = ({ blockNumber, txStatus }: { blockNumber: number; txStatus: string }) => ( - - {txStatus === 'committed' ? ( - {localeNumberString(blockNumber)} - ) : ( - {showTxStatus(txStatus)} - )} - -) - -const transactionParamsIcon = (show: boolean) => { - if (show) { - return isMainnet() ? ArrowUpIcon : ArrowUpBlueIcon - } - return isMainnet() ? ArrowDownIcon : ArrowDownBlueIcon -} - -const TransactionInfoItem = ({ - title, - tooltip, - value, - valueTooltip, - linkUrl, - tag, -}: { - title?: string - tooltip?: string - value: string | ReactNode - valueTooltip?: string - linkUrl?: string - tag?: ReactNode -}) => ( - -
- {title ? ( - <> - {title} - {tooltip && } - : - - ) : ( - '' - )} -
-
-
- {linkUrl ? ( - - {value} - - ) : ( - value - )} - {valueTooltip && } -
- {tag &&
{tag}
} -
-
-) - -const TransactionInfoItemWrapper = ({ - title, - tooltip, - value, - linkUrl, -}: { - title?: string - tooltip?: string - value: string | ReactNode - linkUrl?: string -}) => ( - - - -) - -export const TransactionOverview: FC<{ transaction: State.Transaction }> = ({ transaction }) => { - const [showParams, setShowParams] = useState(false) - const tipBlockNumber = useLatestBlockNumber() - const { - blockNumber, - cellDeps, - headerDeps, - witnesses, - blockTimestamp, - transactionFee, - txStatus, - detailedMessage, - bytes, - largestTxInEpoch, - largestTx, - cycles, - maxCyclesInEpoch, - maxCycles, - } = transaction - const { t } = useTranslation() - const parseFormatConfirmation = useFormatConfirmation() - let confirmation = 0 - if (tipBlockNumber && blockNumber) { - confirmation = tipBlockNumber - blockNumber - } - - const OverviewItems: Array = [ - { - title: t('block.block_height'), - tooltip: t('glossary.block_height'), - content: , - }, - ] - if (txStatus === 'committed') { - if (confirmation >= 0) { - OverviewItems.push( - { - title: t('block.timestamp'), - tooltip: t('glossary.timestamp'), - content: parseSimpleDate(blockTimestamp), - }, - bytes - ? { - title: `${t('transaction.transaction_fee')} | ${t('transaction.fee_rate')}`, - content: ( -
- - {` | ${new BigNumber(transactionFee).multipliedBy(1000).dividedToIntegerBy(bytes).toFormat({ - groupSeparator: ',', - groupSize: 3, - })} shannons/kB`} -
- ), - } - : { - title: t('transaction.transaction_fee'), - content: , - }, - - { - title: t('transaction.status'), - tooltip: t('glossary.transaction_status'), - content: parseFormatConfirmation(confirmation), - }, - ) - } - } else { - OverviewItems.push( - { - title: t('block.timestamp'), - tooltip: t('glossary.timestamp'), - content: showTxStatus(txStatus), - }, - { - title: t('transaction.transaction_fee'), - content: , - }, - { - title: t('transaction.status'), - tooltip: t('glossary.transaction_status'), - content: showTxStatus(txStatus), - valueTooltip: txStatus === 'rejected' ? detailedMessage : undefined, - }, - ) - } - - OverviewItems.push( - { - title: t('transaction.size'), - content: bytes ? ( -
- {`${(bytes - 4).toLocaleString('en')} Bytes`} - - {t('transaction.size_in_block', { - bytes: bytes.toLocaleString('en'), - })} - -
- ) : ( - '' - ), - }, - null, - { - title: t('transaction.cycles'), - content: cycles ? ( -
- {`${cycles.toLocaleString('en')}`} - -
- ) : ( - '-' - ), - }, - ) - - const TransactionParams = [ - { - title: t('transaction.cell_deps'), - tooltip: ( - - ), - }} - /> - ), - content: - cellDeps && cellDeps.length > 0 ? ( - cellDeps.map(cellDep => { - const { - outPoint: { txHash, index }, - depType, - } = cellDep - const hashTag = matchTxHash(txHash, index) - return ( - - } - /> - - - - ) - }) - ) : ( - - ), - }, - { - title: t('transaction.header_deps'), - tooltip: t('glossary.header_deps'), - content: - headerDeps && headerDeps.length > 0 ? ( - headerDeps.map(headerDep => ( - - )) - ) : ( - - ), - }, - { - title: t('transaction.witnesses'), - tooltip: t('glossary.witnesses'), - content: - witnesses && witnesses.length > 0 ? ( - witnesses.map((witness, index) => ( - - )) - ) : ( - - ), - }, - ] - - return ( - - -
- setShowParams(!showParams)}> -
{t('transaction.transaction_parameters')}
- transaction parameters -
- {showParams && ( -
- {TransactionParams.map(item => ( - -
- {item.title} - {item.tooltip && } -
-
{item.content}
-
- ))} -
- )} -
-
-
- ) -} - -const handleCellbaseInputs = (inputs: State.Cell[], outputs: State.Cell[]) => { - if (inputs[0] && inputs[0].fromCellbase && outputs[0] && outputs[0].baseReward) { - const resultInputs = inputs - resultInputs[0] = { - ...resultInputs[0], - baseReward: outputs[0].baseReward, - secondaryReward: outputs[0].secondaryReward, - commitReward: outputs[0].commitReward, - proposalReward: outputs[0].proposalReward, - } - return resultInputs - } - return inputs -} - -export default ({ transaction }: { transaction: State.Transaction }) => { - const { transactionHash, displayInputs, displayOutputs, blockNumber, isCellbase } = transaction - - const { isNew: isAddrNew, setIsNew: setIsAddrNew } = useAddrFormatToggle() - const inputs = handleCellbaseInputs(displayInputs, displayOutputs) - - /// [0, 11] block doesn't show block reward and only cellbase show block reward - return ( - <> -
- {inputs && ( - 0 && isCellbase} - addrToggle={{ - isAddrNew, - setIsAddrNew, - }} - /> - )} -
-
- {displayOutputs && ( - - )} -
- - ) -} diff --git a/src/pages/Transaction/TransactionComp/TransactionComp.tsx b/src/pages/Transaction/TransactionComp/TransactionComp.tsx new file mode 100644 index 000000000..85a578974 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionComp.tsx @@ -0,0 +1,54 @@ +import TransactionCellList from '../TransactionCellList' +import { useAddrFormatToggle } from '../../../utils/hook' + +const handleCellbaseInputs = (inputs: State.Cell[], outputs: State.Cell[]) => { + if (inputs[0] && inputs[0].fromCellbase && outputs[0] && outputs[0].baseReward) { + const resultInputs = inputs + resultInputs[0] = { + ...resultInputs[0], + baseReward: outputs[0].baseReward, + secondaryReward: outputs[0].secondaryReward, + commitReward: outputs[0].commitReward, + proposalReward: outputs[0].proposalReward, + } + return resultInputs + } + return inputs +} + +export const TransactionComp = ({ transaction }: { transaction: State.Transaction }) => { + const { transactionHash, displayInputs, displayOutputs, blockNumber, isCellbase } = transaction + + const { isNew: isAddrNew, setIsNew: setIsAddrNew } = useAddrFormatToggle() + const inputs = handleCellbaseInputs(displayInputs, displayOutputs) + + /// [0, 11] block doesn't show block reward and only cellbase show block reward + return ( + <> +
+ {inputs && ( + 0 && isCellbase} + addrToggle={{ + isAddrNew, + setIsAddrNew, + }} + /> + )} +
+
+ {displayOutputs && ( + + )} +
+ + ) +} diff --git a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.module.scss b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.module.scss new file mode 100644 index 000000000..4f2ee999a --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.module.scss @@ -0,0 +1,16 @@ +.transactionBadge { + font-size: 12px; + line-height: 100%; + padding: 5px 8px; + border-radius: 4px; + border: 1px solid #b0cbfc; + background: #d7e5fd; + + @media (width <= 750px) { + margin-left: 12px; + } +} + +.tootip { + text-align: center; +} diff --git a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.tsx b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.tsx new file mode 100644 index 000000000..0eec5ecd6 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionBadge.tsx @@ -0,0 +1,40 @@ +import { Tooltip } from 'antd' +import styles from './TransactionBadge.module.scss' + +type Props = { + cellType: State.CellType + capacity?: string +} +const cellTypeDisplayMap: Record = { + normal: '', + udt: '', + nervos_dao_deposit: 'Nervos DAO Deposit', + nervos_dao_withdrawing: 'Nervos DAO Withdraw', + spore_cell: '', + spore_cluster: '', + cota_regular: '', + cota_registry: '', + m_nft_issuer: '', + m_nft_class: '', + m_nft_token: '', + nrc_721_token: '', + nrc_721_factory: '', +} + +export const TransactionBadge = ({ cellType, capacity }: Props) => { + const displayName = cellTypeDisplayMap[cellType] + if (!displayName) return null + + return ( + +
{displayName}
+
{capacity} CKB
+
+ } + > +
{displayName}
+
+ ) +} diff --git a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.module.scss b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.module.scss new file mode 100644 index 000000000..54dfb63d4 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.module.scss @@ -0,0 +1,141 @@ +.transactionLiteBox { + .transactionLiteBoxHeader { + width: 100%; + height: auto; + margin-bottom: 16px; + overflow-x: hidden; + + .transactionLiteBoxHeaderAddr { + width: 260px; + max-width: 400px; + display: inline-block; + + div { + color: var(--primary-color); + } + } + + @media (width <= 750px) { + .transactionLiteBoxHeaderAddr { + width: 200px; + display: inline-block; + } + } + + .tag { + background: #e8fff1; + color: var(--primary-color); + font-size: 12px; + padding: 2px 6px; + box-shadow: 0 4px 4px rgb(16 16 16 / 5%); + border: 1px solid #caffdf; + border-radius: 4px; + display: inline-block; + margin-left: 8px; + } + } + + .transactionLiteBoxContent { + width: 100%; + height: auto; + + div { + display: inline-block; + } + + .nftId, + .add { + color: var(--primary-color); + } + + .subtraction { + color: var(--accent-color); + } + + .addressDetailLite { + text-align: right; + font-size: 14px; + } + + .capacityChange { + margin-left: 12px; + height: 24px; + + @media (width <= 750px) { + margin-left: 0; + } + } + + & > div { + width: 100%; + height: 22px; + margin-bottom: 12px; + display: flex; + + div { + flex: 1; + + &:first-child { + text-align: left; + color: #333; + } + + .tag { + box-sizing: border-box; + background: #d7e5fd; + padding: 2px 6px; + border: 1px solid #b0cbfc; + border-radius: 4px; + font-size: 12px; + color: #333; + display: inline-block; + margin-right: 12px; + } + } + } + } + + .transactionLiteBoxContentMobile { + width: 100%; + height: auto; + + div { + display: inline-block; + color: var(--primary-color); + } + + .transactionLiteMobileName { + p { + font-size: 16px; + color: var(--primary-text-color); + } + } + + .transactionLiteMobileContent { + &:first-child { + text-align: left; + } + + &:last-child { + text-align: right; + } + } + + .tag { + box-sizing: border-box; + background: #d7e5fd; + padding: 2px 6px; + border: 1px solid #b0cbfc; + border-radius: 4px; + font-size: 12px; + color: #333; + margin-right: 12px; + float: left; + } + + & > div { + width: 100%; + height: auto; + } + } +} diff --git a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx new file mode 100644 index 000000000..35641a5b0 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx @@ -0,0 +1,152 @@ +/* eslint-disable react/no-array-index-key */ +import { FC } from 'react' +import { useQuery } from 'react-query' +import { useParams } from 'react-router-dom' +import BigNumber from 'bignumber.js' +import styles from './TransactionLite.module.scss' +import DecimalCapacity from '../../../../components/DecimalCapacity' +import { parseCKBAmount, localeNumberString } from '../../../../utils/number' +import { shannonToCkb } from '../../../../utils/util' +import { Addr } from '../../TransactionCell' +import { defaultTransactionLiteDetails } from '../../state' +import { TransactionBadge } from './TransactionBadge' +import { fetchTransactionLiteDetailsByHash } from '../../../../services/ExplorerService/fetcher' +import { useIsMobile } from '../../../../utils/hook' + +const getTransferItemTag = (transfer: State.LiteTransfer) => { + const { cellType, udtInfo, mNftInfo } = transfer + if (cellType === 'm_nft_token' || cellType === 'm_nft_class' || cellType === 'm_nft_issuer') { + return `NFT-${mNftInfo?.className ?? 'Unknown'}` + } + if (cellType === 'udt') { + return udtInfo?.symbol || `Uknown Asset #${udtInfo?.typeHash.substring(udtInfo.typeHash.length - 4)}` + } + if (cellType === 'spore_cell' || cellType === 'spore_cluster') { + return 'Spore' + } + if (cellType === 'cota_regular' || cellType === 'cota_registry') { + return 'Cota' + } + if (cellType === 'nervos_dao_deposit' || cellType === 'nervos_dao_withdrawing') { + return 'Nervos DAO' + } + if (cellType === 'nrc_721_token' || cellType === 'nrc_721_factory') { + return 'NRC-721' + } + return 'CKB' +} + +export const TransactionCompLite: FC<{ isCellbase: boolean }> = ({ isCellbase }) => { + const { hash: txHash } = useParams<{ hash: string }>() + const isMobile = useIsMobile() + + const query = useQuery(['ckb_transaction_details', txHash], async () => { + const ckbTransactionDetails = await fetchTransactionLiteDetailsByHash(txHash) + return ckbTransactionDetails.data + }) + const transactionLiteDetails: State.TransactionLiteDetails[] = query.data ?? defaultTransactionLiteDetails + return ( + <> + {transactionLiteDetails && + transactionLiteDetails.map(item => ( +
+
+
+
+ +
+
+ {isMobile ? : } +
+
+ ))} + + ) +} + +export const DesktopTransferItems = (props: { details: State.TransactionLiteDetails }) => { + const { details } = props + const { transfers } = details + return ( +
+ {transfers.map((transfer, index) => { + return ( +
+
{getTransferItemTag(transfer)}
+
+ + +
+
+ ) + })} +
+ ) +} + +export const MobileTransferItems = (props: { details: State.TransactionLiteDetails }) => { + const { details } = props + const { transfers } = details + return ( +
+ {transfers.map((transfer, index) => { + return ( +
+
{getTransferItemTag(transfer)}
+
+ ) + })} + {transfers.map((transfer, index) => { + return ( +
+
+ + +
+
+ ) + })} +
+ ) +} + +const TransferAmount: FC<{ transfer: State.LiteTransfer }> = ({ transfer }) => { + const isUdt = transfer.cellType === 'udt' + const isNft = transfer.cellType === 'm_nft_token' + + const transferCapacity = new BigNumber(transfer.capacity) + const transferAmount = new BigNumber(transfer.udtInfo?.amount ?? 0) + const isIncome = isUdt ? transferAmount.isPositive() : transferCapacity.isPositive() + const decimalPanelType = isIncome ? 'income' : 'payment' + + const amountChange = localeNumberString(shannonToCkb(transferAmount)) + const capacityChange = localeNumberString(shannonToCkb(transferCapacity)) + const isIncomeColor = isIncome ? styles.add : styles.subtraction + + const getUdtComponent = () => { + if (isUdt) { + return ( + <> + +
{`(${capacityChange} CKB)`}
+ + ) + } + if (isNft) { + return ( +
+ {isIncome ? '' : '-'} + ID: {transfer.mNftInfo?.tokenId ?? 'Unknown'} + {`(${capacityChange} CKB)`} +
+ ) + } + return + } + return ( +
+ {isIncome ? '+' : ''} + {getUdtComponent()} +
+ ) +} diff --git a/src/pages/Transaction/TransactionComp/TransactionOverview.tsx b/src/pages/Transaction/TransactionComp/TransactionOverview.tsx new file mode 100644 index 000000000..67f4a5821 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/TransactionOverview.tsx @@ -0,0 +1,389 @@ +/* eslint-disable react/no-array-index-key */ +import { useState, ReactNode, FC } from 'react' +import { Link } from 'react-router-dom' +import BigNumber from 'bignumber.js' +import { Trans, useTranslation } from 'react-i18next' +import OverviewCard, { OverviewItemData } from '../../../components/Card/OverviewCard' +import DecimalCapacity from '../../../components/DecimalCapacity' +import HashTag from '../../../components/HashTag' +import { HelpTip } from '../../../components/HelpTip' +import SimpleButton from '../../../components/SimpleButton' +import ComparedToMaxTooltip from '../../../components/Tooltip/ComparedToMaxTooltip' +import { LayoutLiteProfessional } from '../../../constants/common' +import { isMainnet } from '../../../utils/chain' +import { parseSimpleDate } from '../../../utils/date' +import ArrowUpIcon from '../../../assets/arrow_up.png' +import ArrowDownIcon from '../../../assets/arrow_down.png' +import ArrowUpBlueIcon from '../../../assets/arrow_up_blue.png' +import ArrowDownBlueIcon from '../../../assets/arrow_down_blue.png' +import { localeNumberString } from '../../../utils/number' +import { shannonToCkb, useFormatConfirmation, matchTxHash } from '../../../utils/util' +import { + TransactionBlockHeightPanel, + TransactionInfoContentItem, + TransactionInfoContentPanel, + TransactionOverviewPanel, + TransactionInfoItemPanel, +} from './styled' +import { useLatestBlockNumber } from '../../../services/ExplorerService' + +const showTxStatus = (txStatus: string) => txStatus?.replace(/^\S/, s => s.toUpperCase()) ?? '-' +const TransactionBlockHeight = ({ blockNumber, txStatus }: { blockNumber: number; txStatus: string }) => ( + + {txStatus === 'committed' ? ( + {localeNumberString(blockNumber)} + ) : ( + {showTxStatus(txStatus)} + )} + +) + +const transactionParamsIcon = (show: boolean) => { + if (show) { + return isMainnet() ? ArrowUpIcon : ArrowUpBlueIcon + } + return isMainnet() ? ArrowDownIcon : ArrowDownBlueIcon +} + +const TransactionInfoItem = ({ + title, + tooltip, + value, + valueTooltip, + linkUrl, + tag, +}: { + title?: string + tooltip?: string + value: string | ReactNode + valueTooltip?: string + linkUrl?: string + tag?: ReactNode +}) => ( + +
+ {title ? ( + <> + {title} + {tooltip && } + : + + ) : ( + '' + )} +
+
+
+ {linkUrl ? ( + + {value} + + ) : ( + value + )} + {valueTooltip && } +
+ {tag &&
{tag}
} +
+
+) + +const TransactionInfoItemWrapper = ({ + title, + tooltip, + value, + linkUrl, +}: { + title?: string + tooltip?: string + value: string | ReactNode + linkUrl?: string +}) => ( + + + +) + +export const TransactionOverview: FC<{ transaction: State.Transaction; layout: LayoutLiteProfessional }> = ({ + transaction, + layout, +}) => { + const [showParams, setShowParams] = useState(false) + const tipBlockNumber = useLatestBlockNumber() + const { + blockNumber, + cellDeps, + headerDeps, + witnesses, + blockTimestamp, + transactionFee, + txStatus, + detailedMessage, + bytes, + largestTxInEpoch, + largestTx, + cycles, + maxCyclesInEpoch, + maxCycles, + } = transaction + + const { t } = useTranslation() + const formatConfirmation = useFormatConfirmation() + let confirmation = 0 + const isProfessional = layout === LayoutLiteProfessional.Professional + + if (tipBlockNumber && blockNumber) { + confirmation = tipBlockNumber - blockNumber + } + + const blockHeightData: OverviewItemData = { + title: t('block.block_height'), + tooltip: t('glossary.block_height'), + content: , + } + const timestampData: OverviewItemData = { + title: t('block.timestamp'), + tooltip: t('glossary.timestamp'), + content: parseSimpleDate(blockTimestamp), + } + const feeWithFeeRateData: OverviewItemData = { + title: `${t('transaction.transaction_fee')} | ${t('transaction.fee_rate')}`, + content: ( +
+ + {` | ${new BigNumber(transactionFee).multipliedBy(1000).dividedToIntegerBy(bytes).toFormat({ + groupSeparator: ',', + groupSize: 3, + })} shannons/kB`} +
+ ), + } + const txFeeData: OverviewItemData = { + title: t('transaction.transaction_fee'), + content: , + } + const txStatusData: OverviewItemData = { + title: t('transaction.status'), + tooltip: t('glossary.transaction_status'), + content: formatConfirmation(confirmation), + } + + const liteTxSizeDataContent = bytes ? ( +
+ {`${(bytes - 4).toLocaleString('en')} Bytes`} + + {t('transaction.size_in_block', { + bytes: bytes.toLocaleString('en'), + })} + +
+ ) : ( + '' + ) + const liteTxSizeData: OverviewItemData = { + title: t('transaction.size'), + content: liteTxSizeDataContent, + } + const liteTxCyclesDataContent = cycles ? ( +
+ {`${cycles.toLocaleString('en')}`} + +
+ ) : ( + '-' + ) + const liteTxCyclesData: OverviewItemData = { + title: t('transaction.cycles'), + content: liteTxCyclesDataContent, + } + const overviewItems: Array = [] + if (txStatus === 'committed') { + overviewItems.push(blockHeightData, timestampData) + if (confirmation >= 0) { + if (isProfessional) { + overviewItems.push(bytes ? feeWithFeeRateData : txFeeData, txStatusData) + } else { + overviewItems.push(txStatusData) + } + } + } else if (txStatus === 'rejected') { + overviewItems.push( + blockHeightData, + { + ...timestampData, + content: 'Rejected', + }, + { + ...txStatusData, + content: 'Rejected', + valueTooltip: detailedMessage, + }, + ) + } else { + // pending + overviewItems.push( + { + ...blockHeightData, + content: '···', + }, + { + ...timestampData, + content: '···', + }, + { + ...txStatusData, + content: 'Pending', + }, + ) + } + if (isProfessional) { + overviewItems.push(liteTxSizeData, liteTxCyclesData) + } + const TransactionParams = [ + { + title: t('transaction.cell_deps'), + tooltip: ( + + ), + }} + /> + ), + content: + cellDeps && cellDeps.length > 0 ? ( + cellDeps.map(cellDep => { + const { + outPoint: { txHash, index }, + depType, + } = cellDep + const hashTag = matchTxHash(txHash, index) + return ( + + } + /> + + + + ) + }) + ) : ( + + ), + }, + { + title: t('transaction.header_deps'), + tooltip: t('glossary.header_deps'), + content: + headerDeps && headerDeps.length > 0 ? ( + headerDeps.map(headerDep => ( + + )) + ) : ( + + ), + }, + { + title: t('transaction.witnesses'), + tooltip: t('glossary.witnesses'), + content: + witnesses && witnesses.length > 0 ? ( + witnesses.map((witness, index) => ( + + )) + ) : ( + + ), + }, + ] + + return ( + + + {isProfessional && ( +
+ setShowParams(!showParams)}> +
{t('transaction.transaction_parameters')}
+ transaction parameters +
+ {showParams && ( +
+ {TransactionParams.map(item => ( + +
+ {item.title} + {item.tooltip && } +
+
{item.content}
+
+ ))} +
+ )} +
+ )} +
+
+ ) +} diff --git a/src/pages/Transaction/styled.tsx b/src/pages/Transaction/TransactionComp/styled.tsx similarity index 92% rename from src/pages/Transaction/styled.tsx rename to src/pages/Transaction/TransactionComp/styled.tsx index fe7d5f84c..375958425 100644 --- a/src/pages/Transaction/styled.tsx +++ b/src/pages/Transaction/TransactionComp/styled.tsx @@ -22,6 +22,19 @@ export const TransactionDiv = styled.div.attrs({ width: 100%; margin-top: 20px; } + + .transactionLite { + width: 100%; + border-radius: 6px; + box-shadow: 2px 2px 6px 0 #dfdfdf; + background-color: #fff; + margin-bottom: 10px; + padding: 16px 36px 12px; + + @media (max-width: 750px) { + padding: 16px 18px; + } + } ` export const TransactionOverviewPanel = styled.div` diff --git a/src/pages/Transaction/index.tsx b/src/pages/Transaction/index.tsx index 4c4ef94d1..ad7511194 100644 --- a/src/pages/Transaction/index.tsx +++ b/src/pages/Transaction/index.tsx @@ -3,13 +3,18 @@ import { useQuery } from 'react-query' import { useTranslation } from 'react-i18next' import TransactionHashCard from '../../components/Card/HashCard' import Content from '../../components/Content' -import { TransactionDiv as TransactionPanel } from './styled' -import TransactionComp, { TransactionOverview } from './TransactionComp' +import { TransactionDiv as TransactionPanel } from './TransactionComp/styled' import { explorerService } from '../../services/ExplorerService' import { QueryResult } from '../../components/QueryResult' import { defaultTransactionInfo } from './state' +import { useSearchParams } from '../../utils/hook' +import { LayoutLiteProfessional } from '../../constants/common' +import { TransactionCompLite } from './TransactionComp/TransactionLite/TransactionLite' +import { TransactionComp } from './TransactionComp/TransactionComp' +import { TransactionOverview } from './TransactionComp/TransactionOverview' export default () => { + const { Professional, Lite } = LayoutLiteProfessional const { t } = useTranslation() const { hash: txHash } = useParams<{ hash: string }>() @@ -21,18 +26,29 @@ export default () => { } return transaction }) + const transaction = query.data ?? defaultTransactionInfo const { blockTimestamp, txStatus } = transaction + const searchParams = useSearchParams('layout') + const layout = searchParams.layout === Lite ? Lite : Professional return ( - {txStatus !== 'committed' || blockTimestamp > 0 ? : null} + {txStatus !== 'committed' || blockTimestamp > 0 ? ( + + ) : null} - - {data => } - + {layout === Professional ? ( + + {transaction => } + + ) : ( + + {transaction => } + + )} ) diff --git a/src/pages/Transaction/state.ts b/src/pages/Transaction/state.ts index 17b87938e..b00f3d128 100644 --- a/src/pages/Transaction/state.ts +++ b/src/pages/Transaction/state.ts @@ -23,3 +23,10 @@ export const defaultTransactionInfo: State.Transaction = { maxCyclesInEpoch: null, maxCycles: null, } + +export const defaultTransactionLiteDetails: State.TransactionLiteDetails[] = [ + { + address: '', + transfers: [], + }, +] diff --git a/src/services/ExplorerService/fetcher.ts b/src/services/ExplorerService/fetcher.ts index 832a4b9f8..18310791c 100644 --- a/src/services/ExplorerService/fetcher.ts +++ b/src/services/ExplorerService/fetcher.ts @@ -51,6 +51,11 @@ export const fetchTransactionByHash = (hash: string) => .get(`transactions/${hash}`) .then((res: AxiosResponse) => toCamelcase>(res.data.data)) +export const fetchTransactionLiteDetailsByHash = (hash: string) => + requesterV2 + .get(`ckb_transactions/${hash}/details`) + .then((res: AxiosResponse) => toCamelcase>(res.data)) + export const fetchTransactions = (page: number, size: number, sort?: string) => requesterV1 .get('transactions', { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ccdbef7c0..652548922 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -100,6 +100,9 @@ declare namespace State { | 'nervos_dao_withdrawing' | 'cota_registry' | 'cota_regular' + | 'nft_transfer' + | 'simple_transfer' + | 'nft_mint' | 'spore_cluster' | 'spore_cell' extraInfo?: never @@ -132,6 +135,47 @@ declare namespace State { type Cell = Cell$NoExtra | Cell$UDT | Cell$NftIssuer | Cell$NftClass | Cell$NftToken | Cell$Nrc721Token + export interface TransactionLiteDetails { + address: string + transfers: LiteTransfer[] + } + + // cell_type comes from: https://github.com/nervosnetwork/ckb-explorer/blob/develop/app/utils/ckb_utils.rb#L380 + type CellType = + | 'normal' + | 'udt' + | 'nervos_dao_deposit' + | 'nervos_dao_withdrawing' + | 'spore_cell' + | 'spore_cluster' + | 'cota_regular' + | 'cota_registry' + | 'm_nft_issuer' + | 'm_nft_class' + | 'm_nft_token' + | 'nrc_721_token' + | 'nrc_721_factory' + interface LiteTransfer { + capacity: string + cellType: CellType + + udtInfo?: { + symbol: string + amount: string + decimal: string + typeHash: string + published: boolean + displayName: string + uan: string + } + + mNftInfo?: { + className: string + tokenId: string // none 0x prefix hex number + total: string // decimal string + } + } + export interface LockInfo { status: 'locked' | 'unlocked' epochNumber: string diff --git a/src/utils/number.ts b/src/utils/number.ts index f35d27b61..e96fc7e46 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -67,9 +67,13 @@ export const handleHashRate = (value: BigNumber | string | number) => { return `${handleDifficulty(value)}/s` } -export const parseUDTAmount = (amount: string, decimal: string) => { +export const parseCKBAmount = (capacity: string) => { + return parseUDTAmount(capacity, 8) +} + +export const parseUDTAmount = (amount: string, decimal: string | number) => { try { - const decimalInt = parseInt(decimal, 10) + const decimalInt = typeof decimal === 'string' ? parseInt(decimal, 10) : decimal const amountBigInt = new BigNumber(amount) const result = amountBigInt.dividedBy(new BigNumber(10).pow(decimalInt)) if (decimalInt > 20) { diff --git a/src/utils/util.ts b/src/utils/util.ts index c8c2f6d1c..1e7ceba87 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -332,6 +332,12 @@ export function randomInt(min: number, max: number) { return min + Math.floor(Math.random() * (max - min + 1)) } +export const isDaoDepositCell = (cellType: State.CellTypes) => cellType === 'nervos_dao_deposit' + +export const isDaoWithdrawCell = (cellType: State.CellTypes) => cellType === 'nervos_dao_withdrawing' + +export const isDaoCell = (cellType: State.CellTypes) => isDaoDepositCell(cellType) || isDaoWithdrawCell(cellType) + export default { copyElementValue, shannonToCkb,