Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge testnet into mainnet #1477

Merged
merged 15 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const Search: FC<{
content?: string
hasButton?: boolean
onEditEnd?: () => void
}> = memo(({ content, hasButton, onEditEnd: handleEditEnd }) => {
onClear?: () => void
}> = memo(({ content, hasButton, onEditEnd: handleEditEnd, onClear: handleClear }) => {
const history = useHistory()
const queryClient = useQueryClient()
const { t } = useTranslation()
Expand Down Expand Up @@ -118,8 +119,8 @@ const Search: FC<{
const onClear = useCallback(() => {
setKeyword('')
resetSearchByName()
setEditEnded(true)
}, [resetSearchByName, setEditEnded])
handleClear?.()
}, [resetSearchByName, handleClear])

return (
<SearchPanel moreHeight={hasButton} hasButton={hasButton}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Toast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface ToastMessage {
const getColor = (type: ToastMessage['type']) => {
switch (type) {
case 'success':
return '#3cc68a'
return 'var(--primary-color)'
case 'warning':
return '#ffae42'
case 'danger':
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@
"hash_type": "Hash Type",
"overview": "Overview",
"user_defined_token": "Simple User Defined Token",
"cell": "Cell",
"cells": "Cells",
"inscription": "Inscription",
"confirmation": "Confirmation",
"confirmations": "Confirmations",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@
"hash_type": "Hash Type",
"overview": "概览",
"user_defined_token": "Simple User Defined Token",
"cell": "Cell",
"cells": "Cells",
"inscription": "铭文",
"confirmation": "确认区块",
"confirmations": "确认区块",
Expand Down
40 changes: 35 additions & 5 deletions src/pages/Address/AddressComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { Transaction } from '../../models/Transaction'
import { Address, UDTAccount } from '../../models/Address'
import { Card, CardCellInfo, CardCellsLayout } from '../../components/Card'
import { CardHeader } from '../../components/Card/CardHeader'
import Cells from './Cells'
import {
AddressCoTAComp,
AddressOmigaInscriptionComp,
Expand All @@ -57,6 +58,7 @@ import {
enum AssetInfo {
UDT = 1,
INSCRIPTION,
CELLs,
}

const lockScriptIcon = (show: boolean) => {
Expand Down Expand Up @@ -188,19 +190,30 @@ export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => {
},
]

const hasAssets = udts.length > 0 || (cotaList?.length && cotaList.length > 0)
const hasInscriptions = inscriptions.length > 0
const hasCells = +address.liveCellsCount > 0

useEffect(() => {
if (!udts.length && !cotaList?.length && inscriptions.length) {
if (hasAssets) {
return
}
if (hasInscriptions) {
setActiveTab(AssetInfo.INSCRIPTION)
return
}
}, [udts.length, cotaList?.length, inscriptions.length])
if (hasCells) {
setActiveTab(AssetInfo.CELLs)
}
}, [hasAssets, hasInscriptions, hasCells, setActiveTab])

return (
<Card className={styles.addressOverviewCard}>
<div className={styles.cardTitle}>{t('address.overview')}</div>

<CardCellsLayout type="leftSingle-right" cells={overviewItems} borderTop />

{udts.length > 0 || (cotaList?.length && cotaList.length > 0) || inscriptions.length > 0 ? (
{hasAssets || hasInscriptions || hasCells ? (
<AddressUDTAssetsPanel className={styles.addressUDTAssetsPanel}>
<AddressAssetsTab animated={false} key={i18n.language} activeKey={activeTab.toString()}>
{(udts.length > 0 || cotaList?.length) && (
Expand Down Expand Up @@ -244,7 +257,7 @@ export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => {
</div>
</AddressAssetsTabPane>
)}
{inscriptions.length > 0 && (
{hasInscriptions ? (
<AddressAssetsTabPane
tab={
<AddressAssetsTabPaneTitle onClick={() => setActiveTab(AssetInfo.INSCRIPTION)}>
Expand All @@ -270,7 +283,22 @@ export const AddressOverviewCard: FC<{ address: Address }> = ({ address }) => {
})}
</div>
</AddressAssetsTabPane>
)}
) : null}

{hasCells ? (
<AddressAssetsTabPane
tab={
<AddressAssetsTabPaneTitle onClick={() => setActiveTab(AssetInfo.CELLs)}>
{t(`address.${+address.liveCellsCount > 1 ? 'cells' : 'cell'}`)}
</AddressAssetsTabPaneTitle>
}
key={AssetInfo.CELLs}
>
<div className="addressUdtAssetsGrid">
<Cells address={address.addressHash} count={+address.liveCellsCount} />
</div>
</AddressAssetsTabPane>
) : null}
</AddressAssetsTab>
</AddressUDTAssetsPanel>
) : null}
Expand Down Expand Up @@ -410,3 +438,5 @@ export const AddressTransactions = ({
</>
)
}

// FIXME: plural in i18n not work, address.cell and address.cells
242 changes: 242 additions & 0 deletions src/pages/Address/Cells.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { type FC, useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import BigNumber from 'bignumber.js'
import { useInfiniteQuery } from '@tanstack/react-query'
import { explorerService } from '../../services/ExplorerService'
import SUDTTokenIcon from '../../assets/sudt_token.png'
import CKBTokenIcon from './ckb_token_icon.png'
import { ReactComponent as CopyIcon } from './copy.svg'
import { ReactComponent as TypeHashIcon } from './type_script.svg'
import { ReactComponent as DataIcon } from './data.svg'
import { ReactComponent as SporeCluterIcon } from './spore_cluster.svg'
import { ReactComponent as SporeCellIcon } from './spore_cell.svg'
import { parseUDTAmount } from '../../utils/number'
import { shannonToCkb } from '../../utils/util'
import { useSetToast } from '../../components/Toast'
import { PAGE_SIZE } from '../../constants/common'
import styles from './cells.module.scss'

const fetchCells = async ({
address,
size = 10,
sort = 'capacity.desc',
page = 1,
}: {
address: string
size: number
sort: string
page: number
}) => {
const res = await explorerService.api.fetchAddressLiveCells(address, page, size, sort)
return {
data: res.data,
nextPage: page + 1,
}
}

const initialPageParams = { size: 10, sort: 'capacity.desc' }

const ATTRIBUTE_LENGTH = 18

const Cells: FC<{ address: string; count: number }> = ({ address, count }) => {
const [params] = useState(initialPageParams)
const loadMoreRef = useRef<HTMLDivElement | null>(null)
const setToast = useSetToast()

const { t } = useTranslation()

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
['address live cells', address, params.size, params.sort],
({ pageParam = 1 }) => fetchCells({ ...params, address, page: pageParam }),
{
getNextPageParam: lastPage => {
if (lastPage.data.length < params.size) return false
return lastPage.nextPage
},
},
)

const isListDisplayed = count && data

useEffect(() => {
const trigger = loadMoreRef.current

if (!isListDisplayed) return
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
fetchNextPage()
}
},
{ threshold: 0.5 },
)
if (trigger) {
observer.observe(trigger)
}
return () => {
if (trigger) {
observer.unobserve(trigger)
}
}
}, [isListDisplayed, fetchNextPage])

const handleCopy = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
const { detail } = e.currentTarget.dataset
if (!detail) return
navigator.clipboard.writeText(detail).then(() => {
setToast({ message: t('common.copied') })
})
}

if (!isListDisplayed) {
return null
}

const cells = data.pages.map(page => page.data).flat()

return (
<div className={styles.container}>
<div className={styles.toolbar}>UTXO: {count.toLocaleString('en')}</div>
<ul>
{cells.map(cell => {
const ckb = Number(shannonToCkb(cell.capacity)).toLocaleString('en')
const title = `${cell.txHash.slice(0, 8)}...${cell.txHash.slice(-8)}#${cell.cellIndex}`
const link = `/transaction/${cell.txHash}?${new URLSearchParams({
page_of_outputs: Math.ceil(+cell.cellIndex / PAGE_SIZE).toString(),
})}`
const assetType: string = cell.extraInfo?.type ?? cell.cellType
let icon: string | React.ReactElement | null = null
let assetName = null
let attribute = null
let detailInfo = null

switch (assetType) {
case 'ckb': {
if (cell.typeHash) {
icon = <TypeHashIcon />
assetName = 'UNKNOWN ASSET'
attribute = `TYPE HASH: ${cell.typeHash.slice(0, 10)}...`
detailInfo = cell.typeHash
break
}
if (cell.data !== '0x') {
// TODO: indicate this is a contentful cell
icon = <DataIcon />
assetName = 'DATA'
if (cell.data.length > ATTRIBUTE_LENGTH) {
attribute = `${cell.data.slice(0, ATTRIBUTE_LENGTH)}...`
} else {
attribute = cell.data
}
detailInfo = cell.data
break
}
icon = CKBTokenIcon
assetName = 'CKB'
attribute = ckb
detailInfo = BigNumber(cell.capacity).toFormat({ groupSeparator: '' })
break
}
case 'udt':
case 'omiga_inscription': {
icon = SUDTTokenIcon
assetName = cell.extraInfo.symbol || t('udt.inscription')
attribute = cell.extraInfo.decimal
? parseUDTAmount(cell.extraInfo.amount, cell.extraInfo.decimal)
: 'Unknown UDT amount'
detailInfo = cell.extraInfo.amount
break
}
case 'spore_cell': {
icon = <SporeCellIcon />
assetName = 'Spore'
if (cell.data.length > ATTRIBUTE_LENGTH) {
attribute = `${cell.data.slice(0, ATTRIBUTE_LENGTH)}...`
} else {
attribute = cell.data
}
detailInfo = cell.data
break
}
case 'spore_cluster': {
icon = <SporeCluterIcon />
assetName = 'Spore Cluster'
if (cell.data.length > ATTRIBUTE_LENGTH) {
attribute = `${cell.data.slice(0, ATTRIBUTE_LENGTH)}...`
} else {
attribute = cell.data
}
detailInfo = cell.data
break
}
case 'nrc_721': {
icon = SUDTTokenIcon
assetName = 'NRC 721'
attribute = '-'
break
}
case 'm_nft': {
icon = SUDTTokenIcon
assetName = cell.extraInfo.className
attribute = `#${parseInt(cell.extraInfo.tokenId, 16)}`
break
}
default: {
icon = SUDTTokenIcon
assetName = 'UNKNOWN'
attribute = '-'
}
}
const outPoint = {
tx_hash: cell.txHash,
index: `0x${cell.cellIndex.toString(16)}`,
}

return (
<li key={cell.txHash + cell.cellIndex} className={styles.card}>
<h5>
<a href={link}>{title}</a>

<button
type="button"
className={styles.copy}
data-detail={JSON.stringify(outPoint)}
onClick={handleCopy}
>
<CopyIcon />
</button>
<span title={`${ckb} CKB`}>{`${ckb} CKB`}</span>
</h5>
<div className={styles.content}>
{typeof icon === 'string' ? <img src={icon} alt={assetName ?? 'sudt'} width="40" height="40" /> : null}
{icon && typeof icon !== 'string' ? icon : null}
<div className={styles.fields}>
<div className={styles.assetName}>{assetName}</div>
<div className={styles.attribute} title={detailInfo ?? attribute}>
{attribute}
{detailInfo ? (
<button type="button" className={styles.copy} data-detail={detailInfo} onClick={handleCopy}>
<CopyIcon />
</button>
) : null}
</div>
</div>
</div>
</li>
)
})}
</ul>
{isFetchingNextPage ? <span className={styles.loading}>Loading...</span> : null}
{!hasNextPage || isFetchingNextPage ? null : (
<div className={styles.loadMore} ref={loadMoreRef}>
<button type="button" onClick={() => fetchNextPage()}>
Load more
</button>
</div>
)}
</div>
)
}
export default Cells
Loading