diff --git a/components/tools/common/api/validator-info.ts b/components/tools/common/api/validator-info.ts index d5ece29657c..2998d6124fb 100644 --- a/components/tools/common/api/validator-info.ts +++ b/components/tools/common/api/validator-info.ts @@ -1,7 +1,11 @@ import { Validator, SubnetInfo, L1ValidatorManagerDetails } from './types'; import { pChainEndpoint } from './consts'; import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; +import { useWalletStore } from "../../../../toolbox/src/stores/walletStore"; +const { isTestnet } = useWalletStore(); + export const avaCloudSDK = new AvaCloudSDK({ + serverURL: isTestnet ? "https://api.avax-test.network" : "https://api.avax.network", chainId: "43114", network: "fuji", }); diff --git a/package.json b/package.json index e3bccef8ebe..4d46682e937 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", @@ -129,4 +129,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 f300ad8f0cc..95ceb445064 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", @@ -77,4 +77,4 @@ "vite": "^6.1.6", "vite-plugin-dts": "^4.5.0" } -} +} \ No newline at end of file diff --git a/toolbox/src/components/BlockchainDetailsDisplay.tsx b/toolbox/src/components/BlockchainDetailsDisplay.tsx new file mode 100644 index 00000000000..eb7d14a0a69 --- /dev/null +++ b/toolbox/src/components/BlockchainDetailsDisplay.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"; +import { SUBNET_EVM_VM_ID } from "../toolbox/Nodes/AvalanchegoDocker"; +import { useWalletStore } from "../stores/walletStore"; + +interface BlockchainDetailsDisplayProps { + type: 'blockchain' | 'subnet' + data: BlockchainInfo | Subnet | null + isLoading?: boolean +} + +export default function BlockchainDetailsDisplay({ type, data, isLoading }: BlockchainDetailsDisplayProps) { + const [copiedText, setCopiedText] = useState(null) + const { isTestnet } = useWalletStore(); + + 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 } : 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 !== SUBNET_EVM_VM_ID && ( +
+ + Non-standard VM ID 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..e3fa64992fc --- /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 { useMemo, useState, useCallback, useEffect } from "react"; +import { utils } from "@avalabs/avalanchejs"; +import { useAvaCloudSDK } from "../stores/useAvaCloudSDK"; + +// Primary network subnet ID +const PRIMARY_NETWORK_SUBNET_ID = "11111111111111111111111111111111LpoYY"; + +export default function InputSubnetId({ + value, + onChange, + error, + label = "Subnet ID", + hidePrimaryNetwork = false, + helperText, + id, + validationDelayMs = 500, + readOnly = false +}: { + value: string, + onChange: (value: string) => void, + error?: string | null, + label?: string, + hidePrimaryNetwork?: boolean + helperText?: string | null + id?: string + validationDelayMs?: number + readOnly?: boolean +}) { + const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId); + const { l1List } = useL1ListStore()(); + const { getSubnetById } = useAvaCloudSDK(); + + const [validationError, setValidationError] = useState(null); + + // Validate subnet ID format and checksum using Base58Check + const validateBase58Format = (subnetId: string): boolean => { + try { + // Validate Base58Check format and checksum (last 4 bytes) + utils.base58check.decode(subnetId); + return true; + } catch (error) { + return false; + } + }; + + // Validate subnet ID using AvaCloud SDK + const validateSubnetId = useCallback(async (subnetId: string) => { + if (!subnetId || subnetId.length < 10) { + setValidationError(null); + return; + } + + // First validate Base58Check format and checksum + if (!validateBase58Format(subnetId)) { + setValidationError("Invalid subnet ID format or checksum"); + return; + } + + try { + setValidationError(null); + + await getSubnetById({ 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"); + } + }, [getSubnetById]); + + // Validate when value changes + useEffect(() => { + const timeoutId = setTimeout(() => { + if (value) { + validateSubnetId(value); + } else { + setValidationError(null); + } + }, validationDelayMs); // Wait for subnet to be available before validation + + return () => clearTimeout(timeoutId); + }, [value, validateSubnetId, validationDelayMs]); + + 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..e91b35e8c1a --- /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 BlockchainDetailsDisplay from "./BlockchainDetailsDisplay"; + +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..4936b80eba6 --- /dev/null +++ b/toolbox/src/components/SelectSubnet.tsx @@ -0,0 +1,96 @@ +"use client" + +import SelectSubnetId from "./SelectSubnetId"; +import { useState, useCallback, useEffect } from "react"; +import { Subnet } from "@avalabs/avacloud-sdk/models/components"; +import BlockchainDetailsDisplay from "./BlockchainDetailsDisplay"; +import { useAvaCloudSDK } from "../stores/useAvaCloudSDK"; + +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 { getSubnetById } = useAvaCloudSDK(); + const [subnetDetails, setSubnetDetails] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + + // Fetch subnet details when needed + const fetchSubnetDetails = useCallback(async (subnetId: string) => { + if (!subnetId || subnetDetails[subnetId]) return; + + try { + setIsLoading(true); + const subnet = await getSubnetById({ subnetId }); + + setSubnetDetails(prev => ({ + ...prev, + [subnetId]: subnet + })); + + // Automatically update the selection with the fetched subnet details + onChange({ + subnetId, + subnet + }); + } catch (error) { + console.error(`Error fetching subnet details for ${subnetId}:`, error); + } finally { + setIsLoading(false); + } + }, [getSubnetById, subnetDetails, onChange]); + + // 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]); + + // Auto-fetch subnet details when component receives a pre-filled value + useEffect(() => { + if (value && !subnetDetails[value]) { + fetchSubnetDetails(value); + } + }, [value, subnetDetails, fetchSubnetDetails]); + + // 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 92660e212d3..1bf2ddfbcd2 100644 --- a/toolbox/src/components/SelectValidationID.tsx +++ b/toolbox/src/components/SelectValidationID.tsx @@ -1,11 +1,9 @@ import { Input, type Suggestion } from "./Input"; import { useMemo, useState, useEffect } from "react"; import { cb58ToHex, hexToCB58 } from "../toolbox/Conversion/FormatConverter"; -import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; -import { useWalletStore } from "../stores/walletStore"; -import { networkIDs } from "@avalabs/avalanchejs"; -import { L1ValidatorDetailsFull, GlobalParamNetwork } from "@avalabs/avacloud-sdk/models/components"; +import { L1ValidatorDetailsFull } from "@avalabs/avacloud-sdk/models/components"; import { formatAvaxBalance } from "../coreViem/utils/format"; +import { useAvaCloudSDK } from "../stores/useAvaCloudSDK"; export type ValidationSelection = { validationId: string; @@ -44,44 +42,34 @@ 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" }) { - const { avalancheNetworkID } = useWalletStore(); + const { listL1Validators } = useAvaCloudSDK(); const [validators, setValidators] = useState([]); const [isLoading, setIsLoading] = useState(false); const [validationIdToNodeId, setValidationIdToNodeId] = useState>({}); - // Network names for display - const networkNames: Record = { - [networkIDs.MainnetID]: "mainnet", - [networkIDs.FujiID]: "fuji", - }; - // Fetch validators from the API 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({ - network: network, + const result = await listL1Validators({ subnetId: subnetId, - includeInactiveL1Validators: true, + pageSize: 100, // Add reasonable page size }); // Handle pagination @@ -89,7 +77,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, filtering out validators with weight 0 @@ -115,14 +103,14 @@ export default function SelectValidationID({ }; fetchValidators(); - }, [subnetId, avalancheNetworkID]); + }, [subnetId, listL1Validators]); // 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(() => { @@ -130,7 +118,7 @@ export default function SelectValidationID({ // Filter out validators with weight 0 and only add suggestions from validators with node IDs const validatorsWithWeight = validators.filter(validator => validator.weight > 0); - + for (const validator of validatorsWithWeight) { if (validator.validationId) { // Use full node ID @@ -138,7 +126,7 @@ export default function SelectValidationID({ const weightDisplay = validator.weight.toLocaleString(); const balanceDisplay = formatAvaxBalance(validator.remainingBalance); const isSelected = nodeId === selectedNodeId; - + // Add just one version based on the format prop if (format === "hex") { try { @@ -185,11 +173,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/stores/useAvaCloudSDK.ts b/toolbox/src/stores/useAvaCloudSDK.ts new file mode 100644 index 00000000000..56ba0e8d19a --- /dev/null +++ b/toolbox/src/stores/useAvaCloudSDK.ts @@ -0,0 +1,97 @@ +import { useMemo, useCallback } from "react"; +import { AvaCloudSDK } from "@avalabs/avacloud-sdk"; +import { GlobalParamNetwork } from "@avalabs/avacloud-sdk/models/components"; +import { useWalletStore } from "./walletStore"; + +// Types for signature aggregation +interface SignatureAggregationParams { + message: string; + signingSubnetId: string; + quorumPercentage?: number; +} + +interface SignatureAggregationResult { + signedMessage: string; +} + +// Types for L1 validators +interface ListL1ValidatorsParams { + subnetId: string; + pageToken?: string; + pageSize?: number; +} + +// Types for subnet operations +interface GetSubnetByIdParams { + subnetId: string; +} + +export const useAvaCloudSDK = (customNetwork?: GlobalParamNetwork) => { + const { isTestnet, getNetworkName } = useWalletStore(); + + // Determine network name + const networkName = useMemo(() => { + if (customNetwork) return customNetwork; + return getNetworkName(); + }, [customNetwork, getNetworkName]); + + // Create SDK instance + const sdk = useMemo(() => { + return new AvaCloudSDK({ + serverURL: isTestnet ? "https://api.avax-test.network" : "https://api.avax.network", + network: networkName, + }); + }, [isTestnet, networkName]); + + // Signature aggregation method + const aggregateSignature = useCallback(async ({ + message, + signingSubnetId, + quorumPercentage = 67, + }: SignatureAggregationParams): Promise => { + const result = await sdk.data.signatureAggregator.aggregate({ + network: networkName, + signatureAggregatorRequest: { + message, + signingSubnetId, + quorumPercentage, + }, + }); + return result; + }, [sdk, networkName]); + + // Primary Network - Subnet operations + const getSubnetById = useCallback(async ({ subnetId }: GetSubnetByIdParams) => { + return await sdk.data.primaryNetwork.getSubnetById({ + network: networkName, + subnetId, + }); + }, [sdk, networkName]); + + // Primary Network - L1 Validator operations + const listL1Validators = useCallback(async ({ + subnetId, + pageToken, + pageSize, + }: ListL1ValidatorsParams) => { + return await sdk.data.primaryNetwork.listL1Validators({ + network: networkName, + subnetId, + pageToken, + pageSize, + }); + }, [sdk, networkName]); + + return { + // Raw SDK access for advanced usage + sdk, + networkName, + + // Signature aggregation (most common pattern) + aggregateSignature, + + // Primary Network API methods + getSubnetById, + listL1Validators, + }; +}; \ No newline at end of file diff --git a/toolbox/src/stores/walletStore.ts b/toolbox/src/stores/walletStore.ts index 9b0a7bb61ff..a35c9ef0d42 100644 --- a/toolbox/src/stores/walletStore.ts +++ b/toolbox/src/stores/walletStore.ts @@ -7,6 +7,7 @@ import { avalancheFuji, avalanche } from 'viem/chains'; import { zeroAddress } from 'viem'; import { getPChainBalance, getNativeTokenBalance, getChains } from '../coreViem/utils/glacier'; import debounce from 'debounce'; +import { GlobalParamNetwork } from "@avalabs/avacloud-sdk/models/components"; let indexedChainsPromise: Promise | null = null; function getIndexedChains() { @@ -107,7 +108,11 @@ export const useWalletStore = create( debouncedUpdatePChainBalance(); debouncedUpdateL1Balance(); debouncedUpdateCChainBalance(); - } + }, + getNetworkName: (): GlobalParamNetwork => { + const { avalancheNetworkID } = get(); + return avalancheNetworkID === networkIDs.MainnetID ? "mainnet" : "fuji"; + }, } }) ) 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" /> (null); const [chainID, setChainID] = useState(""); @@ -45,20 +46,17 @@ export default function CollectConversionSignatures() { setIsConverting(true); try { - const { message, justification, signingSubnetId, networkId } = await coreWalletClient.extractWarpMessageFromPChainTx({ txId: conversionID }); + const { message, justification, signingSubnetId } = await coreWalletClient.extractWarpMessageFromPChainTx({ txId: conversionID }); - const { signedMessage } = await new AvaCloudSDK().data.signatureAggregator.aggregateSignatures({ - network: networkId === networkIDs.FujiID ? "fuji" : "mainnet", + const networkName = getNetworkName(); + + const { signedMessage } = await sdk.data.signatureAggregator.aggregate({ + network: networkName, 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 +79,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..0bd34778cf2 100644 --- a/toolbox/src/toolbox/L1/CreateChain.tsx +++ b/toolbox/src/toolbox/L1/CreateChain.tsx @@ -12,8 +12,8 @@ import { Step, Steps } from "fumadocs-ui/components/steps"; import generateName from 'boring-name-generator' import { Success } from "../../components/Success"; import { RadioGroup } from "../../components/RadioGroup"; - -export const EVM_VM_ID = "srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy" +import InputSubnetId from "../../components/InputSubnetId"; +import { SUBNET_EVM_VM_ID } from "../Nodes/AvalanchegoDocker"; const generateRandomName = () => { //makes sure the name doesn't contain a dash @@ -47,8 +47,12 @@ export default function CreateChain() { const [localChainName, setLocalChainName] = useState(generateRandomName()); const [showVMIdInput, setShowVMIdInput] = useState(false); - const [vmId, setVmId] = useState(EVM_VM_ID); + const [vmId, setVmId] = useState(SUBNET_EVM_VM_ID); + // Wrapper function to handle subnet ID changes properly + const handleSubnetIdChange = (newSubnetId: string) => { + setSubnetID(newSubnetId); + }; async function handleCreateSubnet() { setIsCreatingSubnet(true); @@ -111,7 +115,7 @@ export default function CreateChain() { disabled={true} type="text" /> - + - - {createdSubnetId && ( + + {createdSubnetId && ( +
- )} -
+ + )}

