Skip to content

Commit 41dc87f

Browse files
committed
network selector
1 parent 06521a4 commit 41dc87f

File tree

3 files changed

+214
-77
lines changed

3 files changed

+214
-77
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/data-table.tsx

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import {} from "components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset";
2929
import { useTxNotifications } from "hooks/useTxNotifications";
3030
import Link from "next/link";
31+
import { useState } from "react";
3132
import { useForm } from "react-hook-form";
3233
import {
3334
defineChain,
@@ -47,6 +48,7 @@ import {
4748
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
4849
import { concatHex, padHex } from "thirdweb/utils";
4950
import { z } from "zod";
51+
import { SingleNetworkSelector } from "./single-network-selector";
5052

5153
type CrossChain = {
5254
id: number;
@@ -94,6 +96,7 @@ export function DataTable({
9496
"Successfully deployed contract",
9597
"Failed to deploy contract",
9698
);
99+
const [tableData, setTableData] = useState(data);
97100

98101
const form = useForm<FormSchema>({
99102
resolver: zodResolver(formSchema),
@@ -153,7 +156,7 @@ export function DataTable({
153156
];
154157

155158
const table = useReactTable({
156-
data,
159+
data: tableData,
157160
columns,
158161
getCoreRowModel: getCoreRowModel(),
159162
});
@@ -248,49 +251,78 @@ export function DataTable({
248251
}
249252
};
250253

254+
const handleAddRow = (chain: { chainId: number; name: string }) => {
255+
const existingChain = tableData.find(
256+
(row) => row.chainId === chain.chainId,
257+
);
258+
if (existingChain) {
259+
return;
260+
}
261+
262+
const newRow: CrossChain = {
263+
id: chain.chainId,
264+
network: chain.name,
265+
chainId: chain.chainId,
266+
status: "NOT_DEPLOYED",
267+
};
268+
269+
setTableData((prevData) => [...prevData, newRow]);
270+
};
271+
251272
return (
252273
<Form {...form}>
253274
<form>
254-
<TableContainer>
255-
<Table>
256-
<TableHeader>
257-
{table.getHeaderGroups().map((headerGroup) => (
258-
<TableRow key={headerGroup.id}>
259-
{headerGroup.headers.map((header) => {
260-
return (
261-
<TableHead key={header.id}>
262-
{header.isPlaceholder
263-
? null
264-
: flexRender(
265-
header.column.columnDef.header,
266-
header.getContext(),
267-
)}
268-
</TableHead>
269-
);
270-
})}
271-
</TableRow>
272-
))}
273-
</TableHeader>
274-
<TableBody>
275-
{table.getRowModel().rows.map((row) => (
276-
<TableRow
277-
key={row.id}
278-
data-state={row.getIsSelected() && "selected"}
279-
>
280-
{row.getVisibleCells().map((cell) => (
281-
<TableCell key={cell.id}>
282-
{flexRender(
283-
cell.column.columnDef.cell,
284-
cell.getContext(),
285-
)}
286-
</TableCell>
287-
))}
288-
</TableRow>
289-
))}
290-
</TableBody>
291-
</Table>
292-
<DeployStatusModal deployStatusModal={deployStatusModal} />
293-
</TableContainer>
275+
<div
276+
style={{
277+
maxHeight: "500px",
278+
overflowY: "auto",
279+
}}
280+
>
281+
<TableContainer>
282+
<Table>
283+
<TableHeader>
284+
{table.getHeaderGroups().map((headerGroup) => (
285+
<TableRow key={headerGroup.id}>
286+
{headerGroup.headers.map((header) => {
287+
return (
288+
<TableHead key={header.id}>
289+
{header.isPlaceholder
290+
? null
291+
: flexRender(
292+
header.column.columnDef.header,
293+
header.getContext(),
294+
)}
295+
</TableHead>
296+
);
297+
})}
298+
</TableRow>
299+
))}
300+
</TableHeader>
301+
<TableBody>
302+
{table.getRowModel().rows.map((row) => (
303+
<TableRow
304+
key={row.id}
305+
data-state={row.getIsSelected() && "selected"}
306+
>
307+
{row.getVisibleCells().map((cell) => (
308+
<TableCell key={cell.id}>
309+
{flexRender(
310+
cell.column.columnDef.cell,
311+
cell.getContext(),
312+
)}
313+
</TableCell>
314+
))}
315+
</TableRow>
316+
))}
317+
</TableBody>
318+
</Table>
319+
<DeployStatusModal deployStatusModal={deployStatusModal} />
320+
</TableContainer>
321+
</div>
322+
323+
<div className="mt-4">
324+
<SingleNetworkSelector onAddRow={handleAddRow} className="w-full" />
325+
</div>
294326
</form>
295327
</Form>
296328
);

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/page.tsx

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
parseEventLogs,
88
prepareEvent,
99
} from "thirdweb";
10-
import { defineChain, getChainMetadata } from "thirdweb/chains";
10+
import {
11+
type ChainMetadata,
12+
defineChain,
13+
getChainMetadata,
14+
} from "thirdweb/chains";
1115
import {
1216
type FetchDeployMetadataResult,
1317
getContract,
@@ -30,6 +34,19 @@ export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
3034
);
3135
}
3236

37+
async function fetchChainsFromApi() {
38+
const res = await fetch("https://api.thirdweb.com/v1/chains");
39+
const json = await res.json();
40+
41+
if (json.error) {
42+
throw new Error(json.error.message);
43+
}
44+
45+
return json.data as ChainMetadata[];
46+
}
47+
48+
const allChains = await fetchChainsFromApi();
49+
3350
export default async function Page(props: {
3451
params: Promise<{
3552
contractAddress: string;
@@ -118,41 +135,43 @@ export default async function Page(props: {
118135
creationBlockNumber = event?.blockNumber;
119136
}
120137

121-
const topOPStackTestnetChainIds = [
122-
84532, // Base
123-
11155420, // OP testnet
124-
919, // Mode Network
125-
111557560, // Cyber
126-
999999999, // Zora
127-
11155111, // sepolia
128-
421614,
129-
];
130-
131-
const chainsDeployedOn = await Promise.all(
132-
topOPStackTestnetChainIds.map(async (chainId) => {
133-
// eslint-disable-next-line no-restricted-syntax
134-
const chain = defineChain(chainId);
135-
const chainMetadata = await getChainMetadata(chain);
136-
137-
const rpcRequest = getRpcClient({
138-
client: contract.client,
139-
chain,
140-
});
141-
const code = await eth_getCode(rpcRequest, {
142-
address: params.contractAddress,
143-
});
144-
145-
return {
146-
id: chainId,
147-
network: chainMetadata.name,
148-
chainId: chain.id,
149-
status:
150-
code === originalCode
151-
? ("DEPLOYED" as const)
152-
: ("NOT_DEPLOYED" as const),
153-
};
154-
}),
155-
);
138+
const chainsDeployedOn = (
139+
await Promise.all(
140+
allChains.map(async (c) => {
141+
// eslint-disable-next-line no-restricted-syntax
142+
const chain = defineChain(c.chainId);
143+
144+
try {
145+
const chainMetadata = await getChainMetadata(chain);
146+
147+
const rpcRequest = getRpcClient({
148+
client: contract.client,
149+
chain,
150+
});
151+
const code = await eth_getCode(rpcRequest, {
152+
address: params.contractAddress,
153+
});
154+
155+
return {
156+
id: chain.id,
157+
network: chainMetadata.name,
158+
chainId: chain.id,
159+
status:
160+
code === originalCode
161+
? ("DEPLOYED" as const)
162+
: ("NOT_DEPLOYED" as const),
163+
};
164+
} catch {
165+
return {
166+
id: chain.id,
167+
network: "",
168+
chainId: chain.id,
169+
status: "NOT_DEPLOYED" as const,
170+
};
171+
}
172+
}),
173+
)
174+
).filter((c) => c.status === "DEPLOYED");
156175

157176
const coreMetadata = (
158177
await fetchPublishedContractsFromDeploy({
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { SelectWithSearch } from "@/components/blocks/select-with-search";
2+
import { Badge } from "@/components/ui/badge";
3+
import { ChainIcon } from "components/icons/ChainIcon";
4+
import { useAllChainsData } from "hooks/chains/allChains";
5+
import { useCallback, useMemo } from "react";
6+
7+
type Option = { label: string; value: string };
8+
9+
export function SingleNetworkSelector(props: {
10+
onAddRow: (chain: { chainId: number; name: string }) => void;
11+
className?: string;
12+
}) {
13+
const { allChains, idToChain } = useAllChainsData();
14+
15+
const options = useMemo(() => {
16+
return allChains.map((chain) => ({
17+
label: chain.name,
18+
value: String(chain.chainId),
19+
}));
20+
}, [allChains]);
21+
22+
const searchFn = useCallback(
23+
(option: Option, searchValue: string) => {
24+
const chain = idToChain.get(Number(option.value));
25+
if (!chain) {
26+
return false;
27+
}
28+
29+
if (Number.isInteger(Number.parseInt(searchValue))) {
30+
return String(chain.chainId).startsWith(searchValue);
31+
}
32+
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
33+
},
34+
[idToChain],
35+
);
36+
37+
const renderOption = useCallback(
38+
(option: Option) => {
39+
const chain = idToChain.get(Number(option.value));
40+
if (!chain) {
41+
return option.label;
42+
}
43+
44+
return (
45+
<div className="flex justify-between gap-4">
46+
<span className="flex grow gap-2 truncate text-left">
47+
<ChainIcon
48+
className="size-5"
49+
ipfsSrc={chain.icon?.url}
50+
loading="lazy"
51+
/>
52+
{chain.name}
53+
</span>
54+
<Badge variant="outline" className="gap-2 max-sm:hidden">
55+
<span className="text-muted-foreground">Chain ID</span>
56+
{chain.chainId}
57+
</Badge>
58+
</div>
59+
);
60+
},
61+
[idToChain],
62+
);
63+
64+
const handleChange = (chainId: string) => {
65+
const chain = idToChain.get(Number(chainId));
66+
if (chain) {
67+
props.onAddRow({ chainId: chain.chainId, name: chain.name });
68+
}
69+
};
70+
71+
return (
72+
<SelectWithSearch
73+
searchPlaceholder="Search by Name or Chain ID"
74+
value={undefined} // Reset the search field after selection
75+
options={options}
76+
onValueChange={handleChange}
77+
placeholder={
78+
allChains.length === 0 ? "Loading Chains..." : "Search Chains"
79+
}
80+
overrideSearchFn={searchFn}
81+
renderOption={renderOption}
82+
className={props.className}
83+
disabled={allChains.length === 0}
84+
/>
85+
);
86+
}

0 commit comments

Comments
 (0)