Skip to content

Commit db53847

Browse files
committed
[NEB-248] Show native token balances in assets, show popular tokens on top (#7056)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR enhances the `AssetsSection` component by integrating wallet balance retrieval, modifying how assets are displayed, and implementing new logic for sorting tokens. It also introduces a new constant for native tokens and adjusts rendering based on whether an asset is native. ### Detailed summary - Added `NATIVE_TOKEN_ADDRESS` and `getWalletBalance` import from `thirdweb`. - Updated `key` for `AssetItem` to include `chain_id`. - Introduced `isNativeToken` to conditionally render the `ChainIconClient`. - Modified the `href` for `Link` based on whether the asset is a native token. - Added a new set `tokensToShowOnTop` for sorting specific tokens. - Implemented `nativeBalances` query to fetch wallet balances for various chains. - Combined `nativeBalances` and `assetsQuery` data for rendering in `AssetsSectionUI`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 2057546 commit db53847

File tree

1 file changed

+102
-18
lines changed

1 file changed

+102
-18
lines changed

apps/dashboard/src/app/nebula-app/(app)/components/AssetsSection/AssetsSection.tsx

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import { isProd } from "@/constants/env-utils";
33
import { useQuery } from "@tanstack/react-query";
44
import { XIcon } from "lucide-react";
55
import Link from "next/link";
6-
import { type ThirdwebClient, defineChain, toTokens } from "thirdweb";
6+
import {
7+
NATIVE_TOKEN_ADDRESS,
8+
type ThirdwebClient,
9+
defineChain,
10+
getAddress,
11+
toTokens,
12+
} from "thirdweb";
713
import {
814
Blobbie,
915
TokenIcon,
1016
TokenProvider,
1117
useActiveAccount,
1218
useActiveWalletChain,
1319
} from "thirdweb/react";
20+
import { getWalletBalance } from "thirdweb/wallets";
1421
import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
1522
import { useAllChainsData } from "../../../../../hooks/chains/allChains";
1623
import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
@@ -45,7 +52,7 @@ export function AssetsSectionUI(props: {
4552
{!props.isPending &&
4653
props.data.map((asset) => (
4754
<AssetItem
48-
key={asset.token_address}
55+
key={`${asset.chain_id}-${asset.token_address}`}
4956
asset={asset}
5057
client={props.client}
5158
/>
@@ -78,6 +85,8 @@ function AssetItem(props: {
7885
}) {
7986
const { idToChain } = useAllChainsData();
8087
const chainMeta = idToChain.get(props.asset.chain_id);
88+
const isNativeToken = props.asset.token_address === NATIVE_TOKEN_ADDRESS;
89+
8190
return (
8291
<TokenProvider
8392
address={props.asset.token_address}
@@ -88,7 +97,7 @@ function AssetItem(props: {
8897
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
8998
<div className="relative">
9099
<TokenIcon
91-
className="size-8 rounded-full"
100+
className="size-8 rounded-full border"
92101
loadingComponent={
93102
<Blobbie
94103
address={props.asset.token_address}
@@ -102,25 +111,31 @@ function AssetItem(props: {
102111
/>
103112
}
104113
/>
105-
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
106-
<ChainIconClient
107-
client={props.client}
108-
className="size-3.5"
109-
src={chainMeta?.icon?.url || ""}
110-
/>
111-
</div>
114+
{!isNativeToken && (
115+
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
116+
<ChainIconClient
117+
client={props.client}
118+
className="size-3.5"
119+
src={chainMeta?.icon?.url || ""}
120+
/>
121+
</div>
122+
)}
112123
</div>
113124

114125
<div className="flex min-w-0 flex-col text-sm">
115126
<Link
116-
href={`https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`}
127+
href={
128+
isNativeToken
129+
? `https://thirdweb.com/${props.asset.chain_id}`
130+
: `https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`
131+
}
117132
target="_blank"
118133
className="truncate font-medium before:absolute before:inset-0"
119134
>
120135
{props.asset.name}
121136
</Link>
122137

123-
<p className="text-muted-foreground text-sm">
138+
<p className="truncate text-muted-foreground text-sm">
124139
{`${toTokens(BigInt(props.asset.balance), props.asset.decimals)} ${props.asset.symbol}`}
125140
</p>
126141
</div>
@@ -158,16 +173,85 @@ export function AssetsSection(props: {
158173
data: AssetBalance[];
159174
};
160175

161-
return json.data;
176+
const tokensToShowOnTop = new Set(
177+
[
178+
// base
179+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // usdc,
180+
"0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // wbtc
181+
"0x4200000000000000000000000000000000000006", // wrapped eth
182+
// ethereum
183+
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // usdc
184+
"0xdac17f958d2ee523a2206206994597c13d831ec7", // usdt
185+
"0xB8c77482e45F1F44dE1745F52C74426C631bDD52", // bnb
186+
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // weth
187+
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", // wbtc
188+
// optimism
189+
"0x4200000000000000000000000000000000000042", // op token
190+
"0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1", // world coin
191+
"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", // usdt
192+
"0x0b2c639c533813f4aa9d7837caf62653d097ff85", // usdc
193+
"0x4200000000000000000000000000000000000006", // wrapped eth
194+
// polygon
195+
"0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", // weth
196+
"0xc2132d05d31c914a87c6611c10748aeb04b58e8f", // usdt
197+
"0x3BA4c387f786bFEE076A58914F5Bd38d668B42c3", // bnb
198+
"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // usdc
199+
"0x2791bca1f2de4661ed88a30c99a7a9449aa84174", // usdc.e
200+
].map((x) => getAddress(x)),
201+
);
202+
203+
return json.data.sort((a, b) => {
204+
if (tokensToShowOnTop.has(getAddress(a.token_address))) {
205+
return -1;
206+
}
207+
if (tokensToShowOnTop.has(getAddress(b.token_address))) {
208+
return 1;
209+
}
210+
return 0;
211+
});
162212
},
163213
enabled: !!account && !!activeChain,
164214
});
165215

216+
const nativeBalances = useQuery({
217+
queryKey: ["getWalletBalance", account?.address, activeChain?.id],
218+
queryFn: async () => {
219+
if (!account || !activeChain) {
220+
return [];
221+
}
222+
223+
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
224+
225+
const result = await Promise.allSettled(
226+
chains.map((chain) =>
227+
getWalletBalance({
228+
// eslint-disable-next-line no-restricted-syntax
229+
chain: defineChain(chain),
230+
client: props.client,
231+
address: account.address,
232+
}),
233+
),
234+
);
235+
236+
return result
237+
.filter((r) => r.status === "fulfilled")
238+
.map((r) => ({
239+
chain_id: r.value.chainId,
240+
token_address: r.value.tokenAddress,
241+
balance: r.value.value.toString(),
242+
name: r.value.name,
243+
symbol: r.value.symbol,
244+
decimals: r.value.decimals,
245+
}))
246+
.filter((x) => x.balance !== "0") as AssetBalance[];
247+
},
248+
});
249+
250+
const isPending = assetsQuery.isPending || nativeBalances.isPending;
251+
252+
const data = [...(nativeBalances.data ?? []), ...(assetsQuery.data ?? [])];
253+
166254
return (
167-
<AssetsSectionUI
168-
data={assetsQuery.data ?? []}
169-
isPending={assetsQuery.isPending}
170-
client={props.client}
171-
/>
255+
<AssetsSectionUI data={data} isPending={isPending} client={props.client} />
172256
);
173257
}

0 commit comments

Comments
 (0)