diff --git a/app/console/history/page.tsx b/app/console/history/page.tsx index 735c6d525cf..9c5541a6859 100644 --- a/app/console/history/page.tsx +++ b/app/console/history/page.tsx @@ -110,6 +110,16 @@ export default function ConsoleHistoryPage() { type: 'address' }); } + if (toolboxStore.erc20StakingManagerAddress && toolboxStore.erc20StakingManagerAddress !== '') { + items.push({ + id: 'tb-erc20-staking-mgr', + title: 'ERC20 Token Staking Manager', + description: 'Deployed Contract', + address: toolboxStore.erc20StakingManagerAddress, + chainId, + type: 'address' + }); + } if (toolboxStore.poaManagerAddress && toolboxStore.poaManagerAddress !== '') { items.push({ id: 'tb-poa-mgr', diff --git a/app/console/permissionless-l1s/delegate-native-token/page.tsx b/app/console/permissionless-l1s/delegate-native-token/page.tsx new file mode 100644 index 00000000000..3d315b094d1 --- /dev/null +++ b/app/console/permissionless-l1s/delegate-native-token/page.tsx @@ -0,0 +1,5 @@ +import DelegateToValidator from '@/components/toolbox/console/permissionless-l1s/delegate/native/Delegate'; + +export default function DelegateNativeTokenPage() { + return ; +} \ No newline at end of file diff --git a/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/client-page.tsx b/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/client-page.tsx new file mode 100644 index 00000000000..a717c1d43bf --- /dev/null +++ b/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/client-page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import StepFlow from "@/components/console/step-flow"; +import { steps } from "../steps"; + +export default function ERC20StakingManagerSetupClientPage({ currentStepKey }: { currentStepKey: string }) { + const basePath = "/console/permissionless-l1s/erc20-staking-manager-setup"; + return ( + + ); +} \ No newline at end of file diff --git a/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/page.tsx b/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/page.tsx new file mode 100644 index 00000000000..da3113a2572 --- /dev/null +++ b/app/console/permissionless-l1s/erc20-staking-manager-setup/[step]/page.tsx @@ -0,0 +1,8 @@ +import ERC20StakingManagerSetupClientPage from "./client-page"; + +export default async function Page({ params }: { params: Promise<{ step: string }> }) { + const { step } = await params; + return ( + + ); +} \ No newline at end of file diff --git a/app/console/permissionless-l1s/erc20-staking-manager-setup/page.tsx b/app/console/permissionless-l1s/erc20-staking-manager-setup/page.tsx new file mode 100644 index 00000000000..87c9d57a401 --- /dev/null +++ b/app/console/permissionless-l1s/erc20-staking-manager-setup/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/console/permissionless-l1s/erc20-staking-manager-setup/deploy-erc20-staking-manager"); +} \ No newline at end of file diff --git a/app/console/permissionless-l1s/erc20-staking-manager-setup/steps.tsx b/app/console/permissionless-l1s/erc20-staking-manager-setup/steps.tsx new file mode 100644 index 00000000000..f3eaab28eb5 --- /dev/null +++ b/app/console/permissionless-l1s/erc20-staking-manager-setup/steps.tsx @@ -0,0 +1,27 @@ +import { type StepDefinition } from "@/components/console/step-flow"; +import ReadContract from "@/components/toolbox/console/permissioned-l1s/validator-manager-setup/ReadContract"; +import DeployNativeTokenStakingManager from "@/components/toolbox/console/permissionless-l1s/setup/native/DeployNativeStakingManager"; +import InitializeNativeTokenStakingManager from "@/components/toolbox/console/permissionless-l1s/setup/native/InitializeNativeStakingManager"; +import DeployERC20StakingManager from "@/components/toolbox/console/permissionless-l1s/setup/erc20/DeployERC20StakingManager"; +import InitializeERC20StakingManager from "@/components/toolbox/console/permissionless-l1s/setup/erc20/InitializeERC20StakingManager"; +import DeployExampleRewardCalculator from "@/components/toolbox/console/permissionless-l1s/setup/DeployExampleRewardCalculator"; +import TransferOwnership from "@/components/toolbox/console/permissioned-l1s/multisig-setup/TransferOwnership"; +import EnableStakingManagerMinting from "@/components/toolbox/console/permissionless-l1s/setup/native/EnableStakingManagerMinting"; + +export const steps: StepDefinition[] = [ + { + type: "single", + key: "deploy-erc20-staking-manager", + title: "Deploy ERC20 Token Staking Manager", + component: DeployERC20StakingManager, + }, + { type: "single", key: "deploy-example-reward-calculator", title: "Deploy Example Reward Calculator", component: DeployExampleRewardCalculator }, + { + type: "single", + key: "initialize-erc20-staking-manager", + title: "Initialize ERC20 Token Staking Manager", + component: InitializeERC20StakingManager, + }, + { type: "single", key: "transfer-ownership", title: "Transfer Ownership", component: TransferOwnership }, + { type: "single", key: "read-contract", title: "Read Contract", component: ReadContract }, +]; \ No newline at end of file diff --git a/app/console/permissionless-l1s/stake-native-token/page.tsx b/app/console/permissionless-l1s/stake-native-token/page.tsx new file mode 100644 index 00000000000..2d49ffd6de1 --- /dev/null +++ b/app/console/permissionless-l1s/stake-native-token/page.tsx @@ -0,0 +1,7 @@ +import Stake from "@/components/toolbox/console/permissionless-l1s/staking/native/Stake"; + +export default function Page() { + return ( + + ); +} \ No newline at end of file diff --git a/components/console/console-sidebar.tsx b/components/console/console-sidebar.tsx index 2bb767163b3..fce27aed0e1 100644 --- a/components/console/console-sidebar.tsx +++ b/components/console/console-sidebar.tsx @@ -234,6 +234,21 @@ const data = { url: "/console/permissionless-l1s/native-staking-manager-setup", icon: GitMerge, }, + { + title: "ERC20 Staking Manager Setup", + url: "/console/permissionless-l1s/erc20-staking-manager-setup", + icon: GitMerge, + }, + { + title: "Stake Native Token", + url: "/console/permissionless-l1s/stake-native-token", + icon: Hexagon, + }, + { + title: "Delegate Native Token", + url: "/console/permissionless-l1s/delegate-native-token", + icon: HandCoins, + } ], }, { diff --git a/components/toolbox/console/permissionless-l1s/delegate/native/CompleteDelegatorRegistration.tsx b/components/toolbox/console/permissionless-l1s/delegate/native/CompleteDelegatorRegistration.tsx new file mode 100644 index 00000000000..63ce31a0b36 --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/delegate/native/CompleteDelegatorRegistration.tsx @@ -0,0 +1,199 @@ +import React, { useState, useEffect } from 'react'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { useViemChainStore } from '@/components/toolbox/stores/toolboxStore'; +import { Button } from '@/components/toolbox/components/Button'; +import { Input } from '@/components/toolbox/components/Input'; +import { Success } from '@/components/toolbox/components/Success'; +import { Alert } from '@/components/toolbox/components/Alert'; +import { bytesToHex, hexToBytes } from 'viem'; +import nativeTokenStakingManagerAbi from '@/contracts/icm-contracts/compiled/NativeTokenStakingManager.json'; +import { GetRegistrationJustification } from '@/components/toolbox/console/permissioned-l1s/ValidatorManager/justification'; +import { packL1ValidatorWeightMessage } from '@/components/toolbox/coreViem/utils/convertWarp'; +import { packWarpIntoAccessList } from '@/components/toolbox/console/permissioned-l1s/ValidatorManager/packWarp'; +import { useAvalancheSDKChainkit } from '@/components/toolbox/stores/useAvalancheSDKChainkit'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; + +interface CompleteDelegatorRegistrationProps { + subnetIdL1: string; + delegationID: string; + pChainTxId: string; + stakingManagerAddress: string; + signingSubnetId: string; + onSuccess: (message: string) => void; + onError: (message: string) => void; +} + +const CompleteDelegatorRegistration: React.FC = ({ + subnetIdL1, + delegationID, + pChainTxId, + stakingManagerAddress, + signingSubnetId, + onSuccess, + onError, +}) => { + const { coreWalletClient, publicClient, avalancheNetworkID, walletEVMAddress } = useWalletStore(); + const { aggregateSignature } = useAvalancheSDKChainkit(); + const { notify } = useConsoleNotifications(); + const viemChain = useViemChainStore(); + + const [isProcessing, setIsProcessing] = useState(false); + const [error, setErrorState] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [transactionHash, setTransactionHash] = useState(null); + + const handleCompleteDelegation = async () => { + setErrorState(null); + setSuccessMessage(null); + + if (!pChainTxId.trim()) { + setErrorState("P-Chain transaction ID is required."); + onError("P-Chain transaction ID is required."); + return; + } + + if (!delegationID || delegationID === '0x0000000000000000000000000000000000000000000000000000000000000000') { + setErrorState("Valid delegation ID is required."); + onError("Valid delegation ID is required."); + return; + } + + if (!subnetIdL1) { + setErrorState("L1 Subnet ID is required. Please select a subnet first."); + onError("L1 Subnet ID is required. Please select a subnet first."); + return; + } + + if (!stakingManagerAddress) { + setErrorState("Staking Manager address is not set. Check L1 Subnet selection."); + onError("Staking Manager address is not set. Check L1 Subnet selection."); + return; + } + + if (!coreWalletClient || !publicClient || !viemChain) { + setErrorState("Wallet or chain configuration is not properly initialized."); + onError("Wallet or chain configuration is not properly initialized."); + return; + } + + setIsProcessing(true); + try { + // Step 1: Extract L1ValidatorWeightMessage from P-Chain transaction + const weightMessageData = await coreWalletClient.extractL1ValidatorWeightMessage({ + txId: pChainTxId + }); + + // Step 2: Get justification for the validation (using the extracted validation ID) + const justification = await GetRegistrationJustification( + weightMessageData.validationID, + subnetIdL1, + publicClient + ); + + if (!justification) { + throw new Error("No justification logs found for this validation ID"); + } + + // Step 3: Create P-Chain warp signature using the extracted weight message data + const warpValidationID = hexToBytes(weightMessageData.validationID as `0x${string}`); + const warpNonce = weightMessageData.nonce; + const warpWeight = weightMessageData.weight; + + const weightMessage = packL1ValidatorWeightMessage( + { + validationID: warpValidationID, + nonce: warpNonce, + weight: warpWeight, + }, + avalancheNetworkID, + "11111111111111111111111111111111LpoYY" // always use P-Chain ID + ); + + const aggregateSignaturePromise = aggregateSignature({ + message: bytesToHex(weightMessage), + justification: bytesToHex(justification), + signingSubnetId: signingSubnetId || subnetIdL1, + quorumPercentage: 67, + }); + notify({ + type: 'local', + name: 'Aggregate Signatures' + }, aggregateSignaturePromise); + const signature = await aggregateSignaturePromise; + + // Step 4: Complete the delegator registration on EVM + const signedPChainWarpMsgBytes = hexToBytes(`0x${signature.signedMessage}`); + const accessList = packWarpIntoAccessList(signedPChainWarpMsgBytes); + + const writePromise = coreWalletClient.writeContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: "completeDelegatorRegistration", + args: [delegationID as `0x${string}`, 0], // delegationID and messageIndex (0) + accessList, + account: walletEVMAddress as `0x${string}`, + chain: viemChain, + }); + + notify({ + type: 'call', + name: 'Complete Delegator Registration' + }, writePromise, viemChain ?? undefined); + + const hash = await writePromise; + const finalReceipt = await publicClient.waitForTransactionReceipt({ hash }); + if (finalReceipt.status !== 'success') { + throw new Error(`Transaction failed with status: ${finalReceipt.status}`); + } + + setTransactionHash(hash); + const successMsg = `Delegator registration completed successfully.`; + setSuccessMessage(successMsg); + onSuccess(successMsg); + } catch (err: any) { + const message = err instanceof Error ? err.message : String(err); + setErrorState(`Failed to complete delegator registration: ${message}`); + onError(`Failed to complete delegator registration: ${message}`); + } finally { + setIsProcessing(false); + } + }; + + // Don't render if no subnet is selected + if (!subnetIdL1) { + return ( +
+ Please select an L1 subnet first. +
+ ); + } + + return ( +
+ {error && ( + {error} + )} + +
+

Delegation ID: {delegationID}

+

P-Chain Tx ID: {pChainTxId}

+
+ + + + {transactionHash && ( + + )} +
+ ); +}; + +export default CompleteDelegatorRegistration; \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/delegate/native/Delegate.tsx b/components/toolbox/console/permissionless-l1s/delegate/native/Delegate.tsx new file mode 100644 index 00000000000..5ccddbd50ed --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/delegate/native/Delegate.tsx @@ -0,0 +1,235 @@ +"use client"; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/toolbox/components/Button'; +import SelectSubnetId from '@/components/toolbox/components/SelectSubnetId'; +import { ValidatorManagerDetails } from '@/components/toolbox/components/ValidatorManagerDetails'; +import { useValidatorManagerDetails } from '@/components/toolbox/hooks/useValidatorManagerDetails'; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Success } from '@/components/toolbox/components/Success'; +import SelectValidationID, { ValidationSelection } from '@/components/toolbox/components/SelectValidationID'; + +import InitiateNativeDelegation from '@/components/toolbox/console/permissionless-l1s/delegate/native/InitiateNativeDelegation'; +import SubmitPChainTxDelegationWeightChange from '@/components/toolbox/console/permissionless-l1s/delegate/native/SubmitPChainTxDelegationWeightChange'; +import CompleteDelegatorRegistration from '@/components/toolbox/console/permissionless-l1s/delegate/native/CompleteDelegatorRegistration'; +import { useCreateChainStore } from '@/components/toolbox/stores/createChainStore'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { WalletRequirementsConfigKey } from '@/components/toolbox/hooks/useWalletRequirements'; +import { BaseConsoleToolProps, ConsoleToolMetadata, withConsoleToolMetadata } from '../../../../components/WithConsoleToolMetadata'; +import { Alert } from '@/components/toolbox/components/Alert'; +import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url"; + +const metadata: ConsoleToolMetadata = { + title: "Delegate to Validator", + description: "Delegate your native tokens to an existing validator on your L1", + toolRequirements: [ + WalletRequirementsConfigKey.EVMChainBalance, + WalletRequirementsConfigKey.PChainBalance + ], + githubUrl: generateConsoleToolGitHubUrl(import.meta.url) +}; + +const DelegateToValidator: React.FC = ({ onSuccess }) => { + const [globalError, setGlobalError] = useState(null); + const [globalSuccess, setGlobalSuccess] = useState(null); + const [isValidatorManagerDetailsExpanded, setIsValidatorManagerDetailsExpanded] = useState(false); + + // State for passing data between components + const [validationSelection, setValidationSelection] = useState({ + validationId: '', + nodeId: '' + }); + const [delegationID, setDelegationID] = useState(''); + const [pChainTxId, setPChainTxId] = useState(''); + const [evmTxHash, setEvmTxHash] = useState(''); + const [eventData, setEventData] = useState<{ + validationID: `0x${string}`; + nonce: bigint; + weight: bigint; + messageID: `0x${string}`; + } | null>(null); + + // Form state + const { isTestnet } = useWalletStore(); + const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId); + const [subnetIdL1, setSubnetIdL1] = useState(createChainStoreSubnetId || ""); + const [resetKey, setResetKey] = useState(0); + const [userPChainBalanceNavax, setUserPChainBalanceNavax] = useState(null); + + const { + validatorManagerAddress, + error: validatorManagerError, + isLoading: isLoadingVMCDetails, + blockchainId, + contractOwner, + isOwnerContract, + contractTotalWeight, + l1WeightError, + signingSubnetId, + isLoadingOwnership, + isLoadingL1Weight, + ownershipError, + ownerType, + isDetectingOwnerType + } = useValidatorManagerDetails({ subnetId: subnetIdL1 }); + + const handleReset = () => { + setGlobalError(null); + setGlobalSuccess(null); + setValidationSelection({ validationId: '', nodeId: '' }); + setDelegationID(''); + setPChainTxId(''); + setEvmTxHash(''); + setEventData(null); + setSubnetIdL1(''); + setResetKey(prev => prev + 1); // Force re-render of all child components + }; + + return ( + <> +
+ {globalError && ( + Error: {globalError} + )} + + + +

