Skip to content

Commit f7d2422

Browse files
authored
SelectSubnet, SelectBlockchain, inputSubnetId & useAvaCloudSDK (#2531)
* initial component layout * improved UI - AvaCloud getSubnetById method missing data * upgraded latest Avacloud SDK version + improved UI * fixed Validator Manager latest Avacloud SDK version changes * spacing on succesful subnet creation + fixed compilation errors * created InputSubnetId component * fix build error * `SelectBlockchain` and refactored ChainID to BlockchainId (#2557) * refactored to SelectBlockchainId * selectBlockchain * fixing build error * unified subnet and blockchain details into 1 component * removed old detail components * removed unused imports * resolved AvacloudSDK v1.12.1 build errros * updating serverURL init for Avacloud SDK * fetching VM ID and warning user - pending correctly creating docker command * fixed Ilya's comments * fixed build errors * added testing changes * removed unused files * fixin component UI + input subnet ID on create chain * fix aggregate signature * fix aggregate signature pending buffer issue * fix build errors * fix build errors * added justification to complete validator removal * adding justification to the complete sign aggregate
1 parent 6092418 commit f7d2422

37 files changed

+1377
-495
lines changed

components/tools/common/api/validator-info.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Validator, SubnetInfo, L1ValidatorManagerDetails } from './types';
22
import { pChainEndpoint } from './consts';
33
import { AvaCloudSDK } from "@avalabs/avacloud-sdk";
4+
import { useWalletStore } from "../../../../toolbox/src/stores/walletStore";
5+
const { isTestnet } = useWalletStore();
6+
47
export const avaCloudSDK = new AvaCloudSDK({
8+
serverURL: isTestnet ? "https://api.avax-test.network" : "https://api.avax.network",
59
chainId: "43114",
610
network: "fuji",
711
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"postinstall": "prisma generate && fumadocs-mdx && cd toolbox && yarn"
1212
},
1313
"dependencies": {
14-
"@avalabs/avacloud-sdk": "0.8.7",
14+
"@avalabs/avacloud-sdk": "0.12.1",
1515
"@avalabs/avalanchejs": "^5.0.0",
1616
"@fumadocs/mdx-remote": "^1.2.0",
1717
"@hookform/resolvers": "^4.1.3",
@@ -129,4 +129,4 @@
129129
"typescript": "^5.8.2"
130130
},
131131
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
132-
}
132+
}

toolbox/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"postinstall": "node update_docker_tags.js"
2727
},
2828
"dependencies": {
29-
"@avalabs/avacloud-sdk": "^0.9.0",
29+
"@avalabs/avacloud-sdk": "^0.12.1",
3030
"@avalabs/avalanchejs": "^4.1.2-alpha.4",
3131
"@radix-ui/react-alert-dialog": "^1.1.13",
3232
"@radix-ui/react-checkbox": "^1.2.3",
@@ -77,4 +77,4 @@
7777
"vite": "^6.1.6",
7878
"vite-plugin-dts": "^4.5.0"
7979
}
80-
}
80+
}