Step 2: Create a Chain

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

- setShowVMIdInput(value === "true")} + onChange={(value) => { + const shouldShow = value === "true"; + setShowVMIdInput(shouldShow); + // Reset to standard EVM when switching to uncustomized + if (!shouldShow) { + setVmId(SUBNET_EVM_VM_ID); + } + }} idPrefix={`show-vm-id`} className="mb-4" items={[ @@ -169,7 +181,7 @@ export default function CreateChain() { value={vmId} onChange={setVmId} placeholder="Enter VM ID" - helperText={`For an L1 with an uncustomized EVM use ${EVM_VM_ID}`} + helperText={`For an L1 with an uncustomized EVM use ${SUBNET_EVM_VM_ID}`} /> )} diff --git a/toolbox/src/toolbox/L1/QueryL1Details.tsx b/toolbox/src/toolbox/L1/QueryL1Details.tsx index 08251705948..0840f4ce509 100644 --- a/toolbox/src/toolbox/L1/QueryL1Details.tsx +++ b/toolbox/src/toolbox/L1/QueryL1Details.tsx @@ -1,75 +1,31 @@ -"use client" +"use client"; -import { useWalletStore } from "../../stores/walletStore" import { useState, useEffect } from "react" import { AlertCircle, Info, CheckCircle, - Loader2, Clock, Users, Database, ExternalLink, } from "lucide-react" -import { networkIDs } from "@avalabs/avalanchejs" -import { Button } from "../../components/Button" import { Container } from "../../components/Container" -import { AvaCloudSDK } from "@avalabs/avacloud-sdk" -import { GlobalParamNetwork, Subnet } from "@avalabs/avacloud-sdk/models/components" -import SelectSubnetId from "../../components/SelectSubnetId" +import SelectSubnet, { SubnetSelection } from "../../components/SelectSubnet" export default function QueryL1Details() { - const [subnetId, setSubnetID] = useState("") - const { avalancheNetworkID } = useWalletStore() - const [subnetDetails, setSubnetDetails] = useState(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 [domain, setDomain] = useState(""); const [networkName, setNetworkName] = useState(""); const [networkShortName, setNetworkShortName] = useState(""); @@ -260,6 +264,7 @@ export default function BlockScout() { useEffect(() => { setSubnetIdError(null); setSubnetId(""); + setSubnet(null); if (!chainId) return // Set defaults from L1 store if available @@ -271,11 +276,23 @@ export default function BlockScout() { setTokenSymbol(l1Info.coinName); } - getBlockchainInfo(chainId).then((chainInfo) => { - setSubnetId(chainInfo.subnetId); - }).catch((error) => { - setSubnetIdError((error as Error).message); - }); + 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(() => { @@ -334,14 +351,21 @@ export default function BlockScout() { - + + {/* Show subnet details if available */} + diff --git a/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx b/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx index 680635a51df..d5a6853874a 100644 --- a/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx +++ b/toolbox/src/toolbox/Nodes/AvalanchegoDocker.tsx @@ -5,10 +5,11 @@ import { useState, useEffect } from "react"; import { networkIDs } from "@avalabs/avalanchejs"; import versions from "../../versions.json"; import { Container } from "../../components/Container"; -import { Input } from "../../components/Input"; -import { getBlockchainInfo } from "../../coreViem/utils/glacier"; +import { getBlockchainInfo, getSubnetInfo } from "../../coreViem/utils/glacier"; import InputChainId from "../../components/InputChainId"; +import InputSubnetId from "../../components/InputSubnetId"; import { Checkbox } from "../../components/Checkbox"; +import BlockchainDetailsDisplay from "../../components/BlockchainDetailsDisplay"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; @@ -22,6 +23,8 @@ import { DockerInstallation } from "../../components/DockerInstallation"; import { NodeReadinessValidator } from "../../components/NodeReadinessValidator"; import { HealthCheckButton } from "../../components/HealthCheckButton"; +// Standard subnet-evm VM ID +export const SUBNET_EVM_VM_ID = "srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy"; export const nodeConfigBase64 = (chainId: string, debugEnabled: boolean, pruningEnabled: boolean) => { const vmConfig = debugEnabled ? { @@ -125,6 +128,8 @@ https://${processedDomain}/ext/bc/${chainId}/rpc` export default function AvalanchegoDocker() { const [chainId, setChainId] = useState(""); const [subnetId, setSubnetId] = useState(""); + const [subnet, setSubnet] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [isRPC, setIsRPC] = useState(true); const [rpcCommand, setRpcCommand] = useState(""); const [nodeRunningMode, setNodeRunningMode] = useState("server"); @@ -139,6 +144,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)); @@ -153,24 +183,10 @@ export default function AvalanchegoDocker() { } }, [isRPC]); - - - - useEffect(() => { - setSubnetIdError(null); - setSubnetId(""); - if (!chainId) return - - getBlockchainInfo(chainId).then((chainInfo) => { - setSubnetId(chainInfo.subnetId); - }).catch((error) => { - setSubnetIdError((error as Error).message); - }); - }, [chainId]); - const handleReset = () => { setChainId(""); setSubnetId(""); + setSubnet(null); setIsRPC(true); setChainAddedToWallet(null); setRpcCommand(""); @@ -239,14 +255,19 @@ 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} /> - ([]) const [filteredValidators, setFilteredValidators] = useState([]) const [isLoading, setIsLoading] = useState(false) @@ -22,6 +24,9 @@ export default function QueryL1ValidatorSet() { const [selectedValidator, setSelectedValidator] = useState(null) const [copiedNodeId, setCopiedNodeId] = useState(null) const [subnetId, setSubnetId] = useState("") + const [subnet, setSubnet] = useState(null) + const [isLoadingSubnet, setIsLoadingSubnet] = useState(false) + const [subnetError, setSubnetError] = useState(null) const [searchTerm, setSearchTerm] = useState("") // Network names for display @@ -30,13 +35,45 @@ export default function QueryL1ValidatorSet() { [networkIDs.FujiID]: "fuji", } - async function fetchValidators() { + // Fetch subnet details when subnet ID changes + useEffect(() => { + if (!subnetId) { + setSubnet(null) + setSubnetError(null) + return + } + + setIsLoadingSubnet(true) + setSubnetError(null) + getSubnetInfo(subnetId) + .then((subnetInfo) => { + setSubnet(subnetInfo) + setSubnetError(null) + }) + .catch((error) => { + console.error('Error getting subnet info:', error) + setSubnet(null) + setSubnetError((error as Error).message) + }) + .finally(() => { + setIsLoadingSubnet(false) + }) + }, [subnetId]) + + const fetchValidators = async () => { + if (!subnetId) return + + // Check if there are subnet validation errors + if (subnetError) { + setError(`Invalid subnet: ${subnetError}`) + return + } + setIsLoading(true) setError(null) setSelectedValidator(null) - try { - // Validate that subnet ID is provided + if (!subnetId.trim()) { throw new Error("Subnet ID is required to query L1 validators") } @@ -46,23 +83,27 @@ export default function QueryL1ValidatorSet() { throw new Error("Invalid network selected") } - const result = await new AvaCloudSDK().data.primaryNetwork.listL1Validators({ - network: network, - subnetId: subnetId.trim(), - includeInactiveL1Validators: true, - }); - - // Handle pagination - let validators: L1ValidatorDetailsFull[] = [] + const result = await new AvaCloudSDK({ + serverURL: isTestnet ? "https://api.avax-test.network" : "https://api.avax.network", + network: networkNames[Number(avalancheNetworkID)], + }).data.primaryNetwork.listL1Validators({ + network: networkNames[Number(avalancheNetworkID)], + subnetId, + }) + // Get all pages of results + const allValidators: L1ValidatorDetailsFull[] = []; for await (const page of result) { - validators.push(...page.result.validators) - setValidators(validators) + if ('validators' in page) { + allValidators.push(...(page.validators as L1ValidatorDetailsFull[])); + } } - } catch (error: any) { - setError(error.message || "Failed to fetch validators") - setValidators([]) - console.error("Error fetching validators:", error) + + setValidators(allValidators) + setFilteredValidators(allValidators) + } catch (err) { + console.error("Error fetching validators:", err) + setError("Failed to fetch validators") } finally { setIsLoading(false) } @@ -133,10 +174,17 @@ export default function QueryL1ValidatorSet() {

+ {/* Show subnet details if available */} + +
- + {/* Display Hex Format */}

Validation ID (Hex)

diff --git a/toolbox/src/toolbox/ValidatorManager/RemoveValidator/CompleteValidatorRemoval.tsx b/toolbox/src/toolbox/ValidatorManager/RemoveValidator/CompleteValidatorRemoval.tsx index 928033181db..c78b11181db 100644 --- a/toolbox/src/toolbox/ValidatorManager/RemoveValidator/CompleteValidatorRemoval.tsx +++ b/toolbox/src/toolbox/ValidatorManager/RemoveValidator/CompleteValidatorRemoval.tsx @@ -1,19 +1,18 @@ import React, { useState, useEffect } from 'react'; import { useWalletStore } from '../../../stores/walletStore'; import { useViemChainStore } from '../../../stores/toolboxStore'; -import { AvaCloudSDK } from '@avalabs/avacloud-sdk'; import { Button } from '../../../components/Button'; import { Input } from '../../../components/Input'; import { AlertCircle } from 'lucide-react'; import { Success } from '../../../components/Success'; import { bytesToHex, hexToBytes } from 'viem'; -import { networkIDs } from '@avalabs/avalanchejs'; import validatorManagerAbi from '../../../../contracts/icm-contracts/compiled/ValidatorManager.json'; import poaManagerAbi from '../../../../contracts/icm-contracts/compiled/PoAManager.json'; import { GetRegistrationJustification } from '../justification'; import { packL1ValidatorRegistration } from '../../../coreViem/utils/convertWarp'; import { packWarpIntoAccessList } from '../packWarp'; import { extractL1ValidatorWeightMessage } from '../../../coreViem/methods/extractL1ValidatorWeightMessage'; +import { useAvaCloudSDK } from '../../../stores/useAvaCloudSDK'; interface CompleteValidatorRemovalProps { subnetIdL1: string; @@ -48,6 +47,7 @@ const CompleteValidatorRemoval: React.FC = ({ ownerType, }) => { const { coreWalletClient, publicClient, avalancheNetworkID } = useWalletStore(); + const { aggregateSignature } = useAvaCloudSDK(); const viemChain = useViemChainStore(); const [pChainTxId, setPChainTxId] = useState(initialPChainTxId || ''); @@ -62,8 +62,6 @@ const CompleteValidatorRemoval: React.FC = ({ weight: bigint; } | null>(null); - const networkName = avalancheNetworkID === networkIDs.MainnetID ? 'mainnet' : 'fuji'; - // Determine target contract and ABI based on ownerType const useMultisig = ownerType === 'PoAManager'; const targetContractAddress = useMultisig ? contractOwner : validatorManagerAddress; @@ -144,14 +142,10 @@ const CompleteValidatorRemoval: React.FC = ({ "11111111111111111111111111111111LpoYY" // always use P-Chain ID ); - const signature = await new AvaCloudSDK().data.signatureAggregator.aggregateSignatures({ - network: networkName, - signatureAggregatorRequest: { - message: bytesToHex(removeValidatorMessage), - justification: bytesToHex(justification), - signingSubnetId: signingSubnetId || subnetIdL1, - quorumPercentage: 67, - }, + const signature = await aggregateSignature({ + message: bytesToHex(removeValidatorMessage), + signingSubnetId: signingSubnetId || subnetIdL1, + quorumPercentage: 67, }); setPChainSignature(signature.signedMessage); @@ -174,7 +168,7 @@ const CompleteValidatorRemoval: React.FC = ({ if (finalReceipt.status !== 'success') { throw new Error(`Transaction failed with status: ${finalReceipt.status}`); } - + setTransactionHash(hash); const successMsg = `Validator removal completed successfully.`; setSuccessMessage(successMsg); @@ -221,14 +215,14 @@ const CompleteValidatorRemoval: React.FC = ({ Checking contract ownership...
)} - +

Target Contract: {useMultisig ? 'PoAManager' : 'ValidatorManager'}

Contract Address: {targetContractAddress || 'Not set'}

Contract Owner: { - isContractOwner === true ? 'You are the owner' : - isContractOwner === false ? `Owned by ${ownerType || 'contract'}` : - 'Checking...' + isContractOwner === true ? 'You are the owner' : + isContractOwner === false ? `Owned by ${ownerType || 'contract'}` : + 'Checking...' }

{extractedData && (
@@ -238,19 +232,19 @@ const CompleteValidatorRemoval: React.FC = ({
)} {pChainSignature && ( -

P-Chain Signature: {pChainSignature.substring(0,50)}...

+

P-Chain Signature: {pChainSignature.substring(0, 50)}...

)}
- - {transactionHash && ( - diff --git a/toolbox/src/toolbox/ValidatorManager/RemoveValidator/SubmitPChainTxRemoval.tsx b/toolbox/src/toolbox/ValidatorManager/RemoveValidator/SubmitPChainTxRemoval.tsx index d15cf801404..87256aa6d08 100644 --- a/toolbox/src/toolbox/ValidatorManager/RemoveValidator/SubmitPChainTxRemoval.tsx +++ b/toolbox/src/toolbox/ValidatorManager/RemoveValidator/SubmitPChainTxRemoval.tsx @@ -1,11 +1,10 @@ import React, { useState, useEffect } from 'react'; import { useWalletStore } from '../../../stores/walletStore'; -import { AvaCloudSDK } from '@avalabs/avacloud-sdk'; import { Button } from '../../../components/Button'; import { Input } from '../../../components/Input'; import { AlertCircle } from 'lucide-react'; import { Success } from '../../../components/Success'; -import { networkIDs } from '@avalabs/avalanchejs'; +import { useAvaCloudSDK } from '../../../stores/useAvaCloudSDK'; interface SubmitPChainTxRemovalProps { subnetIdL1: string; @@ -27,7 +26,8 @@ const SubmitPChainTxRemoval: React.FC = ({ onSuccess, onError, }) => { - const { coreWalletClient, pChainAddress, avalancheNetworkID, publicClient } = useWalletStore(); + const { coreWalletClient, pChainAddress, publicClient } = useWalletStore(); + const { aggregateSignature } = useAvaCloudSDK(); const [evmTxHash, setEvmTxHash] = useState(initialEvmTxHash || ''); const [isProcessing, setIsProcessing] = useState(false); const [error, setErrorState] = useState(null); @@ -41,8 +41,6 @@ const SubmitPChainTxRemoval: React.FC = ({ endTime: bigint; } | null>(null); - const networkName = avalancheNetworkID === networkIDs.MainnetID ? "mainnet" : "fuji"; - // Update evmTxHash when initialEvmTxHash prop changes useEffect(() => { if (initialEvmTxHash && initialEvmTxHash !== evmTxHash) { @@ -77,7 +75,7 @@ const SubmitPChainTxRemoval: React.FC = ({ console.log("🔍 [SubmitPChainTxRemoval] Transaction receipt:", receipt); console.log("🔍 [SubmitPChainTxRemoval] Number of logs:", receipt.logs.length); - + // Log all event topics for debugging receipt.logs.forEach((log, index) => { console.log(`🔍 [SubmitPChainTxRemoval] Log ${index}:`, { @@ -94,10 +92,10 @@ const SubmitPChainTxRemoval: React.FC = ({ // This works for both direct and multisig transactions when the warp precompile emits the event const warpMessageTopic = "0x56600c567728a800c0aa927500f831cb451df66a7af570eb4df4dfbf4674887d"; const warpPrecompileAddress = "0x0200000000000000000000000000000000000005"; - + const warpEventLog = receipt.logs.find((log) => { return log && log.address && log.address.toLowerCase() === warpPrecompileAddress.toLowerCase() && - log.topics && log.topics[0] && log.topics[0].toLowerCase() === warpMessageTopic.toLowerCase(); + log.topics && log.topics[0] && log.topics[0].toLowerCase() === warpMessageTopic.toLowerCase(); }); if (warpEventLog && warpEventLog.data) { @@ -129,18 +127,18 @@ const SubmitPChainTxRemoval: React.FC = ({ // InitiatedValidatorWeightUpdate: when resendValidatorRemovalMessage is used (fallback) const removalEventTopic = "0x9e51aa28092b7ac0958967564371c129b31b238c0c0bdb0eb9cb4d1e40d724dc"; const weightUpdateEventTopic = "0x6e350dd49b060d87f297206fd309234ed43156d890ced0f139ecf704310481d3"; - + console.log("🔍 [SubmitPChainTxRemoval] Looking for event topics:"); console.log("🔍 [SubmitPChainTxRemoval] - InitiatedValidatorRemoval:", removalEventTopic); console.log("🔍 [SubmitPChainTxRemoval] - InitiatedValidatorWeightUpdate:", weightUpdateEventTopic); console.log("🔍 [SubmitPChainTxRemoval] - Warp Message:", warpMessageTopic); - + // First try to find the Warp message event from the precompile (already found above) let eventLog = warpEventLog; let isWarpMessageEvent = false; let isWeightUpdateEvent = false; - + if (eventLog) { console.log("🔍 [SubmitPChainTxRemoval] Found Warp message event from precompile"); isWarpMessageEvent = true; @@ -218,7 +216,7 @@ const SubmitPChainTxRemoval: React.FC = ({ endTime, }; } - + console.log("🔍 [SubmitPChainTxRemoval] Parsed event data:", parsedEventData); setEventData(parsedEventData); setErrorState(null); @@ -237,7 +235,7 @@ const SubmitPChainTxRemoval: React.FC = ({ const handleSubmitPChainTx = async () => { setErrorState(null); setTxSuccess(null); - + if (!evmTxHash.trim()) { setErrorState("EVM transaction hash is required."); onError("EVM transaction hash is required."); @@ -272,15 +270,12 @@ const SubmitPChainTxRemoval: React.FC = ({ setIsProcessing(true); try { // Step 1: Sign the warp message - const { signedMessage } = await new AvaCloudSDK().data.signatureAggregator.aggregateSignatures({ - network: networkName, - signatureAggregatorRequest: { - message: unsignedWarpMessage, - signingSubnetId: signingSubnetId || subnetIdL1, - quorumPercentage: 67, - }, + const { signedMessage } = await aggregateSignature({ + message: unsignedWarpMessage, + signingSubnetId: signingSubnetId || subnetIdL1, + quorumPercentage: 67, }); - + setSignedWarpMessage(signedMessage); // Step 2: Submit to P-Chain @@ -293,7 +288,7 @@ const SubmitPChainTxRemoval: React.FC = ({ onSuccess(pChainTxId, eventData); } catch (err: any) { let message = err instanceof Error ? err.message : String(err); - + // Handle specific error types if (message.includes('User rejected')) { message = 'Transaction was rejected by user'; @@ -304,7 +299,7 @@ const SubmitPChainTxRemoval: React.FC = ({ } else if (message.includes('nonce')) { message = 'Transaction nonce error. Please try again.'; } - + setErrorState(`P-Chain transaction failed: ${message}`); onError(`P-Chain transaction failed: ${message}`); } finally { @@ -340,14 +335,14 @@ const SubmitPChainTxRemoval: React.FC = ({ {unsignedWarpMessage && (
-

Unsigned Warp Message: {unsignedWarpMessage.substring(0,50)}...

+

Unsigned Warp Message: {unsignedWarpMessage.substring(0, 50)}...

{signedWarpMessage && ( -

Signed Warp Message: {signedWarpMessage.substring(0,50)}...

+

Signed Warp Message: {signedWarpMessage.substring(0, 50)}...

)}
)} -