Skip to content

Commit 0dc6dbe

Browse files
authored
feature: view asset transfer transactions
1 parent e04c340 commit 0dc6dbe

25 files changed

+885
-98
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"class-variance-authority": "^0.7.0",
3737
"clsx": "^2.1.0",
3838
"date-fns": "^3.5.0",
39+
"decimal.js": "^10.4.3",
3940
"jotai": "^2.7.2",
4041
"jotai-effect": "^0.6.0",
4142
"lucide-react": "^0.356.0",

src/features/assets/data.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { atom, useAtomValue, useStore } from 'jotai'
2+
import { useMemo } from 'react'
3+
import { AssetLookupResult, AssetResult } from '@algorandfoundation/algokit-utils/types/indexer'
4+
import { atomEffect } from 'jotai-effect'
5+
import { loadable } from 'jotai/utils'
6+
import { indexer } from '../common/data'
7+
8+
// TODO: Size should be capped at some limit, so memory usage doesn't grow indefinitely
9+
export const assetsAtom = atom<AssetResult[]>([])
10+
11+
export const useAssetAtom = (assetIndex: number) => {
12+
const store = useStore()
13+
14+
return useMemo(() => {
15+
const syncEffect = atomEffect((get, set) => {
16+
;(async () => {
17+
try {
18+
const asset = await get(assetAtom)
19+
set(assetsAtom, (prev) => {
20+
return prev.concat(asset)
21+
})
22+
} catch (e) {
23+
// Ignore any errors as there is nothing to sync
24+
}
25+
})()
26+
})
27+
const assetAtom = atom((get) => {
28+
// store.get prevents the atom from being subscribed to changes in assetsAtom
29+
const assets = store.get(assetsAtom)
30+
const asset = assets.find((a) => a.index === assetIndex)
31+
if (asset) {
32+
return asset
33+
}
34+
35+
get(syncEffect)
36+
37+
return indexer
38+
.lookupAssetByID(assetIndex)
39+
.do()
40+
.then((result) => {
41+
return (result as AssetLookupResult).asset
42+
})
43+
})
44+
return assetAtom
45+
}, [store, assetIndex])
46+
}
47+
48+
export const useLoadableAsset = (assetId: number) => {
49+
return useAtomValue(
50+
// Unfortunately we can't leverage Suspense here, as react doesn't support async useMemo inside the Suspense component
51+
// https://github.com/facebook/react/issues/20877
52+
loadable(useAssetAtom(assetId))
53+
)
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer'
2+
import { AssetModel } from '../models'
3+
4+
export const asAsset = (assetResult: AssetResult): AssetModel => {
5+
return {
6+
id: assetResult.index,
7+
name: assetResult.params.name,
8+
total: assetResult.params.total,
9+
decimals: assetResult.params.decimals,
10+
unitName: assetResult.params['unit-name'],
11+
}
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type AssetModel = {
2+
id: number
3+
name?: string
4+
total: number | bigint
5+
decimals: number | bigint
6+
unitName?: string
7+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { AssetModel } from '@/features/assets/models'
2+
import Decimal from 'decimal.js'
3+
4+
type Props = {
5+
amount: number | bigint
6+
asset: AssetModel
7+
}
8+
9+
export const DisplayAssetAmount = ({ amount, asset }: Props) => {
10+
// asset decimals value must be from 0 to 19 so it is safe to use .toString() here
11+
const decimals = asset.decimals.toString()
12+
// the amount is uint64, should be safe to be .toString()
13+
const amountAsString = amount.toString()
14+
15+
return (
16+
<div>
17+
{new Decimal(amountAsString).div(new Decimal(10).pow(decimals)).toString()} {asset.unitName ?? ''}
18+
</div>
19+
)
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<div>
2+
<div
3+
class="relative grid"
4+
style="grid-template-columns: minmax(128px, 128px) repeat(3, 128px); grid-template-rows: repeat(2, 40px);"
5+
>
6+
<div />
7+
<div
8+
class="p-2 flex justify-center"
9+
>
10+
<h1
11+
class="text-l font-semibold"
12+
>
13+
6MO6...HSJM
14+
</h1>
15+
</div>
16+
<div
17+
class="p-2 flex justify-center"
18+
>
19+
<h1
20+
class="text-l font-semibold"
21+
>
22+
OCD5...4IFA
23+
</h1>
24+
</div>
25+
<div />
26+
<div
27+
class="absolute right-0 -z-10"
28+
style="top: 40px;"
29+
>
30+
<div>
31+
<div
32+
class="p-0"
33+
/>
34+
<div
35+
class="p-0"
36+
style="height: 40px; width: 256px;"
37+
>
38+
<div
39+
class="grid h-full"
40+
style="grid-template-columns: repeat(2, minmax(0, 1fr)); height: 40px;"
41+
>
42+
<div
43+
class="flex justify-center"
44+
>
45+
<div
46+
class="border-muted h-full border-dashed"
47+
style="border-left-width: 2px;"
48+
/>
49+
</div>
50+
<div
51+
class="flex justify-center"
52+
>
53+
<div
54+
class="border-muted h-full border-dashed"
55+
style="border-left-width: 2px;"
56+
/>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
<div
63+
class="p-0 relative pr-8"
64+
>
65+
<div
66+
class="relative h-full p-0 flex items-center px-0"
67+
style="margin-left: 0px;"
68+
>
69+
<div
70+
class="inline"
71+
style="margin-left: 16px;"
72+
>
73+
JBDSQEI...
74+
</div>
75+
</div>
76+
</div>
77+
<div
78+
class="flex items-center justify-center"
79+
data-state="closed"
80+
style="grid-column-start: 2; grid-column-end: 4; color: rgb(126 200 191);"
81+
>
82+
<svg
83+
height="20"
84+
viewBox="0 0 21 21"
85+
width="20"
86+
xmlns="http://www.w3.org/2000/svg"
87+
xmlns:xlink="http://www.w3.org/1999/xlink"
88+
>
89+
<g
90+
transform="matrix(1 0 0 1 -153 -143 )"
91+
>
92+
<path
93+
d="M 163.5 143 C 169.38 143 174 147.62 174 153.5 C 174 159.38 169.38 164 163.5 164 C 157.62 164 153 159.38 153 153.5 C 153 147.62 157.62 143 163.5 143 Z "
94+
fill="currentColor"
95+
fill-rule="nonzero"
96+
stroke="none"
97+
/>
98+
</g>
99+
</svg>
100+
<div
101+
class="relative"
102+
style="width: calc(50.00% - 20px); height: 20px;"
103+
>
104+
<div
105+
class="h-1/2"
106+
style="border-bottom-width: 2px;"
107+
/>
108+
<svg
109+
class="absolute top-0 right-0"
110+
height="19px"
111+
preserveAspectRatio="xMinYMid meet"
112+
viewBox="340 139 1 13"
113+
width="11px"
114+
xmlns="http://www.w3.org/2000/svg"
115+
xmlns:xlink="http://www.w3.org/1999/xlink"
116+
>
117+
<path
118+
d="M 340.3 151.3 L 347 145.3 L 340.3 139.3 L 342.6 145.3 L 340.3 151.3 Z"
119+
fill="currentColor"
120+
fill-rule="nonzero"
121+
stroke="none"
122+
/>
123+
</svg>
124+
</div>
125+
<div
126+
class="absolute z-20 bg-card p-2 text-foreground w-20 text-xs"
127+
>
128+
Transfer
129+
<div>
130+
0.3
131+
132+
AKTA
133+
</div>
134+
</div>
135+
<svg
136+
height="20"
137+
viewBox="0 0 21 21"
138+
width="20"
139+
xmlns="http://www.w3.org/2000/svg"
140+
xmlns:xlink="http://www.w3.org/1999/xlink"
141+
>
142+
<g
143+
transform="matrix(1 0 0 1 -153 -143 )"
144+
>
145+
<path
146+
d="M 163.5 143 C 169.38 143 174 147.62 174 153.5 C 174 159.38 169.38 164 163.5 164 C 157.62 164 153 159.38 153 153.5 C 153 147.62 157.62 143 163.5 143 Z "
147+
fill="currentColor"
148+
fill-rule="nonzero"
149+
stroke="none"
150+
/>
151+
</g>
152+
</svg>
153+
</div>
154+
</div>
155+
</div>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { cn } from '@/features/common/utils'
2+
import { useMemo } from 'react'
3+
import { AssetTransferTransactionModel } from '../models'
4+
import { DescriptionList } from '@/features/common/components/description-list'
5+
import { transactionSenderLabel, transactionReceiverLabel, transactionAmountLabel } from './transaction-view-table'
6+
import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
7+
8+
type Props = {
9+
transaction: AssetTransferTransactionModel
10+
}
11+
12+
export const assetLabel = 'Asset'
13+
export const transactionCloseRemainderToLabel = 'Close Remainder To'
14+
export const transactionCloseRemainderAmountLabel = 'Close Remainder Amount'
15+
16+
export function AssetTransferTransactionInfo({ transaction }: Props) {
17+
const items = useMemo(
18+
() => [
19+
{
20+
dt: transactionSenderLabel,
21+
dd: (
22+
<a href="#" className={cn('text-primary underline')}>
23+
{transaction.sender}
24+
</a>
25+
),
26+
},
27+
{
28+
dt: transactionReceiverLabel,
29+
dd: (
30+
<a href="#" className={cn('text-primary underline')}>
31+
{transaction.receiver}
32+
</a>
33+
),
34+
},
35+
{
36+
dt: assetLabel,
37+
dd: (
38+
<a href="#" className={cn('text-primary underline')}>
39+
{transaction.asset.id} {`${transaction.asset.name ? `(${transaction.asset.name})` : ''}`}
40+
</a>
41+
),
42+
},
43+
{
44+
dt: transactionAmountLabel,
45+
dd: <DisplayAssetAmount amount={transaction.amount} asset={transaction.asset} />,
46+
},
47+
...(transaction.closeRemainder
48+
? [
49+
{
50+
dt: transactionCloseRemainderToLabel,
51+
dd: (
52+
<a href="#" className={cn('text-primary underline')}>
53+
{transaction.closeRemainder.to}
54+
</a>
55+
),
56+
},
57+
{
58+
dt: transactionCloseRemainderAmountLabel,
59+
dd: <DisplayAssetAmount amount={transaction.closeRemainder.amount} asset={transaction.asset} />,
60+
},
61+
]
62+
: []),
63+
],
64+
[transaction.sender, transaction.receiver, transaction.asset, transaction.amount, transaction.closeRemainder]
65+
)
66+
67+
return (
68+
<div className={cn('space-y-2')}>
69+
<div className={cn('flex items-center justify-between')}>
70+
<h1 className={cn('text-2xl text-primary font-bold')}>Asset Transfer</h1>
71+
</div>
72+
<DescriptionList items={items} />
73+
</div>
74+
)
75+
}

0 commit comments

Comments
 (0)