Skip to content

Commit 1264028

Browse files
committed
Asset info
1 parent b0c61e9 commit 1264028

File tree

7 files changed

+178
-71
lines changed

7 files changed

+178
-71
lines changed

explorer/src/contexts/DexieContext.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface Token {
99
}
1010

1111
export interface DexieContextType {
12-
tokens: Record<string, Token>;
12+
getToken: (id: string) => Token | null;
1313
}
1414

1515
// eslint-disable-next-line react-refresh/only-export-components
@@ -42,7 +42,11 @@ export function DexieProvider({ children }: { children: ReactNode }) {
4242
});
4343
}, []);
4444

45+
const getToken = (id: string) => tokens[id.replace('0x', '')] ?? null;
46+
4547
return (
46-
<DexieContext.Provider value={{ tokens }}>{children}</DexieContext.Provider>
48+
<DexieContext.Provider value={{ getToken }}>
49+
{children}
50+
</DexieContext.Provider>
4751
);
4852
}

explorer/src/contexts/MintGardenContext.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface Nft {
1414
}
1515

1616
export interface MintGardenContextType {
17-
fetchNft: (launcherId: string) => Promise<Nft>;
17+
fetchNft: (launcherId: string) => Promise<Nft | null>;
1818
}
1919

2020
// eslint-disable-next-line react-refresh/only-export-components
@@ -31,13 +31,23 @@ export function MintGardenProvider({ children }: { children: ReactNode }) {
3131
return nfts[launcherId];
3232
}
3333

34-
const bech32 = toAddress(launcherId, 'nft');
35-
const response = await fetch(`https://api.mintgarden.io/nfts/${bech32}`);
36-
const nft: Nft = await response.json();
34+
try {
35+
const bech32 = launcherId.startsWith('nft')
36+
? launcherId
37+
: toAddress(launcherId, 'nft');
38+
const response = await fetch(
39+
`https://api.mintgarden.io/nfts/${bech32}`,
40+
);
41+
const nft: Nft = await response.json();
3742

38-
setNfts((prev) => ({ ...prev, [launcherId]: nft }));
43+
setNfts((prev) => ({ ...prev, [launcherId]: nft }));
3944

40-
return nft;
45+
return nft;
46+
} catch (error) {
47+
console.error(error);
48+
49+
return null;
50+
}
4151
},
4252
[nfts, setNfts],
4353
);

explorer/src/lib/parser/coinSpend.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { bytesEqual, Constants, toHex } from 'chia-wallet-sdk-wasm';
1+
import { bytesEqual, Constants, Puzzle, toHex } from 'chia-wallet-sdk-wasm';
2+
import { toAddress } from '../conversions';
23
import { parseCoin, ParsedCoin } from './coin';
34
import { parseCondition, ParsedCondition } from './conditions';
45
import { ParserContext } from './context';
@@ -13,6 +14,15 @@ export interface ParsedCoinSpend {
1314
cost: string;
1415
conditions: ParsedCondition[];
1516
layer: ParsedLayer;
17+
assetType: AssetType;
18+
assetId: string;
19+
}
20+
21+
export enum AssetType {
22+
Token,
23+
Nft,
24+
Did,
25+
Singleton,
1626
}
1727

1828
export function parseCoinSpend(
@@ -88,6 +98,11 @@ export function parseCoinSpend(
8898
}
8999
}
90100

101+
const cat = puzzle.parseCat();
102+
const nft = puzzle.parseNft();
103+
const did = puzzle.parseDid();
104+
const singleton = parseSingleton(puzzle);
105+
91106
return {
92107
coin: parseCoin(coinSpend.coin),
93108
puzzleReveal: toHex(coinSpend.puzzleReveal),
@@ -97,5 +112,33 @@ export function parseCoinSpend(
97112
parseCondition(coinSpend.coin, condition, ctx, isFastForwardable),
98113
),
99114
layer: parseLayer(puzzle),
115+
assetId: cat
116+
? `0x${toHex(cat.info.assetId)}`
117+
: nft
118+
? toAddress(toHex(nft.info.launcherId), 'nft')
119+
: did
120+
? toAddress(toHex(did.info.launcherId), 'did:chia:')
121+
: singleton
122+
? toAddress(toHex(singleton), 'vault')
123+
: 'xch',
124+
assetType: nft
125+
? AssetType.Nft
126+
: did
127+
? AssetType.Did
128+
: singleton
129+
? AssetType.Singleton
130+
: AssetType.Token,
100131
};
101132
}
133+
134+
function parseSingleton(puzzle: Puzzle) {
135+
if (!bytesEqual(puzzle.modHash, Constants.singletonTopLayerV11Hash())) {
136+
return undefined;
137+
}
138+
139+
const singletonStruct = puzzle.program.uncurry()?.args[0];
140+
const pair = singletonStruct?.toPair()?.rest.toPair();
141+
const launcherId = pair?.first.toAtom();
142+
143+
return launcherId;
144+
}

