Skip to content

Commit 10cc69d

Browse files
authored
Merge pull request #43 from macalinao/igm/balance
Add helpers for ATA balance/token balance
2 parents 367f904 + aabb7d1 commit 10cc69d

File tree

8 files changed

+283
-1
lines changed

8 files changed

+283
-1
lines changed

.changeset/short-parrots-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@macalinao/grill": patch
3+
---
4+
5+
Add balance helpers

apps/example-dapp/src/components/layout/examples/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Coins,
55
Database,
66
LayoutDashboard,
7+
Wallet,
78
Zap,
89
} from "lucide-react";
910
import type * as React from "react";
@@ -54,6 +55,11 @@ const exampleNavItems: ExampleNavItem[] = [
5455
href: "/examples/tokens",
5556
icon: Database,
5657
},
58+
{
59+
title: "Token Balances",
60+
href: "/examples/token-balances",
61+
icon: Wallet,
62+
},
5763
{
5864
title: "Batch Accounts",
5965
href: "/examples/batch-accounts",

apps/example-dapp/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Route as ExamplesIndexRouteImport } from "./routes/examples/index.tsx"
1515
import { Route as ExamplesWrappedSolRouteImport } from "./routes/examples/wrapped-sol.tsx"
1616
import { Route as ExamplesTransferSolRouteImport } from "./routes/examples/transfer-sol.tsx"
1717
import { Route as ExamplesTokensRouteImport } from "./routes/examples/tokens.tsx"
18+
import { Route as ExamplesTokenBalancesRouteImport } from "./routes/examples/token-balances.tsx"
1819
import { Route as ExamplesDashboardRouteImport } from "./routes/examples/dashboard.tsx"
1920
import { Route as ExamplesBatchAccountsRouteImport } from "./routes/examples/batch-accounts.tsx"
2021

