Skip to content

Commit 825d6de

Browse files
feat: Add asset page and render basic details (#37)
* feat: add asset link component * feat: add asset page and basic info --------- Co-authored-by: Patrick Dinh <[email protected]>
1 parent b36918d commit 825d6de

File tree

12 files changed

+237
-51
lines changed

12 files changed

+237
-51
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Card, CardContent } from '@/features/common/components/card'
2+
import { DescriptionList } from '@/features/common/components/description-list'
3+
import { useMemo } from 'react'
4+
import { cn } from '@/features/common/utils'
5+
import { Asset } from '../models'
6+
import { isDefined } from '@/utils/is-defined'
7+
import Decimal from 'decimal.js'
8+
import { AccountLink } from '@/features/accounts/components/account-link'
9+
import { ZERO_ADDRESS } from '@/features/common/constants'
10+
11+
type Props = {
12+
asset: Asset
13+
}
14+
15+
export const assetIdLabel = 'Asset ID'
16+
export const assetNameLabel = 'Name'
17+
export const assetUnitNameLabel = 'Unit'
18+
export const assetDecimalsLabel = 'Decimals'
19+
export const assetTotalSupplyLabel = 'Total Supply'
20+
export const assetMetadataHashLabel = 'Metadata Hash'
21+
export const assetDefaultFrozenLabel = 'Default Frozen'
22+
export const assetUrlLabel = 'URL'
23+
24+
export const assetAddressesLabel = 'Asset Addresses'
25+
export const assetCreatorLabel = 'Creator'
26+
export const assetManagerLabel = 'Manager'
27+
export const assetReserveLabel = 'Reserve'
28+
export const assetFreezeLabel = 'Freeze'
29+
export const assetClawbackLabel = 'Clawback'
30+
31+
export const assetJsonLabel = 'Asset JSON'
32+
33+
export function AssetDetails({ asset }: Props) {
34+
const assetItems = useMemo(
35+
() => [
36+
{
37+
dt: assetIdLabel,
38+
dd: asset.id,
39+
},
40+
asset.name
41+
? {
42+
dt: assetNameLabel,
43+
dd: asset.name,
44+
}
45+
: undefined,
46+
asset.unitName
47+
? {
48+
dt: assetUnitNameLabel,
49+
dd: asset.unitName,
50+
}
51+
: undefined,
52+
{
53+
dt: assetTotalSupplyLabel,
54+
dd: `${new Decimal(asset.total.toString()).div(new Decimal(10).pow(asset.decimals.toString()))} ${asset.unitName}`,
55+
},
56+
{
57+
dt: assetDecimalsLabel,
58+
dd: asset.decimals.toString(),
59+
},
60+
{
61+
dt: assetDefaultFrozenLabel,
62+
dd: asset.defaultFrozen ? 'Yes' : 'No',
63+
},
64+
asset.url
65+
? {
66+
dt: assetUrlLabel,
67+
dd: (
68+
<a href={asset.url} className={cn('text-primary underline')}>
69+
{asset.url}
70+
</a>
71+
),
72+
}
73+
: undefined,
74+
],
75+
[asset.decimals, asset.defaultFrozen, asset.id, asset.name, asset.total, asset.unitName, asset.url]
76+
).filter(isDefined)
77+
78+
const assetAddresses = useMemo(
79+
() => [
80+
{
81+
dt: assetCreatorLabel,
82+
dd: <AccountLink address={asset.creator} />,
83+
},
84+
asset.manager && asset.manager !== ZERO_ADDRESS
85+
? {
86+
dt: assetManagerLabel,
87+
dd: <AccountLink address={asset.manager} />,
88+
}
89+
: undefined,
90+
asset.reserve && asset.reserve !== ZERO_ADDRESS
91+
? {
92+
dt: assetReserveLabel,
93+
dd: <AccountLink address={asset.reserve} />,
94+
}
95+
: undefined,
96+
asset.freeze && asset.freeze !== ZERO_ADDRESS
97+
? {
98+
dt: assetFreezeLabel,
99+
dd: <AccountLink address={asset.freeze} />,
100+
}
101+
: undefined,
102+
asset.clawback && asset.clawback !== ZERO_ADDRESS
103+
? {
104+
dt: assetClawbackLabel,
105+
dd: <AccountLink address={asset.clawback} />,
106+
}
107+
: undefined,
108+
],
109+
[asset.clawback, asset.creator, asset.freeze, asset.manager, asset.reserve]
110+
).filter(isDefined)
111+
112+
return (
113+
<div className={cn('space-y-6 pt-7')}>
114+
<Card className={cn('p-4')}>
115+
<CardContent className={cn('text-sm space-y-2')}>
116+
<DescriptionList items={assetItems} />
117+
</CardContent>
118+
</Card>
119+
<Card className={cn('p-4')}>
120+
<CardContent className={cn('text-sm space-y-2')}>
121+
<h1 className={cn('text-2xl text-primary font-bold')}>{assetAddressesLabel}</h1>
122+
<DescriptionList items={assetAddresses} />
123+
</CardContent>
124+
</Card>
125+
<Card className={cn('p-4')}>
126+
<CardContent className={cn('text-sm space-y-2')}>
127+
<h1 className={cn('text-2xl text-primary font-bold')}>{assetJsonLabel}</h1>
128+
<div className={cn('border-solid border-2 border-border h-96 grid')}>
129+
<pre className={cn('overflow-scroll p-4')}>{asset.json}</pre>
130+
</div>
131+
</CardContent>
132+
</Card>
133+
</div>
134+
)
135+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { cn } from '@/features/common/utils'
2+
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
3+
import { Urls } from '@/routes/urls'
4+
import { PropsWithChildren } from 'react'
5+
6+
type Props = PropsWithChildren<{
7+
assetId: number
8+
assetName?: string
9+
className?: string
10+
}>
11+
12+
export function AssetLink({ assetId, assetName, className, children }: Props) {
13+
return (
14+
<span>
15+
<TemplatedNavLink
16+
className={cn(!children && 'text-primary underline', className)}
17+
urlTemplate={Urls.Explore.Asset.ById}
18+
urlParams={{ assetId: assetId.toString() }}
19+
>
20+
{children ? children : assetId}
21+
</TemplatedNavLink>
22+
{assetName && ` (${assetName})`}
23+
</span>
24+
)
25+
}

src/features/assets/mappers/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer'
22
import { Asset } from '../models'
3+
import { asJson } from '@/utils/as-json'
34

45
export const asAsset = (assetResult: AssetResult): Asset => {
56
return {
@@ -8,6 +9,13 @@ export const asAsset = (assetResult: AssetResult): Asset => {
89
total: assetResult.params.total,
910
decimals: assetResult.params.decimals,
1011
unitName: assetResult.params['unit-name'],
12+
defaultFrozen: assetResult.params['default-frozen'] ?? false,
13+
url: assetResult.params.url,
14+
creator: assetResult.params.creator,
15+
manager: assetResult.params.manager,
16+
reserve: assetResult.params.reserve,
17+
freeze: assetResult.params.freeze,
1118
clawback: assetResult.params.clawback,
19+
json: asJson(assetResult),
1220
}
1321
}

src/features/assets/models/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,12 @@ export type Asset = {
44
total: number | bigint
55
decimals: number | bigint
66
unitName?: string
7+
defaultFrozen: boolean
8+
url?: string
9+
creator: string
10+
manager?: string
11+
reserve?: string
12+
freeze?: string
713
clawback?: string
14+
json: string
815
}

src/features/assets/pages/asset-page.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,39 @@ import { UrlParams } from '../../../routes/urls'
33
import { useRequiredParam } from '../../common/hooks/use-required-param'
44
import { cn } from '@/features/common/utils'
55
import { isInteger } from '@/utils/is-integer'
6+
import { useLoadableAsset } from '../data'
7+
import { RenderLoadable } from '@/features/common/components/render-loadable'
8+
import { is404 } from '@/utils/error'
9+
import { AssetDetails } from '../components/asset-details'
10+
11+
const transformError = (e: Error) => {
12+
if (is404(e)) {
13+
return new Error(assetNotFoundMessage)
14+
}
15+
16+
// eslint-disable-next-line no-console
17+
console.error(e)
18+
return new Error(assetFailedToLoadMessage)
19+
}
620

721
export const assetPageTitle = 'Asset'
22+
export const assetNotFoundMessage = 'Asset not found'
823
export const assetInvalidIdMessage = 'Asset Id is invalid'
24+
export const assetFailedToLoadMessage = 'Asset failed to load'
925

1026
export function AssetPage() {
11-
const { assetId } = useRequiredParam(UrlParams.AssetId)
12-
invariant(isInteger(assetId), assetInvalidIdMessage)
27+
const { assetId: _assetId } = useRequiredParam(UrlParams.AssetId)
28+
invariant(isInteger(_assetId), assetInvalidIdMessage)
29+
30+
const assetId = parseInt(_assetId, 10)
31+
const loadableAsset = useLoadableAsset(assetId)
1332

1433
return (
1534
<div>
1635
<h1 className={cn('text-2xl text-primary font-bold')}>{assetPageTitle}</h1>
17-
{assetId}
36+
<RenderLoadable loadable={loadableAsset} transformError={transformError}>
37+
{(asset) => <AssetDetails asset={asset} />}
38+
</RenderLoadable>
1839
</div>
1940
)
2041
}

src/features/common/data/atom-with-debounce.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { atom, SetStateAction } from 'jotai'
22

3-
export const atomWithDebounce = <T>(initialValue: T, delayMilliseconds = 600, shouldDebounceOnReset = false) => {
3+
export const atomWithDebounce = <T>(initialValue: T, delayMilliseconds = 500, shouldDebounceOnReset = false) => {
44
const prevTimeoutAtom = atom<ReturnType<typeof setTimeout> | undefined>(undefined)
55

66
// DO NOT EXPORT currentValueAtom as using this atom to set state can cause

src/features/transactions/components/asset-config-transaction-info.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { cn } from '@/features/common/utils'
22
import { useMemo } from 'react'
3-
import { AssetConfigTransaction, InnerAssetConfigTransaction } from '../models'
3+
import { AssetConfigTransaction, AssetConfigTransactionSubType, InnerAssetConfigTransaction } from '../models'
44
import { DescriptionList } from '@/features/common/components/description-list'
55
import { transactionSenderLabel } from './transactions-table'
66
import { AccountLink } from '@/features/accounts/components/account-link'
77
import { isDefined } from '@/utils/is-defined'
8+
import { AssetLink } from '@/features/assets/components/asset-link'
9+
import Decimal from 'decimal.js'
810

911
type Props = {
1012
transaction: AssetConfigTransaction | InnerAssetConfigTransaction
1113
}
1214

13-
export const assetIdLabel = 'Asset ID'
15+
export const assetLabel = 'Asset'
1416
export const assetUrlLabel = 'URL'
1517
export const assetUnitLabel = 'Unit'
1618
export const assetDecimalsLabel = 'Decimals'
@@ -30,13 +32,8 @@ export function AssetConfigTransactionInfo({ transaction }: Props) {
3032
dd: <AccountLink address={transaction.sender} />,
3133
},
3234
{
33-
dt: assetIdLabel,
34-
dd: (
35-
<a href="#" className={cn('text-primary underline')}>
36-
{transaction.assetId}
37-
{transaction.name && ` (${transaction.name})`}
38-
</a>
39-
),
35+
dt: assetLabel,
36+
dd: <AssetLink assetId={transaction.assetId} assetName={transaction.name} />,
4037
},
4138
transaction.url
4239
? {
@@ -54,18 +51,22 @@ export function AssetConfigTransactionInfo({ transaction }: Props) {
5451
dd: transaction.unitName,
5552
}
5653
: undefined,
57-
transaction.decimals != null
58-
? {
59-
dt: assetDecimalsLabel,
60-
dd: transaction.decimals.toString(),
61-
}
62-
: undefined,
63-
transaction.total != null
64-
? {
65-
dt: assetTotalSupplyLabel,
66-
dd: transaction.total.toString(),
67-
}
68-
: undefined,
54+
...(transaction.subType === AssetConfigTransactionSubType.Create
55+
? [
56+
transaction.total != null
57+
? {
58+
dt: assetTotalSupplyLabel,
59+
dd: `${new Decimal(transaction.total.toString()).div(new Decimal(10).pow((transaction.decimals ?? 0).toString()))} ${transaction.unitName}`,
60+
}
61+
: undefined,
62+
transaction.decimals != null
63+
? {
64+
dt: assetDecimalsLabel,
65+
dd: transaction.decimals.toString(),
66+
}
67+
: undefined,
68+
]
69+
: []),
6970
transaction.manager
7071
? {
7172
dt: assetManagerLabel,
@@ -107,6 +108,7 @@ export function AssetConfigTransactionInfo({ transaction }: Props) {
107108
transaction.name,
108109
transaction.reserve,
109110
transaction.sender,
111+
transaction.subType,
110112
transaction.total,
111113
transaction.unitName,
112114
transaction.url,

src/features/transactions/components/asset-transfer-transaction-info.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DescriptionList } from '@/features/common/components/description-list'
55
import { transactionSenderLabel, transactionReceiverLabel, transactionAmountLabel } from './transactions-table'
66
import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
77
import { AccountLink } from '@/features/accounts/components/account-link'
8+
import { AssetLink } from '@/features/assets/components/asset-link'
89

910
type Props = {
1011
transaction: AssetTransferTransaction | InnerAssetTransferTransaction
@@ -20,15 +21,11 @@ export function AssetTransferTransactionInfo({ transaction }: Props) {
2021
() => [
2122
{
2223
dt: transactionSenderLabel,
23-
dd: <AccountLink address={transaction.sender}></AccountLink>,
24+
dd: <AccountLink address={transaction.sender} />,
2425
},
2526
{
2627
dt: transactionReceiverLabel,
27-
dd: (
28-
<a href="#" className={cn('text-primary underline')}>
29-
{transaction.receiver}
30-
</a>
31-
),
28+
dd: <AccountLink address={transaction.receiver} />,
3229
},
3330
...(transaction.subType === AssetTransferTransactionSubType.Clawback
3431
? [
@@ -44,11 +41,7 @@ export function AssetTransferTransactionInfo({ transaction }: Props) {
4441
: []),
4542
{
4643
dt: assetLabel,
47-
dd: (
48-
<a href="#" className={cn('text-primary underline')}>
49-
{transaction.asset.id} {`${transaction.asset.name ? `(${transaction.asset.name})` : ''}`}
50-
</a>
51-
),
44+
dd: <AssetLink assetId={transaction.asset.id} assetName={transaction.asset.name} />,
5245
},
5346
{
5447
dt: transactionAmountLabel,
@@ -58,11 +51,7 @@ export function AssetTransferTransactionInfo({ transaction }: Props) {
5851
? [
5952
{
6053
dt: transactionCloseRemainderToLabel,
61-
dd: (
62-
<a href="#" className={cn('text-primary underline')}>
63-
{transaction.closeRemainder.to}
64-
</a>
65-
),
54+
dd: <AccountLink address={transaction.closeRemainder.to} />,
6655
},
6756
{
6857
dt: transactionCloseRemainderAmountLabel,

0 commit comments

Comments
 (0)