explorer/src/lib/parser/layers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ export function parseLayer(puzzle: Puzzle): ParsedLayer {
4848
value: publicKey && `0x${toHex(publicKey)}`,
4949
type: ArgType.Copiable,
5050
};
51+
} else if (bytesEqual(puzzle.modHash, Constants.catPuzzleHash())) {
52+
name = 'CAT_V2';
53+
54+
const assetId = arg(1)?.toAtom();
55+
const innerPuzzle = arg(2);
56+
57+
args.asset_id = {
58+
value: assetId && `0x${toHex(assetId)}`,
59+
type: ArgType.Copiable,
60+
};
61+
62+
if (innerPuzzle) children.inner_puzzle = parseLayer(innerPuzzle.puzzle());
5163
} else if (bytesEqual(puzzle.puzzleHash, Constants.settlementPaymentHash())) {
5264
name = 'SETTLEMENT_PAYMENT';
5365
} else if (bytesEqual(puzzle.puzzleHash, Constants.singletonLauncherHash())) {

explorer/src/pages/Block.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ interface CoinCardProps {
196196
}
197197

198198
function CoinCard({ coinRecord, block }: CoinCardProps) {
199-
const { tokens } = useDexie();
199+
const { getToken } = useDexie();
200200
const { fetchNft } = useMintGarden();
201201
const [nft, setNft] = useState<Nft | null>(null);
202202
const isCreated = coinRecord.created_height === block?.height;
@@ -210,9 +210,9 @@ function CoinCard({ coinRecord, block }: CoinCardProps) {
210210

211211
const token =
212212
coinRecord.type === 'cat' && 'asset_id' in coinRecord
213-
? tokens[coinRecord.asset_id.replace('0x', '')]
213+
? getToken(coinRecord.asset_id)
214214
: coinRecord.type === 'unknown' || coinRecord.type === 'reward'
215-
? tokens['xch']
215+
? getToken('xch')
216216
: null;
217217

218218
return (

explorer/src/pages/Coin.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useParams } from 'react-router-dom';
2020

2121
export function Coin() {
2222
const { id } = useParams();
23-
const { tokens } = useDexie();
23+
const { getToken } = useDexie();
2424
const { fetchNft } = useMintGarden();
2525

2626
const [coin, setCoin] = useState<CoinRecord | null>(null);
@@ -49,9 +49,9 @@ export function Coin() {
4949

5050
const token = coin
5151
? coin.type === 'cat'
52-
? tokens[coin.asset_id.replace('0x', '')]
52+
? getToken(coin.asset_id)
5353
: coin.type === 'reward'
54-
? tokens['xch']
54+
? getToken('xch')
5555
: null
5656
: null;
5757

explorer/src/pages/Tools.tsx

Lines changed: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { DropdownSelector } from '@/components/DropdownSelector';
22
import { Layout } from '@/components/Layout';
33
import { Truncated } from '@/components/Truncated';
44
import { Textarea } from '@/components/ui/textarea';
5+
import { Nft } from '@/contexts/MintGardenContext';
56
import { useDexie } from '@/hooks/useDexie';
7+
import { useMintGarden } from '@/hooks/useMintGarden';
68
import { Precision, toDecimal } from '@/lib/conversions';
79
import { parseJson } from '@/lib/json';
810
import {
11+
AssetType,
912
ConditionType,
1013
ParsedCoinSpend,
1114
ParsedCondition,
@@ -22,7 +25,7 @@ import {
2225
SpendBundle,
2326
} from 'chia-wallet-sdk-wasm';
2427
import { CoinsIcon, TriangleAlertIcon } from 'lucide-react';
25-
import { useMemo, useState } from 'react';
28+
import { useEffect, useMemo, useState } from 'react';
2629
import { useLocalStorage } from 'usehooks-ts';
2730

2831
export function Tools() {
@@ -79,7 +82,90 @@ function BundleViewer({ bundle }: BundleViewerProps) {
7982
const [selectedSpend, setSelectedSpend] = useState<ParsedCoinSpend | null>(
8083
bundle.coinSpends[0] ?? null,
8184
);
82-
const { tokens } = useDexie();
85+
const { getToken } = useDexie();
86+
const { fetchNft } = useMintGarden();
87+
88+
const [nfts, setNfts] = useState<Record<string, Nft | null>>({});
89+
90+
useEffect(() => {
91+
// Fetch NFT data for all NFT spends
92+
bundle.coinSpends.forEach((spend) => {
93+
if (spend.assetType === AssetType.Nft) {
94+
const launcherId = spend.assetId;
95+
if (!nfts[launcherId]) {
96+
fetchNft(launcherId).then((nft) => {
97+
setNfts((prev) => ({ ...prev, [launcherId]: nft }));
98+
});
99+
}
100+
}
101+
});
102+
}, [bundle.coinSpends, fetchNft, nfts]);
103+
104+
const renderCoinInfo = (spend: ParsedCoinSpend) => {
105+
const nft = spend.assetType === AssetType.Nft ? nfts[spend.assetId] : null;
106+
const token =
107+
spend.assetType === AssetType.Token ? getToken(spend.assetId) : null;
108+
109+
console.log(nft);
110+
111+
return (
112+
<div className='flex items-center gap-2 w-full'>
113+
{nft ? (
114+
<img
115+
src={nft.data?.thumbnail_uri}
116+
alt={nft.data?.metadata_json?.name ?? 'Unnamed'}
117+
className='w-6 h-6 rounded flex-shrink-0 object-cover'
118+
/>
119+
) : token?.icon ? (
120+
<img
121+
src={token.icon}
122+
alt={token.name}
123+
className='w-6 h-6 rounded-full flex-shrink-0'
124+
/>
125+
) : (
126+
<div className='w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0'>
127+
<CoinsIcon className='w-3.5 h-3.5 text-primary' />
128+
</div>
129+
)}
130+
<div className='flex flex-col min-w-0'>
131+
<div className='font-medium flex flex-wrap items-center gap-1.5'>
132+
{nft ? (
133+
<span className='break-all'>
134+
{nft.data?.metadata_json?.name ?? 'Unnamed'}
135+
</span>
136+
) : (
137+
<>
138+
<span className='break-all'>
139+
{toDecimal(
140+
spend.coin.amount,
141+
spend.assetType === AssetType.Token
142+
? spend.assetId === 'xch'
143+
? Precision.Xch
144+
: Precision.Cat
145+
: Precision.Singleton,
146+
)}
147+
</span>
148+
<span className='text-muted-foreground font-normal'>
149+
{spend.assetType === AssetType.Token
150+
? token?.code || (spend.assetId === 'xch' ? 'XCH' : 'CAT')
151+
: spend.assetType === AssetType.Nft
152+
? 'NFT'
153+
: spend.assetType === AssetType.Did
154+
? 'DID'
155+
: spend.assetType === AssetType.Singleton
156+
? 'VAULT'
157+
: ''}
158+
</span>
159+
</>
160+
)}
161+
</div>
162+
<div className='font-mono text-xs text-muted-foreground truncate'>
163+
<Truncated value={spend.coin.coinId} disableCopy />
164+
</div>
165+
</div>
166+
</div>
167+
);
168+
};
83169

84170
return (
85171
<div className='flex flex-col gap-4 mt-4'>
@@ -108,64 +194,12 @@ function BundleViewer({ bundle }: BundleViewerProps) {
108194
<DropdownSelector
109195
loadedItems={bundle.coinSpends}
110196
onSelect={setSelectedSpend}
111-
renderItem={(spend) => (
112-
<div className='flex items-center gap-2 w-full'>
113-
{tokens?.xch?.icon ? (
114-
<img
115-
src={tokens.xch.icon}
116-
alt='XCH'
117-
className='w-6 h-6 rounded-full flex-shrink-0'
118-
/>
119-
) : (
120-
<div className='w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0'>
121-
<CoinsIcon className='w-3.5 h-3.5 text-primary' />
122-
</div>
123-
)}
124-
<div className='flex flex-col min-w-0'>
125-
<div className='font-medium flex flex-wrap items-center gap-1.5'>
126-
<span className='break-all'>
127-
{toDecimal(spend.coin.amount, Precision.Xch)}
128-
</span>
129-
<span className='text-muted-foreground font-normal'>
130-
XCH
131-
</span>
132-
</div>
133-
<div className='font-mono text-xs text-muted-foreground truncate'>
134-
<Truncated value={spend.coin.coinId} disableCopy />
135-
</div>
136-
</div>
137-
</div>
138-
)}
197+
renderItem={(spend) => renderCoinInfo(spend)}
139198
width='w-[350px]'
140199
className='rounded-b-none'
141200
>
142201
{selectedSpend ? (
143-
<div className='flex items-center gap-2 min-w-0'>
144-
{tokens?.xch?.icon ? (
145-
<img
146-
src={tokens.xch.icon}
147-
alt='XCH'
148-
className='w-6 h-6 rounded-full flex-shrink-0'
149-
/>
150-
) : (
151-
<div className='w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0'>
152-
<CoinsIcon className='w-3.5 h-3.5 text-primary' />
153-
</div>
154-
)}
155-
<div className='flex flex-col min-w-0'>
156-
<div className='font-medium flex flex-wrap items-center gap-1.5'>
157-
<span className='break-all'>
158-
{toDecimal(selectedSpend.coin.amount, Precision.Xch)}
159-
</span>
160-
<span className='text-muted-foreground font-normal'>
161-
XCH
162-
</span>
163-
</div>
164-
<div className='font-mono text-xs text-muted-foreground truncate'>
165-
<Truncated value={selectedSpend.coin.coinId} disableCopy />
166-
</div>
167-
</div>
168-
</div>
202+
renderCoinInfo(selectedSpend)
169203
) : (
170204
<div className='text-muted-foreground'>
171205
Select a spend to view
@@ -226,6 +260,10 @@ function SpendViewer({ spend }: SpendViewerProps) {
226260
<div className='text-muted-foreground'>Cost</div>
227261
<div>{spend.cost}</div>
228262
</div>
263+
<div className='flex flex-col'>
264+
<div className='text-muted-foreground'>Asset ID</div>
265+
<Truncated value={spend.assetId} />
266+
</div>
229267
</div>
230268
</div>
231269

0 commit comments

Comments
 (0)