Select L1 Subnet

+

+ Choose the L1 subnet where you want to delegate to a validator. +

+
+ + setIsValidatorManagerDetailsExpanded(!isValidatorManagerDetailsExpanded)} + /> +
+
+ + +

Select Validator to Delegate To

+

+ Select the validator you want to delegate to. The dropdown shows all active validators with their weights and remaining balances. +

+ + {ownerType && ownerType !== 'StakingManager' && ( + + This L1 is not using a Staking Manager. This tool is only for L1s with Native Token Staking Managers that support delegation. + + )} + + + + {validationSelection.nodeId && ( +
+

Selected Validator Node ID: {validationSelection.nodeId}

+
+ )} +
+ + +

Initiate Delegation

+

+ Call the initiateDelegatorRegistration function on the Native Token Staking Manager contract with your delegation amount. This transaction will emit a warp message for P-Chain registration. +

+ + { + setDelegationID(data.delegationID); + setEvmTxHash(data.txHash); + setGlobalError(null); + }} + onError={(message) => setGlobalError(message)} + /> +
+ + +

Submit SetL1ValidatorWeightTx to P-Chain

+

+ Sign the emitted warp message and submit a SetL1ValidatorWeightTx to P-Chain. This transaction will update the validator's weight to include your delegation. +

+ { + setPChainTxId(pChainTxId); + setEventData(data); + setGlobalError(null); + }} + onError={(message) => setGlobalError(message)} + /> +
+ + +

Complete Delegator Registration

+

+ Complete the delegator registration by signing the P-Chain L1ValidatorRegistrationMessage and calling the completeDelegatorRegistration function on the Staking Manager contract. +

