diff --git a/src/locales/en.json b/src/locales/en.json index 278e40285d..ad6cc115b8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -368,6 +368,7 @@ "hash_type": "Hash Type", "overview": "Overview", "user_defined_token": "Simple User Defined Token", + "live_cell": "Live Cell(s)", "inscription": "Inscription", "confirmation": "Confirmation", "confirmations": "Confirmations", diff --git a/src/locales/zh.json b/src/locales/zh.json index b2c12326f0..8c625dc1ac 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -369,6 +369,7 @@ "overview": "概览", "user_defined_token": "Simple User Defined Token", "inscription": "铭文", + "live_cell": "Live Cell(s)", "confirmation": "确认区块", "confirmations": "确认区块", "unable_decode_address": "地址解析失败", diff --git a/src/models/Address/UDTAccount.ts b/src/models/Address/UDTAccount.ts index 28e0431894..f7e57dafc4 100644 --- a/src/models/Address/UDTAccount.ts +++ b/src/models/Address/UDTAccount.ts @@ -1,3 +1,5 @@ +import { OutPoint, Script } from '../Script' + export interface SUDT { symbol: string decimal: string @@ -59,4 +61,15 @@ export interface OmigaInscription { udtType: 'omiga_inscription' } +export interface LiveCell { + outpoint: OutPoint + amount: string + capacity: string + time: string + block: string + type: Script + cellType: string + uan?: string +} + export type UDTAccount = SUDT | MNFT | NRC721 | CoTA | Spore | OmigaInscription diff --git a/src/models/Script/index.ts b/src/models/Script/index.ts index 01698a5c8b..f76b01407f 100644 --- a/src/models/Script/index.ts +++ b/src/models/Script/index.ts @@ -3,3 +3,8 @@ export interface Script { args: string hashType: string } + +export interface OutPoint { + txHash: string + index: string +} diff --git a/src/pages/Address/AddressAssetComp.tsx b/src/pages/Address/AddressAssetComp.tsx index bd2f347e6e..83db9d9731 100644 --- a/src/pages/Address/AddressAssetComp.tsx +++ b/src/pages/Address/AddressAssetComp.tsx @@ -3,13 +3,14 @@ import { ReactEventHandler, useEffect, useState } from 'react' import { Base64 } from 'js-base64' import { hexToBytes } from '@nervosnetwork/ckb-sdk-utils' import axios, { AxiosResponse } from 'axios' -import { AddressUDTItemPanel } from './styled' -import { CoTA, OmigaInscription, MNFT, NRC721, SUDT, Spore } from '../../models/Address' +import { AddressUDTItemPanel, LiveCellTable } from './styled' +import { CoTA, OmigaInscription, MNFT, NRC721, SUDT, Spore, LiveCell } from '../../models/Address' import SUDTTokenIcon from '../../assets/sudt_token.png' -import { parseUDTAmount } from '../../utils/number' +import { parseCKBAmount, parseUDTAmount } from '../../utils/number' import { parseSporeCellData } from '../../utils/spore' import { handleNftImgError, patchMibaoImg } from '../../utils/util' import { sliceNftName } from '../../utils/string' +import CKBTokenIcon from './ckb_token_icon.png' export const AddressAssetComp = ({ href, @@ -208,3 +209,70 @@ export const AddressOmigaInscriptionComp = ({ account }: { account: OmigaInscrip /> ) } + +export const AddressLiveCellComp = ({ account }: { account: LiveCell }) => { + const { amount, capacity, time, uan, outpoint } = account + return ( + + ) +} + +export const AddressLiveCellTableComp = ({ liveCells }: { liveCells: LiveCell[] }) => { + const liveCellColumns = [ + { align: 'center' as const, title: 'Date', dataIndex: 'date', key: 'date' }, + { align: 'center' as const, title: 'Block #', dataIndex: 'block', key: 'block' }, + { align: 'center' as const, title: 'OutPoint', dataIndex: 'outpoint', key: 'outpoint' }, + { align: 'center' as const, title: 'UID', dataIndex: 'uid', key: 'uid' }, + { align: 'center' as const, title: 'Capacity(CKB)', dataIndex: 'capacity', key: 'capacity' }, + { align: 'center' as const, title: 'Type(i)', dataIndex: 'type', key: 'type' }, + ] + return ( + { + const cellType = () => { + switch (liveCell.cellType) { + case 'sudt': + return 'UDT' + case 'spore_cell': + case 'm_nft_token': + case 'cota': + case 'nrc_721_token': + return 'NFT' + case 'omiga_inscription': + case 'omiga_inscription_info': + return 'INSCRIPTION' + case 'normal': + return 'CKB' + default: + return 'UNKNOWN' + } + } + return { + date: new Date(parseInt(liveCell.time, 10)).toISOString().slice(0, 19).replace('T', ' ').replace('Z', ' '), + block: liveCell.block, + outpoint: `${liveCell.outpoint.txHash}:${liveCell.outpoint.index}`, + uid: liveCell.outpoint.txHash, + capacity: parseCKBAmount(liveCell.capacity), + type: cellType(), + } + })} + /> + ) +} diff --git a/src/pages/Address/AddressComp.tsx b/src/pages/Address/AddressComp.tsx index d857d638eb..41f1207763 100644 --- a/src/pages/Address/AddressComp.tsx +++ b/src/pages/Address/AddressComp.tsx @@ -8,18 +8,25 @@ import { explorerService } from '../../services/ExplorerService' import { localeNumberString } from '../../utils/number' import { shannonToCkb, deprecatedAddrToNewAddr } from '../../utils/util' import { + AddressAssetsDescription, AddressAssetsTab, AddressAssetsTabPane, AddressAssetsTabPaneTitle, AddressLockScriptController, AddressLockScriptPanel, AddressTransactionsPanel, + AddressUDTAssetsContent, + AddressUDTAssetsList, AddressUDTAssetsPanel, + Sort, } from './styled' import Capacity from '../../components/Capacity' import CKBTokenIcon from './ckb_token_icon.png' import { ReactComponent as TimeDownIcon } from './time_down.svg' import { ReactComponent as TimeUpIcon } from './time_up.svg' +import { ReactComponent as AmountSortIcon } from './amount_sort.svg' +import { ReactComponent as CellTableIcon } from './cell_table.svg' +import { ReactComponent as CellGridIcon } from './cell_grid.svg' import { OrderByType, useIsMobile, @@ -43,7 +50,7 @@ import { omit } from '../../utils/object' import { CsvExport } from '../../components/CsvExport' import PaginationWithRear from '../../components/PaginationWithRear' import { Transaction } from '../../models/Transaction' -import { Address, UDTAccount } from '../../models/Address' +import { Address, LiveCell, UDTAccount } from '../../models/Address' import { Card, CardCellInfo, CardCellsLayout } from '../../components/Card' import { CardHeader } from '../../components/Card/CardHeader' import { @@ -52,11 +59,14 @@ import { AddressMNFTComp, AddressSporeComp, AddressSudtComp, + AddressLiveCellComp, + AddressLiveCellTableComp, } from './AddressAssetComp' enum AssetInfo { UDT = 1, INSCRIPTION, + LIVE_CELL, } const lockScriptIcon = (show: boolean) => { @@ -120,10 +130,24 @@ const AddressLockScript: FC<{ address: Address }> = ({ address }) => { ) } -export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => { +export const AddressOverviewCard: FC<{ + address: Address + liveCellFetcher: ( + page: number, + size: number, + sort: string, + ) => Promise<{ pageSize: number; total: number; liveCells: LiveCell[] }> +}> = ({ address, liveCellFetcher }) => { const { t, i18n } = useTranslation() const { udtAccounts = [] } = address const [activeTab, setActiveTab] = useState(AssetInfo.UDT) + const [liveCells, setLiveCells] = useState([]) + const [liveCellPage, setLiveCellPage] = useState(1) + const [totalLiveCell, setTotalLiveCell] = useState(0) + const [timeOrderBy, setTimeOrderBy] = useState('desc') + const [liveCellCapacityOrderBy, setLiveCellCapacityOrderBy] = useState('desc') + const [liveCellDisplay, setLiveCellDisplay] = useState<'list' | 'table'>('list') + const [liveCellOrderBy, setLiveCellOrderBy] = useState(`block_timestamp.${timeOrderBy}`) const [udts, inscriptions] = udtAccounts.reduce( (acc, cur) => { @@ -188,21 +212,107 @@ export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => { }, ] + const appendLiveCells = () => { + liveCellFetcher(liveCellPage, 10, liveCellOrderBy).then(res => { + if (res.liveCells.length > 0) { + setLiveCells(liveCells.concat(res.liveCells)) + setLiveCellPage(liveCellPage + 1) + setTotalLiveCell(res.total) + } + }) + } + useEffect(() => { if (!udts.length && !cotaList?.length && inscriptions.length) { - setActiveTab(AssetInfo.INSCRIPTION) + setActiveTab(AssetInfo.LIVE_CELL) } }, [udts.length, cotaList?.length, inscriptions.length]) + useEffect(() => { + liveCellFetcher(1, 10, liveCellOrderBy).then(res => { + setLiveCells(res.liveCells) + setLiveCellPage(2) + setTotalLiveCell(res.total) + }) + }, [liveCellOrderBy]) + + const onScroll = (e: React.UIEvent) => { + if (e.currentTarget.scrollHeight - e.currentTarget.scrollTop === e.currentTarget.clientHeight) { + appendLiveCells() + } + } + + const handleTimeOrderChange = () => { + setTimeOrderBy(timeOrderBy === 'asc' ? 'desc' : 'asc') + setLiveCellOrderBy(`block_timestamp.${timeOrderBy}`) + } + + const handleLiveCellCapacityOrderChange = () => { + setLiveCellCapacityOrderBy(liveCellCapacityOrderBy === 'asc' ? 'desc' : 'asc') + setLiveCellOrderBy(`capacity.${liveCellCapacityOrderBy}`) + } + + const handleLiveCellDisplayChange = () => { + setLiveCellDisplay(liveCellDisplay === 'list' ? 'table' : 'list') + } + return (
{t('address.overview')}
- {udts.length > 0 || (cotaList?.length && cotaList.length > 0) || inscriptions.length > 0 ? ( + {udts.length > 0 || + (cotaList?.length && cotaList.length > 0) || + inscriptions.length > 0 || + liveCells.length > 0 ? ( + setActiveTab(AssetInfo.LIVE_CELL)}> + {t('address.live_cell')} + + } + key={AssetInfo.LIVE_CELL} + > + + +
UTXO: {totalLiveCell}
+
+ + {timeOrderBy === 'asc' ? ( + + ) : ( + + )} + + + + + + {liveCellDisplay === 'list' ? ( + + ) : ( + + )} + +
+
+ + {liveCellDisplay === 'list' ? ( + liveCells.map(liveCell => ( + + )) + ) : ( + + )} + +
+
{(udts.length > 0 || cotaList?.length) && ( = ({ address }) => { } key={AssetInfo.UDT} > -
- {udts.map(udt => { - switch (udt.udtType) { - case 'sudt': - return - - case 'spore_cell': - return - - case 'm_nft_token': - return - default: - return null - } - })} - {cotaList?.map(cota => ( - - )) ?? null} -
+ + + {udts.map(udt => { + switch (udt.udtType) { + case 'sudt': + return + + case 'spore_cell': + return + + case 'm_nft_token': + return + default: + return null + } + })} + {cotaList?.map(cota => ( + + )) ?? null} + +
)} {inscriptions.length > 0 && ( @@ -253,22 +365,24 @@ export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => { } key={AssetInfo.INSCRIPTION} > -
- {inscriptions.map(inscription => { - switch (inscription.udtType) { - case 'omiga_inscription': - return ( - - ) - - default: - return null - } - })} -
+ + + {inscriptions.map(inscription => { + switch (inscription.udtType) { + case 'omiga_inscription': + return ( + + ) + + default: + return null + } + })} + + )}
@@ -339,7 +453,7 @@ export const AddressTransactions = ({ const searchOptionsAndModeSwitch = (
-
+
{timeOrderBy === 'asc' ? : }
+ + + diff --git a/src/pages/Address/cell_grid.svg b/src/pages/Address/cell_grid.svg new file mode 100644 index 0000000000..a1436e6662 --- /dev/null +++ b/src/pages/Address/cell_grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pages/Address/cell_table.svg b/src/pages/Address/cell_table.svg new file mode 100644 index 0000000000..0589253f0d --- /dev/null +++ b/src/pages/Address/cell_table.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/pages/Address/index.tsx b/src/pages/Address/index.tsx index 5b5199ec65..3db938edd2 100644 --- a/src/pages/Address/index.tsx +++ b/src/pages/Address/index.tsx @@ -102,6 +102,27 @@ export const Address = () => { const deprecatedAddr = useDeprecatedAddr(address) const counterpartAddr = newAddr === address ? deprecatedAddr : newAddr + const liveCellFetcher = async (page: number, size: number, order: string) => { + const { data, pageSize, total } = await explorerService.api.fetchAddressLiveCells(address, page, size, order) + return { + liveCells: data.map(cell => ({ + outpoint: { + txHash: cell.txHash, + index: cell.cellIndex.toString(16), + }, + capacity: cell.capacity, + amount: cell.extraInfo?.amount ?? '0', + block: cell.blockNumber, + time: cell.blockTimestamp, + cellType: cell.cellType, + type: cell.typeScript, + uan: cell.extraInfo?.uan ?? cell.extraInfo?.displayName ?? cell.extraInfo?.symbol, + })), + pageSize, + total, + } + } + return ( @@ -133,7 +154,7 @@ export const Address = () => { - {data => (data ? :
)} + {data => (data ? :
)} diff --git a/src/pages/Address/styled.tsx b/src/pages/Address/styled.tsx index c9e1b2441d..c2932542b9 100644 --- a/src/pages/Address/styled.tsx +++ b/src/pages/Address/styled.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { Tabs } from 'antd' +import { Table, Tabs } from 'antd' import TabPane from 'antd/lib/tabs/TabPane' import SimpleButton from '../../components/SimpleButton' import { TransactionPanel } from '../../components/TransactionItem/styled' @@ -144,6 +144,12 @@ export const AddressAssetsTab = styled(Tabs)` export const AddressAssetsTabPane = styled(TabPane)`` +export const AddressAssetsDescription = styled.div` + width: 100%; + display: flex; + justify-content: space-between; +` + export const AddressAssetsTabPaneTitle = styled.span` font-size: 16px; font-weight: 500; @@ -152,6 +158,48 @@ export const AddressAssetsTabPaneTitle = styled.span` text-align: left; ` +export const LiveCellTable = styled(Table)` + background: white; + width: 100%; + border-radius: 6px; +` + +export const Sort = styled.div` + display: flex; + align-items: center; + + svg { + cursor: pointer; + } +` + +export const AddressUDTAssetsList = styled.div` + overflow-y: scroll; + width: 100%; + display: flex; + gap: 16px; + background-color: #f1f1f1; + flex-flow: row wrap; + + @media (min-width: ${variables.extraLargeBreakPoint}) { + max-height: 153px; + } + + @media (max-width: ${variables.extraLargeBreakPoint}) { + max-height: 252px; + } +` + +export const AddressUDTAssetsContent = styled.div` + background-color: #f1f1f1; + padding: 16px 25px; + width: 100%; + display: flex; + gap: 16px; + flex-flow: row wrap; + max-height: 400px; +` + export const AddressUDTAssetsPanel = styled.div` display: flex; flex-direction: column; @@ -159,28 +207,11 @@ export const AddressUDTAssetsPanel = styled.div` > span { font-size: 14px; + font-family: Roboto, inherit, sans-serif; font-weight: 600; color: #000; } - .addressUdtAssetsGrid { - margin-top: 10px; - background-color: #f1f1f1; - padding: 6px 25px; - width: 100%; - display: flex; - flex-flow: row wrap; - overflow-y: scroll; - - @media (min-width: ${variables.extraLargeBreakPoint}) { - max-height: 220px; - } - - @media (max-width: ${variables.extraLargeBreakPoint}) { - max-height: 310px; - } - } - @media (max-width: ${variables.mobileBreakPoint}) { padding-top: 16px; border-top: 1px solid #f5f5f5; @@ -190,7 +221,6 @@ export const AddressUDTAssetsPanel = styled.div` export const AddressUDTItemPanel = styled.a` display: flex; flex-direction: column; - margin: 6px 15px; background: #fff; width: 260px; border-radius: 4px; diff --git a/src/services/ExplorerService/fetcher.ts b/src/services/ExplorerService/fetcher.ts index 75081b6318..1628fd6722 100644 --- a/src/services/ExplorerService/fetcher.ts +++ b/src/services/ExplorerService/fetcher.ts @@ -70,6 +70,37 @@ export const apiFetcher = { }), ), + fetchAddressLiveCells: (address: string, page: number, size: number, sort?: string) => { + return v1GetUnwrappedPagedList<{ + cellType: string + txHash: string + cellIndex: number + typeHash: string + data: string + capacity: string + occupiedCapacity: string + blockTimestamp: string + blockNumber: string + typeScript: Script + lockScript: Script + extraInfo: { + symbol: string + amount: string + decimal: string + typeHash: string + published: boolean + displayName: string + uan: string + } + }>(`address_live_cells/${address}`, { + params: { + page, + page_size: size, + sort, + }, + }) + }, + fetchTransactionsByAddress: (address: string, page: number, size: number, sort?: string, txTypeFilter?: string) => v1GetUnwrappedPagedList(`address_transactions/${address}`, { params: {