diff --git a/components/tools/common/api/validator-info.ts b/components/tools/common/api/validator-info.ts index d5ece29657c..3d8dd606fb4 100644 --- a/components/tools/common/api/validator-info.ts +++ b/components/tools/common/api/validator-info.ts @@ -2,6 +2,7 @@ import { Validator, SubnetInfo, L1ValidatorManagerDetails } from './types'; import { pChainEndpoint } from './consts'; import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; export const avaCloudSDK = new AvaCloudSDK({ + serverURL: "https://api.avax.network", chainId: "43114", network: "fuji", }); diff --git a/package.json b/package.json index 91a1b96a275..4a6f6baa817 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "postinstall": "prisma generate && fumadocs-mdx && cd toolbox && yarn" }, "dependencies": { - "@avalabs/avacloud-sdk": "0.8.7", + "@avalabs/avacloud-sdk": "0.12.1", "@avalabs/avalanchejs": "^5.0.0", "@fumadocs/mdx-remote": "^1.2.0", "@hookform/resolvers": "^4.1.3", @@ -128,4 +128,4 @@ "typescript": "^5.8.2" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} +} \ No newline at end of file diff --git a/toolbox/package.json b/toolbox/package.json index fce5d3ec7b0..ead300e0986 100644 --- a/toolbox/package.json +++ b/toolbox/package.json @@ -26,7 +26,7 @@ "postinstall": "node update_docker_tags.js" }, "dependencies": { - "@avalabs/avacloud-sdk": "^0.9.0", + "@avalabs/avacloud-sdk": "^0.12.1", "@avalabs/avalanchejs": "^4.1.2-alpha.4", "@radix-ui/react-alert-dialog": "^1.1.13", "@radix-ui/react-checkbox": "^1.2.3", @@ -72,4 +72,4 @@ "vite": "^6.1.6", "vite-plugin-dts": "^4.5.0" } -} +} \ No newline at end of file diff --git a/toolbox/src/components/DetailsDisplay.tsx b/toolbox/src/components/DetailsDisplay.tsx new file mode 100644 index 00000000000..3d5095ddd8a --- /dev/null +++ b/toolbox/src/components/DetailsDisplay.tsx @@ -0,0 +1,283 @@ +"use client" + +import { Calendar, Users, Database, Key, Copy, AlertTriangle, FileText, Globe } from "lucide-react" +import { useState } from "react" +import { Subnet } from "@avalabs/avacloud-sdk/models/components"; +import type { BlockchainInfo } from "./SelectBlockchain"; + +interface DetailsDisplayProps { + type: 'blockchain' | 'subnet' + data: BlockchainInfo | Subnet | null + isLoading?: boolean +} + +export default function DetailsDisplay({ type, data, isLoading }: DetailsDisplayProps) { + const [copiedText, setCopiedText] = useState(null) + + // Standard EVM VM ID + const STANDARD_EVM_VM_ID = "srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy" + + if (isLoading) { + return ( +
+
+
+
+ + Loading {type} details... + +
+
+
+ ) + } + + if (!data) { + return null + } + + const formatTimestamp = (timestamp: number) => { + const date = new Date(timestamp * 1000) + return date.toLocaleDateString("en-US", { + year: "2-digit", + month: "short", + day: "numeric", + }) + } + + const copyToClipboard = (text: string | number | undefined | null) => { + if (!text) return + navigator.clipboard.writeText(text.toString()) + setCopiedText(text.toString()) + setTimeout(() => setCopiedText(null), 2000) + } + + // Determine if we're showing subnet or blockchain + const isSubnet = type === 'subnet' + const subnet = isSubnet ? (data as Subnet) : null + + // Get blockchain data - either directly or from subnet's first blockchain + const blockchain = isSubnet + ? (subnet?.blockchains?.[0] ? { ...(subnet.blockchains[0] as any), isTestnet: false } : null) + : (data as BlockchainInfo) + + return ( +
+ {/* Header */} +
+
+

+ {isSubnet ? 'Subnet Details' : 'Blockchain Details'} +

