diff --git a/src/components/Transaction/TransactionCellArrow/index.tsx b/src/components/Transaction/TransactionCellArrow/index.tsx index 155ea78de..3a9b0d5cd 100644 --- a/src/components/Transaction/TransactionCellArrow/index.tsx +++ b/src/components/Transaction/TransactionCellArrow/index.tsx @@ -33,16 +33,21 @@ export const CellInputIcon = ({ cell }: { cell: Partial ) : null -export const CellOutputIcon = ({ cell }: { cell: Pick }) => { +export const CellOutputIcon = ({ cell }: { cell: Partial> }) => { const { t } = useTranslation() - if (cell.status === 'dead') { + if (cell.status === 'dead' && cell.consumedTxHash) { return ( ) } + + if (cell.status === 'dead') { + return + } + return ( diff --git a/src/components/TransactionItem/NodeTransactionItem.tsx b/src/components/TransactionItem/NodeTransactionItem.tsx index 444d50eaa..9b8fd0adb 100644 --- a/src/components/TransactionItem/NodeTransactionItem.tsx +++ b/src/components/TransactionItem/NodeTransactionItem.tsx @@ -3,17 +3,30 @@ import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { Transaction } from '@ckb-lumos/base' import styles from './styles.module.scss' +import { useParsedDate } from '../../hooks' import { ReactComponent as DirectionIcon } from '../../assets/direction.svg' import NodeTransactionItemCell from './TransactionItemCell/NodeTransactionItemCell' import { FullPanel, TransactionHashBlockPanel, TransactionCellPanel, TransactionPanel } from './styled' import TransactionCellListPanel from './TransactionItemCellList/styled' +import { localeNumberString, isBlockNumber } from '../../utils/number' import { getTransactionOutputCells, checkIsCellBase } from '../../utils/transaction' import Loading from '../Loading' import AddressText from '../AddressText' import { useCKBNode } from '../../hooks/useCKBNode' import Cellbase from '../Transaction/Cellbase' +import { IOType } from '../../constants/common' -const NodeTransactionItem = ({ transaction, blockNumber }: { transaction: Transaction; blockNumber?: number }) => { +const NodeTransactionItem = ({ + transaction, + blockHashOrNumber, + highlightAddress, + showBlock = true, +}: { + transaction: Transaction + blockHashOrNumber?: string + highlightAddress?: string + showBlock?: boolean +}) => { const { t } = useTranslation() const ref = useRef(null) const { nodeService } = useCKBNode() @@ -23,6 +36,22 @@ const NodeTransactionItem = ({ transaction, blockNumber }: { transaction: Transa () => nodeService.getInputCells(transaction.inputs), ) + const { data: blockHeader } = useQuery( + ['node', 'header', blockHashOrNumber], + () => { + if (!blockHashOrNumber) return null + + return isBlockNumber(blockHashOrNumber) + ? nodeService.rpc.getHeaderByNumber(`0x${parseInt(blockHashOrNumber, 10).toString(16)}`) + : nodeService.rpc.getHeader(blockHashOrNumber) + }, + { + enabled: !!blockHashOrNumber, + }, + ) + + const localTime = useParsedDate(blockHeader?.timestamp ?? 0) + const outputCells = getTransactionOutputCells(transaction) return ( @@ -41,18 +70,29 @@ const NodeTransactionItem = ({ transaction, blockNumber }: { transaction: Transa {transaction.hash!} + {blockHashOrNumber && blockHeader && showBlock && ( +
+ +
+ )}
{checkIsCellBase(transaction) ? ( - + ) : null} - {inputCells.map((cell, index) => ( - // eslint-disable-next-line react/no-array-index-key - + {inputCells.map(cell => ( + ))}
@@ -63,7 +103,7 @@ const NodeTransactionItem = ({ transaction, blockNumber }: { transaction: Transa {outputCells.map((cell, index) => ( // eslint-disable-next-line react/no-array-index-key - + ))} diff --git a/src/components/TransactionItem/TransactionItemCell/NodeCellCapacityAmount.tsx b/src/components/TransactionItem/TransactionItemCell/NodeCellCapacityAmount.tsx new file mode 100644 index 000000000..25ab872ff --- /dev/null +++ b/src/components/TransactionItem/TransactionItemCell/NodeCellCapacityAmount.tsx @@ -0,0 +1,45 @@ +import type { Cell } from '@ckb-lumos/base' +import { useTranslation } from 'react-i18next' +import { useQuery } from '@tanstack/react-query' +import { parseUDTAmount } from '../../../utils/number' +import { shannonToCkb } from '../../../utils/util' +import { explorerService } from '../../../services/ExplorerService' +import { UDT_CELL_TYPES, getCellType, calculateScriptHash, getUDTAmountByData } from '../../../utils/cell' +import Capacity from '../../Capacity' + +export const NodeCellCapacityAmount = ({ cell }: { cell: Cell }) => { + const { t } = useTranslation() + const cellType = getCellType(cell) + const isUDTCell = UDT_CELL_TYPES.findIndex(type => type === cellType) !== -1 + const udtTypeHash = isUDTCell ? calculateScriptHash(cell.cellOutput.type!) : undefined + const udtInfo = useQuery( + ['udt', udtTypeHash], + () => { + if (!udtTypeHash) return undefined + return explorerService.api.fetchSimpleUDT(udtTypeHash) + }, + { + enabled: isUDTCell, + staleTime: Infinity, + }, + ) + + if (isUDTCell && udtTypeHash && udtInfo.data) { + const amount = getUDTAmountByData(cell.data) + if (cellType === 'udt' && udtInfo.data.published) { + return {`${parseUDTAmount(amount, udtInfo.data.decimal)} ${udtInfo.data.symbol}`} + } + + if (cellType === 'xudt' && udtInfo.data.decimal && udtInfo.data.symbol) { + return {`${parseUDTAmount(amount, udtInfo.data.decimal)} ${udtInfo.data.symbol}`} + } + + return {`${t('udt.unknown_token')} #${udtTypeHash.substring(udtTypeHash.length - 4)}`} + } + + if (isUDTCell && udtTypeHash) { + return {`${t('udt.unknown_token')} #${udtTypeHash.substring(udtTypeHash.length - 4)}`} + } + + return +} diff --git a/src/components/TransactionItem/TransactionItemCell/NodeTransactionItemCell.tsx b/src/components/TransactionItem/TransactionItemCell/NodeTransactionItemCell.tsx index 2ffffa247..82e9b44cd 100644 --- a/src/components/TransactionItem/TransactionItemCell/NodeTransactionItemCell.tsx +++ b/src/components/TransactionItem/TransactionItemCell/NodeTransactionItemCell.tsx @@ -2,15 +2,20 @@ import { FC } from 'react' import { Cell } from '@ckb-lumos/base' import { Tooltip } from 'antd' import classNames from 'classnames' +import { useQuery } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' import { Link } from '../../Link' -import { shannonToCkb } from '../../../utils/util' import { TransactionCellPanel, TransactionCellCapacityPanel } from './styled' -import Capacity from '../../Capacity' -import { encodeNewAddress } from '../../../utils/address' +import { NodeCellCapacityAmount } from './NodeCellCapacityAmount' +import CurrentAddressIcon from '../../../assets/current_address.svg' +import { encodeNewAddress, compareAddress } from '../../../utils/address' import styles from './index.module.scss' import { useBoolean } from '../../../hooks' import CopyTooltipText from '../../Text/CopyTooltipText' import EllipsisMiddle from '../../EllipsisMiddle' +import { IOType } from '../../../constants/common' +import { CellInputIcon, CellOutputIcon } from '../../Transaction/TransactionCellArrow' +import { useCKBNode } from '../../../hooks/useCKBNode' const Address: FC<{ address: string @@ -39,17 +44,49 @@ const Address: FC<{ ) } -const NodeTransactionItemCell = ({ cell }: { cell: Cell }) => { +const NodeTransactionItemCell = ({ + cell, + ioType, + highlightAddress, +}: { + cell: Cell + ioType?: IOType + highlightAddress?: string +}) => { const address = encodeNewAddress(cell.cellOutput.lock) + const { t } = useTranslation() + const { nodeService } = useCKBNode() + + const cellStatus = useQuery( + ['cellStatus', cell.outPoint], + async () => { + if (!cell.outPoint) return 'dead' + const liveCell = await nodeService.rpc.getLiveCell(cell.outPoint, false) + if (liveCell.status === 'live') return 'live' + return 'dead' + }, + { enabled: cell.outPoint && ioType && ioType === IOType.Output }, + ) + + const highLight = !highlightAddress || !compareAddress(highlightAddress, address) return ( - + + {ioType && ioType === IOType.Input && ( + + )}
+ {ioType === IOType.Output && } + {!highLight && ( + + current Address + + )}
- +
diff --git a/src/hooks/transaction.ts b/src/hooks/transaction.ts new file mode 100644 index 000000000..13a7854ba --- /dev/null +++ b/src/hooks/transaction.ts @@ -0,0 +1,55 @@ +import { RPC } from '@ckb-lumos/rpc' +import { Hash, Transaction } from '@ckb-lumos/base' +import { useInfiniteQuery } from '@tanstack/react-query' +import { useCKBNode } from './useCKBNode' + +export const useTransactions = ({ + searchKey, + pageSize = 100, + order = 'desc', +}: { + searchKey: Parameters['0'] + pageSize?: number + order?: 'desc' | 'asc' +}) => { + const { nodeService } = useCKBNode() + + return useInfiniteQuery({ + queryKey: ['node', 'transactions', searchKey, pageSize, order], + queryFn: async ({ pageParam = undefined }) => { + const { lastCursor, objects } = await nodeService.rpc.getTransactions( + { ...searchKey, groupByTransaction: true }, + order, + `0x${pageSize.toString(16)}`, + pageParam, + ) + + if (objects.length === 0) { + return { + lastCursor: undefined, + txs: [], + } + } + + const txHashes = objects.map(tx => tx.txHash) + + const txs = await nodeService.rpc + .createBatchRequest<'getTransaction', [Hash], CKBComponents.TransactionWithStatus[]>( + txHashes.map(txHash => ['getTransaction', txHash]), + ) + .exec() + + return { + lastCursor, + txs: txs.map(tx => ({ + transaction: tx.transaction as Transaction, + txStatus: tx.txStatus, + })), + } + }, + getNextPageParam: options => { + if (options.txs.length < pageSize) return undefined + return options.lastCursor + }, + }) +} diff --git a/src/locales/en.json b/src/locales/en.json index 06cc9fe87..4ba116df3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -709,7 +709,9 @@ "current_page": "Page", "of_page": "of", "only_first_pages_visible": "Showing first {{pages}} pages", - "show_rows": "Show Rows" + "show_rows": "Show Rows", + "load_more": "Load More", + "no_more_data": "No more data" }, "udt": { "sudt": "sUDT", diff --git a/src/locales/zh.json b/src/locales/zh.json index 9e65faadb..974a027e9 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -710,7 +710,9 @@ "current_page": "第", "of_page": "页,共", "only_first_pages_visible": "显示最近 {{pages}} 页", - "show_rows": "每页显示" + "show_rows": "每页显示", + "load_more": "加载更多", + "no_more_data": "没有更多数据了" }, "udt": { "sudt": "sUDT", diff --git a/src/pages/Address/AddressComp.tsx b/src/pages/Address/AddressComp.tsx index c30b1033f..0c48e3193 100644 --- a/src/pages/Address/AddressComp.tsx +++ b/src/pages/Address/AddressComp.tsx @@ -1,11 +1,13 @@ import { useState, FC, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' +import { addressToScript } from '@nervosnetwork/ckb-sdk-utils' import { Radio } from 'antd' import { useTranslation } from 'react-i18next' import { EyeOpenIcon, EyeClosedIcon } from '@radix-ui/react-icons' import { utils } from '@ckb-lumos/base' import { Link } from '../../components/Link' import TransactionItem from '../../components/TransactionItem/index' +import NodeTransactionItem from '../../components/TransactionItem/NodeTransactionItem' import { explorerService, RawBtcRPC } from '../../services/ExplorerService' import { localeNumberString } from '../../utils/number' import { shannonToCkb, deprecatedAddrToNewAddr } from '../../utils/util' @@ -46,6 +48,8 @@ import { CardHeader } from '../../components/Card/CardHeader' import Cells from './Cells' import DefinedTokens from './DefinedTokens' import { AddressOmigaInscriptionComp } from './AddressAssetComp' +import { useCKBNode } from '../../hooks/useCKBNode' +import { useTransactions } from '../../hooks/transaction' enum AssetInfo { UDT = 1, @@ -445,3 +449,127 @@ export const AddressTransactions = ({ } // FIXME: plural in i18n not work, address.cell and address.cells + +export const NodeAddressOverviewCard: FC<{ address: string }> = ({ address }) => { + const { t } = useTranslation() + const [isScriptDisplayed, setIsScriptDisplayed] = useState(false) + const { nodeService } = useCKBNode() + + const lockScript = addressToScript(address) + const lockScriptHash = utils.computeScriptHash(lockScript) + + const capacityQuery = useQuery( + ['node', 'address', 'capacity', address], + () => nodeService.rpc.getCellsCapacity({ script: lockScript, scriptType: 'lock' }), + { staleTime: 1000 * 60 }, + ) + + const occupiedCapacityQuery = useQuery( + ['node', 'address', 'occupied', 'capacity', address], + () => + nodeService.rpc.getCellsCapacity({ + script: lockScript, + scriptType: 'lock', + filter: { scriptLenRange: ['0x1', '0x1000'] }, + }), + { staleTime: 1000 * 60 }, + ) + + const overviewItems: CardCellInfo<'left' | 'right'>[] = [ + { + slot: 'left', + cell: { + icon: item icon, + title: t('common.ckb_unit'), + content: capacityQuery.data ? : 'loading...', + }, + }, + { + title: t('address.occupied'), + tooltip: t('glossary.occupied'), + content: occupiedCapacityQuery.data ? ( + + ) : ( + 'loading...' + ), + }, + ] + + return ( + +
{t('address.overview')}
+ +
+ +
+ + setIsScriptDisplayed(!isScriptDisplayed)}> + {isScriptDisplayed ? ( +
+ +
{t('address.lock_script')}
+
+ ) : ( +
+ +
{t('address.lock_script_hash')}
+
+ )} +
+ {isScriptDisplayed ? ( +