From 501abeae3de2da4140f62114a2fe45b2f72dd6bc Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Thu, 13 Mar 2025 18:24:54 +0100 Subject: [PATCH 1/2] add L1s networks manager, provide also information --- package.json | 3 +- toolbox/src/demo/ToolboxApp.tsx | 6 + toolbox/src/demo/examples/Wallet/AddL1s.tsx | 623 ++++++++++++++++++++ 3 files changed, 631 insertions(+), 1 deletion(-) create mode 100644 toolbox/src/demo/examples/Wallet/AddL1s.tsx diff --git a/package.json b/package.json index 62fa6e5875e..2391dfebc8f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "uuid": "^11.1.0", "viem": "^2.23.10", "vitest": "^3.0.8", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "recharts": "^2.10.3" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.9", diff --git a/toolbox/src/demo/ToolboxApp.tsx b/toolbox/src/demo/ToolboxApp.tsx index 6c6914ffeeb..22e01ba8b22 100644 --- a/toolbox/src/demo/ToolboxApp.tsx +++ b/toolbox/src/demo/ToolboxApp.tsx @@ -25,6 +25,12 @@ const componentGroups: Record = { label: "Switch Chain", component: lazy(() => import('./examples/Wallet/SwitchChain')), fileNames: ["toolbox/src/demo/examples/Wallet/SwitchChain.tsx"] + }, + { + id: 'addL1s', + label: "Add L1s", + component: lazy(() => import('./examples/Wallet/AddL1s')), + fileNames: [] } ], 'Create an L1': [ diff --git a/toolbox/src/demo/examples/Wallet/AddL1s.tsx b/toolbox/src/demo/examples/Wallet/AddL1s.tsx new file mode 100644 index 00000000000..cfbe55e76fb --- /dev/null +++ b/toolbox/src/demo/examples/Wallet/AddL1s.tsx @@ -0,0 +1,623 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { useErrorBoundary } from "react-error-boundary"; +import { Button, Select } from "../../ui"; +import { createWalletClient, custom, AddEthereumChainParameter } from 'viem'; +import { useExampleStore } from "../../utils/store"; + +interface Chain { + chainId: string; + status: string; + chainName: string; + description: string; + rpcUrl: string; + wsUrl?: string; + isTestnet: boolean; + networkToken: { + name: string; + symbol: string; + decimals: number; + logoUri: string; + description: string; + }; + chainLogoUri: string; + enabledFeatures?: string[]; + platformChainId?: string; + subnetId?: string; + vmId?: string; + explorerUrl?: string; +} + +export default function AddL1s() { + const { showBoundary } = useErrorBoundary(); + const { + walletChainId, + walletEVMAddress, + setWalletChainId, + setWalletEVMAddress + } = useExampleStore(); + + const [chains, setChains] = useState([]); + const [filteredChains, setFilteredChains] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [networkType, setNetworkType] = useState("mainnet"); + const [searchTerm, setSearchTerm] = useState(""); + const [addingChainId, setAddingChainId] = useState(null); + const walletConnected = useRef(false); + const [selectedChain, setSelectedChain] = useState(null); + const [copiedField, setCopiedField] = useState(null); + + // Connect to wallet and update store state - only once + useEffect(() => { + async function connectWallet() { + if (walletConnected.current) return; + + try { + if (!window.avalanche) { + console.warn("Core wallet not detected"); + return; + } + + walletConnected.current = true; + const walletClient = createWalletClient({ + transport: custom(window.avalanche), + }); + + // Get chain ID + const chainIdHex = await walletClient.getChainId(); + setWalletChainId(Number(chainIdHex)); + + // Get wallet address - only do this if we don't already have an address + if (!walletEVMAddress) { + const [address] = await walletClient.requestAddresses(); + if (address) { + setWalletEVMAddress(address); + } + } + + // Listen for chain changes + window.avalanche.on('chainChanged', (chainId: string) => { + setWalletChainId(Number(chainId)); + }); + + // Listen for account changes + window.avalanche.on('accountsChanged', (accounts: string[]) => { + if (accounts.length > 0) { + setWalletEVMAddress(accounts[0]); + } + }); + } catch (err) { + console.error("Error connecting to wallet:", err); + } + } + + connectWallet(); + return () => { + // Clean up event listeners if needed + if (window.avalanche) { + window.avalanche.removeListener('chainChanged', () => {}); + window.avalanche.removeListener('accountsChanged', () => {}); + } + }; + }, [setWalletChainId, setWalletEVMAddress, walletEVMAddress]); + + useEffect(() => { + fetchChains(); + }, [networkType]); + + async function fetchChains() { + setIsLoading(true); + setError(null); + try { + // Use the actual Glacier API endpoint + const url = new URL('https://glacier-api.avax.network/v1/chains'); + + // Add the network parameter for filtering + if (networkType) { + url.searchParams.append('network', networkType); + } + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Error fetching chains: ${response.status}`); + } + + const data = await response.json(); + if (data.chains && data.chains.length > 0) { + setChains(data.chains); + } else { + setChains([]); + setError("No chains found for this network type"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch chains"); + console.error("Error fetching chains:", err); + setChains([]); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + if (searchTerm.trim() === '') { + setFilteredChains(chains); + } else { + const term = searchTerm.toLowerCase(); + const filtered = chains.filter(chain => + chain.chainName.toLowerCase().includes(term) || + chain.networkToken.symbol.toLowerCase().includes(term) + ); + setFilteredChains(filtered); + } + }, [chains, searchTerm]); + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + }; + + // Copy to clipboard function + const copyToClipboard = (text: string, field: string) => { + navigator.clipboard.writeText(text).then( + () => { + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + }, + (err) => { + console.error('Could not copy text: ', err); + } + ); + }; + + async function switchToChain(chainId: string) { + try { + if (!window.avalanche) { + throw new Error("Core wallet not detected. Please install Core wallet extension."); + } + + const walletClient = createWalletClient({ + transport: custom(window.avalanche!), + }); + + // Convert decimal chainId to hex + const chainIdHex = `0x${parseInt(chainId).toString(16)}`; + + await walletClient.request({ + id: "switch-chain", + method: "wallet_switchEthereumChain", + params: [{ chainId: chainIdHex }], + }); + + // After switching, update the chain ID in our state + const newChainId = await walletClient.getChainId(); + setWalletChainId(Number(newChainId)); + + return true; + } catch (error: any) { + // Check if the error is because the chain has not been added yet + if (error.code === 4902) { + // Chain not added yet, return false to let the caller know + return false; + } + throw error; + } + } + + async function handleAddToWallet(chain: Chain) { + setAddingChainId(chain.chainId); + try { + if (!window.avalanche) { + throw new Error("Core wallet not detected. Please install Core wallet extension."); + } + + // First try to just switch to the chain, in case it's already added + const switched = await switchToChain(chain.chainId).catch(() => false); + + // If switching was successful, we're done + if (switched) { + // Close the modal if it was open + setSelectedChain(null); + return; + } + + // If we get here, the chain needs to be added first + const walletClient = createWalletClient({ + transport: custom(window.avalanche!), + }); + + // Convert decimal chainId to hex + const chainIdHex = `0x${parseInt(chain.chainId).toString(16)}`; + + const chainConfig: AddEthereumChainParameter = { + chainId: chainIdHex, + chainName: chain.chainName, + nativeCurrency: { + name: chain.networkToken.name, + symbol: chain.networkToken.symbol, + decimals: chain.networkToken.decimals, + }, + rpcUrls: [chain.rpcUrl], + }; + + // Add the chain to wallet + await walletClient.request({ + id: "1", + method: "wallet_addEthereumChain", + params: [{ ...chainConfig, isTestnet: chain.isTestnet } as unknown as AddEthereumChainParameter], + }); + + // Then request to switch to the newly added chain (this is already handled by most wallets after adding) + try { + await walletClient.request({ + id: "2", + method: "wallet_switchEthereumChain", + params: [{ chainId: chainIdHex }], + }); + } catch (switchError) { + console.warn("Error switching chain, but chain was added successfully:", switchError); + } + + // After adding chain, check if the current chain changed + const newChainId = await walletClient.getChainId(); + setWalletChainId(Number(newChainId)); + + // Close the modal if it was open + setSelectedChain(null); + } catch (error) { + showBoundary(error); + } finally { + setAddingChainId(null); + } + } + + const handleNetworkTypeChange = (value: string | number) => { + setNetworkType(value.toString()); + }; + + // Function to show chain details modal + const openChainDetails = (chain: Chain) => { + setSelectedChain(chain); + }; + + // Function to close the chain details modal + const closeChainDetails = () => { + setSelectedChain(null); + }; + + // Utility function to abbreviate long strings + const abbreviateString = (str: string, startChars = 6, endChars = 4) => { + if (!str || str.length <= startChars + endChars + 3) return str; + return `${str.substring(0, startChars)}...${str.substring(str.length - endChars)}`; + }; + + // Updated utility function to truncate strings in the middle if needed + const truncateMiddle = (str: string, maxLength = 40) => { + if (!str || str.length <= maxLength) return str; + + // Calculate how many characters to show at the beginning and end + // For very long strings, we want to show more at both ends + const halfLength = Math.floor((maxLength - 3) / 2); // -3 for the ellipsis + + // For platform chain IDs and subnet IDs, we want to show more characters + if (str.length > 50) { + return `${str.substring(0, 25)}...${str.substring(str.length - 25)}`; + } + + return `${str.substring(0, maxLength - halfLength - 3)}...${str.substring(str.length - halfLength)}`; + }; + + // Component for copyable field + const CopyableField = ({ label, value, fieldId }: { label: string, value: string, fieldId: string }) => { + if (!value) return null; + + // Determine if the field should use special display based on field type + const isSpecialId = fieldId === 'platformChainId' || fieldId === 'subnetId'; + // URLs should be displayed in full + const isUrl = fieldId === 'rpcUrl' || fieldId === 'wsUrl' || label.includes('URL'); + // Token names should be handled differently + const isTokenName = fieldId === 'name' || label === 'Name'; + + // Select appropriate display method based on field type + let displayValue = value; + if (isUrl) { + // Display URLs in full without truncation + displayValue = value; + } else if (isSpecialId) { + displayValue = truncateMiddle(value, 60); + } else if (isTokenName && value.length > 20) { + displayValue = value.substring(0, 20) + '...'; + } else if (value.length > 25) { + displayValue = abbreviateString(value, 10, 6); + } + + return ( +
+
{label}:
+
+
copyToClipboard(value, fieldId)} + title={`Click to copy: ${value}`} + > +
+ {displayValue} +
+
+ {copiedField === fieldId ? ( + + + + ) : ( + + + + )} +
+
+
+
+ ); + }; + + // Non-copyable field component for consistent styling + const Field = ({ label, value }: { label: string, value: string | number | boolean }) => { + if (value === undefined || value === null) return null; + + // Handle long text values + const displayValue = typeof value === 'string' && value.length > 25 + ? value.substring(0, 25) + '...' + : value.toString(); + + return ( +
+
{label}:
+
+
+ {displayValue} +
+
+
+ ); + }; + + // URL field with link component + const UrlField = ({ label, url }: { label: string, url: string }) => { + if (!url) return null; + + // Show the full URL without abbreviation + const displayUrl = url; + + return ( +
+
{label}:
+ +
+ ); + }; + + return ( +
+

Add L1 Chains

+ +
+
+
+ + {searchTerm && ( + + )} +
+
+
+ + {error &&
{error}
} + + {isLoading ? ( +
Loading chains...
+ ) : ( +
+ {filteredChains.length > 0 ? ( + filteredChains.map((chain) => ( +
openChainDetails(chain)} + > +
+ {chain.chainLogoUri && ( + {`${chain.chainName} { + // Handle image loading errors + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + )} +
+
{chain.chainName}
+
{chain.networkToken.symbol}
+
+
+
{ + e.stopPropagation(); // Prevent opening the modal + handleAddToWallet(chain); + }} + > + +
+
+ )) + ) : ( +
+ {searchTerm ? + `No chains found matching "${searchTerm}"` : + "No chains found for this network type" + } +
+ )} +
+ )} +
+ + {/* Chain details modal */} + {selectedChain && ( +
+
+ {/* Modal header */} +
+
+ {selectedChain.chainLogoUri && ( + {`${selectedChain.chainName} { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + )} +

{selectedChain.chainName}

+
+ +
+ + {/* Modal content - scrollable */} +
+ {/* Description section */} +
+

Description

+
+ {selectedChain.description || "No description available"} +
+
+ + {/* Chain Details */} +
+

Chain Details

+
+ + + + {selectedChain.platformChainId && ( + + )} + {selectedChain.subnetId && ( + + )} +
+
+ + {/* Network Token */} +
+

Network Token

+
+ + + +
+
+ + {/* URLs */} +
+

URLs

+
+ + {selectedChain.wsUrl && ( + + )} + {selectedChain.explorerUrl && ( + + )} +
+
+
+ + {/* Modal footer - sticky */} +
+ +
+
+
+ )} + + ); +} \ No newline at end of file From 09d3a7335da27ac683f9045a6fb2a0f8ad7c95b9 Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Mon, 17 Mar 2025 18:25:42 +0100 Subject: [PATCH 2/2] Use avacloudSDK for fetching chains, removed and fixed the connection wallet --- toolbox/src/demo/examples/Wallet/AddL1s.tsx | 121 +++----------------- 1 file changed, 16 insertions(+), 105 deletions(-) diff --git a/toolbox/src/demo/examples/Wallet/AddL1s.tsx b/toolbox/src/demo/examples/Wallet/AddL1s.tsx index cfbe55e76fb..04a988c7f6b 100644 --- a/toolbox/src/demo/examples/Wallet/AddL1s.tsx +++ b/toolbox/src/demo/examples/Wallet/AddL1s.tsx @@ -1,108 +1,32 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState } from "react"; import { useErrorBoundary } from "react-error-boundary"; import { Button, Select } from "../../ui"; import { createWalletClient, custom, AddEthereumChainParameter } from 'viem'; import { useExampleStore } from "../../utils/store"; +import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; +import { ChainInfo } from "@avalabs/avacloud-sdk/models/components"; -interface Chain { - chainId: string; - status: string; - chainName: string; - description: string; - rpcUrl: string; - wsUrl?: string; - isTestnet: boolean; - networkToken: { - name: string; - symbol: string; - decimals: number; - logoUri: string; - description: string; - }; - chainLogoUri: string; - enabledFeatures?: string[]; - platformChainId?: string; - subnetId?: string; - vmId?: string; - explorerUrl?: string; -} +const sdk = new AvaCloudSDK(); export default function AddL1s() { const { showBoundary } = useErrorBoundary(); const { walletChainId, - walletEVMAddress, - setWalletChainId, - setWalletEVMAddress + setWalletChainId } = useExampleStore(); - const [chains, setChains] = useState([]); - const [filteredChains, setFilteredChains] = useState([]); + const [chains, setChains] = useState([]); + const [filteredChains, setFilteredChains] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [networkType, setNetworkType] = useState("mainnet"); const [searchTerm, setSearchTerm] = useState(""); const [addingChainId, setAddingChainId] = useState(null); - const walletConnected = useRef(false); - const [selectedChain, setSelectedChain] = useState(null); + const [selectedChain, setSelectedChain] = useState(null); const [copiedField, setCopiedField] = useState(null); - // Connect to wallet and update store state - only once - useEffect(() => { - async function connectWallet() { - if (walletConnected.current) return; - - try { - if (!window.avalanche) { - console.warn("Core wallet not detected"); - return; - } - - walletConnected.current = true; - const walletClient = createWalletClient({ - transport: custom(window.avalanche), - }); - - // Get chain ID - const chainIdHex = await walletClient.getChainId(); - setWalletChainId(Number(chainIdHex)); - - // Get wallet address - only do this if we don't already have an address - if (!walletEVMAddress) { - const [address] = await walletClient.requestAddresses(); - if (address) { - setWalletEVMAddress(address); - } - } - - // Listen for chain changes - window.avalanche.on('chainChanged', (chainId: string) => { - setWalletChainId(Number(chainId)); - }); - - // Listen for account changes - window.avalanche.on('accountsChanged', (accounts: string[]) => { - if (accounts.length > 0) { - setWalletEVMAddress(accounts[0]); - } - }); - } catch (err) { - console.error("Error connecting to wallet:", err); - } - } - - connectWallet(); - return () => { - // Clean up event listeners if needed - if (window.avalanche) { - window.avalanche.removeListener('chainChanged', () => {}); - window.avalanche.removeListener('accountsChanged', () => {}); - } - }; - }, [setWalletChainId, setWalletEVMAddress, walletEVMAddress]); - useEffect(() => { fetchChains(); }, [networkType]); @@ -111,28 +35,15 @@ export default function AddL1s() { setIsLoading(true); setError(null); try { - // Use the actual Glacier API endpoint - const url = new URL('https://glacier-api.avax.network/v1/chains'); - - // Add the network parameter for filtering - if (networkType) { - url.searchParams.append('network', networkType); - } - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, + // Use AvaCloudSDK to get chains + const result = await sdk.data.evm.chains.supportedChains({ + network: networkType as "mainnet" | "testnet" }); - if (!response.ok) { - throw new Error(`Error fetching chains: ${response.status}`); - } + const chainsData = result.chains; - const data = await response.json(); - if (data.chains && data.chains.length > 0) { - setChains(data.chains); + if (chainsData && chainsData.length > 0) { + setChains(chainsData); } else { setChains([]); setError("No chains found for this network type"); @@ -210,7 +121,7 @@ export default function AddL1s() { } } - async function handleAddToWallet(chain: Chain) { + async function handleAddToWallet(chain: ChainInfo) { setAddingChainId(chain.chainId); try { if (!window.avalanche) { @@ -282,7 +193,7 @@ export default function AddL1s() { }; // Function to show chain details modal - const openChainDetails = (chain: Chain) => { + const openChainDetails = (chain: ChainInfo) => { setSelectedChain(chain); };