+ { + setGlobalSuccess(message); + setGlobalError(null); + onSuccess?.(); + }} + onError={(message) => setGlobalError(message)} + /> +
+
+ + {globalSuccess && ( + + )} + + {(pChainTxId || globalError || globalSuccess) && ( + + )} +
+ + ); +}; + +export default withConsoleToolMetadata(DelegateToValidator, metadata); \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/delegate/native/InitiateNativeDelegation.tsx b/components/toolbox/console/permissionless-l1s/delegate/native/InitiateNativeDelegation.tsx new file mode 100644 index 00000000000..61ef2670fe9 --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/delegate/native/InitiateNativeDelegation.tsx @@ -0,0 +1,361 @@ +import React, { useState, useEffect } from 'react'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { useViemChainStore } from '@/components/toolbox/stores/toolboxStore'; +import { Button } from '@/components/toolbox/components/Button'; +import { Input } from '@/components/toolbox/components/Input'; +import { Success } from '@/components/toolbox/components/Success'; +import { Alert } from '@/components/toolbox/components/Alert'; +import nativeTokenStakingManagerAbi from '@/contracts/icm-contracts/compiled/NativeTokenStakingManager.json'; +import { parseEther, formatEther } from 'viem'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; + +interface InitiateNativeDelegationProps { + subnetId: string; + stakingManagerAddress: string; + validationID: string; + onSuccess: (data: { delegationID: string; txHash: string }) => void; + onError: (message: string) => void; +} + +const InitiateNativeDelegation: React.FC = ({ + subnetId, + stakingManagerAddress, + validationID, + onSuccess, + onError, +}) => { + const { coreWalletClient, publicClient, walletEVMAddress } = useWalletStore(); + const { notify } = useConsoleNotifications(); + const viemChain = useViemChainStore(); + + const [delegationAmount, setDelegationAmount] = useState(''); + const [rewardRecipient, setRewardRecipient] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setErrorState] = useState(null); + const [txHash, setTxHash] = useState(null); + const [delegationID, setDelegationID] = useState(null); + const [isLoadingExisting, setIsLoadingExisting] = useState(false); + const [existingDelegations, setExistingDelegations] = useState([]); + + // Set default reward recipient to connected wallet + useEffect(() => { + if (walletEVMAddress && !rewardRecipient) { + setRewardRecipient(walletEVMAddress); + } + }, [walletEVMAddress]); + + // Check for existing delegations when validation ID or wallet changes + useEffect(() => { + const checkExistingDelegations = async () => { + if (!publicClient || !stakingManagerAddress || !validationID || !walletEVMAddress) { + setExistingDelegations([]); + return; + } + + setIsLoadingExisting(true); + try { + // Query InitiatedDelegatorRegistration events for this validator and delegator + const logs = await publicClient.getLogs({ + address: stakingManagerAddress as `0x${string}`, + event: { + type: 'event', + name: 'InitiatedDelegatorRegistration', + inputs: [ + { name: 'delegationID', type: 'bytes32', indexed: true }, + { name: 'validationID', type: 'bytes32', indexed: true }, + { name: 'delegatorAddress', type: 'address', indexed: true }, + { name: 'nonce', type: 'uint64', indexed: false }, + { name: 'validatorWeight', type: 'uint64', indexed: false }, + { name: 'delegatorWeight', type: 'uint64', indexed: false }, + { name: 'setWeightMessageID', type: 'bytes32', indexed: false }, + { name: 'rewardRecipient', type: 'address', indexed: false }, + ], + }, + args: { + validationID: validationID as `0x${string}`, + delegatorAddress: walletEVMAddress as `0x${string}`, + }, + fromBlock: 'earliest', + toBlock: 'latest', + }); + + const delegationIDs = logs.map(log => log.topics[1] as string).filter(Boolean); + setExistingDelegations(delegationIDs); + } catch (err) { + console.error('Error checking existing delegations:', err); + setExistingDelegations([]); + } finally { + setIsLoadingExisting(false); + } + }; + + checkExistingDelegations(); + }, [publicClient, stakingManagerAddress, validationID, walletEVMAddress]); + + const handleResendDelegation = async (delegationIdToResend: string) => { + setErrorState(null); + setTxHash(null); + + if (!coreWalletClient || !publicClient || !viemChain) { + setErrorState("Wallet or chain configuration is not properly initialized."); + return; + } + + if (!stakingManagerAddress) { + setErrorState("Staking Manager address is required."); + return; + } + + setIsProcessing(true); + try { + // Call resendUpdateDelegator to re-emit the warp message + const writePromise = coreWalletClient.writeContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: "resendUpdateDelegator", + args: [delegationIdToResend as `0x${string}`], + account: walletEVMAddress as `0x${string}`, + chain: viemChain, + }); + + notify({ + type: 'call', + name: 'Resend Delegator Update' + }, writePromise, viemChain ?? undefined); + + const hash = await writePromise; + setTxHash(hash); + setDelegationID(delegationIdToResend); + + // Wait for confirmation + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== 'success') { + throw new Error(`Transaction failed with status: ${receipt.status}`); + } + + onSuccess({ + delegationID: delegationIdToResend, + txHash: hash, + }); + } catch (err: any) { + const message = err instanceof Error ? err.message : String(err); + setErrorState(`Failed to resend delegation: ${message}`); + } finally { + setIsProcessing(false); + } + }; + + const handleInitiateDelegation = async () => { + setErrorState(null); + setTxHash(null); + setDelegationID(null); + + if (!coreWalletClient || !publicClient || !viemChain) { + setErrorState("Wallet or chain configuration is not properly initialized."); + onError("Wallet or chain configuration is not properly initialized."); + return; + } + + if (!stakingManagerAddress) { + setErrorState("Staking Manager address is required."); + onError("Staking Manager address is required."); + return; + } + + if (!validationID || validationID === '0x0000000000000000000000000000000000000000000000000000000000000000') { + setErrorState("Valid validation ID is required."); + onError("Valid validation ID is required."); + return; + } + + if (!delegationAmount || parseFloat(delegationAmount) <= 0) { + setErrorState("Delegation amount must be greater than 0."); + onError("Delegation amount must be greater than 0."); + return; + } + + if (!rewardRecipient || !rewardRecipient.startsWith('0x')) { + setErrorState("Valid reward recipient address is required."); + onError("Valid reward recipient address is required."); + return; + } + + setIsProcessing(true); + try { + const delegationAmountWei = parseEther(delegationAmount); + + // Read minimum stake amount from contract + const minStakeAmount = await publicClient.readContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: "getStakingManagerSettings", + }) as any; + + if (delegationAmountWei < minStakeAmount.minimumStakeAmount) { + const minFormatted = formatEther(minStakeAmount.minimumStakeAmount); + throw new Error(`Delegation amount must be at least ${minFormatted} tokens`); + } + + // Call initiateDelegatorRegistration + const writePromise = coreWalletClient.writeContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: "initiateDelegatorRegistration", + args: [validationID as `0x${string}`, rewardRecipient as `0x${string}`], + value: delegationAmountWei, + account: walletEVMAddress as `0x${string}`, + chain: viemChain, + }); + + notify({ + type: 'call', + name: 'Initiate Delegator Registration' + }, writePromise, viemChain ?? undefined); + + const hash = await writePromise; + setTxHash(hash); + + // Wait for transaction receipt + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== 'success') { + throw new Error(`Transaction failed with status: ${receipt.status}`); + } + + // Extract delegationID from logs + const initiateDelegatorRegistrationTopic = "0x77499a5603260ef2b34698d88b31f7b1acf28c7b134ad4e3fa636501e6064d77"; + const delegationLog = receipt.logs.find((log) => + log.topics[0]?.toLowerCase() === initiateDelegatorRegistrationTopic.toLowerCase() + ); + + if (!delegationLog || !delegationLog.topics[1]) { + throw new Error("Failed to extract delegation ID from transaction receipt"); + } + + const extractedDelegationID = delegationLog.topics[1]; + setDelegationID(extractedDelegationID); + + onSuccess({ + delegationID: extractedDelegationID, + txHash: hash, + }); + } catch (err: any) { + let message = err instanceof Error ? err.message : String(err); + + // Provide more helpful error messages + if (message.includes('Unable to calculate gas limit')) { + message = 'Transaction would fail. Possible reasons:\n' + + '- The validation ID is not a valid PoS validator\n' + + '- The validator has reached maximum delegation capacity\n' + + '- The validator minimum stake duration has not been met\n' + + 'Please verify the validation ID and try again.'; + } else if (message.includes('InvalidValidationStatus')) { + message = 'This validator is not accepting delegations. The validator may not be active or registered yet.'; + } else if (message.includes('InvalidDelegationID')) { + message = 'Invalid validation ID provided. Please check the validation ID and try again.'; + } else if (message.includes('MaxWeightExceeded')) { + message = 'This delegation would exceed the maximum allowed weight for this validator.'; + } + + setErrorState(`Failed to initiate delegation: ${message}`); + onError(`Failed to initiate delegation: ${message}`); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+ {error && ( + {error} + )} + + {isLoadingExisting && ( +
+ Checking for existing delegations... +
+ )} + + {existingDelegations.length > 0 && !txHash && ( + +
+

Found {existingDelegations.length} existing delegation(s) to this validator:

+
+ {existingDelegations.map((delId, index) => ( +
+ {delId} + +
+ ))} +
+
+
+ )} + + {!txHash && ( + <> + { }} // Read-only, set by parent + placeholder="Validation ID of the validator to delegate to" + disabled={true} + /> + + + + + + + + )} + + {txHash && ( + <> + + + {delegationID && ( + + )} + + )} +
+ ); +}; + +export default InitiateNativeDelegation; \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/delegate/native/SubmitPChainTxDelegationWeightChange.tsx b/components/toolbox/console/permissionless-l1s/delegate/native/SubmitPChainTxDelegationWeightChange.tsx new file mode 100644 index 00000000000..9ac5093e656 --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/delegate/native/SubmitPChainTxDelegationWeightChange.tsx @@ -0,0 +1,290 @@ +import React, { useState, useEffect } from 'react'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { Button } from '@/components/toolbox/components/Button'; +import { Input } from '@/components/toolbox/components/Input'; +import { Success } from '@/components/toolbox/components/Success'; +import { Alert } from '@/components/toolbox/components/Alert'; +import { useAvalancheSDKChainkit } from '@/components/toolbox/stores/useAvalancheSDKChainkit'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; + +interface SubmitPChainTxDelegationWeightChangeProps { + subnetIdL1: string; + initialEvmTxHash?: string; + signingSubnetId: string; + onSuccess: (pChainTxId: string, eventData: { + validationID: `0x${string}`; + nonce: bigint; + weight: bigint; + messageID: `0x${string}`; + }) => void; + onError: (message: string) => void; +} + +const SubmitPChainTxDelegationWeightChange: React.FC = ({ + subnetIdL1, + initialEvmTxHash, + signingSubnetId, + onSuccess, + onError, +}) => { + const { coreWalletClient, pChainAddress, publicClient } = useWalletStore(); + const { aggregateSignature } = useAvalancheSDKChainkit(); + const { notify } = useConsoleNotifications(); + const [evmTxHash, setEvmTxHash] = useState(initialEvmTxHash || ''); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setErrorState] = useState(null); + const [txSuccess, setTxSuccess] = useState(null); + const [unsignedWarpMessage, setUnsignedWarpMessage] = useState(null); + const [signedWarpMessage, setSignedWarpMessage] = useState(null); + const [eventData, setEventData] = useState<{ + validationID: `0x${string}`; + nonce: bigint; + weight: bigint; + messageID: `0x${string}`; + } | null>(null); + + // Update evmTxHash when initialEvmTxHash prop changes + useEffect(() => { + if (initialEvmTxHash && initialEvmTxHash !== evmTxHash) { + setEvmTxHash(initialEvmTxHash); + } + }, [initialEvmTxHash]); + + const validateAndCleanTxHash = (hash: string): `0x${string}` | null => { + if (!hash) return null; + const cleanHash = hash.trim().toLowerCase(); + if (!cleanHash.startsWith('0x')) return null; + if (cleanHash.length !== 66) return null; + return cleanHash as `0x${string}`; + }; + + // Extract warp message and event data when transaction hash changes + useEffect(() => { + const extractWarpMessage = async () => { + const validTxHash = validateAndCleanTxHash(evmTxHash); + if (!publicClient || !validTxHash) { + setUnsignedWarpMessage(null); + setEventData(null); + setSignedWarpMessage(null); + return; + } + + try { + const receipt = await publicClient.waitForTransactionReceipt({ hash: validTxHash }); + if (!receipt.logs || receipt.logs.length === 0) { + throw new Error("Failed to get warp message from transaction receipt."); + } + + console.log("[WarpExtract] Transaction receipt:", receipt); + console.log("[WarpExtract] Number of logs:", receipt.logs.length); + + // Log all transaction logs for debugging + receipt.logs.forEach((log, index) => { + console.log(`[WarpExtract] Log #${index}:`, { + address: log.address, + topics: log.topics, + data: log.data?.substring(0, 100) + "...", + logIndex: log.logIndex, + transactionIndex: log.transactionIndex, + }); + }); + + // Look for warp message + let unsignedWarpMessage: string | null = null; + 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(); + }); + + if (warpEventLog && warpEventLog.data) { + console.log("[WarpExtract] Found warp message from precompile event"); + unsignedWarpMessage = warpEventLog.data; + } else { + if (receipt.logs.length > 1 && receipt.logs[1].data) { + console.log("[WarpExtract] Using receipt.logs[1].data for potential multisig transaction"); + unsignedWarpMessage = receipt.logs[1].data; + } else if (receipt.logs[0].data) { + console.log("[WarpExtract] Using receipt.logs[0].data as fallback"); + unsignedWarpMessage = receipt.logs[0].data; + } + } + + if (!unsignedWarpMessage) { + throw new Error("Could not extract warp message from any log in the transaction receipt."); + } + + console.log("[WarpExtract] Extracted warp message:", unsignedWarpMessage.substring(0, 60) + "..."); + setUnsignedWarpMessage(unsignedWarpMessage); + + // Extract event data from InitiatedDelegatorRegistration event + const delegationTopic = "0x77499a5603260ef2b34698d88b31f7b1acf28c7b134ad4e3fa636501e6064d77"; + const eventLog = receipt.logs.find((log) => { + return log && log.topics && log.topics[0] && log.topics[0].toLowerCase() === delegationTopic.toLowerCase(); + }); + + if (!eventLog) { + throw new Error("Failed to find InitiatedDelegatorRegistration event log."); + } + + const dataWithoutPrefix = eventLog.data.slice(2); + const nonce = BigInt("0x" + dataWithoutPrefix.slice(0, 64)); + const validatorWeight = BigInt("0x" + dataWithoutPrefix.slice(64, 128)); + const delegatorWeight = BigInt("0x" + dataWithoutPrefix.slice(128, 192)); + const messageID = "0x" + dataWithoutPrefix.slice(192, 256); + + const parsedEventData = { + validationID: eventLog.topics[2] as `0x${string}`, // validationID is topics[2] for delegation + nonce, + messageID: messageID as `0x${string}`, + weight: validatorWeight, // Use validator weight (includes delegation) + }; + setEventData(parsedEventData); + setErrorState(null); + } catch (err: any) { + const message = err instanceof Error ? err.message : String(err); + setErrorState(`Failed to extract warp message: ${message}`); + setUnsignedWarpMessage(null); + setEventData(null); + setSignedWarpMessage(null); + } + }; + + extractWarpMessage(); + }, [evmTxHash, publicClient]); + + const handleSubmitPChainTx = async () => { + setErrorState(null); + setTxSuccess(null); + + if (!coreWalletClient) { + setErrorState("Core wallet not found"); + return; + } + + if (!evmTxHash.trim()) { + setErrorState("EVM transaction hash is required."); + onError("EVM transaction hash is required."); + return; + } + if (!subnetIdL1) { + setErrorState("L1 Subnet ID is required. Please select a subnet first."); + onError("L1 Subnet ID is required. Please select a subnet first."); + return; + } + if (!unsignedWarpMessage) { + setErrorState("Unsigned warp message not found. Check the transaction hash."); + onError("Unsigned warp message not found. Check the transaction hash."); + return; + } + if (!eventData) { + setErrorState("Event data not found. Check the transaction hash."); + onError("Event data not found. Check the transaction hash."); + return; + } + if (typeof window === 'undefined' || !window.avalanche) { + setErrorState("Core wallet not found. Please ensure Core is installed and active."); + onError("Core wallet not found. Please ensure Core is installed and active."); + return; + } + if (!pChainAddress) { + setErrorState("P-Chain address is missing from wallet. Please connect your wallet properly."); + onError("P-Chain address is missing from wallet. Please connect your wallet properly."); + return; + } + + setIsProcessing(true); + try { + // Step 1: Sign the warp message + const aggregateSignaturePromise = aggregateSignature({ + message: unsignedWarpMessage, + signingSubnetId: signingSubnetId || subnetIdL1, + quorumPercentage: 67, + }); + notify({ + type: 'local', + name: 'Aggregate Signatures' + }, aggregateSignaturePromise); + const { signedMessage } = await aggregateSignaturePromise; + + setSignedWarpMessage(signedMessage); + + // Step 2: Submit to P-Chain + const pChainTxIdPromise = coreWalletClient.setL1ValidatorWeight({ + signedWarpMessage: signedMessage, + }); + notify('setL1ValidatorWeight', pChainTxIdPromise); + const pChainTxId = await pChainTxIdPromise; + setTxSuccess(`P-Chain transaction successful! ID: ${pChainTxId}`); + 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'; + } else if (message.includes('insufficient funds')) { + message = 'Insufficient funds for transaction'; + } else if (message.includes('execution reverted')) { + message = `Transaction reverted: ${message}`; + } 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 { + setIsProcessing(false); + } + }; + + const handleTxHashChange = (value: string) => { + setEvmTxHash(value); + setErrorState(null); + setTxSuccess(null); + setSignedWarpMessage(null); + }; + + // Don't render if no subnet is selected + if (!subnetIdL1) { + return ( +
+ Please select an L1 subnet first. +
+ ); + } + + return ( +
+ + + + + {error && ( + {error} + )} + + {txSuccess && ( + + )} +
+ ); +}; + +export default SubmitPChainTxDelegationWeightChange; \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/setup/erc20/DeployERC20StakingManager.tsx b/components/toolbox/console/permissionless-l1s/setup/erc20/DeployERC20StakingManager.tsx new file mode 100644 index 00000000000..1bd6d5f075a --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/setup/erc20/DeployERC20StakingManager.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useState } from "react"; +import { useWalletStore } from "@/components/toolbox/stores/walletStore"; +import { useToolboxStore, useViemChainStore } from "@/components/toolbox/stores/toolboxStore"; +import { Button } from "@/components/toolbox/components/Button"; +import { Success } from "@/components/toolbox/components/Success"; +import { ConsoleToolMetadata, withConsoleToolMetadata } from '../../../../components/WithConsoleToolMetadata'; +import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url"; +import { WalletRequirementsConfigKey } from "@/components/toolbox/hooks/useWalletRequirements"; +import ERC20TokenStakingManager from "@/contracts/icm-contracts/compiled/ERC20TokenStakingManager.json"; +import versions from '@/scripts/versions.json'; +import { keccak256 } from 'viem'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; + +const ICM_COMMIT = versions["ava-labs/icm-contracts"]; +const ERC20_TOKEN_STAKING_MANAGER_SOURCE_URL = `https://github.com/ava-labs/icm-contracts/blob/main/contracts/validator-manager/ERC20TokenStakingManager.sol`; + +// this should be pulled into a shared utils file with other contract deployments +function calculateLibraryHash(libraryPath: string) { + const hash = keccak256( + new TextEncoder().encode(libraryPath) + ).slice(2); + return hash.slice(0, 34); +} + +const metadata: ConsoleToolMetadata = { + title: "Deploy ERC20 Token Staking Manager", + description: "Deploy the ERC20 Token Staking Manager contract to the EVM network.", + toolRequirements: [ + WalletRequirementsConfigKey.EVMChainBalance, + ], + githubUrl: generateConsoleToolGitHubUrl(import.meta.url) +}; + +function DeployERC20StakingManager() { + const [criticalError, setCriticalError] = useState(null); + const [isDeploying, setIsDeploying] = useState(false); + + const { coreWalletClient, publicClient, walletEVMAddress } = useWalletStore(); + const viemChain = useViemChainStore(); + const { erc20StakingManagerAddress, setErc20StakingManagerAddress, validatorMessagesLibAddress } = useToolboxStore(); + const { notify } = useConsoleNotifications(); + + // Throw critical errors during render + if (criticalError) { + throw criticalError; + } + + const getLinkedBytecode = () => { + if (!validatorMessagesLibAddress) { + throw new Error('ValidatorMessages library must be deployed first. Please deploy it in the Validator Manager setup.'); + } + + const libraryPath = `${Object.keys(ERC20TokenStakingManager.bytecode.linkReferences)[0]}:${Object.keys(Object.values(ERC20TokenStakingManager.bytecode.linkReferences)[0])[0]}`; + const libraryHash = calculateLibraryHash(libraryPath); + const libraryPlaceholder = `__$${libraryHash}$__`; + + const linkedBytecode = ERC20TokenStakingManager.bytecode.object + .split(libraryPlaceholder) + .join(validatorMessagesLibAddress.slice(2).padStart(40, '0')); + + if (linkedBytecode.includes("$__")) { + throw new Error("Failed to replace library placeholder with actual address"); + } + + return linkedBytecode as `0x${string}`; + }; + + async function deployERC20StakingManager() { + setIsDeploying(true); + setErc20StakingManagerAddress(""); + try { + if (!viemChain) throw new Error("Viem chain not found"); + if (!coreWalletClient) throw new Error("Wallet not connected"); + if (!walletEVMAddress) throw new Error("Wallet address not available"); + + // Check for library first + if (!validatorMessagesLibAddress) { + throw new Error('ValidatorMessages library must be deployed first. Please go to Validator Manager Setup and deploy the library.'); + } + + // Follow exact pattern from ValidatorManager deployment + await coreWalletClient.addChain({ chain: viemChain }); + await coreWalletClient.switchChain({ id: viemChain!.id }); + + const deployPromise = coreWalletClient.deployContract({ + abi: ERC20TokenStakingManager.abi as any, + bytecode: getLinkedBytecode(), // Use linked bytecode with library + args: [0], // ICMInitializable.Allowed + chain: viemChain, + account: walletEVMAddress as `0x${string}`, + }); + + notify({ + type: 'deploy', + name: 'ERC20 Token Staking Manager' + }, deployPromise, viemChain ?? undefined); + + const hash = await deployPromise; + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + + if (!receipt.contractAddress) { + throw new Error('No contract address in receipt'); + } + + setErc20StakingManagerAddress(receipt.contractAddress); + } catch (error) { + setCriticalError(error instanceof Error ? error : new Error(String(error))); + } finally { + setIsDeploying(false); + } + } + + return ( + +
+