toolbox/src/components/BlockchainDetailsDisplay.tsx

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client"
2+
3+
import { Input, type Suggestion } from "./Input";
4+
import { useL1ListStore } from "../stores/l1ListStore";
5+
import { useCreateChainStore } from "../stores/createChainStore";
6+
import { useMemo, useState, useCallback, useEffect } from "react";
7+
import { utils } from "@avalabs/avalanchejs";
8+
import { useAvaCloudSDK } from "../stores/useAvaCloudSDK";
9+
10+
// Primary network subnet ID
11+
export const PRIMARY_NETWORK_SUBNET_ID = "11111111111111111111111111111111LpoYY";
12+
13+
export default function InputSubnetId({
14+
value,
15+
onChange,
16+
error,
17+
label = "Subnet ID",
18+
hidePrimaryNetwork = true,
19+
helperText,
20+
id,
21+
validationDelayMs = 500,
22+
readOnly = false,
23+
hideSuggestions = false,
24+
placeholder
25+
}: {
26+
value: string,
27+
onChange: (value: string) => void,
28+
error?: string | null,
29+
label?: string,
30+
hidePrimaryNetwork?: boolean
31+
helperText?: string | null
32+
id?: string
33+
validationDelayMs?: number
34+
readOnly?: boolean
35+
hideSuggestions?: boolean
36+
placeholder?: string
37+
}) {
38+
const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId);
39+
const { l1List } = useL1ListStore()();
40+
const { getSubnetById } = useAvaCloudSDK();
41+
42+
const [validationError, setValidationError] = useState<string | null>(null);
43+
44+
// Validate subnet ID format and checksum using Base58Check
45+
const validateBase58Format = (subnetId: string): boolean => {
46+
try {
47+
// Validate Base58Check format and checksum (last 4 bytes)
48+
utils.base58check.decode(subnetId);
49+
return true;
50+
} catch (error) {
51+
return false;
52+
}
53+
};
54+
55+
// Validate subnet ID using AvaCloud SDK
56+
const validateSubnetId = useCallback(async (subnetId: string) => {
57+
if (!subnetId || subnetId.length < 10) {
58+
setValidationError(null);
59+
return;
60+
}
61+
62+
// First validate Base58Check format and checksum
63+
if (!validateBase58Format(subnetId)) {
64+
setValidationError("Invalid subnet ID format or checksum");
65+
return;
66+
}
67+
68+
try {
69+
setValidationError(null);
70+
71+
await getSubnetById({ subnetId });
72+
73+
// If we get here, the subnet exists
74+
setValidationError(null);
75+
} catch (error) {
76+
// Show validation error for invalid subnet IDs
77+
setValidationError("Subnet ID not found or invalid");
78+
}
79+
}, [getSubnetById]);
80+
81+
// Validate when value changes
82+
useEffect(() => {
83+
const timeoutId = setTimeout(() => {
84+
if (value) {
85+
validateSubnetId(value);
86+
} else {
87+
setValidationError(null);
88+
}
89+
}, validationDelayMs); // Wait for subnet to be available before validation
90+
91+
return () => clearTimeout(timeoutId);
92+
}, [value, validateSubnetId, validationDelayMs]);
93+
94+
const subnetIdSuggestions: Suggestion[] = useMemo(() => {
95+
const result: Suggestion[] = [];
96+
const seen = new Set<string>();
97+
98+
// Add subnet from create chain store first
99+
if (createChainStoreSubnetId && !(hidePrimaryNetwork && createChainStoreSubnetId === PRIMARY_NETWORK_SUBNET_ID)) {
100+
result.push({
101+
title: createChainStoreSubnetId,
102+
value: createChainStoreSubnetId,
103+
description: "The Subnet that you have just created in the \"Create Chain\" tool"
104+
});
105+
seen.add(createChainStoreSubnetId);
106+
}
107+
108+
// Add subnets from L1 list
109+
for (const l1 of l1List) {
110+
const { subnetId, name } = l1;
111+
112+
if (!subnetId || seen.has(subnetId)) continue;
113+
114+
if (hidePrimaryNetwork && subnetId === PRIMARY_NETWORK_SUBNET_ID) {
115+
continue;
116+
}
117+
118+
result.push({
119+
title: `${name} (${subnetId})`,
120+
value: subnetId,
121+
description: l1.description || "A subnet that was added to your L1 list.",
122+
});
123+
124+
seen.add(subnetId);
125+
}
126+
127+
return result;
128+
}, [createChainStoreSubnetId, l1List, hidePrimaryNetwork]);
129+
130+
// Combine validation error with passed error
131+
const combinedError = error || validationError;
132+
133+
return (
134+
<Input
135+
id={id}
136+
label={label}
137+
value={value}
138+
onChange={onChange}
139+
suggestions={readOnly || hideSuggestions ? [] : subnetIdSuggestions}
140+
error={combinedError}
141+
helperText={helperText}
142+
placeholder={
143+
readOnly
144+
? "Automatically filled from Blockchain ID"
145+
: placeholder || "Enter subnet ID"
146+
}
147+
disabled={readOnly}
148+
/>
149+
);
150+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client"
2+
3+
import SelectBlockchainId from "./SelectBlockchainId";
4+
import { useState, useCallback } from "react";
5+
import { useWalletStore } from "../stores/walletStore";
6+
import { networkIDs } from "@avalabs/avalanchejs";
7+
8+
// API Response type from AvaCloud - matches the official API response
9+
export type BlockchainApiResponse = {
10+
createBlockTimestamp: number;
11+
createBlockNumber: string;
12+
blockchainId: string;
13+
vmId: string;
14+
subnetId: string;
15+
blockchainName: string;
16+
evmChainId: number;
17+
}
18+
19+
// Extended type with additional metadata
20+
export type BlockchainInfo = BlockchainApiResponse & {
21+
isTestnet: boolean;
22+
}
23+
24+
export type BlockchainSelection = {
25+
blockchainId: string;
26+
blockchain: BlockchainInfo | null;
27+
}
28+
29+
// Import the unified details display component
30+
import BlockchainDetailsDisplay from "./BlockchainDetailsDisplay";
31+
32+
export default function SelectBlockchain({
33+
value,
34+
onChange,
35+
error,
36+
label = "Select Avalanche Blockchain ID"
37+
}: {
38+
value: string,
39+
onChange: (selection: BlockchainSelection) => void,
40+
error?: string | null,
41+
label?: string
42+
}) {
43+
const { avalancheNetworkID } = useWalletStore();
44+
const [blockchainDetails, setBlockchainDetails] = useState<Record<string, BlockchainInfo>>({});
45+
const [isLoading, setIsLoading] = useState(false);
46+
47+
// Network names for API calls
48+
const networkNames: Record<number, string> = {
49+
[networkIDs.MainnetID]: "mainnet",
50+
[networkIDs.FujiID]: "fuji",
51+
};
52+
53+
// Fetch blockchain details when needed
54+
const fetchBlockchainDetails = useCallback(async (blockchainId: string) => {
55+
if (!blockchainId || blockchainDetails[blockchainId]) return;
56+
57+
try {
58+
const network = networkNames[Number(avalancheNetworkID)];
59+
if (!network) return;
60+
61+
setIsLoading(true);
62+
63+
// Use direct API call as shown in AvaCloud documentation
64+
// https://developers.avacloud.io/data-api/primary-network/get-blockchain-details-by-id
65+
const response = await fetch(`https://glacier-api.avax.network/v1/networks/${network}/blockchains/${blockchainId}`, {
66+
method: 'GET',
67+
headers: {
68+
'accept': 'application/json',
69+
},
70+
});
71+
72+
if (!response.ok) {
73+
throw new Error(`Failed to fetch blockchain details: ${response.statusText}`);
74+
}
75+
76+
const blockchain: BlockchainApiResponse = await response.json();
77+
78+
setBlockchainDetails(prev => ({
79+
...prev,
80+
[blockchainId]: {
81+
...blockchain,
82+
isTestnet: network === "fuji"
83+
}
84+
}));
85+
} catch (error) {
86+
console.error(`Error fetching blockchain details for ${blockchainId}:`, error);
87+
} finally {
88+
setIsLoading(false);
89+
}
90+
}, [avalancheNetworkID, networkNames, blockchainDetails]);
91+
92+
// Handle value change and fetch details if needed
93+
const handleValueChange = useCallback((newValue: string) => {
94+
if (newValue && !blockchainDetails[newValue]) {
95+
fetchBlockchainDetails(newValue);
96+
}
97+
98+
onChange({
99+
blockchainId: newValue,
100+
blockchain: blockchainDetails[newValue] || null
101+
});
102+
}, [fetchBlockchainDetails, blockchainDetails, onChange]);
103+
104+
// Get current blockchain details for display
105+
const currentBlockchain = value ? blockchainDetails[value] || null : null;
106+
const isLoadingCurrent = value && !blockchainDetails[value] && isLoading;
107+
108+
return (
109+
<div>
110+
<SelectBlockchainId
111+
value={value}
112+
onChange={handleValueChange}
113+
error={error}
114+
label={label}
115+
/>
116+
117+
{/* Display blockchain details when a blockchain is selected */}
118+
{value && <BlockchainDetailsDisplay blockchain={currentBlockchain} isLoading={!!isLoadingCurrent} />}
119+
</div>
120+
);
121+
}

