Skip to content

Commit 8bd459b

Browse files
authored
feat(entrykit): show only deposit chains with balance (#3730)
1 parent f06e8f2 commit 8bd459b

5 files changed

Lines changed: 96 additions & 58 deletions

File tree

.changeset/ninety-kings-suffer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/entrykit": patch
3+
---
4+
5+
The chain selector dropdown for bridged deposits now only displays chains with available funds.

packages/entrykit/src/formatBalance.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { formatEther } from "viem";
33
export function formatBalance(wei: bigint) {
44
// TODO: should this support non-ether decimals?
55
const formatted = formatEther(wei);
6-
const magnitude = Math.floor(parseFloat(formatted)).toString().length;
7-
return parseFloat(formatted).toLocaleString("en-US", { maximumFractionDigits: Math.max(0, 6 - magnitude) });
6+
const parsed = parseFloat(formatted);
7+
8+
if (parsed > 0 && parsed < 0.00001) {
9+
return "<0.00001";
10+
}
11+
12+
const magnitude = Math.floor(parsed).toString().length;
13+
return parsed.toLocaleString("en-US", { maximumFractionDigits: Math.max(0, 6 - magnitude) });
814
}

packages/entrykit/src/onboarding/deposit/ChainBalance.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/entrykit/src/onboarding/deposit/ChainSelect.tsx

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { useEffect, useMemo } from "react";
2-
import { useAccount, useSwitchChain } from "wagmi";
2+
import { Chain } from "viem";
3+
import { useSwitchChain } from "wagmi";
34
import { twMerge } from "tailwind-merge";
45
import * as Select from "@radix-ui/react-select";
6+
import { RelayChain } from "@reservoir0x/relay-sdk";
57
import { useFrame } from "../../ui/FrameProvider";
68
import { useTheme } from "../../useTheme";
79
import { ChevronUpIcon } from "../../icons/ChevronUpIcon";
810
import { ChevronDownIcon } from "../../icons/ChevronDownIcon";
911
import { Input } from "../../ui/Input";
10-
import { ChainBalance } from "./ChainBalance";
1112
import { ChainIcon } from "./ChainIcon";
1213
import { useRelay } from "./useRelay";
14+
import { useShowQueryError } from "../../errors/useShowQueryError";
15+
import { useChainBalances } from "./useChainBalances";
16+
import { Balance } from "../../ui/Balance";
17+
import { PendingIcon } from "../../icons/PendingIcon";
18+
19+
export type ChainWithRelay = Chain & {
20+
relayChain?: RelayChain & {
21+
icon?: Record<string, string>;
22+
};
23+
};
1324

1425
export type Props = {
1526
value: number;
@@ -19,7 +30,6 @@ export type Props = {
1930
export function ChainSelect({ value, onChange }: Props) {
2031
const theme = useTheme();
2132
const { frame } = useFrame();
22-
const userAccount = useAccount();
2333
const { chains, switchChain } = useSwitchChain();
2434
const relay = useRelay();
2535
const relayChains = relay.data?.chains;
@@ -31,28 +41,40 @@ export function ChainSelect({ value, onChange }: Props) {
3141
return {
3242
...sourceChain,
3343
relayChain,
34-
};
44+
} satisfies ChainWithRelay;
3545
})
3646
.filter((c) => c.relayChain);
3747
}, [chains, relayChains]);
3848

3949
const selectedChain = sourceChains.find((c) => c.id === value)!;
50+
const { data: chainsBalances, isLoading } = useShowQueryError(useChainBalances({ chains: sourceChains }));
51+
52+
const renderedChains = useMemo(() => {
53+
if (!chainsBalances) return [];
54+
const chainsWithBalance = chainsBalances
55+
.filter(({ balance }) => balance.value > 0n)
56+
.map(({ chain, balance }) => ({ chain, balance: balance.value }));
57+
58+
return chainsWithBalance.length > 0 ? chainsWithBalance : sourceChains.map((chain) => ({ chain, balance: 0n }));
59+
}, [chainsBalances, sourceChains]);
4060

4161
useEffect(() => {
42-
if (sourceChains.length > 0 && !selectedChain) {
43-
const defaultChain = sourceChains[0];
62+
if (
63+
renderedChains.length > 0 &&
64+
(!selectedChain || !renderedChains.find((c) => c.chain.id === selectedChain?.id))
65+
) {
66+
const defaultChain = renderedChains[0].chain;
4467
onChange(defaultChain.id);
4568
switchChain({ chainId: defaultChain.id });
4669
}
47-
}, [value, selectedChain, sourceChains, onChange, switchChain]);
70+
}, [value, selectedChain, renderedChains, onChange, switchChain]);
4871