+ This will deploy the ERC20TokenStakingManager contract to the EVM network {viemChain?.id}. + The ERC20 Token Staking Manager enables permissionless staking on your L1 using a custom ERC20 token. +

+

+ Contract source: ERC20TokenStakingManager.sol @ {ICM_COMMIT.slice(0, 7)} +

+ {walletEVMAddress && ( +

+ Connected wallet: {walletEVMAddress} +

+ )} + + {!validatorMessagesLibAddress ? ( +
+

+ Required: ValidatorMessages library must be deployed first. + Please go to the Validator Manager Setup section and deploy the ValidatorMessages library. +

+
+ ) : ( +
+

+ Ready: ValidatorMessages library found at: {validatorMessagesLibAddress} +

+
+ )} + + + +

Deployment Status: {erc20StakingManagerAddress || "Not deployed"}

+ + {erc20StakingManagerAddress && ( + + )} +
+ ); +} + +export default withConsoleToolMetadata(DeployERC20StakingManager, metadata); \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/setup/erc20/InitializeERC20StakingManager.tsx b/components/toolbox/console/permissionless-l1s/setup/erc20/InitializeERC20StakingManager.tsx new file mode 100644 index 00000000000..bbfcd583f70 --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/setup/erc20/InitializeERC20StakingManager.tsx @@ -0,0 +1,413 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useWalletStore } from "@/components/toolbox/stores/walletStore"; +import { useToolboxStore, useViemChainStore } from "@/components/toolbox/stores/toolboxStore"; +import { Button } from "@/components/toolbox/components/Button"; +import { Input } from "@/components/toolbox/components/Input"; +import { EVMAddressInput } from "@/components/toolbox/components/EVMAddressInput"; +import { ConsoleToolMetadata, withConsoleToolMetadata } from '../../../../components/WithConsoleToolMetadata'; +import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url"; +import { WalletRequirementsConfigKey } from "@/components/toolbox/hooks/useWalletRequirements"; +import { ResultField } from "@/components/toolbox/components/ResultField"; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import SelectSubnet, { SubnetSelection } from '@/components/toolbox/components/SelectSubnet'; +import { useValidatorManagerDetails } from '@/components/toolbox/hooks/useValidatorManagerDetails'; +import { Callout } from "fumadocs-ui/components/callout"; +import ERC20TokenStakingManager from "@/contracts/icm-contracts/compiled/ERC20TokenStakingManager.json"; +import { parseEther } from "viem"; +import versions from '@/scripts/versions.json'; +import { cb58ToHex } from '@/components/toolbox/console/utilities/format-converter/FormatConverter'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; + +const ICM_COMMIT = versions["ava-labs/icm-contracts"]; +const INITIALIZE_FUNCTION_SOURCE_URL = `https://github.com/ava-labs/icm-contracts/blob/main/contracts/validator-manager/ERC20TokenStakingManager.sol#L53`; + +const metadata: ConsoleToolMetadata = { + title: "Initialize ERC20 Token Staking Manager", + description: "Initialize the ERC20 Token Staking Manager contract with the required configuration.", + toolRequirements: [ + WalletRequirementsConfigKey.EVMChainBalance, + ], + githubUrl: generateConsoleToolGitHubUrl(import.meta.url) +}; + +function InitializeERC20StakingManager() { + const [criticalError, setCriticalError] = useState(null); + const [stakingManagerAddressInput, setStakingManagerAddressInput] = useState(""); + const [isChecking, setIsChecking] = useState(false); + const [isInitializing, setIsInitializing] = useState(false); + const [isInitialized, setIsInitialized] = useState(null); + const [initEvent, setInitEvent] = useState(null); + const [subnetSelection, setSubnetSelection] = useState({ + subnetId: "", + subnet: null + }); + + // Initialization parameters + const [minimumStakeAmount, setMinimumStakeAmount] = useState("1"); + const [maximumStakeAmount, setMaximumStakeAmount] = useState("1000000"); + const [minimumStakeDuration, setMinimumStakeDuration] = useState("86400"); // 1 day default + const [minimumDelegationFeeBips, setMinimumDelegationFeeBips] = useState("100"); // 1% default + const [maximumStakeMultiplier, setMaximumStakeMultiplier] = useState("10"); + const [weightToValueFactor, setWeightToValueFactor] = useState("1"); + const [rewardCalculatorAddress, setRewardCalculatorAddress] = useState(""); + const [stakingTokenAddress, setStakingTokenAddress] = useState(""); + + const { coreWalletClient, publicClient, walletEVMAddress } = useWalletStore(); + const viemChain = useViemChainStore(); + const { erc20StakingManagerAddress: storedStakingManagerAddress, rewardCalculatorAddress: storedRewardCalculatorAddress } = useToolboxStore(); + const { notify } = useConsoleNotifications(); + + // Get validator manager details from subnet ID + const { + validatorManagerAddress, + error: validatorManagerError, + isLoading: isLoadingVMCDetails, + } = useValidatorManagerDetails({ subnetId: subnetSelection.subnetId }); + + // Extract blockchain ID from subnet data + const blockchainId = subnetSelection.subnet?.blockchains?.[0]?.blockchainId || null; + + // Throw critical errors during render + if (criticalError) { + throw criticalError; + } + + // Auto-fill addresses from store if available + useEffect(() => { + if (storedStakingManagerAddress && !stakingManagerAddressInput) { + setStakingManagerAddressInput(storedStakingManagerAddress); + } + }, [storedStakingManagerAddress, stakingManagerAddressInput]); + + useEffect(() => { + if (storedRewardCalculatorAddress && !rewardCalculatorAddress) { + setRewardCalculatorAddress(storedRewardCalculatorAddress); + } + }, [storedRewardCalculatorAddress, rewardCalculatorAddress]); + + async function checkIfInitialized() { + if (!stakingManagerAddressInput) return; + + setIsChecking(true); + try { + // Try to check initialization by reading a setting that would be 0 if not initialized + const data = await publicClient.readContract({ + address: stakingManagerAddressInput as `0x${string}`, + abi: ERC20TokenStakingManager.abi, + functionName: 'minimumStakeAmount', + }); + + const initialized = BigInt(data as string) > 0n; + setIsInitialized(initialized); + + if (initialized) { + // If initialized, get more details + const [settings, token] = await Promise.all([ + publicClient.readContract({ + address: stakingManagerAddressInput as `0x${string}`, + abi: ERC20TokenStakingManager.abi, + functionName: 'minimumStakeAmount', + }), + publicClient.readContract({ + address: stakingManagerAddressInput as `0x${string}`, + abi: ERC20TokenStakingManager.abi, + functionName: 'erc20', + }) + ]); + setInitEvent({ minimumStakeAmount: settings, stakingToken: token }); + } + } catch (error) { + setIsInitialized(false); + } finally { + setIsChecking(false); + } + } + + async function handleInitialize() { + if (!stakingManagerAddressInput) return; + + setIsInitializing(true); + try { + if (!coreWalletClient) throw new Error("Wallet not connected"); + if (!validatorManagerAddress) throw new Error("Validator Manager address required"); + if (!rewardCalculatorAddress) throw new Error("Reward Calculator address required"); + if (!stakingTokenAddress) throw new Error("Staking Token address required"); + if (!blockchainId) throw new Error("Blockchain ID not found. Please select a valid subnet."); + + // Convert blockchain ID from CB58 to hex + let hexBlockchainId: string; + try { + hexBlockchainId = cb58ToHex(blockchainId); + // Ensure it's 32 bytes (64 hex chars) + if (hexBlockchainId.length < 64) { + // Pad with zeros on the left to make it 32 bytes + hexBlockchainId = hexBlockchainId.padStart(64, '0'); + } + hexBlockchainId = `0x${hexBlockchainId}` as `0x${string}`; + } catch (error) { + throw new Error(`Failed to convert blockchain ID to hex: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + // Create settings object + const settings = { + manager: validatorManagerAddress as `0x${string}`, + minimumStakeAmount: parseEther(minimumStakeAmount), + maximumStakeAmount: parseEther(maximumStakeAmount), + minimumStakeDuration: BigInt(minimumStakeDuration), + minimumDelegationFeeBips: parseInt(minimumDelegationFeeBips), + maximumStakeMultiplier: parseInt(maximumStakeMultiplier), + weightToValueFactor: parseEther(weightToValueFactor), + rewardCalculator: rewardCalculatorAddress as `0x${string}`, + uptimeBlockchainID: hexBlockchainId as `0x${string}` + }; + + // Estimate gas for initialization + const gasEstimate = await publicClient.estimateContractGas({ + address: stakingManagerAddressInput as `0x${string}`, + abi: ERC20TokenStakingManager.abi, + functionName: 'initialize', + args: [settings, stakingTokenAddress as `0x${string}`], + account: walletEVMAddress as `0x${string}`, + }); + + // Add 20% buffer to gas estimate for safety + const gasWithBuffer = gasEstimate + (gasEstimate * 20n / 100n); + + const writePromise = coreWalletClient.writeContract({ + address: stakingManagerAddressInput as `0x${string}`, + abi: ERC20TokenStakingManager.abi, + functionName: 'initialize', + args: [settings, stakingTokenAddress as `0x${string}`], + chain: viemChain, + gas: gasWithBuffer, + account: walletEVMAddress as `0x${string}`, + }); + + notify({ + type: 'call', + name: 'Initialize ERC20 Token Staking Manager' + }, writePromise, viemChain ?? undefined); + + const hash = await writePromise; + await publicClient.waitForTransactionReceipt({ hash }); + await checkIfInitialized(); + } catch (error) { + setCriticalError(error instanceof Error ? error : new Error(String(error))); + } finally { + setIsInitializing(false); + } + } + + return ( + <> + + +

