Skip to content

Commit 7d1aa78

Browse files
committed
Clean up token info stuff
1 parent d8d24b7 commit 7d1aa78

File tree

4 files changed

+422
-58
lines changed

4 files changed

+422
-58
lines changed

packages/grill/src/hooks/use-token-info.ts

Lines changed: 43 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { TokenInfo } from "@macalinao/token-utils";
2+
import { createTokenInfo } from "@macalinao/token-utils";
23
import { tokenMetadataSchema } from "@macalinao/zod-solana";
34
import type { Address } from "@solana/kit";
45
import type { UseQueryResult } from "@tanstack/react-query";
@@ -21,74 +22,58 @@ export function useTokenInfo({
2122
const onChainSymbol = metadataAccount?.data.data.symbol;
2223

2324
return useQuery<TokenInfo | null>({
24-
queryKey: [
25-
GRILL_HOOK_CLIENT_KEY,
26-
"tokenInfo",
27-
mint,
28-
uri,
29-
decimals,
30-
onChainName,
31-
onChainSymbol,
32-
] as const,
25+
// eslint-disable-next-line @tanstack/query/exhaustive-deps
26+
queryKey: [GRILL_HOOK_CLIENT_KEY, "tokenInfo", mint] as const,
3327
queryFn: async () => {
34-
if (!mint || decimals === undefined || !onChainName || !onChainSymbol) {
28+
if (!mint || decimals === undefined) {
3529
return null;
3630
}
3731

38-
// If no URI, just return the on-chain data
39-
if (!uri) {
40-
return {
41-
mint,
42-
name: onChainName,
43-
symbol: onChainSymbol,
44-
decimals,
45-
};
46-
}
32+
// Prepare metadata account data
33+
let metadataAccountData: { name: string; symbol: string } | null =
34+
onChainName && onChainSymbol
35+
? { name: onChainName, symbol: onChainSymbol }
36+
: null;
4737

48-
try {
49-
const response = await fetch(uri);
50-
if (!response.ok) {
51-
// If fetch fails, return on-chain data
52-
return {
53-
mint,
54-
name: onChainName,
55-
symbol: onChainSymbol,
56-
decimals,
57-
};
58-
}
59-
const json = (await response.json()) as unknown;
60-
const result = tokenMetadataSchema.safeParse(json);
38+
// Prepare metadata URI JSON data
39+
let metadataUriJson: { image: string } | null = null;
6140

62-
if (result.success) {
63-
return {
64-
mint,
65-
name: result.data.name,
66-
symbol: result.data.symbol,
67-
decimals,
68-
iconURL: result.data.image,
69-
};
41+
// Try to fetch metadata from URI if available
42+
if (uri && metadataAccountData) {
43+
try {
44+
const response = await fetch(uri);
45+
if (response.ok) {
46+
const json = (await response.json()) as unknown;
47+
const result = tokenMetadataSchema.safeParse(json);
48+
49+
if (result.success) {
50+
// Override with data from URI JSON
51+
metadataAccountData = {
52+
name: result.data.name,
53+
symbol: result.data.symbol,
54+
};
55+
if (result.data.image) {
56+
metadataUriJson = { image: result.data.image };
57+
}
58+
} else {
59+
console.error("Invalid token metadata:", result.error);
60+
}
61+
}
62+
} catch (error) {
63+
console.error("Error fetching token info:", error);
7064
}
71-
// If parsing fails, return on-chain data
72-
console.error("Invalid token metadata:", result.error);
73-
return {
74-
mint,
75-
name: onChainName,
76-
symbol: onChainSymbol,
77-
decimals,
78-
};
79-
} catch (error) {
80-
console.error("Error fetching token info:", error);
81-
// If error, return on-chain data
82-
return {
83-
mint,
84-
name: onChainName,
85-
symbol: onChainSymbol,
86-
decimals,
87-
};
8865
}
66+
67+
// Create token info with all collected data
68+
return createTokenInfo({
69+
mint,
70+
mintAccount: { decimals },
71+
metadataAccount: metadataAccountData,
72+
metadataUriJson,
73+
});
8974
},
9075
enabled:
91-
!!mint && decimals !== undefined && !!onChainName && !!onChainSymbol,
76+
!!mint && mintAccount !== undefined && metadataAccount !== undefined,
9277
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
9378
gcTime: 60 * 60 * 1000, // Keep in cache for 1 hour
9479
});
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { address } from "@solana/kit";
3+
import { createTokenInfo } from "./create-token-info.js";
4+
5+
describe("createTokenInfo", () => {
6+
describe("with full metadata", () => {
7+
it("should create TokenInfo with all metadata provided", () => {
8+
const result = createTokenInfo({
9+
mint: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
10+
mintAccount: { decimals: 6 },
11+
metadataAccount: { name: "USD Coin", symbol: "USDC" },
12+
metadataUriJson: { image: "https://example.com/usdc.png" },
13+
});
14+
15+
expect(result).toEqual({
16+
mint: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
17+
name: "USD Coin",
18+
symbol: "USDC",
19+
decimals: 6,
20+
iconURL: "https://example.com/usdc.png",
21+
});
22+
});
23+
24+
it("should create TokenInfo without image when metadataUriJson is null", () => {
25+
const result = createTokenInfo({
26+
mint: address("So11111111111111111111111111111111111111112"),
27+
mintAccount: { decimals: 9 },
28+
metadataAccount: { name: "Wrapped SOL", symbol: "SOL" },
29+
metadataUriJson: null,
30+
});
31+
32+
expect(result).toEqual({
33+
mint: address("So11111111111111111111111111111111111111112"),
34+
name: "Wrapped SOL",
35+
symbol: "SOL",
36+
decimals: 9,
37+
});
38+
});
39+
40+
it("should create TokenInfo without image when metadataUriJson has no image", () => {
41+
const result = createTokenInfo({
42+
mint: address("So11111111111111111111111111111111111111112"),
43+
mintAccount: { decimals: 9 },
44+
metadataAccount: { name: "Wrapped SOL", symbol: "SOL" },
45+
metadataUriJson: { image: "" },
46+
});
47+
48+
expect(result).toEqual({
49+
mint: address("So11111111111111111111111111111111111111112"),
50+
name: "Wrapped SOL",
51+
symbol: "SOL",
52+
decimals: 9,
53+
});
54+
});
55+
});
56+
57+
describe("without metadata account", () => {
58+
it("should derive name and symbol from mint address", () => {
59+
const result = createTokenInfo({
60+
mint: address("7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"),
61+
mintAccount: { decimals: 8 },
62+
metadataAccount: null,
63+
metadataUriJson: null,
64+
});
65+
66+
expect(result).toEqual({
67+
mint: address("7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"),
68+
name: "7vfCXT...",
69+
symbol: "7VFC",
70+
decimals: 8,
71+
});
72+
});
73+
74+
it("should handle addresses with mostly numbers", () => {
75+
const result = createTokenInfo({
76+
mint: address("11111111111111111111111111111111"),
77+
mintAccount: { decimals: 0 },
78+
metadataAccount: null,
79+
metadataUriJson: null,
80+
});
81+
82+
expect(result).toEqual({
83+
mint: address("11111111111111111111111111111111"),
84+
name: "111111...",
85+
symbol: "1111",
86+
decimals: 0,
87+
});
88+
});
89+
90+
it("should handle addresses with special characters", () => {
91+
const result = createTokenInfo({
92+
mint: address("So11111111111111111111111111111111111111112"),
93+
mintAccount: { decimals: 9 },
94+
metadataAccount: null,
95+
metadataUriJson: null,
96+
});
97+
98+
expect(result).toEqual({
99+
mint: address("So11111111111111111111111111111111111111112"),
100+
name: "So1111...",
101+
symbol: "SO11",
102+
decimals: 9,
103+
});
104+
});
105+
106+
it("should derive symbol from first 4 alphanumeric characters", () => {
107+
const result = createTokenInfo({
108+
mint: address("BZTTfsRpY9s9hSgBdjakUoLFLUzr8x9kMKMxFypLcUGK"),
109+
mintAccount: { decimals: 4 },
110+
metadataAccount: null,
111+
metadataUriJson: null,
112+
});
113+
114+
expect(result).toEqual({
115+
mint: address("BZTTfsRpY9s9hSgBdjakUoLFLUzr8x9kMKMxFypLcUGK"),
116+
name: "BZTTfs...",
117+
symbol: "BZTT",
118+
decimals: 4,
119+
});
120+
});
121+
122+
it("should handle addresses starting with lowercase letters", () => {
123+
const result = createTokenInfo({
124+
mint: address("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"),
125+
mintAccount: { decimals: 9 },
126+
metadataAccount: null,
127+
metadataUriJson: null,
128+
});
129+
130+
expect(result).toEqual({
131+
mint: address("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"),
132+
name: "mSoLzY...",
133+
symbol: "MSOL",
134+
decimals: 9,
135+
});
136+
});
137+
138+
it("should still include image if provided without metadata account", () => {
139+
const result = createTokenInfo({
140+
mint: address("TknWMSqRnP4dqhq5rBx8tPGGMzqrfBQYYp7VbeR5xEk"),
141+
mintAccount: { decimals: 6 },
142+
metadataAccount: null,
143+
metadataUriJson: { image: "https://example.com/token.png" },
144+
});
145+
146+
expect(result).toEqual({
147+
mint: address("TknWMSqRnP4dqhq5rBx8tPGGMzqrfBQYYp7VbeR5xEk"),
148+
name: "TknWMS...",
149+
symbol: "TKNW",
150+
decimals: 6,
151+
// No iconURL since metadataAccount is null
152+
});
153+
});
154+
});
155+
156+
describe("edge cases", () => {
157+
it("should handle empty name and symbol in metadata", () => {
158+
const result = createTokenInfo({
159+
mint: address("EmSjmSVNbXGfMqLXnQPJNayVrx8TdPWKQ3rUWqCrcT9t"),
160+
mintAccount: { decimals: 3 },
161+
metadataAccount: { name: "", symbol: "" },
162+
metadataUriJson: null,
163+
});
164+
165+
expect(result).toEqual({
166+
mint: address("EmSjmSVNbXGfMqLXnQPJNayVrx8TdPWKQ3rUWqCrcT9t"),
167+
name: "",
168+
symbol: "",
169+
decimals: 3,
170+
});
171+
});
172+
173+
it("should handle very long name and symbol", () => {
174+
const longName =
175+
"This is a very long token name that exceeds normal length";
176+
const longSymbol = "VERYLONGSYMBOL";
177+
178+
const result = createTokenInfo({
179+
mint: address("LnTRntk96Lc7WMZs6XbRQ9wCtBQszTyRLgdpkczUWsT"),
180+
mintAccount: { decimals: 9 },
181+
metadataAccount: { name: longName, symbol: longSymbol },
182+
metadataUriJson: null,
183+
});
184+
185+
expect(result).toEqual({
186+
mint: address("LnTRntk96Lc7WMZs6XbRQ9wCtBQszTyRLgdpkczUWsT"),
187+
name: longName,
188+
symbol: longSymbol,
189+
decimals: 9,
190+
});
191+
});
192+
193+
it("should handle zero decimals", () => {
194+
const result = createTokenInfo({
195+
mint: address("NFTxPQ2aULDsaBgn5BdgKGZvQF6QKPDSipWrmNGVwUp"),
196+
mintAccount: { decimals: 0 },
197+
metadataAccount: { name: "Cool NFT", symbol: "CNFT" },
198+
metadataUriJson: { image: "https://example.com/nft.jpg" },
199+
});
200+
201+
expect(result).toEqual({
202+
mint: address("NFTxPQ2aULDsaBgn5BdgKGZvQF6QKPDSipWrmNGVwUp"),
203+
name: "Cool NFT",
204+
symbol: "CNFT",
205+
decimals: 0,
206+
iconURL: "https://example.com/nft.jpg",
207+
});
208+
});
209+
210+
it("should handle maximum decimals", () => {
211+
const result = createTokenInfo({
212+
mint: address("MxZ5KhSytcnFEPJk9xxnSBkwvveKGVbWBNUBfrkvuMp"),
213+
mintAccount: { decimals: 18 },
214+
metadataAccount: { name: "Max Decimals", symbol: "MAX" },
215+
metadataUriJson: null,
216+
});
217+
218+
expect(result).toEqual({
219+
mint: address("MxZ5KhSytcnFEPJk9xxnSBkwvveKGVbWBNUBfrkvuMp"),
220+
name: "Max Decimals",
221+
symbol: "MAX",
222+
decimals: 18,
223+
});
224+
});
225+
226+
it("should preserve type information with specific decimal types", () => {
227+
const result = createTokenInfo<string, 6>({
228+
mint: address("TYpEDsQ5BankR5WL6KYSjKfnjrZTQQvh7JJPkBTQHkk"),
229+
mintAccount: { decimals: 6 as const },
230+
metadataAccount: { name: "Typed Token", symbol: "TYPED" },
231+
metadataUriJson: null,
232+
});
233+
234+
expect(result.decimals).toBe(6);
235+
expect(typeof result.decimals).toBe("number");
236+
});
237+
});
238+
239+
describe("symbol derivation", () => {
240+
it("should handle address with mostly numbers", () => {
241+
const result = createTokenInfo({
242+
mint: address("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"),
243+
mintAccount: { decimals: 0 },
244+
metadataAccount: null,
245+
metadataUriJson: null,
246+
});
247+
248+
expect(result.symbol).toBe("2B1K");
249+
});
250+
251+
it("should handle address with mixed case", () => {
252+
const result = createTokenInfo({
253+
mint: address("AbCDefGhJ7VfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj"),
254+
mintAccount: { decimals: 9 },
255+
metadataAccount: null,
256+
metadataUriJson: null,
257+
});
258+
259+
expect(result.symbol).toBe("ABCD");
260+
});
261+
262+
it("should handle address starting with numbers", () => {
263+
const result = createTokenInfo({
264+
mint: address("3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"),
265+
mintAccount: { decimals: 6 },
266+
metadataAccount: null,
267+
metadataUriJson: null,
268+
});
269+
270+
// Should use first 4 alphanumeric
271+
expect(result.symbol).toBe("3NZ9");
272+
});
273+
});
274+
});

0 commit comments

Comments
 (0)