4972
return (
5073
<Select.Root
5174
value={value.toString()}
5275
onValueChange={(value) => {
53-
// TODO: figure out why onValueChange triggers twice, once with value and once without
5476
if (value) {
55-
const chain = sourceChains.find((chain) => chain.id.toString() === value);
77+
const chain = renderedChains.find((item) => item.chain.id.toString() === value)?.chain;
5678
if (!chain) throw new Error(`Unknown chain selected: ${value}`);
5779
onChange(chain.id);
5880
}
@@ -64,7 +86,6 @@ export function ChainSelect({ value, onChange }: Props) {
6486
<ChainIcon
6587
id={selectedChain?.id}
6688
name={selectedChain?.name}
67-
// TODO: define our own set of icons for each chain
6889
url={selectedChain?.relayChain?.icon?.[theme]}
6990
className="w-8"
7091
/>
@@ -80,33 +101,42 @@ export function ChainSelect({ value, onChange }: Props) {
80101

81102
{frame.contentDocument ? (
82103
<Select.Portal container={frame.contentDocument.body}>
83-
{/* TODO: hardcoded width */}
84-
<Select.Content position="popper" className="w-[352px] mt-1 animate-in fade-in slide-in-from-top-2">
104+
<Select.Content
105+
position="popper"
106+
className="w-[352px] max-h-[230px] overflow-y-auto mt-1 animate-in fade-in slide-in-from-top-2"
107+
>
85108
<Select.Viewport>
86109
<Select.Group
87110
className={twMerge(
88111
"flex flex-col border divide-y",
89112
"bg-neutral-800 text-neutral-300 border-neutral-700 divide-neutral-700",
90113
)}
91114
>
92-
{sourceChains.map((chain) => (
93-
// TODO: figure out why up/down arrow jump to top/bottom rather than cycling through items
94-
<Select.Item
95-
key={chain.id}
96-
value={chain.id.toString()}
97-
className={twMerge(
98-
"group flex p-2.5 gap-2.5 items-center cursor-pointer outline-none",
99-
// TODO: different style for checked/active state
100-
"text-white focus:bg-neutral-700 data-[state=checked]:bg-neutral-900",
101-
)}
102-
>
103-
<ChainIcon id={chain.id} name={chain.name} url={chain.relayChain?.icon?.[theme]} />
104-
<span className="flex-grow flex-shrink-0">{chain.name}</span>
105-
<span className="flex-shrink-0 font-mono text-sm text-neutral-400">
106-
<ChainBalance chainId={chain.id} address={userAccount.address} />
107-
</span>
108-
</Select.Item>
109-
))}
115+
{isLoading ? (
116+
<div className="flex items-center justify-center p-4">
117+
<PendingIcon className="h-6 w-6 animate-spin text-gray-400" />
118+
</div>
119+
) : (
120+
renderedChains.map(({ chain, balance }) => {
121+
// TODO: figure out why up/down arrow jump to top/bottom rather than cycling through items
122+
return (
123+
<Select.Item
124+
key={chain.id}
125+
value={chain.id.toString()}
126+
className={twMerge(
127+
"group flex p-2.5 gap-2.5 items-center cursor-pointer outline-none",
128+
"text-white focus:bg-neutral-700 data-[state=checked]:bg-neutral-900",
129+
)}
130+
>
131+
<ChainIcon id={chain.id} name={chain.name} url={chain.relayChain?.icon?.[theme]} />
132+
<span className="flex-grow flex-shrink-0">{chain.name}</span>
133+
<span className="flex-shrink-0 font-mono text-sm text-neutral-400">
134+
<Balance wei={balance} />
135+
</span>
136+
</Select.Item>
137+
);
138+
})
139+
)}
110140
</Select.Group>
111141
</Select.Viewport>
112142
</Select.Content>
Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
import { useQuery } from "@tanstack/react-query";
2-
import { Hex } from "viem";
3-
import { useChains, useConfig as useWagmiConfig } from "wagmi";
1+
import { useAccount, useConfig as useWagmiConfig } from "wagmi";
42
import { getBalance } from "wagmi/actions";
3+
import { useQuery, skipToken } from "@tanstack/react-query";
4+
import { isNotNull } from "@latticexyz/common/utils";
5+
import { ChainWithRelay } from "./ChainSelect";
56

6-
export type UseChainBalancesOptions = {
7-
address: Hex;
7+
type Props = {
8+
chains: ChainWithRelay[];
89
};
910

10-
export function useChainBalances({ address }: UseChainBalancesOptions) {
11+
export function useChainBalances({ chains }: Props) {
12+
const { address: userAddress } = useAccount();
1113
const wagmiConfig = useWagmiConfig();
12-
const chains = useChains();
1314
const chainIds = chains.map((chain) => chain.id);
15+
1416
return useQuery({
15-
queryKey: ["chainBalances", chainIds],
16-
queryFn: async () => {
17-
return await Promise.all(chains.map(async (chain) => getBalance(wagmiConfig, { chainId: chain.id, address })));
18-
},
19-
// TODO: allow customizing this?
17+
queryKey: ["chainBalances", chainIds, userAddress],
18+
queryFn: userAddress
19+
? async () => {
20+
const chainBalances = await Promise.allSettled(
21+
chains.map(async (chain) => {
22+
const balance = await getBalance(wagmiConfig, { chainId: chain.id, address: userAddress });
23+
return { chain, balance };
24+
}),
25+
);
26+
27+
return chainBalances.map((result) => (result.status === "fulfilled" ? result.value : null)).filter(isNotNull);
28+
}
29+
: skipToken,
2030
refetchInterval: 1000 * 60,
31+
retry: 1,
2132
});
2233
}

0 commit comments

Comments
 (0)