Select L1 Subnet

+

+ Choose the L1 subnet where the ERC20 Token Staking Manager will be initialized. The Validator Manager address and blockchain ID will be automatically derived from your selection. +

+ + + {subnetSelection.subnet && !subnetSelection.subnet.isL1 && ( +
+

+ Note: This subnet has not been converted to an L1 yet. ERC20 Token Staking Manager can only be initialized for L1s. +

+
+ )} + + {validatorManagerAddress && ( +
+

+ Validator Manager Address: {validatorManagerAddress} +

+
+ )} +
+ + +

Select ERC20 Token Staking Manager

+

+ Select the ERC20 Token Staking Manager contract you want to initialize. +

+

+ Initialize function source: initialize() @ {ICM_COMMIT.slice(0, 7)} +

+ + + + +
+ + +

Configure Staking Token & Rewards

+

+ Set the ERC20 token that will be used for staking and the Reward Calculator contract address. The Validator Manager address and Uptime Blockchain ID are automatically derived from your subnet selection. +

+ +
+ {validatorManagerAddress && ( +
+

+ Validator Manager Address: {validatorManagerAddress} +

+

+ Uptime Blockchain ID (CB58): {blockchainId || 'Loading...'} +

+ {blockchainId && ( +

+ Uptime Blockchain ID (Hex): + {(() => { + try { + const hex = cb58ToHex(blockchainId); + return `0x${hex.padStart(64, '0')}`; + } catch { + return 'Invalid CB58'; + } + })()} + +

+ )} +
+ )} + + + + +