@@ -48,6 +49,11 @@ const ExamplesTokensRoute = ExamplesTokensRouteImport.update({
4849
path: "/tokens",
4950
getParentRoute: () => ExamplesRoute,
5051
} as any)
52+
const ExamplesTokenBalancesRoute = ExamplesTokenBalancesRouteImport.update({
53+
id: "/token-balances",
54+
path: "/token-balances",
55+
getParentRoute: () => ExamplesRoute,
56+
} as any)
5157
const ExamplesDashboardRoute = ExamplesDashboardRouteImport.update({
5258
id: "/dashboard",
5359
path: "/dashboard",
@@ -64,6 +70,7 @@ export interface FileRoutesByFullPath {
6470
"/examples": typeof ExamplesRouteWithChildren
6571
"/examples/batch-accounts": typeof ExamplesBatchAccountsRoute
6672
"/examples/dashboard": typeof ExamplesDashboardRoute
73+
"/examples/token-balances": typeof ExamplesTokenBalancesRoute
6774
"/examples/tokens": typeof ExamplesTokensRoute
6875
"/examples/transfer-sol": typeof ExamplesTransferSolRoute
6976
"/examples/wrapped-sol": typeof ExamplesWrappedSolRoute
@@ -73,6 +80,7 @@ export interface FileRoutesByTo {
7380
"/": typeof IndexRoute
7481
"/examples/batch-accounts": typeof ExamplesBatchAccountsRoute
7582
"/examples/dashboard": typeof ExamplesDashboardRoute
83+
"/examples/token-balances": typeof ExamplesTokenBalancesRoute
7684
"/examples/tokens": typeof ExamplesTokensRoute
7785
"/examples/transfer-sol": typeof ExamplesTransferSolRoute
7886
"/examples/wrapped-sol": typeof ExamplesWrappedSolRoute
@@ -84,6 +92,7 @@ export interface FileRoutesById {
8492
"/examples": typeof ExamplesRouteWithChildren
8593
"/examples/batch-accounts": typeof ExamplesBatchAccountsRoute
8694
"/examples/dashboard": typeof ExamplesDashboardRoute
95+
"/examples/token-balances": typeof ExamplesTokenBalancesRoute
8796
"/examples/tokens": typeof ExamplesTokensRoute
8897
"/examples/transfer-sol": typeof ExamplesTransferSolRoute
8998
"/examples/wrapped-sol": typeof ExamplesWrappedSolRoute
@@ -96,6 +105,7 @@ export interface FileRouteTypes {
96105
| "/examples"
97106
| "/examples/batch-accounts"
98107
| "/examples/dashboard"
108+
| "/examples/token-balances"
99109
| "/examples/tokens"
100110
| "/examples/transfer-sol"
101111
| "/examples/wrapped-sol"
@@ -105,6 +115,7 @@ export interface FileRouteTypes {
105115
| "/"
106116
| "/examples/batch-accounts"
107117
| "/examples/dashboard"
118+
| "/examples/token-balances"
108119
| "/examples/tokens"
109120
| "/examples/transfer-sol"
110121
| "/examples/wrapped-sol"
@@ -115,6 +126,7 @@ export interface FileRouteTypes {
115126
| "/examples"
116127
| "/examples/batch-accounts"
117128
| "/examples/dashboard"
129+
| "/examples/token-balances"
118130
| "/examples/tokens"
119131
| "/examples/transfer-sol"
120132
| "/examples/wrapped-sol"
@@ -170,6 +182,13 @@ declare module "@tanstack/react-router" {
170182
preLoaderRoute: typeof ExamplesTokensRouteImport
171183
parentRoute: typeof ExamplesRoute
172184
}
185+
"/examples/token-balances": {
186+
id: "/examples/token-balances"
187+
path: "/token-balances"
188+
fullPath: "/examples/token-balances"
189+
preLoaderRoute: typeof ExamplesTokenBalancesRouteImport
190+
parentRoute: typeof ExamplesRoute
191+
}
173192
"/examples/dashboard": {
174193
id: "/examples/dashboard"
175194
path: "/dashboard"
@@ -190,6 +209,7 @@ declare module "@tanstack/react-router" {
190209
interface ExamplesRouteChildren {
191210
ExamplesBatchAccountsRoute: typeof ExamplesBatchAccountsRoute
192211
ExamplesDashboardRoute: typeof ExamplesDashboardRoute
212+
ExamplesTokenBalancesRoute: typeof ExamplesTokenBalancesRoute
193213
ExamplesTokensRoute: typeof ExamplesTokensRoute
194214
ExamplesTransferSolRoute: typeof ExamplesTransferSolRoute
195215
ExamplesWrappedSolRoute: typeof ExamplesWrappedSolRoute
@@ -199,6 +219,7 @@ interface ExamplesRouteChildren {
199219
const ExamplesRouteChildren: ExamplesRouteChildren = {
200220
ExamplesBatchAccountsRoute: ExamplesBatchAccountsRoute,
201221
ExamplesDashboardRoute: ExamplesDashboardRoute,
222+
ExamplesTokenBalancesRoute: ExamplesTokenBalancesRoute,
202223
ExamplesTokensRoute: ExamplesTokensRoute,
203224
ExamplesTransferSolRoute: ExamplesTransferSolRoute,
204225
ExamplesWrappedSolRoute: ExamplesWrappedSolRoute,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useATABalance } from "@macalinao/grill";
2+
import { formatTokenAmount } from "@macalinao/token-utils";
3+
import { address } from "@solana/kit";
4+
import { useWallet } from "@solana/wallet-adapter-react";
5+
import { createFileRoute } from "@tanstack/react-router";
6+
7+
export const Route = createFileRoute("/examples/token-balances")({
8+
component: TokenBalances,
9+
});
10+
11+
// Common token mints on mainnet
12+
const TOKENS = [
13+
{
14+
symbol: "USDC",
15+
mint: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
16+
name: "USD Coin",
17+
},
18+
{
19+
symbol: "JUP",
20+
mint: address("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"),
21+
name: "Jupiter",
22+
},
23+
{
24+
symbol: "BONK",
25+
mint: address("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"),
26+
name: "Bonk",
27+
},
28+
{
29+
symbol: "WIF",
30+
mint: address("EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm"),
31+
name: "dogwifhat",
32+
},
33+
{
34+
symbol: "PYTH",
35+
mint: address("HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"),
36+
name: "Pyth Network",
37+
},
38+
{
39+
symbol: "RENDER",
40+
mint: address("rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof"),
41+
name: "Render Token",
42+
},
43+
];
44+
45+
function TokenBalanceRow({
46+
token,
47+
owner,
48+
}: {
49+
token: (typeof TOKENS)[0];
50+
owner: ReturnType<typeof address> | undefined;
51+
}) {
52+
const balance = useATABalance({
53+
mint: token.mint,
54+
owner,
55+
});
56+
57+
return (
58+
<tr className="border-b">
59+
<td className="px-4 py-2 font-medium">{token.symbol}</td>
60+
<td className="px-4 py-2 text-sm text-gray-600">{token.name}</td>
61+
<td className="px-4 py-2 text-right font-mono">
62+
{balance ? formatTokenAmount(balance) : "0"}
63+
</td>
64+
</tr>
65+
);
66+
}
67+
68+
function TokenBalances() {
69+
const { publicKey } = useWallet();
70+
const owner = publicKey ? address(publicKey.toBase58()) : undefined;
71+
72+
return (
73+
<div className="container mx-auto p-6">
74+
<h1 className="text-3xl font-bold mb-6">Token Balances</h1>
75+
76+
{!owner ? (
77+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
78+
<p className="text-yellow-800">
79+
Please connect your wallet to view token balances.
80+
</p>
81+
</div>
82+
) : (
83+
<>
84+
<div className="mb-4">
85+
<p className="text-sm text-gray-600">
86+
Wallet: <span className="font-mono">{owner}</span>
87+
</p>
88+
</div>
89+
90+
<div className="bg-white rounded-lg shadow overflow-hidden">
91+
<table className="w-full">
92+
<thead className="bg-gray-50">
93+
<tr>
94+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
95+
Token
96+
</th>
97+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
98+
Name
99+
</th>
100+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
101+
Balance
102+
</th>
103+
</tr>
104+
</thead>
105+
<tbody className="bg-white divide-y divide-gray-200">
106+
{TOKENS.map((token) => (
107+
<TokenBalanceRow
108+
key={token.mint}
109+
token={token}
110+
owner={owner}
111+
/>
112+
))}
113+
</tbody>
114+
</table>
115+
</div>
116+
117+
<div className="mt-6 text-sm text-gray-500">
118+
<p>
119+
This example demonstrates the <code>useATABalance</code> hook
120+
which fetches token balances for Associated Token Accounts.
121+
</p>
122+
<p className="mt-2">
123+
The hook automatically handles:
124+
<ul className="list-disc list-inside mt-1 ml-4">
125+
<li>Computing the ATA address for each token</li>
126+
<li>Fetching token metadata and decimals</li>
127+
<li>Returning zero balance if the ATA doesn't exist</li>
128+
<li>Formatting the balance with proper decimal places</li>
129+
</ul>
130+
</p>
131+
</div>
132+
</>
133+
)}
134+
</div>
135+
);
136+
}

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

packages/grill/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ export * from "./use-account.js";
55
export * from "./use-accounts.js";
66
export * from "./use-associated-token-account.js";
77
export * from "./use-associated-token-pda.js";
8+
export * from "./use-ata-balance.js";
89
export { useConnectedWallet } from "./use-connected-wallet.js";
910
export { useKitWallet } from "./use-kit-wallet.js";
1011
export * from "./use-mint-account.js";
1112
export * from "./use-send-tx.js";
1213
export * from "./use-token-account.js";
14+
export * from "./use-token-balance.js";
1315
export * from "./use-token-info.js";
1416
export * from "./use-token-metadata-account.js";
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { TokenAmount } from "@macalinao/token-utils";
2+
import { createTokenAmount } from "@macalinao/token-utils";
3+
import type { Address } from "@solana/kit";
4+
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
5+
import { useMemo } from "react";
6+
import { useAssociatedTokenAccount } from "./use-associated-token-account.js";
7+
import { useTokenInfo } from "./use-token-info.js";
8+
9+
export interface UseATABalanceOptions {
10+
/** The mint address of the token */
11+
mint: Address | null | undefined;
12+
/** The owner address of the token account */
13+
owner: Address | null | undefined;
14+
/** The token program address (defaults to TOKEN_PROGRAM_ADDRESS) */
15+
tokenProgram?: Address;
16+
}
17+
18+
/**
19+
* Hook that fetches an Associated Token Account balance as a TokenAmount.
20+
* This combines useAssociatedTokenAccount and useTokenInfo to provide the complete balance information.
21+
*
22+
* @example
23+
* ```typescript
24+
* const balance = useATABalance({
25+
* mint: mintAddress,
26+
* owner: walletAddress,
27+
* });
28+
*
29+
* if (balance) {
30+
* console.log("Balance:", formatTokenAmount(balance));
31+
* }
32+
* ```
33+
*/
34+
export function useATABalance(
35+
options: UseATABalanceOptions,
36+
): TokenAmount | null {
37+
const { mint, owner, tokenProgram = TOKEN_PROGRAM_ADDRESS } = options;
38+
39+
// Fetch the ATA and its data
40+
const { data: ataAccount } = useAssociatedTokenAccount({
41+
mint,
42+
owner,
43+
tokenProgram,
44+
});
45+
46+
// Fetch the token info
47+
const { data: tokenInfo } = useTokenInfo({ mint });
48+
49+
return useMemo(() => {
50+
if (!tokenInfo) {
51+
return null;
52+
}
53+
54+
// If ATA doesn't exist, return zero balance
55+
if (!ataAccount) {
56+
return createTokenAmount(tokenInfo, 0n);
57+
}
58+
59+
return createTokenAmount(tokenInfo, ataAccount.data.amount);
60+
}, [ataAccount, tokenInfo]);
61+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { TokenAmount } from "@macalinao/token-utils";
2+
import { createTokenAmount } from "@macalinao/token-utils";
3+
import type { Address } from "@solana/kit";
4+
import { useMemo } from "react";
5+
import { useTokenAccount } from "./use-token-account.js";
6+
import { useTokenInfo } from "./use-token-info.js";
7+
8+
export interface UseTokenBalanceOptions {
9+
/** The address of the token account */
10+
address: Address | null | undefined;
11+
/** The mint address of the token (optional, will be fetched from token account) */
12+
mint?: Address | null | undefined;
13+
}
14+
15+
/**
16+
* Hook that fetches a token account and its token info to return a TokenAmount.
17+
* This combines useTokenAccount and useTokenInfo to provide the complete balance information.
18+
*
19+
* @example
20+
* ```typescript
21+
* const balance = useTokenBalance({
22+
* address: tokenAccountAddress,
23+
* });
24+
*
25+
* if (balance) {
26+
* console.log("Balance:", formatTokenAmount(balance));
27+
* }
28+
* ```
29+
*/
30+
export function useTokenBalance(
31+
options: UseTokenBalanceOptions,
32+
): TokenAmount | null {
33+
const { address, mint: providedMint } = options;
34+
35+
// Fetch the token account data
36+
const { data: tokenAccount } = useTokenAccount({ address });
37+
38+
// Use provided mint or get it from the token account
39+
const mint = providedMint ?? tokenAccount?.data.mint;
40+
41+
// Fetch the token info
42+
const { data: tokenInfo } = useTokenInfo({ mint });
43+
44+
return useMemo(() => {
45+
if (!(tokenAccount && tokenInfo)) {
46+
return null;
47+
}
48+
49+
return createTokenAmount(tokenInfo, tokenAccount.data.amount);
50+
}, [tokenAccount, tokenInfo]);
51+
}

0 commit comments

Comments
 (0)