toolbox/src/components/SelectChainID.tsx renamed to toolbox/src/components/SelectBlockchainId.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { useMemo } from "react";
55
import { cn } from "../lib/utils";
66
import { Globe } from 'lucide-react';
77

8-
interface ChainOption {
8+
interface BlockchainOption {
99
id: string;
1010
name: string;
1111
description: string;
1212
logoUrl?: string;
1313
}
1414

15-
export default function SelectChainID({
15+
export default function SelectBlockchainId({
1616
value,
1717
onChange,
1818
error,
@@ -28,8 +28,8 @@ export default function SelectChainID({
2828
const { l1List } = useL1ListStore()();
2929
const selectId = useId();
3030

31-
const options: ChainOption[] = useMemo(() => {
32-
const result: ChainOption[] = [];
31+
const options: BlockchainOption[] = useMemo(() => {
32+
const result: BlockchainOption[] = [];
3333

3434
if (createChainStorechainID) {
3535
result.push({
@@ -90,7 +90,7 @@ export default function SelectChainID({
9090
</div>
9191
</div>
9292
) : (
93-
<div className="text-zinc-400 dark:text-zinc-500">Select a chain ID</div>
93+
<div className="text-zinc-400 dark:text-zinc-500">Select a blockchain ID</div>
9494
)}
9595
</button>
9696

@@ -129,4 +129,4 @@ export default function SelectChainID({
129129
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
130130
</div>
131131
);
132-
}
132+
}

0 commit comments

Comments
 (0)