Important: Token Requirements

+

The ERC20 token must implement the IERC20Mintable interface. + This allows the staking manager to mint rewards. Care should be taken to enforce that only + authorized users (i.e., the staking manager contract) are able to mint the ERC20 staking token.

+
+ + +
+
+ + +

Set Staking Parameters

+

+ Configure the staking parameters that define how validators and delegators can participate in securing the network. +

+ +
+ + + + + + + + + + + +
+ + +
+
+ + {isInitialized === true && ( + + )} + + ); +} + +export default withConsoleToolMetadata(InitializeERC20StakingManager, metadata); + +function jsonStringifyWithBigint(value: unknown) { + return JSON.stringify(value, (_, v) => + typeof v === 'bigint' ? v.toString() : v + , 2); +} \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/staking/native/InitiateNativeStakeRegistration.tsx b/components/toolbox/console/permissionless-l1s/staking/native/InitiateNativeStakeRegistration.tsx new file mode 100644 index 00000000000..94c961d8605 --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/staking/native/InitiateNativeStakeRegistration.tsx @@ -0,0 +1,428 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useViemChainStore } from '@/components/toolbox/stores/toolboxStore'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { useL1ListStore } from '@/components/toolbox/stores/l1ListStore'; +import { Button } from '@/components/toolbox/components/Button'; +import { Input } from '@/components/toolbox/components/Input'; +import { ConvertToL1Validator } from '@/components/toolbox/components/ValidatorListInput'; +import { validateStakePercentage } from '@/components/toolbox/coreViem/hooks/getTotalStake'; +import nativeTokenStakingManagerAbi from '@/contracts/icm-contracts/compiled/NativeTokenStakingManager.json'; +import { Success } from '@/components/toolbox/components/Success'; +import { parseNodeID } from '@/components/toolbox/coreViem/utils/ids'; +import { fromBytes, parseEther, formatEther } from 'viem'; +import { utils } from '@avalabs/avalanchejs'; +import useConsoleNotifications from '@/hooks/useConsoleNotifications'; +import { Alert } from '@/components/toolbox/components/Alert'; + +interface InitiateNativeStakeRegistrationProps { + subnetId: string; + stakingManagerAddress: string; + validators: ConvertToL1Validator[]; + onSuccess: (data: { + txHash: `0x${string}`; + nodeId: string; + validationId: string; + weight: string; + unsignedWarpMessage: string; + validatorBalance: string; + blsProofOfPossession: string; + }) => void; + onError: (message: string) => void; + contractTotalWeight: bigint; + l1WeightError: string | null; +} + +const InitiateNativeStakeRegistration: React.FC = ({ + subnetId, + stakingManagerAddress, + validators, + onSuccess, + onError, + contractTotalWeight, +}) => { + const { coreWalletClient, publicClient, walletEVMAddress, walletChainId } = useWalletStore(); + const { notify } = useConsoleNotifications(); + const viemChain = useViemChainStore(); + const l1List = useL1ListStore()((state: any) => state.l1List); + + const tokenSymbol = useMemo(() => { + const currentL1 = l1List.find((l1: any) => l1.evmChainId === walletChainId); + return currentL1?.coinName || 'AVAX'; + }, [l1List, walletChainId]); + + const [isProcessing, setIsProcessing] = useState(false); + const [error, setErrorState] = useState(null); + const [txSuccess, setTxSuccess] = useState(null); + + // Staking-specific fields + const [stakeAmount, setStakeAmount] = useState(''); + const [delegationFeeBips, setDelegationFeeBips] = useState('200'); // 2% default + const [minStakeDuration, setMinStakeDuration] = useState('0'); // 0 seconds default + const [rewardRecipient, setRewardRecipient] = useState(''); + + // Staking manager settings + const [stakingSettings, setStakingSettings] = useState<{ + minimumStakeAmount: bigint; + maximumStakeAmount: bigint; + minimumStakeDuration: bigint; + minimumDelegationFeeBips: number; + } | null>(null); + const [isLoadingSettings, setIsLoadingSettings] = useState(false); + + // Fetch staking manager settings + useEffect(() => { + const fetchStakingSettings = async () => { + if (!publicClient || !stakingManagerAddress) { + setStakingSettings(null); + return; + } + + setIsLoadingSettings(true); + try { + const settings = await publicClient.readContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: 'getStakingManagerSettings', + }) as any; + + setStakingSettings({ + minimumStakeAmount: settings.minimumStakeAmount, + maximumStakeAmount: settings.maximumStakeAmount, + minimumStakeDuration: settings.minimumStakeDuration, + minimumDelegationFeeBips: settings.minimumDelegationFeeBips, + }); + + // Update defaults based on fetched settings (only if current values are below minimum) + + // Set default stake amount to minimum if not set + if (!stakeAmount) { + setStakeAmount(formatEther(settings.minimumStakeAmount)); + } + + const currentFeeBips = parseInt(delegationFeeBips) || 0; + if (settings.minimumDelegationFeeBips > currentFeeBips) { + setDelegationFeeBips(settings.minimumDelegationFeeBips.toString()); + } + + const currentDuration = BigInt(minStakeDuration || '0'); + if (settings.minimumStakeDuration > currentDuration) { + setMinStakeDuration(settings.minimumStakeDuration.toString()); + } + } catch (err) { + console.error('Failed to fetch staking settings:', err); + setStakingSettings(null); + } finally { + setIsLoadingSettings(false); + } + }; + + fetchStakingSettings(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [publicClient, stakingManagerAddress]); + + // Update reward recipient when wallet address changes + useEffect(() => { + if (walletEVMAddress && !rewardRecipient) { + setRewardRecipient(walletEVMAddress); + } + }, [walletEVMAddress, rewardRecipient]); + + const validateInputs = (): boolean => { + if (validators.length === 0) { + setErrorState("Please add a validator to continue"); + return false; + } + + // Validate stake amount + const stakeNum = parseFloat(stakeAmount); + if (isNaN(stakeNum) || stakeNum <= 0) { + setErrorState("Please enter a valid stake amount"); + return false; + } + + const stakeAmountWei = parseEther(stakeAmount); + + // Validate stake amount against staking manager settings + if (stakingSettings) { + if (stakeAmountWei < stakingSettings.minimumStakeAmount) { + setErrorState(`Stake amount (${formatEther(stakeAmountWei)} ${tokenSymbol}) is below the minimum required stake (${formatEther(stakingSettings.minimumStakeAmount)} ${tokenSymbol})`); + return false; + } + + if (stakeAmountWei > stakingSettings.maximumStakeAmount) { + setErrorState(`Stake amount (${formatEther(stakeAmountWei)} ${tokenSymbol}) exceeds the maximum allowed stake (${formatEther(stakingSettings.maximumStakeAmount)} ${tokenSymbol})`); + return false; + } + } + + // Validate delegation fee (0-10000 basis points = 0-100%) + const feeBips = parseInt(delegationFeeBips); + if (isNaN(feeBips) || feeBips < 0 || feeBips > 10000) { + setErrorState("Delegation fee must be between 0 and 10000 basis points (0-100%)"); + return false; + } + + // Validate against minimum delegation fee from staking manager + if (stakingSettings && feeBips < stakingSettings.minimumDelegationFeeBips) { + setErrorState(`Delegation fee (${feeBips} bips) is below the minimum required (${stakingSettings.minimumDelegationFeeBips} bips)`); + return false; + } + + // Validate min stake duration + const duration = parseInt(minStakeDuration); + if (isNaN(duration) || duration < 0) { + setErrorState("Minimum stake duration must be a positive number"); + return false; + } + + // Validate against minimum stake duration from staking manager + if (stakingSettings && BigInt(duration) < stakingSettings.minimumStakeDuration) { + setErrorState(`Minimum stake duration (${duration}s) is below the required minimum (${stakingSettings.minimumStakeDuration.toString()}s)`); + return false; + } + + // Validate reward recipient address + if (!rewardRecipient || !rewardRecipient.match(/^0x[a-fA-F0-9]{40}$/)) { + setErrorState("Please provide a valid reward recipient address"); + return false; + } + + return true; + }; + + const handleInitiateStakingValidatorRegistration = async () => { + setErrorState(null); + setTxSuccess(null); + + if (!coreWalletClient) { + setErrorState("Core wallet not found"); + return; + } + + if (!validateInputs()) { + return; + } + + if (!stakingManagerAddress) { + setErrorState("Staking Manager Address is required. Please select a valid L1 subnet."); + return; + } + + setIsProcessing(true); + try { + const validator = validators[0]; + const [account] = await coreWalletClient.requestAddresses(); + + // Process P-Chain Addresses + const pChainRemainingBalanceOwnerAddressesHex = validator.remainingBalanceOwner.addresses.map(address => { + const addressBytes = utils.bech32ToBytes(address); + return fromBytes(addressBytes, "hex"); + }); + + const pChainDisableOwnerAddressesHex = validator.deactivationOwner.addresses.map(address => { + const addressBytes = utils.bech32ToBytes(address); + return fromBytes(addressBytes, "hex"); + }); + + // Build arguments for staking manager transaction + const args = [ + parseNodeID(validator.nodeID), + validator.nodePOP.publicKey, + { + threshold: validator.remainingBalanceOwner.threshold, + addresses: pChainRemainingBalanceOwnerAddressesHex, + }, + { + threshold: validator.deactivationOwner.threshold, + addresses: pChainDisableOwnerAddressesHex, + }, + parseInt(delegationFeeBips), // delegationFeeBips + parseInt(minStakeDuration), // minStakeDuration + rewardRecipient as `0x${string}` // rewardRecipient + ]; + + // Calculate stake amount in wei + const stakeAmountWei = parseEther(stakeAmount); + + let receipt; + + try { + const writePromise = coreWalletClient.writeContract({ + address: stakingManagerAddress as `0x${string}`, + abi: nativeTokenStakingManagerAbi.abi, + functionName: "initiateValidatorRegistration", + args, + value: stakeAmountWei, + account, + chain: viemChain + }); + + notify({ + type: 'call', + name: 'Initiate Staking Validator Registration' + }, writePromise, viemChain ?? undefined); + + receipt = await publicClient.waitForTransactionReceipt({ hash: await writePromise }); + + if (receipt.status === 'reverted') { + setErrorState(`Transaction reverted. Hash: ${receipt.transactionHash}`); + onError(`Transaction reverted. Hash: ${receipt.transactionHash}`); + return; + } + + const unsignedWarpMessage = receipt.logs[0].data ?? ""; + const validationIdHex = receipt.logs[1].topics[1] ?? ""; + + setTxSuccess(`Transaction successful! Hash: ${receipt.transactionHash}`); + onSuccess({ + txHash: receipt.transactionHash as `0x${string}`, + nodeId: validator.nodeID, + validationId: validationIdHex, + weight: '0', // Weight is calculated by staking manager, not needed here + unsignedWarpMessage: unsignedWarpMessage, + validatorBalance: stakeAmount, // Use the actual stake amount entered + blsProofOfPossession: validator.nodePOP.proofOfPossession, + }); + + } catch (txError: any) { + let message = txError instanceof Error ? txError.message : String(txError); + + if (message.includes('User rejected')) { + message = 'Transaction was rejected by user'; + } else if (message.includes('insufficient funds')) { + message = 'Insufficient funds for transaction'; + } else if (message.includes('execution reverted')) { + message = `Transaction reverted: ${message}`; + } else if (message.includes('nonce')) { + message = 'Transaction nonce error. Please try again.'; + } + + setErrorState(`Transaction failed: ${message}`); + onError(`Transaction failed: ${message}`); + } + } catch (err: any) { + let message = err instanceof Error ? err.message : String(err); + + if (message.includes('User rejected')) { + message = 'Transaction was rejected by user'; + } else if (message.includes('insufficient funds')) { + message = 'Insufficient funds for transaction'; + } else if (message.includes('execution reverted')) { + message = `Transaction reverted: ${message}`; + } else if (message.includes('nonce')) { + message = 'Transaction nonce error. Please try again.'; + } + + setErrorState(`Transaction failed: ${message}`); + onError(`Transaction failed: ${message}`); + } finally { + setIsProcessing(false); + } + }; + + if (!subnetId) { + return ( +
+ Please select an L1 subnet first. +
+ ); + } + + if (validators.length === 0) { + return ( +
+ Please add a validator in the previous step. +
+ ); + } + + return ( +
+
+

+ Staking Parameters +

+ +
+ + + + + + + +
+
+ + + + {error && ( + {error} + )} + + {txSuccess && ( + + )} +
+ ); +}; + +export default InitiateNativeStakeRegistration; \ No newline at end of file diff --git a/components/toolbox/console/permissionless-l1s/staking/native/Stake.tsx b/components/toolbox/console/permissionless-l1s/staking/native/Stake.tsx new file mode 100644 index 00000000000..e62e9665fce --- /dev/null +++ b/components/toolbox/console/permissionless-l1s/staking/native/Stake.tsx @@ -0,0 +1,318 @@ +"use client" +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/toolbox/components/Button'; +import SelectSubnetId from '@/components/toolbox/components/SelectSubnetId'; +import { ValidatorManagerDetails } from '@/components/toolbox/components/ValidatorManagerDetails'; +import { useValidatorManagerDetails } from '@/components/toolbox/hooks/useValidatorManagerDetails'; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Success } from '@/components/toolbox/components/Success'; + +import InitiateNativeStakeRegistration from '@/components/toolbox/console/permissionless-l1s/staking/native/InitiateNativeStakeRegistration'; +import SubmitPChainTxRegisterL1Validator from '@/components/toolbox/console/permissioned-l1s/AddValidator/SubmitPChainTxRegisterL1Validator'; +import CompleteValidatorRegistration from '@/components/toolbox/console/permissioned-l1s/AddValidator/CompleteValidatorRegistration'; +import { ValidatorListInput, ConvertToL1Validator } from '@/components/toolbox/components/ValidatorListInput'; +import { useCreateChainStore } from '@/components/toolbox/stores/createChainStore'; +import { useWalletStore } from '@/components/toolbox/stores/walletStore'; +import { WalletRequirementsConfigKey } from '@/components/toolbox/hooks/useWalletRequirements'; +import { BaseConsoleToolProps, ConsoleToolMetadata, withConsoleToolMetadata } from '../../../../components/WithConsoleToolMetadata'; +import { Alert } from '@/components/toolbox/components/Alert'; +import { generateConsoleToolGitHubUrl } from "@/components/toolbox/utils/github-url"; + +// Helper functions for BigInt serialization +const serializeValidators = (validators: ConvertToL1Validator[]) => { + return validators.map(validator => ({ + ...validator, + validatorWeight: validator.validatorWeight.toString(), + validatorBalance: validator.validatorBalance.toString(), + })); +}; + +const deserializeValidators = (serializedValidators: any[]): ConvertToL1Validator[] => { + return serializedValidators.map(validator => ({ + ...validator, + validatorWeight: BigInt(validator.validatorWeight), + validatorBalance: BigInt(validator.validatorBalance), + })); +}; + +const STORAGE_KEY = 'stakeValidator_validators'; + +const metadata: ConsoleToolMetadata = { + title: "Stake New Validator", + description: "Stake a new validator to your L1 by following these steps in order", + toolRequirements: [ + WalletRequirementsConfigKey.EVMChainBalance, + WalletRequirementsConfigKey.PChainBalance + ], + githubUrl: generateConsoleToolGitHubUrl(import.meta.url) +}; + +const StakeValidator: React.FC = ({ onSuccess }) => { + const [globalError, setGlobalError] = useState(null); + const [globalSuccess, setGlobalSuccess] = useState(null); + const [isValidatorManagerDetailsExpanded, setIsValidatorManagerDetailsExpanded] = useState(false); + + // State for passing data between components + const [pChainTxId, setPChainTxId] = useState(''); + const [validatorBalance, setValidatorBalance] = useState(''); + const [blsProofOfPossession, setBlsProofOfPossession] = useState(''); + const [evmTxHash, setEvmTxHash] = useState(''); + + // Form state with local persistence + const { walletEVMAddress, pChainAddress, isTestnet } = useWalletStore(); + const createChainStoreSubnetId = useCreateChainStore()(state => state.subnetId); + const [subnetIdL1, setSubnetIdL1] = useState(createChainStoreSubnetId || ""); + const [resetKey, setResetKey] = useState(0); + const [userPChainBalanceNavax, setUserPChainBalanceNavax] = useState(null); + + // Local validators state with localStorage persistence + const [validators, setValidatorsState] = useState(() => { + if (typeof window !== 'undefined') { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const serialized = JSON.parse(saved); + return deserializeValidators(serialized); + } + } catch (error) { + console.error('Error loading validators from localStorage:', error); + } + } + return []; + }); + + // Wrapper function to save to localStorage + const setValidators = (newValidators: ConvertToL1Validator[]) => { + setValidatorsState(newValidators); + if (typeof window !== 'undefined') { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeValidators(newValidators))); + } catch (error) { + console.error('Error saving validators to localStorage:', error); + } + } + }; + + const { + validatorManagerAddress, + error: validatorManagerError, + isLoading: isLoadingVMCDetails, + blockchainId, + contractOwner, + isOwnerContract, + contractTotalWeight, + l1WeightError, + signingSubnetId, + isLoadingOwnership, + isLoadingL1Weight, + ownershipError, + ownerType, + isDetectingOwnerType + } = useValidatorManagerDetails({ subnetId: subnetIdL1 }); + + // Restore intermediate state from persisted validators data when available + useEffect(() => { + if (validators.length > 0 && !validatorBalance && !blsProofOfPossession) { + const validator = validators[0]; + setValidatorBalance((Number(validator.validatorBalance) / 1e9).toString()); + setBlsProofOfPossession(validator.nodePOP.proofOfPossession); + } else if (validators.length === 0) { + // Clear values when all validators are removed + setValidatorBalance(''); + setBlsProofOfPossession(''); + } + }, [validators, validatorBalance, blsProofOfPossession]); + + // Keep validatorBalance in sync with current validators selection + useEffect(() => { + if (validators.length > 0) { + const validator = validators[0]; + setValidatorBalance((Number(validator.validatorBalance) / 1e9).toString()); + } else { + setValidatorBalance(''); + } + }, [validators]); + + const handleReset = () => { + setGlobalError(null); + setGlobalSuccess(null); + setPChainTxId(''); + setValidatorBalance(''); + setBlsProofOfPossession(''); + setEvmTxHash(''); + setSubnetIdL1(''); + setValidators([]); + // Clear localStorage + if (typeof window !== 'undefined') { + localStorage.removeItem(STORAGE_KEY); + } + setResetKey(prev => prev + 1); // Force re-render of all child components + }; + + return ( + <> +
+ {globalError && ( + Error: {globalError} + )} + + + +