+
+ {isSubnet && subnet?.isL1 && ( + + L1 Chain + + )} + {blockchain?.isTestnet !== undefined && ( + + {blockchain.isTestnet ? "Testnet" : "Mainnet"} + + )} +
+
+
+ + {/* Content Area */} +
+ {/* Subnet-specific information */} + {isSubnet && subnet && ( + <> + {/* Basic Subnet Information */} +
+
+
+ + Created: + {formatTimestamp(subnet.createBlockTimestamp)} +
+
+ + Chains: + {subnet.blockchains?.length || 0} +
+
+
+ {subnet.subnetOwnershipInfo.addresses && subnet.subnetOwnershipInfo.addresses.length > 0 && ( +
+
+ + Owner: +
+ + {subnet.subnetOwnershipInfo.addresses[0]} + + +
+
+
+ {subnet.subnetOwnershipInfo.threshold} of {subnet.subnetOwnershipInfo.addresses.length} signatures required +
+
+ )} +
+
+ + {/* L1 Conversion Information */} + {subnet.isL1 && subnet.l1ValidatorManagerDetails && ( +
+
+ + L1 Conversion +
+
+
+ Validator Manager: +
+ + {subnet.l1ValidatorManagerDetails?.contractAddress} + + +
+
+
+
+ )} + + )} + + {/* Blockchain Information */} + {blockchain && ( +
+
+ + + {isSubnet ? 'Blockchain Details' : 'Details'} + +
+ + {/* Basic blockchain info for blockchain-only view */} + {!isSubnet && ( +
+
+
+ + Created: + {formatTimestamp(blockchain.createBlockTimestamp)} +
+
+ + Name: + {blockchain.blockchainName || "Unknown"} +
+
+
+
+ + Create Block: + {blockchain.createBlockNumber} +
+
+
+ )} + +
+
+
+ EVM Chain ID: +
+ + {blockchain.evmChainId} + + +
+
+ +
+ Blockchain ID: +
+ + {blockchain.blockchainId} + + +
+
+ +
+ Subnet ID: +
+ + {blockchain.subnetId} + + +
+
+ +
+ VM ID: +
+ + {blockchain.vmId} + + +
+
+
+ + {/* Warning for non-standard VM */} + {blockchain.vmId && blockchain.vmId !== STANDARD_EVM_VM_ID && ( +
+ + Non-standard VM detected +
+ )} +
+
+ )} + + {/* Copy feedback */} + {copiedText && ( +
+ Copied to clipboard! +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/toolbox/src/components/InputSubnetId.tsx b/toolbox/src/components/InputSubnetId.tsx new file mode 100644 index 00000000000..124f195fb82 --- /dev/null +++ b/toolbox/src/components/InputSubnetId.tsx @@ -0,0 +1,142 @@ +"use client" + +import { Input, type Suggestion } from "./Input"; +import { useL1ListStore } from "../stores/l1ListStore"; +import { useCreateChainStore } from "../stores/createChainStore"; +import { useWalletStore } from "../stores/walletStore"; +import { useMemo, useState, useCallback, useEffect } from "react"; +import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; +import { networkIDs } from "@avalabs/avalanchejs"; +import { GlobalParamNetwork } from "@avalabs/avacloud-sdk/models/components"; + +// Primary network subnet ID +const PRIMARY_NETWORK_SUBNET_ID = "11111111111111111111111111111111LpoYY"; + +export default function InputSubnetId({ + value, + onChange, + error, + label = "Subnet ID", + hidePrimaryNetwork = false, + helperText, + id +}: { + value: string, + onChange: (value: string) => void, + error?: string | null, + label?: string, + hidePrimaryNetwork?: boolean + helperText?: string | null + id?: string +}) { + const { avalancheNetworkID } = useWalletStore(); + const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId); + const { l1List } = useL1ListStore()(); + + const [validationError, setValidationError] = useState(null); + + // Network names for API calls + const networkNames: Record = { + [networkIDs.MainnetID]: "mainnet", + [networkIDs.FujiID]: "fuji", + }; + + // Validate subnet ID using AvaCloud SDK + const validateSubnetId = useCallback(async (subnetId: string) => { + if (!subnetId || subnetId.length < 10) { + setValidationError(null); + return; + } + + try { + setValidationError(null); + + const network = networkNames[Number(avalancheNetworkID)]; + if (!network) { + setValidationError(null); + return; + } + + const sdk = new AvaCloudSDK({ + serverURL: "https://api.avax.network", + network: network, + }); + + await sdk.data.primaryNetwork.getSubnetById({ + network: network, + subnetId, + }); + + // If we get here, the subnet exists + setValidationError(null); + } catch (error) { + // Show validation error for invalid subnet IDs + setValidationError("Subnet ID not found or invalid"); + } + }, [avalancheNetworkID, networkNames]); + + // Validate when value changes + useEffect(() => { + const timeoutId = setTimeout(() => { + if (value) { + validateSubnetId(value); + } else { + setValidationError(null); + } + }, 500); // Debounce validation + + return () => clearTimeout(timeoutId); + }, [value, validateSubnetId]); + + const subnetIdSuggestions: Suggestion[] = useMemo(() => { + const result: Suggestion[] = []; + const seen = new Set(); + + // Add subnet from create chain store first + if (createChainStoreSubnetId && !(hidePrimaryNetwork && createChainStoreSubnetId === PRIMARY_NETWORK_SUBNET_ID)) { + result.push({ + title: createChainStoreSubnetId, + value: createChainStoreSubnetId, + description: "The Subnet that you have just created in the \"Create Chain\" tool" + }); + seen.add(createChainStoreSubnetId); + } + + // Add subnets from L1 list + for (const l1 of l1List) { + const { subnetId, name } = l1; + + if (!subnetId || seen.has(subnetId)) continue; + + if (hidePrimaryNetwork && subnetId === PRIMARY_NETWORK_SUBNET_ID) { + continue; + } + + result.push({ + title: `${name} (${subnetId})`, + value: subnetId, + description: l1.description || "A subnet that was added to your L1 list.", + }); + + seen.add(subnetId); + } + + return result; + }, [createChainStoreSubnetId, l1List, hidePrimaryNetwork]); + + // Combine validation error with passed error + const combinedError = error || validationError; + + return ( + + ); +} \ No newline at end of file diff --git a/toolbox/src/components/SelectBlockchain.tsx b/toolbox/src/components/SelectBlockchain.tsx new file mode 100644 index 00000000000..e548c732516 --- /dev/null +++ b/toolbox/src/components/SelectBlockchain.tsx @@ -0,0 +1,121 @@ +"use client" + +import SelectBlockchainId from "./SelectBlockchainId"; +import { useState, useCallback } from "react"; +import { useWalletStore } from "../stores/walletStore"; +import { networkIDs } from "@avalabs/avalanchejs"; + +// API Response type from AvaCloud - matches the official API response +export type BlockchainApiResponse = { + createBlockTimestamp: number; + createBlockNumber: string; + blockchainId: string; + vmId: string; + subnetId: string; + blockchainName: string; + evmChainId: number; +} + +// Extended type with additional metadata +export type BlockchainInfo = BlockchainApiResponse & { + isTestnet: boolean; +} + +export type BlockchainSelection = { + blockchainId: string; + blockchain: BlockchainInfo | null; +} + +// Import the unified details display component +import DetailsDisplay from "./DetailsDisplay"; + +export default function SelectBlockchain({ + value, + onChange, + error, + label = "Select Avalanche Blockchain ID" +}: { + value: string, + onChange: (selection: BlockchainSelection) => void, + error?: string | null, + label?: string +}) { + const { avalancheNetworkID } = useWalletStore(); + const [blockchainDetails, setBlockchainDetails] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + + // Network names for API calls + const networkNames: Record = { + [networkIDs.MainnetID]: "mainnet", + [networkIDs.FujiID]: "fuji", + }; + + // Fetch blockchain details when needed + const fetchBlockchainDetails = useCallback(async (blockchainId: string) => { + if (!blockchainId || blockchainDetails[blockchainId]) return; + + try { + const network = networkNames[Number(avalancheNetworkID)]; + if (!network) return; + + setIsLoading(true); + + // Use direct API call as shown in AvaCloud documentation + // https://developers.avacloud.io/data-api/primary-network/get-blockchain-details-by-id + const response = await fetch(`https://glacier-api.avax.network/v1/networks/${network}/blockchains/${blockchainId}`, { + method: 'GET', + headers: { + 'accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch blockchain details: ${response.statusText}`); + } + + const blockchain: BlockchainApiResponse = await response.json(); + + setBlockchainDetails(prev => ({ + ...prev, + [blockchainId]: { + ...blockchain, + isTestnet: network === "fuji" + } + })); + } catch (error) { + console.error(`Error fetching blockchain details for ${blockchainId}:`, error); + } finally { + setIsLoading(false); + } + }, [avalancheNetworkID, networkNames, blockchainDetails]); + + // Handle value change and fetch details if needed + const handleValueChange = useCallback((newValue: string) => { + if (newValue && !blockchainDetails[newValue]) { + fetchBlockchainDetails(newValue); + } + + onChange({ + blockchainId: newValue, + blockchain: blockchainDetails[newValue] || null + }); + }, [fetchBlockchainDetails, blockchainDetails, onChange]); + + // Get current blockchain details for display + const currentBlockchain = value ? blockchainDetails[value] || null : null; + const isLoadingCurrent = value && !blockchainDetails[value] && isLoading; + + return ( +
+ + + {/* Display blockchain details when a blockchain is selected */} + {value && } +
+ ); +} \ No newline at end of file diff --git a/toolbox/src/components/SelectChainID.tsx b/toolbox/src/components/SelectBlockchainId.tsx similarity index 96% rename from toolbox/src/components/SelectChainID.tsx rename to toolbox/src/components/SelectBlockchainId.tsx index 33e49960fee..2535a2a762d 100644 --- a/toolbox/src/components/SelectChainID.tsx +++ b/toolbox/src/components/SelectBlockchainId.tsx @@ -5,14 +5,14 @@ import { useMemo } from "react"; import { cn } from "../lib/utils"; import { Globe } from 'lucide-react'; -interface ChainOption { +interface BlockchainOption { id: string; name: string; description: string; logoUrl?: string; } -export default function SelectChainID({ +export default function SelectBlockchainId({ value, onChange, error, @@ -28,8 +28,8 @@ export default function SelectChainID({ const { l1List } = useL1ListStore()(); const selectId = useId(); - const options: ChainOption[] = useMemo(() => { - const result: ChainOption[] = []; + const options: BlockchainOption[] = useMemo(() => { + const result: BlockchainOption[] = []; if (createChainStorechainID) { result.push({ @@ -90,7 +90,7 @@ export default function SelectChainID({ ) : ( -
Select a chain ID
+
Select a blockchain ID
)} @@ -129,4 +129,4 @@ export default function SelectChainID({ {error &&

{error}

} ); -} +} \ No newline at end of file diff --git a/toolbox/src/components/SelectSubnet.tsx b/toolbox/src/components/SelectSubnet.tsx new file mode 100644 index 00000000000..6fc770401e3 --- /dev/null +++ b/toolbox/src/components/SelectSubnet.tsx @@ -0,0 +1,102 @@ +"use client" + +import SelectSubnetId from "./SelectSubnetId"; +import { useState, useCallback } from "react"; +import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; +import { useWalletStore } from "../stores/walletStore"; +import { networkIDs } from "@avalabs/avalanchejs"; +import { GlobalParamNetwork, Subnet } from "@avalabs/avacloud-sdk/models/components"; +import DetailsDisplay from "./DetailsDisplay"; + +export type SubnetSelection = { + subnetId: string; + subnet: Subnet | null; +} + +export default function SelectSubnet({ + value, + onChange, + error, + onlyNotConverted = false, + hidePrimaryNetwork = false +}: { + value: string, + onChange: (selection: SubnetSelection) => void, + error?: string | null, + onlyNotConverted?: boolean, + hidePrimaryNetwork?: boolean +}) { + const { avalancheNetworkID } = useWalletStore(); + const [subnetDetails, setSubnetDetails] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + + // Network names for API calls + const networkNames: Record = { + [networkIDs.MainnetID]: "mainnet", + [networkIDs.FujiID]: "fuji", + }; + + // Fetch subnet details when needed + const fetchSubnetDetails = useCallback(async (subnetId: string) => { + if (!subnetId || subnetDetails[subnetId]) return; + + try { + const network = networkNames[Number(avalancheNetworkID)]; + if (!network) return; + + setIsLoading(true); + const sdk = new AvaCloudSDK({ + serverURL: "https://api.avax.network", + network: network, + }); + + const subnet = await sdk.data.primaryNetwork.getSubnetById({ + network: network, + subnetId, + }); + + setSubnetDetails(prev => ({ + ...prev, + [subnetId]: subnet + })); + } catch (error) { + console.error(`Error fetching subnet details for ${subnetId}:`, error); + } finally { + setIsLoading(false); + } + }, [avalancheNetworkID, networkNames, subnetDetails]); + + // Handle value change and fetch details if needed + const handleValueChange = useCallback((newValue: string) => { + if (newValue && !subnetDetails[newValue]) { + fetchSubnetDetails(newValue); + } + + onChange({ + subnetId: newValue, + subnet: subnetDetails[newValue] || null + }); + }, [fetchSubnetDetails, subnetDetails, onChange]); + + // Get current subnet details for display + const currentSubnet = value ? subnetDetails[value] || null : null; + const isLoadingCurrent = value && !subnetDetails[value] && isLoading; + + return ( +
+ + + {/* Display subnet details when a subnet is selected */} + {value && } +
+ ); +} \ No newline at end of file diff --git a/toolbox/src/components/SelectSubnetId.tsx b/toolbox/src/components/SelectSubnetId.tsx index 948bd742cf6..37720a3cd74 100644 --- a/toolbox/src/components/SelectSubnetId.tsx +++ b/toolbox/src/components/SelectSubnetId.tsx @@ -1,17 +1,45 @@ +import InputSubnetId from "./InputSubnetId"; import { Input, type Suggestion } from "./Input"; import { useCreateChainStore } from "../stores/createChainStore"; import { useL1ListStore } from "../stores/l1ListStore"; import { useMemo } from "react"; -export default function InputSubnetId({ value, onChange, error, onlyNotConverted = false, hidePrimaryNetwork = false }: { value: string, onChange: (value: string) => void, error?: string | null, onlyNotConverted?: boolean, hidePrimaryNetwork?: boolean }) { +export default function SelectSubnetId({ + value, + onChange, + error, + onlyNotConverted = false, + hidePrimaryNetwork = false, + label = "Subnet ID", + helperText, + id +}: { + value: string, + onChange: (value: string) => void, + error?: string | null, + onlyNotConverted?: boolean, + hidePrimaryNetwork?: boolean, + label?: string, + helperText?: string | null, + id?: string +}) { + // This component adds filtering logic on top of InputSubnetId + // If onlyNotConverted is true, it filters out converted subnets + const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId); const l1List = useL1ListStore()(state => state.l1List); - const subnetIdSuggestions: Suggestion[] = useMemo(() => { + // Create filtered suggestions if onlyNotConverted is true + const filteredSuggestions: Suggestion[] | undefined = useMemo(() => { + if (!onlyNotConverted) { + // If no filtering needed, let InputSubnetId handle suggestions + return undefined; + } + const result: Suggestion[] = []; const seen = new Set(); const PRIMARY_NETWORK_ID = "11111111111111111111111111111111LpoYY"; - + if (createChainStoreSubnetId) { result.push({ title: createChainStoreSubnetId, @@ -20,37 +48,57 @@ export default function InputSubnetId({ value, onChange, error, onlyNotConverted }); seen.add(createChainStoreSubnetId); } - + for (const l1 of l1List) { const { subnetId, name, validatorManagerAddress } = l1; - + if (!subnetId || seen.has(subnetId)) continue; - + const isPrimary = subnetId === PRIMARY_NETWORK_ID; const isConverted = !!validatorManagerAddress; - + + // Filter out converted subnets when onlyNotConverted is true if ((onlyNotConverted && (isPrimary || isConverted)) || (hidePrimaryNetwork && isPrimary)) { continue; } - + result.push({ title: `${name} (${subnetId})`, value: subnetId, description: l1.description || 'A chain that was added to your L1 list.', }); - + seen.add(subnetId); } - + return result; }, [createChainStoreSubnetId, l1List, onlyNotConverted, hidePrimaryNetwork]); - - - return + + // If we need custom filtering, use Input directly with filtered suggestions + // Otherwise, use InputSubnetId which has its own suggestions and validation + if (onlyNotConverted && filteredSuggestions) { + return ( + + ); + } + + return ( + + ); } diff --git a/toolbox/src/components/SelectValidationID.tsx b/toolbox/src/components/SelectValidationID.tsx index a45df13ff7e..b0349c4e747 100644 --- a/toolbox/src/components/SelectValidationID.tsx +++ b/toolbox/src/components/SelectValidationID.tsx @@ -43,16 +43,16 @@ export type ValidationSelection = { * @param props.subnetId - Optional subnet ID to filter validators * @param props.format - Format for validation ID: "cb58" (default) or "hex" */ -export default function SelectValidationID({ - value, - onChange, +export default function SelectValidationID({ + value, + onChange, error, subnetId = "", - format = "cb58" -}: { - value: string, - onChange: (selection: ValidationSelection) => void, - error?: string | null, + format = "cb58" +}: { + value: string, + onChange: (selection: ValidationSelection) => void, + error?: string | null, subnetId?: string, format?: "cb58" | "hex" }) { @@ -71,13 +71,18 @@ export default function SelectValidationID({ useEffect(() => { const fetchValidators = async () => { if (!subnetId) return; - + setIsLoading(true); try { const network = networkNames[Number(avalancheNetworkID)]; if (!network) return; - const result = await new AvaCloudSDK().data.primaryNetwork.listL1Validators({ + const sdk = new AvaCloudSDK({ + serverURL: "https://api.avax.network", + network: network, + }); + + const result = await sdk.data.primaryNetwork.listL1Validators({ network: network, subnetId: subnetId, }); @@ -87,7 +92,7 @@ export default function SelectValidationID({ for await (const page of result) { validatorsList.push(...page.result.validators); } - + setValidators(validatorsList); // Create a mapping of validation IDs to node IDs @@ -117,10 +122,10 @@ export default function SelectValidationID({ // Get the currently selected node ID const selectedNodeId = useMemo(() => { - return validationIdToNodeId[value] || - (value && value.startsWith("0x") && validationIdToNodeId[value]) || - (value && !value.startsWith("0x") && validationIdToNodeId["0x" + cb58ToHex(value)]) || - ""; + return validationIdToNodeId[value] || + (value && value.startsWith("0x") && validationIdToNodeId[value]) || + (value && !value.startsWith("0x") && validationIdToNodeId["0x" + cb58ToHex(value)]) || + ""; }, [value, validationIdToNodeId]); const validationIDSuggestions: Suggestion[] = useMemo(() => { @@ -133,7 +138,7 @@ export default function SelectValidationID({ const nodeId = validator.nodeId; const weightDisplay = validator.weight.toLocaleString(); const isSelected = nodeId === selectedNodeId; - + // Add just one version based on the format prop if (format === "hex") { try { @@ -180,11 +185,11 @@ export default function SelectValidationID({ // Look up the nodeId for this validation ID let nodeId = validationIdToNodeId[formattedValue] || ""; - + // If not found directly, try the alternate format if (!nodeId) { - const alternateFormat = format === "hex" - ? hexToCB58(formattedValue.slice(2)) + const alternateFormat = format === "hex" + ? hexToCB58(formattedValue.slice(2)) : "0x" + cb58ToHex(formattedValue); nodeId = validationIdToNodeId[alternateFormat] || ""; } diff --git a/toolbox/src/toolbox/ICM/SendICMMessage.tsx b/toolbox/src/toolbox/ICM/SendICMMessage.tsx index cd0223f3e42..1858d64e422 100644 --- a/toolbox/src/toolbox/ICM/SendICMMessage.tsx +++ b/toolbox/src/toolbox/ICM/SendICMMessage.tsx @@ -11,7 +11,7 @@ import ICMDemoABI from "../../../contracts/example-contracts/compiled/ICMDemo.js import { utils } from "@avalabs/avalanchejs"; import { Input } from "../../components/Input"; import { Container } from "../../components/Container"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchainId from "../../components/SelectBlockchainId"; import { useL1ByChainId, useSelectedL1 } from "../../stores/l1ListStore"; import { useEffect } from "react"; const predeployedDemos: Record = { @@ -174,7 +174,7 @@ export default function SendICMMessage() { required type="number" /> - setDestinationChainId(value)} diff --git a/toolbox/src/toolbox/ICTT/AddCollateral.tsx b/toolbox/src/toolbox/ICTT/AddCollateral.tsx index fe44a83dd9b..8e436ff886e 100644 --- a/toolbox/src/toolbox/ICTT/AddCollateral.tsx +++ b/toolbox/src/toolbox/ICTT/AddCollateral.tsx @@ -12,7 +12,7 @@ import { Input, Suggestion } from "../../components/Input"; import { EVMAddressInput } from "../../components/EVMAddressInput"; import { utils } from "@avalabs/avalanchejs"; import { Note } from "../../components/Note"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchainId from "../../components/SelectBlockchainId"; import { Container } from "../../components/Container"; import ERC20TokenRemoteABI from "../../../contracts/icm-contracts/compiled/ERC20TokenRemote.json"; import { getToolboxStore, useViemChainStore } from "../../stores/toolboxStore"; @@ -324,7 +324,7 @@ export default function AddCollateral() { description="Approve and add collateral (ERC20 tokens) to the Token Home contract on the source chain for a remote bridge contract on the current chain." > - setSourceChainId(value)} diff --git a/toolbox/src/toolbox/ICTT/DeployERC20TokenRemote.tsx b/toolbox/src/toolbox/ICTT/DeployERC20TokenRemote.tsx index 178662ae00b..f98bd2c6535 100644 --- a/toolbox/src/toolbox/ICTT/DeployERC20TokenRemote.tsx +++ b/toolbox/src/toolbox/ICTT/DeployERC20TokenRemote.tsx @@ -15,7 +15,7 @@ import { Note } from "../../components/Note"; import { utils } from "@avalabs/avalanchejs"; import ERC20TokenHomeABI from "../../../contracts/icm-contracts/compiled/ERC20TokenHome.json"; import ExampleERC20 from "../../../contracts/icm-contracts/compiled/ExampleERC20.json"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchainId from "../../components/SelectBlockchainId"; import { Container } from "../../components/Container"; import TeleporterRegistryAddressInput from "../../components/TeleporterRegistryAddressInput"; @@ -220,7 +220,7 @@ export default function DeployERC20TokenRemote() {

} - setSourceChainId(value)} diff --git a/toolbox/src/toolbox/ICTT/DeployNativeTokenRemote.tsx b/toolbox/src/toolbox/ICTT/DeployNativeTokenRemote.tsx index c83cf6c60a2..90af6dac77a 100644 --- a/toolbox/src/toolbox/ICTT/DeployNativeTokenRemote.tsx +++ b/toolbox/src/toolbox/ICTT/DeployNativeTokenRemote.tsx @@ -15,7 +15,7 @@ import { Note } from "../../components/Note"; import { utils } from "@avalabs/avalanchejs"; import ERC20TokenHomeABI from "../../../contracts/icm-contracts/compiled/ERC20TokenHome.json"; import ExampleERC20 from "../../../contracts/icm-contracts/compiled/ExampleERC20.json"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchainId from "../../components/SelectBlockchainId"; import { CheckPrecompile } from "../../components/CheckPrecompile"; import { Container } from "../../components/Container"; import TeleporterRegistryAddressInput from "../../components/TeleporterRegistryAddressInput"; @@ -207,7 +207,7 @@ export default function DeployNativeTokenRemote() {

} - setSourceChainId(value)} diff --git a/toolbox/src/toolbox/ICTT/RegisterWithHome.tsx b/toolbox/src/toolbox/ICTT/RegisterWithHome.tsx index f0913e84dd1..ee798017edf 100644 --- a/toolbox/src/toolbox/ICTT/RegisterWithHome.tsx +++ b/toolbox/src/toolbox/ICTT/RegisterWithHome.tsx @@ -14,7 +14,7 @@ import { Suggestion } from "../../components/Input"; import { EVMAddressInput } from "../../components/EVMAddressInput"; import { utils } from "@avalabs/avalanchejs"; import { ListContractEvents } from "../../components/ListContractEvents"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchainId from "../../components/SelectBlockchainId"; import { Container } from "../../components/Container"; export default function RegisterWithHome() { @@ -179,7 +179,7 @@ export default function RegisterWithHome() {

- setSourceChainId(value)} diff --git a/toolbox/src/toolbox/ICTT/TestSend.tsx b/toolbox/src/toolbox/ICTT/TestSend.tsx index 96cc36a7071..a07499a3783 100644 --- a/toolbox/src/toolbox/ICTT/TestSend.tsx +++ b/toolbox/src/toolbox/ICTT/TestSend.tsx @@ -18,7 +18,7 @@ import { Suggestion } from "../../components/TokenInput"; import { EVMAddressInput } from "../../components/EVMAddressInput"; import { Token, TokenInput } from "../../components/TokenInputToolbox"; import { utils } from "@avalabs/avalanchejs"; -import SelectChainID from "../../components/SelectChainID"; +import SelectBlockchain, { type BlockchainSelection } from "../../components/SelectBlockchain"; import { Container } from "../../components/Container"; import { Toggle } from "../../components/Toggle"; import { Ellipsis } from "lucide-react"; @@ -32,7 +32,7 @@ export default function TokenBridge() { const selectedL1 = useSelectedL1()(); // Only need to select destination chain (source is current chain) - const [destinationChainId, setDestinationChainId] = useState(""); + const [destinationSelection, setDestinationSelection] = useState({ blockchainId: "", blockchain: null }); // Contract addresses const [sourceContractAddress, setSourceContractAddress] = useState
(""); @@ -72,17 +72,17 @@ export default function TokenBridge() { const [tokenAllowance, setTokenAllowance] = useState(null); // Get chain info - source is current chain, destination is selected - const destL1 = useL1ByChainId(destinationChainId)(); - const destToolboxStore = getToolboxStore(destinationChainId)(); + const destL1 = useL1ByChainId(destinationSelection.blockchainId)(); + const destToolboxStore = getToolboxStore(destinationSelection.blockchainId)(); const { erc20TokenHomeAddress, nativeTokenHomeAddress } = useToolboxStore(); // Destination chain validation let destChainError: string | undefined = undefined; - if (!destinationChainId) { - destChainError = "Please select a destination chain"; - } else if (destinationChainId === selectedL1?.id) { - destChainError = "Source and destination chains must be different"; + if (!destinationSelection.blockchainId) { + destChainError = "Please select a destination blockchain"; + } else if (destinationSelection.blockchainId === selectedL1?.id) { + destChainError = "Source and destination blockchains must be different"; } // Generate hex blockchain ID for the destination chain @@ -91,7 +91,7 @@ export default function TokenBridge() { try { return utils.bufferToHex(utils.base58check.decode(destL1.id)); } catch (e) { - console.error("Error decoding destination chain ID:", e); + console.error("Error decoding destination blockchain ID:", e); return null; } }, [destL1?.id]); @@ -478,10 +478,10 @@ export default function TokenBridge() { description={`Send tokens from the current chain (${selectedL1?.name}) to another chain.`} > - setDestinationChainId(value)} + @@ -497,14 +497,14 @@ export default function TokenBridge() { /> setDestinationContractAddress(value as Address)} verify={(value) => fetchTokenInfoFromBridgeContract(value as Address, "destination")} - disabled={!destinationChainId || isProcessingSend || isProcessingApproval} + disabled={!destinationSelection.blockchainId || isProcessingSend || isProcessingApproval} suggestions={destinationContractSuggestions} - placeholder="0x... Bridge contract on destination chain" + placeholder="0x... Bridge contract on destination blockchain" /> = { + [networkIDs.MainnetID]: "mainnet", + [networkIDs.FujiID]: "fuji", +}; export default function CollectConversionSignatures() { const { coreWalletClient } = useWalletStore(); @@ -47,18 +55,16 @@ export default function CollectConversionSignatures() { try { const { message, justification, signingSubnetId, networkId } = await coreWalletClient.extractWarpMessageFromPChainTx({ txId: conversionID }); - const { signedMessage } = await new AvaCloudSDK().data.signatureAggregator.aggregateSignatures({ - network: networkId === networkIDs.FujiID ? "fuji" : "mainnet", + const { signedMessage } = await new AvaCloudSDK({ + serverURL: "https://api.avax.network", + network: networkNames[Number(networkId)], + }).data.signatureAggregator.aggregate({ + network: networkNames[Number(networkId)], signatureAggregatorRequest: { message: message, justification: justification, signingSubnetId: signingSubnetId, - quorumPercentage: 67, // Default threshold for subnet validation - }, - }, { - retries: { - strategy: "backoff", - backoff: { initialInterval: 1000, maxInterval: 10000, exponent: 1.5, maxElapsedTime: 30 * 1000 }, + quorumPercentage: 67, } }); @@ -81,10 +87,9 @@ export default function CollectConversionSignatures() { onChange={setChainID} error={chainIdError} /> - ({ + subnetId: storeSubnetId, + subnet: null + }); const [chainID, setChainID] = useState(storeChainID); const [isConverting, setIsConverting] = useState(false); const [validators, setValidators] = useState([]); @@ -59,7 +62,7 @@ export default function ConvertToL1() { try { const txID = await coreWalletClient.convertToL1({ managerAddress, - subnetId: subnetId, + subnetId: selection.subnetId, chainId: chainID, subnetAuth: [0], validators @@ -79,9 +82,9 @@ export default function ConvertToL1() { description="This will convert your Subnet to an L1." >
- @@ -120,10 +123,10 @@ export default function ConvertToL1() {
diff --git a/toolbox/src/toolbox/L1/CreateChain.tsx b/toolbox/src/toolbox/L1/CreateChain.tsx index 446ed623161..d9aeebfbca7 100644 --- a/toolbox/src/toolbox/L1/CreateChain.tsx +++ b/toolbox/src/toolbox/L1/CreateChain.tsx @@ -12,6 +12,7 @@ import { Step, Steps } from "fumadocs-ui/components/steps"; import generateName from 'boring-name-generator' import { Success } from "../../components/Success"; import { RadioGroup } from "../../components/RadioGroup"; +import InputSubnetId from "../../components/InputSubnetId"; export const EVM_VM_ID = "srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy" @@ -49,6 +50,10 @@ export default function CreateChain() { const [showVMIdInput, setShowVMIdInput] = useState(false); const [vmId, setVmId] = useState(EVM_VM_ID); + // Wrapper function to handle subnet ID changes properly + const handleSubnetIdChange = (newSubnetId: string) => { + setSubnetID(newSubnetId); + }; async function handleCreateSubnet() { setIsCreatingSubnet(true); @@ -111,7 +116,7 @@ export default function CreateChain() { disabled={true} type="text" /> - + - - {createdSubnetId && ( + + {createdSubnetId && ( +
- )} -
+ + )}

Step 2: Create a Chain

@@ -134,12 +140,11 @@ export default function CreateChain() { Enter the parameters for your new chain.

- (null) - const [isLoading, setIsLoading] = useState(false) + const [selection, setSelection] = useState({ subnetId: '', subnet: null }) const [error, setError] = useState(null) - const [success, setSuccess] = useState(false) - // Network names for display - const networkNames: Record = { - [networkIDs.MainnetID]: "mainnet", - [networkIDs.FujiID]: "fuji", - } + // Update error state when subnet details change useEffect(() => { - if (subnetId) { - fetchSubnetDetails() - } - }, [subnetId, avalancheNetworkID]) - - async function fetchSubnetDetails() { - if (!subnetId) { - setError("Please enter a subnet ID") - return + if (selection.subnetId && !selection.subnet) { + setError("Failed to fetch subnet details") + } else { + setError(null) } - - setIsLoading(true) - setError(null) - setSuccess(false) - - try { - const network = networkNames[Number(avalancheNetworkID)] - if (!network) { - throw new Error("Invalid network selected") - } - - const subnet = await new AvaCloudSDK().data.primaryNetwork.getSubnetById({ - network: network, - subnetId, - }); - - setSubnetDetails(subnet) - setSuccess(true) - } catch (error: any) { - setError(error.message || "Failed to fetch subnet details") - setSubnetDetails(null) - console.error("Error fetching subnet details:", error) - } finally { - setIsLoading(false) - } - } + }, [selection]) function formatTimestamp(timestamp: number): string { return new Date(timestamp * 1000).toLocaleString() @@ -90,43 +46,16 @@ export default function QueryL1Details() { )} - {success && !subnetDetails && ( -
-
- - Subnet details retrieved successfully -
-
- )} -
-
- - - {subnetDetails && ( + {selection.subnet && (
{/* Background gradient effect - blue for both light and dark mode */} @@ -144,7 +73,7 @@ export default function QueryL1Details() {
Subnet ID: - {subnetDetails.subnetId} + {selection.subnet.subnetId}
@@ -152,16 +81,16 @@ export default function QueryL1Details() {
- {subnetDetails.isL1 ? "Sovereign L1" : "Subnet"} + {selection.subnet.isL1 ? "Sovereign L1" : "Subnet"}
@@ -182,13 +111,13 @@ export default function QueryL1Details() {
Created:

- {formatTimestamp(subnetDetails.createBlockTimestamp)} + {formatTimestamp(selection.subnet.createBlockTimestamp)}

Block Index:

- {subnetDetails.createBlockIndex} + {selection.subnet.createBlockIndex}

@@ -196,7 +125,7 @@ export default function QueryL1Details() { {/* L1 Specific Information */} - {subnetDetails.isL1 && ( + {selection.subnet.isL1 && (
@@ -208,22 +137,22 @@ export default function QueryL1Details() {
- {subnetDetails.l1ValidatorManagerDetails && ( + {selection.subnet.l1ValidatorManagerDetails && (
Validator Manager Blockchain ID:

- {subnetDetails.l1ValidatorManagerDetails.blockchainId} + {selection.subnet.l1ValidatorManagerDetails.blockchainId}

Validator Manager Contract Address:

- {subnetDetails.l1ValidatorManagerDetails.contractAddress} + {selection.subnet.l1ValidatorManagerDetails.contractAddress}

)} - {subnetDetails.l1ConversionTransactionHash && ( + {selection.subnet.l1ConversionTransactionHash && (
L1 Conversion P-Chain Transaction ID: @@ -293,7 +222,7 @@ export default function QueryL1Details() {
Owner Addresses:
- {subnetDetails.subnetOwnershipInfo.addresses.map((address, index) => ( + {selection.subnet.subnetOwnershipInfo.addresses.map((address, index) => (
{/* Blockchains */} - {subnetDetails.blockchains && subnetDetails.blockchains.length > 0 && ( + {selection.subnet.blockchains && selection.subnet.blockchains.length > 0 && (
@@ -318,14 +247,14 @@ export default function QueryL1Details() {
- {subnetDetails.blockchains.length} + {selection.subnet.blockchains.length}
- {subnetDetails.blockchains.map((blockchain, index) => ( + {selection.subnet.blockchains.map((blockchain, index) => (
(null); + const [isLoading, setIsLoading] = useState(false); const [isRPC, setIsRPC] = useState(true); const [rpcCommand, setRpcCommand] = useState(""); const [nodeRunningMode, setNodeRunningMode] = useState("server"); @@ -139,6 +142,31 @@ export default function AvalanchegoDocker() { const { avalancheNetworkID } = useWalletStore(); const { addL1 } = useL1ListStore()(); + useEffect(() => { + setSubnetIdError(null); + setSubnetId(""); + setSubnet(null); + if (!chainId) return; + + setIsLoading(true); + getBlockchainInfo(chainId) + .then(async (chainInfo) => { + setSubnetId(chainInfo.subnetId); + try { + const subnetInfo = await getSubnetInfo(chainInfo.subnetId); + setSubnet(subnetInfo); + } catch (error) { + setSubnetIdError((error as Error).message); + } + }) + .catch((error) => { + setSubnetIdError((error as Error).message); + }) + .finally(() => { + setIsLoading(false); + }); + }, [chainId]); + useEffect(() => { try { setRpcCommand(generateDockerCommand([subnetId], isRPC, avalancheNetworkID, chainId, enableDebugTrace, pruningEnabled)); @@ -154,8 +182,6 @@ export default function AvalanchegoDocker() { }, [isRPC]); - - useEffect(() => { setSubnetIdError(null); setSubnetId(""); @@ -171,6 +197,7 @@ export default function AvalanchegoDocker() { const handleReset = () => { setChainId(""); setSubnetId(""); + setSubnet(null); setIsRPC(true); setChainAddedToWallet(null); setRpcCommand(""); @@ -239,14 +266,18 @@ export default function AvalanchegoDocker() { - - + + {/* Show subnet details if available */} + diff --git a/toolbox/src/toolbox/Nodes/PerformanceMonitor.tsx b/toolbox/src/toolbox/Nodes/PerformanceMonitor.tsx index 0056573ec9f..f2c10baf465 100644 --- a/toolbox/src/toolbox/Nodes/PerformanceMonitor.tsx +++ b/toolbox/src/toolbox/Nodes/PerformanceMonitor.tsx @@ -9,6 +9,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Responsi import { BlockWatcher, BlockInfo } from "./BlockWatcher"; import { ChainInfo } from "./chainInfo"; import { RPCURLInput } from "../../components/RPCURLInput"; +import InputSubnetId from "../../components/InputSubnetId"; interface BucketedData { transactions: number; @@ -19,7 +20,7 @@ interface BucketedData { export default function PerformanceMonitor() { const [nodeRpcUrl, setNodeRpcUrl] = useState(''); const [chainID, setChainID] = useState(''); - const [subnetId, setSubnetID] = useState(''); + const [subnetId, setSubnetId] = useState(''); const [evmChainRpcUrl, setEvmChainRpcUrl] = useState(''); @@ -260,7 +261,6 @@ export default function PerformanceMonitor() { label="RPC URL excluding /ext/bc/..." value={nodeRpcUrl} onChange={setNodeRpcUrl} - disabled={isMonitoring} /> -