Ensure L1 Node is Running

+

+ Before staking a validator, you must have an L1 node set up and running. If you haven't done this yet, + visit the L1 Node Setup Tool first. + {isTestnet && ( + <> + {' '}On testnet, you can also use our free testnet infrastructure. + + )} +

+
+ + +

Select L1 Subnet

+

+ Choose the L1 subnet where you want to stake the validator. +

+
+ + setIsValidatorManagerDetailsExpanded(!isValidatorManagerDetailsExpanded)} + /> +
+
+ + +

Add Validator Details

+

+ Add the validator details including node credentials. Your stake amount will be entered in the next step. +

+ + 0n ? contractTotalWeight : null} + userPChainBalanceNavax={userPChainBalanceNavax} + maxValidators={1} + hideConsensusWeight={true} + /> +
+ + +

Initiate Staking Validator Registration

+

+ Call the initiateValidatorRegistration function on the Native Token Staking Manager contract with your stake. This transaction will emit a RegisterL1ValidatorMessage warp message. +

+ + {ownerType && ownerType !== 'StakingManager' && ( + + This L1 is not using a Staking Manager. This tool is only for L1s with Native Token Staking Managers. + + )} + + { + setValidatorBalance(data.validatorBalance); + setBlsProofOfPossession(data.blsProofOfPossession); + setEvmTxHash(data.txHash); + setGlobalError(null); + }} + onError={(message) => setGlobalError(message)} + /> +
+ + +

Sign RegisterL1ValidatorMessage & Submit RegisterL1ValidatorTx to P-Chain

+

+ Sign the emitted RegisterL1ValidatorMessage and submit a RegisterL1ValidatorTx to P-Chain. This transaction will emit a L1ValidatorRegistrationMessage warp message. +

+ { + setPChainTxId(pChainTxId); + setGlobalError(null); + }} + onError={(message) => setGlobalError(message)} + userPChainBalanceNavax={userPChainBalanceNavax} + /> +
+ + +

Sign L1ValidatorRegistrationMessage & Submit completeValidatorRegistration on Staking Manager contract

+

+ Complete the validator registration by signing the P-Chain L1ValidatorRegistrationMessage and calling the completeValidatorRegistration function on the Staking Manager contract. +

+ { + setGlobalSuccess(message); + setGlobalError(null); + onSuccess?.(); + }} + onError={(message) => setGlobalError(message)} + /> +
+
+ + {globalSuccess && ( + + )} + + {(pChainTxId || globalError || globalSuccess) && ( + + )} +
+ + ); +}; + +export default withConsoleToolMetadata(StakeValidator, metadata); \ No newline at end of file diff --git a/components/toolbox/stores/toolboxStore.ts b/components/toolbox/stores/toolboxStore.ts index e00e50e3dbe..fcb11f916e7 100644 --- a/components/toolbox/stores/toolboxStore.ts +++ b/components/toolbox/stores/toolboxStore.ts @@ -12,6 +12,7 @@ const toolboxInitialState = { validatorManagerAddress: "", rewardCalculatorAddress: "", nativeStakingManagerAddress: "", + erc20StakingManagerAddress: "", teleporterRegistryAddress: "", icmReceiverAddress: "", exampleErc20Address: "", @@ -30,6 +31,7 @@ export const getToolboxStore = (chainId: string) => create( setValidatorManagerAddress: (validatorManagerAddress: string) => set({ validatorManagerAddress }), setRewardCalculatorAddress: (rewardCalculatorAddress: string) => set({ rewardCalculatorAddress }), setNativeStakingManagerAddress: (nativeStakingManagerAddress: string) => set({ nativeStakingManagerAddress }), + setErc20StakingManagerAddress: (erc20StakingManagerAddress: string) => set({ erc20StakingManagerAddress }), setTeleporterRegistryAddress: (address: string) => set({ teleporterRegistryAddress: address }), setIcmReceiverAddress: (address: string) => set({ icmReceiverAddress: address }), setExampleErc20Address: (address: string) => set({ exampleErc20Address: address }),