From 3c9fe8321795be610b12de14d4b5755fd17e9d8c Mon Sep 17 00:00:00 2001 From: Fornax <23104993+fornax2@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:04:52 -0300 Subject: [PATCH 1/4] Add the feature to make multiple deposits on a single tx --- bindings/node/deposit.go | 32 ++ rocketpool-cli/megapool/commands.go | 16 +- rocketpool-cli/megapool/deposit.go | 139 ++++-- rocketpool-cli/megapool/dissolve-validator.go | 2 +- rocketpool/api/node/commands.go | 72 ++- rocketpool/api/node/deposit.go | 410 ++++++++++++++++++ shared/services/rocketpool/node.go | 32 ++ shared/services/state/network-state.go | 2 +- shared/types/api/node.go | 58 ++- 9 files changed, 679 insertions(+), 84 deletions(-) diff --git a/bindings/node/deposit.go b/bindings/node/deposit.go index 903a2f227..e0066975b 100644 --- a/bindings/node/deposit.go +++ b/bindings/node/deposit.go @@ -14,6 +14,16 @@ import ( "github.com/rocket-pool/smartnode/bindings/utils/eth" ) +type NodeDeposit struct { + BondAmount *big.Int `json:"bondAmount"` + UseExpressTicket bool `json:"useExpressTicket"` + ValidatorPubkey []byte `json:"validatorPubkey"` + ValidatorSignature []byte `json:"validatorSignature"` + DepositDataRoot common.Hash `json:"depositDataRoot"` +} + +type Deposits []NodeDeposit + // Estimate the gas of Deposit func EstimateDepositGas(rp *rocketpool.RocketPool, bondAmount *big.Int, useExpressTicket bool, validatorPubkey rptypes.ValidatorPubkey, validatorSignature rptypes.ValidatorSignature, depositDataRoot common.Hash, opts *bind.TransactOpts) (rocketpool.GasInfo, error) { rocketNodeDeposit, err := getRocketNodeDeposit(rp, nil) @@ -36,6 +46,28 @@ func Deposit(rp *rocketpool.RocketPool, bondAmount *big.Int, useExpressTicket bo return tx, nil } +// Estimate the gas of DepositMulti +func EstimateDepositMultiGas(rp *rocketpool.RocketPool, deposits Deposits, opts *bind.TransactOpts) (rocketpool.GasInfo, error) { + rocketNodeDeposit, err := getRocketNodeDeposit(rp, nil) + if err != nil { + return rocketpool.GasInfo{}, err + } + return rocketNodeDeposit.GetTransactionGasInfo(opts, "depositMulti", deposits) +} + +// Make multiple node deposits +func DepositMulti(rp *rocketpool.RocketPool, deposits Deposits, opts *bind.TransactOpts) (*types.Transaction, error) { + rocketNodeDeposit, err := getRocketNodeDeposit(rp, nil) + if err != nil { + return nil, err + } + tx, err := rocketNodeDeposit.Transact(opts, "depositMulti", deposits) + if err != nil { + return nil, fmt.Errorf("error making multiple node deposits: %w", err) + } + return tx, nil +} + // Estimate the gas to WithdrawETH func EstimateWithdrawEthGas(rp *rocketpool.RocketPool, nodeAccount common.Address, ethAmount *big.Int, opts *bind.TransactOpts) (rocketpool.GasInfo, error) { rocketNodeDeposit, err := getRocketNodeDeposit(rp, nil) diff --git a/rocketpool-cli/megapool/commands.go b/rocketpool-cli/megapool/commands.go index ad074d06b..335a6279a 100644 --- a/rocketpool-cli/megapool/commands.go +++ b/rocketpool-cli/megapool/commands.go @@ -1,6 +1,8 @@ package megapool import ( + "fmt" + "github.com/urfave/cli" cliutils "github.com/rocket-pool/smartnode/shared/utils/cli" @@ -30,8 +32,8 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { { Name: "deposit", Aliases: []string{"d"}, - Usage: "Make a deposit and create a new validator on the megapool", - UsageText: "rocketpool node deposit [options]", + Usage: "Make a deposit and create a new validator on the megapool. Optionally specify count to make multiple deposits.", + UsageText: "rocketpool megapool deposit [options]", Flags: []cli.Flag{ cli.BoolFlag{ Name: "yes, y", @@ -41,6 +43,11 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { Name: "use-express-ticket, e", Usage: "Use an express ticket to create a new validator", }, + cli.UintFlag{ + Name: "count, c", + Usage: "Number of deposits to make", + Value: 0, + }, }, Action: func(c *cli.Context) error { @@ -49,6 +56,11 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { return err } + // Validate count + if c.Uint("count") == 0 { + return fmt.Errorf("Count must be greater than 0") + } + // Run return nodeMegapoolDeposit(c) diff --git a/rocketpool-cli/megapool/deposit.go b/rocketpool-cli/megapool/deposit.go index b8355060d..6d6995f95 100644 --- a/rocketpool-cli/megapool/deposit.go +++ b/rocketpool-cli/megapool/deposit.go @@ -3,6 +3,7 @@ package megapool import ( "fmt" "math/big" + "strconv" "github.com/rocket-pool/smartnode/bindings/utils/eth" "github.com/urfave/cli" @@ -64,24 +65,6 @@ func nodeMegapoolDeposit(c *cli.Context) error { return nil } - /* - // Check if the fee distributor has been initialized - isInitializedResponse, err := rp.IsFeeDistributorInitialized() - if err != nil { - return err - } - if !isInitializedResponse.IsInitialized { - fmt.Println("Your fee distributor has not been initialized yet so you cannot create a new validator.\nPlease run `rocketpool node initialize-fee-distributor` to initialize it first.") - return nil - } - - // Post a warning about fee distribution - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: By creating a new validator, your node will automatically claim and distribute any balance you have in your fee distributor contract. If you don't want to claim your balance at this time, you should not create a new minipool.%s\nWould you like to continue?", colorYellow, colorReset))) { - fmt.Println("Cancelled.") - return nil - } - */ - useExpressTicket := false var wg errgroup.Group @@ -118,7 +101,20 @@ func nodeMegapoolDeposit(c *cli.Context) error { return err } - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: You are about to create a new megapool validator with a %.0f ETH deposit.%s\nWould you like to continue?", colorYellow, amount, colorReset))) { + count := c.Uint64("count") + + // If the count was not provided, prompt the user for the number of deposits + for count == 0 { + countStr := prompt.Prompt("How many validators would you like to create?", "^\\d+$", "Invalid number.") + count, err = strconv.ParseUint(countStr, 10, 64) + if err != nil { + fmt.Println("Invalid number. Please try again.") + continue + } + break + } + + if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: You are about to create %d new megapool validators, each with a %.0f ETH deposit (total: %.0f ETH).%s\nWould you like to continue?", colorYellow, count, amount, amount*float64(count), colorReset))) { fmt.Println("Cancelled.") return nil } @@ -139,7 +135,7 @@ func nodeMegapoolDeposit(c *cli.Context) error { fmt.Printf("You have %d express tickets available.", expressTicketCount) fmt.Println() // Prompt for confirmation - if c.Bool("yes") || prompt.Confirm("Would you like to use an express ticket?") { + if c.Bool("yes") || prompt.Confirm("Would you like to use your express tickets?") { useExpressTicket = true } } @@ -149,12 +145,18 @@ func nodeMegapoolDeposit(c *cli.Context) error { minNodeFee := 0.0 // Check deposit can be made - canDeposit, err := rp.CanNodeDeposit(amountWei, minNodeFee, big.NewInt(0), useExpressTicket) + var canDeposit api.CanNodeDepositResponse + if count > 1 { + canDeposit, err = rp.CanNodeDeposits(count, amountWei, minNodeFee, big.NewInt(0), useExpressTicket) + } else { + canDeposit, err = rp.CanNodeDeposit(amountWei, minNodeFee, big.NewInt(0), useExpressTicket) + } if err != nil { return err } if !canDeposit.CanDeposit { - fmt.Println("Cannot make node deposit:") + fmt.Printf("Cannot make %d node deposits:\n", count) + if canDeposit.InsufficientBalanceWithoutCredit { nodeBalance := eth.WeiToEth(canDeposit.NodeBalance) fmt.Printf("There is not enough ETH in the staking pool to use your credit balance (it needs at least 1 ETH but only has %.2f ETH) and you don't have enough ETH in your wallet (%.6f ETH) to cover the deposit amount yourself. If you want to continue creating a minipool, you will either need to wait for the staking pool to have more ETH deposited or add more ETH to your node wallet.", eth.WeiToEth(canDeposit.DepositBalance), nodeBalance) @@ -162,7 +164,11 @@ func nodeMegapoolDeposit(c *cli.Context) error { if canDeposit.InsufficientBalance { nodeBalance := eth.WeiToEth(canDeposit.NodeBalance) creditBalance := eth.WeiToEth(canDeposit.CreditBalance) - fmt.Printf("The node's balance of %.6f ETH and credit balance of %.6f ETH are not enough to create a megapool validator with a %.1f ETH bond.", nodeBalance, creditBalance, amount) + if count > 1 { + fmt.Printf("The node's balance of %.6f ETH and credit balance of %.6f ETH are not enough to create %d megapool validators with a %.1f ETH bond each (total: %.1f ETH).", nodeBalance, creditBalance, count, amount, amount*float64(count)) + } else { + fmt.Printf("The node's balance of %.6f ETH and credit balance of %.6f ETH are not enough to create a megapool validator with a %.1f ETH bond.", nodeBalance, creditBalance, amount) + } } if canDeposit.InvalidAmount { fmt.Println("The deposit amount is invalid.") @@ -174,12 +180,13 @@ func nodeMegapoolDeposit(c *cli.Context) error { } useCreditBalance := false + totalAmountWei := big.NewInt(0).Mul(amountWei, big.NewInt(int64(count))) fmt.Printf("You currently have %.2f ETH in your credit balance plus ETH staked on your behalf.\n", eth.WeiToEth(canDeposit.CreditBalance)) if canDeposit.CreditBalance.Cmp(big.NewInt(0)) > 0 { if canDeposit.CanUseCredit { useCreditBalance = true // Get how much credit to use - remainingAmount := big.NewInt(0).Sub(amountWei, canDeposit.CreditBalance) + remainingAmount := big.NewInt(0).Sub(totalAmountWei, canDeposit.CreditBalance) if remainingAmount.Cmp(big.NewInt(0)) > 0 { fmt.Printf("This deposit will use all %.6f ETH from your credit balance plus ETH staked on your behalf and %.6f ETH from your node.\n\n", eth.WeiToEth(canDeposit.CreditBalance), eth.WeiToEth(remainingAmount)) } else { @@ -189,6 +196,7 @@ func nodeMegapoolDeposit(c *cli.Context) error { fmt.Printf("%sNOTE: Your credit balance *cannot* currently be used to create a new megapool validator; there is not enough ETH in the staking pool to cover the initial deposit on your behalf (it needs at least 1 ETH but only has %.2f ETH).%s\nIf you want to continue creating this megapool validator now, you will have to pay for the full bond amount.\n\n", colorYellow, eth.WeiToEth(canDeposit.DepositBalance), colorReset) } } + // Prompt for confirmation if !(c.Bool("yes") || prompt.Confirm("Would you like to continue?")) { fmt.Println("Cancelled.") @@ -224,40 +232,77 @@ func nodeMegapoolDeposit(c *cli.Context) error { } // Prompt for confirmation + if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf( - "You are about to deposit %.6f ETH to create a new megapool validator.\n"+ - "%sARE YOU SURE YOU WANT TO DO THIS? Exiting this validator and retrieving your capital cannot be done until the validator has been *active* on the Beacon Chain for 256 epochs (approx. 27 hours).%s\n", + "You are about to deposit %.6f ETH to create %d new megapool validators (%.6f ETH total).\n"+ + "%sARE YOU SURE YOU WANT TO DO THIS? Exiting these validators and retrieving your capital cannot be done until each validator has been *active* on the Beacon Chain for 256 epochs (approx. 27 hours).%s\n", math.RoundDown(eth.WeiToEth(amountWei), 6), + count, + math.RoundDown(eth.WeiToEth(amountWei), 6)*float64(count), colorYellow, colorReset))) { fmt.Println("Cancelled.") return nil } - // Make deposit - response, err := rp.NodeDeposit(amountWei, minNodeFee, big.NewInt(0), useCreditBalance, useExpressTicket, true) - if err != nil { - return err - } + // Make deposit(s) + if count == 1 { + // Single deposit + response, err := rp.NodeDeposit(amountWei, minNodeFee, big.NewInt(0), useCreditBalance, useExpressTicket, true) + if err != nil { + return err + } - // Log and wait for the megapool validator deposit - fmt.Printf("Creating megapool validator...\n") - cliutils.PrintTransactionHash(rp, response.TxHash) - _, err = rp.WaitForTransaction(response.TxHash) - if err != nil { - return err - } + // Log and wait for the megapool validator deposit + fmt.Printf("Creating megapool validator...\n") + cliutils.PrintTransactionHash(rp, response.TxHash) + _, err = rp.WaitForTransaction(response.TxHash) + if err != nil { + return err + } + + // Log & return + fmt.Printf("The node deposit of %.6f ETH was made successfully!\n", math.RoundDown(eth.WeiToEth(amountWei), 6)) + fmt.Printf("The validator pubkey is: %s\n\n", response.ValidatorPubkey.Hex()) - // Log & return - fmt.Printf("The node deposit of %.6f ETH was made successfully!\n", math.RoundDown(eth.WeiToEth(amountWei), 6)) - fmt.Printf("The validator pubkey is: %s\n\n", response.ValidatorPubkey.Hex()) + fmt.Println("The new megapool validator has been created.") + fmt.Println("Once your validator progresses through the queue, ETH will be assigned and a 1 ETH prestake submitted.") + fmt.Printf("After the prestake, your node will automatically perform a stake transaction, to complete the progress.") + fmt.Println("") + fmt.Println("To check the status of your validators use `rocketpool megapool validators`") + fmt.Println("To monitor the stake transaction use `rocketpool service logs node`") + } else { + // Multiple deposits + responses, err := rp.NodeDeposits(count, amountWei, minNodeFee, big.NewInt(0), useCreditBalance, useExpressTicket, true) + if err != nil { + return err + } - fmt.Println("The new megapool validator has been created.") - fmt.Println("Once your validator progresses through the queue, ETH will be assigned and a 1 ETH prestake submitted.") - fmt.Printf("After the prestake, your node will automatically perform a stake transaction, to complete the progress.") - fmt.Println("") - fmt.Println("To check the status of your validators use `rocketpool megapool validators`") - fmt.Println("To monitor the stake transaction use `rocketpool service logs node`") + // Log and wait for the megapool validator deposits + fmt.Printf("Creating %d megapool validators in a single transaction...\n", count) + cliutils.PrintTransactionHash(rp, responses.TxHash) + _, err = rp.WaitForTransaction(responses.TxHash) + if err != nil { + return err + } + + // Log & return + fmt.Printf("The node deposits of %.6f ETH each (%.6f ETH total) were made successfully!\n", + math.RoundDown(eth.WeiToEth(amountWei), 6), + math.RoundDown(eth.WeiToEth(amountWei), 6)*float64(count)) + fmt.Printf("Validator pubkeys:\n") + for i, pubkey := range responses.ValidatorPubkeys { + fmt.Printf(" %d. %s\n", i+1, pubkey.Hex()) + } + fmt.Println() + + fmt.Printf("The %d new megapool validators have been created.\n", count) + fmt.Println("Once your validators progress through the queue, ETH will be assigned and a 1 ETH prestake submitted for each.") + fmt.Printf("After the prestake, your node will automatically perform a stake transaction for each validator, to complete the progress.") + fmt.Println("") + fmt.Println("To check the status of your validators use `rocketpool megapool validators`") + fmt.Println("To monitor the stake transactions use `rocketpool service logs node`") + } return nil diff --git a/rocketpool-cli/megapool/dissolve-validator.go b/rocketpool-cli/megapool/dissolve-validator.go index 5b0b27ed8..8a292c212 100644 --- a/rocketpool-cli/megapool/dissolve-validator.go +++ b/rocketpool-cli/megapool/dissolve-validator.go @@ -38,7 +38,7 @@ func dissolveValidator(c *cli.Context) error { // Get Megapool status status, err := rp.MegapoolStatus(false) if err != nil { - return err + return err } validatorsInPrestake := []api.MegapoolValidatorDetails{} diff --git a/rocketpool/api/node/commands.go b/rocketpool/api/node/commands.go index 67eb04aaf..16a23cc21 100644 --- a/rocketpool/api/node/commands.go +++ b/rocketpool/api/node/commands.go @@ -1,6 +1,8 @@ package node import ( + "fmt" + "github.com/urfave/cli" "github.com/rocket-pool/smartnode/shared/utils/api" @@ -936,13 +938,14 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { { Name: "can-deposit", - Usage: "Check whether the node can make a deposit", - UsageText: "rocketpool api node can-deposit amount min-fee salt use-express-ticket", + Usage: "Check whether the node can make a deposit. Optionally specify count to check multiple deposits.", + UsageText: "rocketpool api node can-deposit amount min-fee salt use-express-ticket [count]", Action: func(c *cli.Context) error { - // Validate args - if err := cliutils.ValidateArgCount(c, 4); err != nil { - return err + // Validate args - count is optional + argCount := c.NArg() + if argCount < 4 || argCount > 5 { + return fmt.Errorf("Invalid argument count. Expected 4 or 5 arguments, got %d", argCount) } amountWei, err := cliutils.ValidatePositiveWeiAmount("deposit amount", c.Args().Get(0)) if err != nil { @@ -964,8 +967,24 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { return err } + // Check if count is provided + var count uint64 = 1 + if argCount == 5 { + count, err = cliutils.ValidateUint("count", c.Args().Get(4)) + if err != nil { + return err + } + if count == 0 { + return fmt.Errorf("Count must be greater than 0") + } + } + // Run - api.PrintResponse(canNodeDeposit(c, amountWei, minNodeFee, salt, useExpressTicket)) + if count == 1 { + api.PrintResponse(canNodeDeposit(c, amountWei, minNodeFee, salt, useExpressTicket)) + } else { + api.PrintResponse(canNodeDeposits(c, count, amountWei, minNodeFee, salt, useExpressTicket)) + } return nil }, @@ -973,13 +992,14 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { { Name: "deposit", Aliases: []string{"d"}, - Usage: "Make a deposit and create a minipool, or just make and sign the transaction (when submit = false)", - UsageText: "rocketpool api node deposit amount min-node-fee salt use-credit-balance use-express-ticket submit", + Usage: "Make a deposit and create a minipool, or just make and sign the transaction (when submit = false). Optionally specify count to make multiple deposits.", + UsageText: "rocketpool api node deposit amount min-node-fee salt use-credit-balance use-express-ticket submit [count]", Action: func(c *cli.Context) error { - // Validate args - if err := cliutils.ValidateArgCount(c, 6); err != nil { - return err + // Validate args - count is optional + argCount := c.NArg() + if argCount < 6 || argCount > 7 { + return fmt.Errorf("Invalid argument count. Expected 6 or 7 arguments, got %d", argCount) } amountWei, err := cliutils.ValidatePositiveWeiAmount("deposit amount", c.Args().Get(0)) if err != nil { @@ -1012,16 +1032,32 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { return err } - if err != nil { - return err + // Check if count is provided + var count uint64 = 1 + if argCount == 7 { + count, err = cliutils.ValidateUint("count", c.Args().Get(6)) + if err != nil { + return err + } + if count == 0 { + return fmt.Errorf("Count must be greater than 0") + } } // Run - response, err := nodeDeposit(c, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) - if submit { - api.PrintResponse(response, err) - } // else nodeDeposit already printed the encoded transaction - return nil + if count == 1 { + response, err := nodeDeposit(c, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) + if submit { + api.PrintResponse(response, err) + } // else nodeDeposit already printed the encoded transaction + return nil + } else { + response, err := nodeDeposits(c, count, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) + if submit { + api.PrintResponse(response, err) + } // else nodeDeposits already printed the encoded transaction + return nil + } }, }, diff --git a/rocketpool/api/node/deposit.go b/rocketpool/api/node/deposit.go index 79b7c32e7..e73cd4d4a 100644 --- a/rocketpool/api/node/deposit.go +++ b/rocketpool/api/node/deposit.go @@ -280,6 +280,224 @@ func canNodeDeposit(c *cli.Context, amountWei *big.Int, minNodeFee float64, salt } +func canNodeDeposits(c *cli.Context, count uint64, amountWei *big.Int, minNodeFee float64, salt *big.Int, useExpressTicket bool) (*api.CanNodeDepositResponse, error) { + + // Get services + if err := services.RequireNodeRegistered(c); err != nil { + return nil, err + } + w, err := services.GetWallet(c) + if err != nil { + return nil, err + } + ec, err := services.GetEthClient(c) + if err != nil { + return nil, err + } + rp, err := services.GetRocketPool(c) + if err != nil { + return nil, err + } + bc, err := services.GetBeaconClient(c) + if err != nil { + return nil, err + } + + // Get eth2 config + eth2Config, err := bc.GetEth2Config() + if err != nil { + return nil, err + } + + // Response + response := api.CanNodeDepositResponse{ + ValidatorPubkeys: make([]rptypes.ValidatorPubkey, count), + } + + // Get node account + nodeAccount, err := w.GetNodeAccount() + if err != nil { + return nil, err + } + + saturnDeployed, err := state.IsSaturnDeployed(rp, nil) + if err != nil { + return nil, err + } + + if !saturnDeployed { + return nil, fmt.Errorf("Multiple deposits are only supported after Saturn deployment") + } + + // Data + var wg1 errgroup.Group + var depositPoolBalance *big.Int + var expressTicketCount uint64 + + // Check credit balance + wg1.Go(func() error { + ethBalanceWei, err := node.GetNodeCreditAndBalance(rp, nodeAccount.Address, nil) + if err == nil { + response.CreditBalance = ethBalanceWei + } + return err + }) + + // Check node balance + wg1.Go(func() error { + ethBalanceWei, err := ec.BalanceAt(context.Background(), nodeAccount.Address, nil) + if err == nil { + response.NodeBalance = ethBalanceWei + } + return err + }) + + // Check node deposits are enabled + wg1.Go(func() error { + depositEnabled, err := protocol.GetNodeDepositEnabled(rp, nil) + if err == nil { + response.DepositDisabled = !depositEnabled + } + return err + }) + + // Get deposit pool balance + wg1.Go(func() error { + var err error + depositPoolBalance, err = deposit.GetBalance(rp, nil) + return err + }) + + // Get the express ticket count + wg1.Go(func() error { + var err error + expressTicketCount, err = node.GetExpressTicketCount(rp, nodeAccount.Address, nil) + return err + }) + + // Wait for data + if err := wg1.Wait(); err != nil { + return nil, err + } + + // Calculate total amount needed for all deposits + totalAmountWei := big.NewInt(0).Mul(amountWei, big.NewInt(int64(count))) + + // Check for insufficient balance + totalBalance := big.NewInt(0).Add(response.NodeBalance, response.CreditBalance) + response.InsufficientBalance = (totalAmountWei.Cmp(totalBalance) > 0) + + // Check if the credit balance can be used + response.DepositBalance = depositPoolBalance + response.CanUseCredit = (depositPoolBalance.Cmp(eth.EthToWei(1)) >= 0) && totalBalance.Cmp(totalAmountWei) >= 0 + + // Update response + response.CanDeposit = !(response.InsufficientBalance || response.InvalidAmount || response.DepositDisabled) + if !response.CanDeposit { + return &response, nil + } + + if response.CanDeposit && !response.CanUseCredit && response.NodeBalance.Cmp(totalAmountWei) < 0 { + // Can't use credit and there's not enough ETH in the node wallet to deposit so error out + response.InsufficientBalanceWithoutCredit = true + response.CanDeposit = false + } + + // Break before the gas estimator if depositing won't work + if !response.CanDeposit { + return &response, nil + } + + // Get gas estimate + opts, err := w.GetNodeAccountTransactor() + if err != nil { + return nil, err + } + + // Get how much credit to use + if response.CanUseCredit { + remainingAmount := big.NewInt(0).Sub(amountWei, response.CreditBalance) + if remainingAmount.Cmp(big.NewInt(0)) > 0 { + // Send the remaining amount if the credit isn't enough to cover the whole deposit + opts.Value = remainingAmount + } + } else { + opts.Value = amountWei + } + + // Get the megapool address + megapoolAddress, err := megapool.GetMegapoolExpectedAddress(rp, nodeAccount.Address, nil) + if err != nil { + return nil, err + } + + // Get the withdrawal credentials + withdrawalCredentials := services.CalculateMegapoolWithdrawalCredentials(megapoolAddress) + + // Create deposit data for all deposits (for gas estimation) + // We need to create unique validator keys for each deposit to get accurate gas estimates + depositAmount := uint64(1e9) // 1 ETH in gwei + depositsSlice := node.Deposits{} + + keyCount, err := w.GetValidatorKeyCount() + if err != nil { + return nil, err + } + + // Get the next validator key for gas estimation + validatorKeys, err := w.GetValidatorKeys(keyCount, uint(count)) + if err != nil { + return nil, err + } + + canUseExpressTicket := expressTicketCount > 0 && useExpressTicket + for i := uint64(0); i < count; i++ { + + // If we can use an express ticket count the number of tickets used + if canUseExpressTicket && expressTicketCount > 0 { + expressTicketCount-- + } else { + canUseExpressTicket = false + } + // Get validator deposit data and associated parameters + depositData, depositDataRoot, err := validator.GetDepositData(validatorKeys[i].PrivateKey, withdrawalCredentials, eth2Config, depositAmount) + if err != nil { + return nil, err + } + pubKey := rptypes.BytesToValidatorPubkey(depositData.PublicKey) + signature := rptypes.BytesToValidatorSignature(depositData.Signature) + + // Add to deposits array + depositsSlice = append(depositsSlice, node.NodeDeposit{ + BondAmount: amountWei, + UseExpressTicket: canUseExpressTicket, + ValidatorPubkey: pubKey[:], + ValidatorSignature: signature[:], + DepositDataRoot: depositDataRoot, + }) + + // Store the pubkey in the response + response.ValidatorPubkeys[i] = pubKey + } + + opts.Value = amountWei + + // Run the deposit multi gas estimator + // Ensure count is valid + if count == 0 { + return nil, fmt.Errorf("count must be greater than 0") + } + + gasInfo, err := node.EstimateDepositMultiGas(rp, depositsSlice, opts) + if err != nil { + return nil, fmt.Errorf("error estimating gas for depositMulti: %w", err) + } + response.GasInfo = gasInfo + + return &response, nil + +} + func nodeDeposit(c *cli.Context, amountWei *big.Int, minNodeFee float64, salt *big.Int, useCreditBalance bool, useExpressTicket bool, submit bool) (*api.NodeDepositResponse, error) { // Get services @@ -511,6 +729,198 @@ func nodeDeposit(c *cli.Context, amountWei *big.Int, minNodeFee float64, salt *b } +func nodeDeposits(c *cli.Context, count uint64, amountWei *big.Int, minNodeFee float64, salt *big.Int, useCreditBalance bool, useExpressTicket bool, submit bool) (*api.NodeDepositsResponse, error) { + + // Get services + if err := services.RequireNodeRegistered(c); err != nil { + return nil, err + } + w, err := services.GetWallet(c) + if err != nil { + return nil, err + } + rp, err := services.GetRocketPool(c) + if err != nil { + return nil, err + } + bc, err := services.GetBeaconClient(c) + if err != nil { + return nil, err + } + + // Get eth2 config + eth2Config, err := bc.GetEth2Config() + if err != nil { + return nil, err + } + + // Get node account + nodeAccount, err := w.GetNodeAccount() + if err != nil { + return nil, err + } + + saturnDeployed, err := state.IsSaturnDeployed(rp, nil) + if err != nil { + return nil, err + } + + // Response + response := api.NodeDepositsResponse{} + + if !saturnDeployed { + return nil, fmt.Errorf("Multiple deposits are only supported after Saturn deployment") + } + + // Make sure ETH2 is on the correct chain + depositContractInfo, err := getDepositContractInfo(c) + if err != nil { + return nil, err + } + if depositContractInfo.RPNetwork != depositContractInfo.BeaconNetwork || + depositContractInfo.RPDepositContract != depositContractInfo.BeaconDepositContract { + return nil, fmt.Errorf("Beacon network mismatch! Expected %s on chain %d, but beacon is using %s on chain %d.", + depositContractInfo.RPDepositContract.Hex(), + depositContractInfo.RPNetwork, + depositContractInfo.BeaconDepositContract.Hex(), + depositContractInfo.BeaconNetwork) + } + + // Get the scrub period + scrubPeriodUnix, err := trustednode.GetScrubPeriod(rp, nil) + if err != nil { + return nil, err + } + scrubPeriod := time.Duration(scrubPeriodUnix) * time.Second + response.ScrubPeriod = scrubPeriod + + // Get the megapool address + megapoolAddress, err := megapool.GetMegapoolExpectedAddress(rp, nodeAccount.Address, nil) + if err != nil { + return nil, err + } + + // Get the withdrawal credentials + withdrawalCredentials := services.CalculateMegapoolWithdrawalCredentials(megapoolAddress) + + // Get transactor + opts, err := w.GetNodeAccountTransactor() + if err != nil { + return nil, err + } + + // Calculate total amount needed + totalAmountWei := big.NewInt(0).Mul(amountWei, big.NewInt(int64(count))) + + // For multi-deposit, credit balance is not supported, so send the full amount + opts.Value = totalAmountWei + + // Create validator keys and deposit data for all deposits + // Since gas estimation already created the keys, we reuse them instead of creating new ones + depositAmount := uint64(1e9) // 1 ETH in gwei + depositsSlice := make([]node.NodeDeposit, count) + response.ValidatorPubkeys = make([]rptypes.ValidatorPubkey, count) + + for i := uint64(0); i < count; i++ { + validatorKey, err := w.CreateValidatorKey() + if err != nil { + return nil, err + } + // Get validator deposit data and associated parameters + depositData, depositDataRoot, err := validator.GetDepositData(validatorKey, withdrawalCredentials, eth2Config, depositAmount) + if err != nil { + return nil, err + } + pubKey := rptypes.BytesToValidatorPubkey(depositData.PublicKey) + signature := rptypes.BytesToValidatorSignature(depositData.Signature) + + // Make sure a validator with this pubkey doesn't already exist + status, err := bc.GetValidatorStatus(pubKey, nil) + if err != nil { + return nil, fmt.Errorf("Error checking for existing validator status for deposit %d/%d: %w\nYour funds have not been deposited for your own safety.", i+1, count, err) + } + if status.Exists { + return nil, fmt.Errorf("**** ALERT ****\n"+ + "The following validator pubkey is already in use on the Beacon chain:\n\t%s\n"+ + "Rocket Pool will not allow you to deposit this validator for your own safety so you do not get slashed.\n"+ + "PLEASE REPORT THIS TO THE ROCKET POOL DEVELOPERS.\n"+ + "***************\n", pubKey.Hex()) + } + + // Do a final sanity check + err = validateDepositInfo(eth2Config, depositAmount, pubKey, withdrawalCredentials, signature) + if err != nil { + return nil, fmt.Errorf("Your deposit %d/%d failed the validation safety check: %w\n"+ + "For your safety, this deposit will not be submitted and your ETH will not be staked.\n"+ + "PLEASE REPORT THIS TO THE ROCKET POOL DEVELOPERS and include the following information:\n"+ + "\tDomain Type: 0x%s\n"+ + "\tGenesis Fork Version: 0x%s\n"+ + "\tGenesis Validator Root: 0x%s\n"+ + "\tDeposit Amount: %d gwei\n"+ + "\tValidator Pubkey: %s\n"+ + "\tWithdrawal Credentials: %s\n"+ + "\tSignature: %s\n", + i+1, count, err, + hex.EncodeToString(eth2types.DomainDeposit[:]), + hex.EncodeToString(eth2Config.GenesisForkVersion), + hex.EncodeToString(eth2types.ZeroGenesisValidatorsRoot), + depositAmount, + pubKey.Hex(), + withdrawalCredentials.Hex(), + signature.Hex(), + ) + } + + // Set deposits array element + depositsSlice[i] = node.NodeDeposit{ + BondAmount: amountWei, + UseExpressTicket: useExpressTicket, + ValidatorPubkey: pubKey[:], + ValidatorSignature: signature[:], + DepositDataRoot: depositDataRoot, + } + + response.ValidatorPubkeys[i] = pubKey + } + + // Override the provided pending TX if requested + err = eth1.CheckForNonceOverride(c, opts) + if err != nil { + return nil, fmt.Errorf("Error checking for nonce override: %w", err) + } + + // Do not send transaction unless requested + opts.NoSend = !submit + + // Make multiple deposits in a single transaction + // Convert to Deposits type for proper ABI encoding + deposits := node.Deposits(depositsSlice) + tx, err := node.DepositMulti(rp, deposits, opts) + if err != nil { + return nil, err + } + + // Save wallet + if err := w.Save(); err != nil { + return nil, err + } + + // Print transaction if requested + if !submit { + b, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + fmt.Printf("%x\n", b) + } + + response.TxHash = tx.Hash() + + // Return response + return &response, nil + +} + func validateDepositInfo(eth2Config beacon.Eth2Config, depositAmount uint64, pubkey rptypes.ValidatorPubkey, withdrawalCredentials common.Hash, signature rptypes.ValidatorSignature) error { // Get the deposit domain based on the eth2 config diff --git a/shared/services/rocketpool/node.go b/shared/services/rocketpool/node.go index 4bcf62758..7434b691b 100644 --- a/shared/services/rocketpool/node.go +++ b/shared/services/rocketpool/node.go @@ -713,6 +713,22 @@ func (c *Client) CanNodeDeposit(amountWei *big.Int, minFee float64, salt *big.In return response, nil } +// Check whether the node can make multiple deposits +func (c *Client) CanNodeDeposits(count uint64, amountWei *big.Int, minFee float64, salt *big.Int, useExpressTicket bool) (api.CanNodeDepositResponse, error) { + responseBytes, err := c.callAPI(fmt.Sprintf("node can-deposit %s %f %s %t %d", amountWei.String(), minFee, salt.String(), useExpressTicket, count)) + if err != nil { + return api.CanNodeDepositResponse{}, fmt.Errorf("Could not get can node deposits status: %w", err) + } + var response api.CanNodeDepositResponse + if err := json.Unmarshal(responseBytes, &response); err != nil { + return api.CanNodeDepositResponse{}, fmt.Errorf("Could not decode can node deposits response: %w", err) + } + if response.Error != "" { + return api.CanNodeDepositResponse{}, fmt.Errorf("Could not get can node deposits status: %s", response.Error) + } + return response, nil +} + // Make a node deposit func (c *Client) NodeDeposit(amountWei *big.Int, minFee float64, salt *big.Int, useCreditBalance bool, useExpressTicket bool, submit bool) (api.NodeDepositResponse, error) { responseBytes, err := c.callAPI(fmt.Sprintf("node deposit %s %f %s %t %t %t", amountWei.String(), minFee, salt.String(), useCreditBalance, useExpressTicket, submit)) @@ -729,6 +745,22 @@ func (c *Client) NodeDeposit(amountWei *big.Int, minFee float64, salt *big.Int, return response, nil } +// Make multiple node deposits +func (c *Client) NodeDeposits(count uint64, amountWei *big.Int, minFee float64, salt *big.Int, useCreditBalance bool, useExpressTicket bool, submit bool) (api.NodeDepositsResponse, error) { + responseBytes, err := c.callAPI(fmt.Sprintf("node deposit %s %f %s %t %t %t %d", amountWei.String(), minFee, salt.String(), useCreditBalance, useExpressTicket, submit, count)) + if err != nil { + return api.NodeDepositsResponse{}, fmt.Errorf("Could not make node deposits: %w", err) + } + var response api.NodeDepositsResponse + if err := json.Unmarshal(responseBytes, &response); err != nil { + return api.NodeDepositsResponse{}, fmt.Errorf("Could not decode node deposits response: %w", err) + } + if response.Error != "" { + return api.NodeDepositsResponse{}, fmt.Errorf("Could not make node deposits: %s", response.Error) + } + return response, nil +} + // Check whether the node can send tokens func (c *Client) CanNodeSend(amountRaw float64, token string, toAddress common.Address) (api.CanNodeSendResponse, error) { responseBytes, err := c.callAPI(fmt.Sprintf("node can-send %.10f %s %s", amountRaw, token, toAddress.Hex())) diff --git a/shared/services/state/network-state.go b/shared/services/state/network-state.go index 70e798cd4..b144e145b 100644 --- a/shared/services/state/network-state.go +++ b/shared/services/state/network-state.go @@ -301,7 +301,7 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkSta if err != nil { return err } - megapoolDetails, err := rpstate.GetNodeMegapoolDetails(m.rp, nodeAddress, opts) + megapoolDetails, err := rpstate.GetNodeMegapoolDetails(m.rp, nodeAddress, opts) if err != nil { return err } diff --git a/shared/types/api/node.go b/shared/types/api/node.go index 1e55d39d0..581b0294c 100644 --- a/shared/types/api/node.go +++ b/shared/types/api/node.go @@ -428,21 +428,22 @@ type CanNodeWithdrawRplv1_3_1Response struct { } type CanNodeDepositResponse struct { - Status string `json:"status"` - Error string `json:"error"` - CanDeposit bool `json:"canDeposit"` - CreditBalance *big.Int `json:"creditBalance"` - DepositBalance *big.Int `json:"depositBalance"` - CanUseCredit bool `json:"canUseCredit"` - NodeBalance *big.Int `json:"nodeBalance"` - InsufficientBalance bool `json:"insufficientBalance"` - InsufficientBalanceWithoutCredit bool `json:"insufficientBalanceWithoutCredit"` - InvalidAmount bool `json:"invalidAmount"` - DepositDisabled bool `json:"depositDisabled"` - InConsensus bool `json:"inConsensus"` - MinipoolAddress common.Address `json:"minipoolAddress"` - MegapoolAddress common.Address `json:"megapoolAddress"` - GasInfo rocketpool.GasInfo `json:"gasInfo"` + Status string `json:"status"` + Error string `json:"error"` + CanDeposit bool `json:"canDeposit"` + CreditBalance *big.Int `json:"creditBalance"` + DepositBalance *big.Int `json:"depositBalance"` + CanUseCredit bool `json:"canUseCredit"` + NodeBalance *big.Int `json:"nodeBalance"` + InsufficientBalance bool `json:"insufficientBalance"` + InsufficientBalanceWithoutCredit bool `json:"insufficientBalanceWithoutCredit"` + InvalidAmount bool `json:"invalidAmount"` + DepositDisabled bool `json:"depositDisabled"` + InConsensus bool `json:"inConsensus"` + MinipoolAddress common.Address `json:"minipoolAddress"` + MegapoolAddress common.Address `json:"megapoolAddress"` + ValidatorPubkeys []rptypes.ValidatorPubkey `json:"validatorPubkeys"` + GasInfo rocketpool.GasInfo `json:"gasInfo"` } type NodeDepositResponse struct { Status string `json:"status"` @@ -453,6 +454,33 @@ type NodeDepositResponse struct { ScrubPeriod time.Duration `json:"scrubPeriod"` } +type CanNodeDepositsResponse struct { + Status string `json:"status"` + Error string `json:"error"` + CanDeposit bool `json:"canDeposit"` + CreditBalance *big.Int `json:"creditBalance"` + DepositBalance *big.Int `json:"depositBalance"` + CanUseCredit bool `json:"canUseCredit"` + NodeBalance *big.Int `json:"nodeBalance"` + InsufficientBalance bool `json:"insufficientBalance"` + InsufficientBalanceWithoutCredit bool `json:"insufficientBalanceWithoutCredit"` + InvalidAmount bool `json:"invalidAmount"` + DepositDisabled bool `json:"depositDisabled"` + InConsensus bool `json:"inConsensus"` + MinipoolAddress common.Address `json:"minipoolAddress"` + MegapoolAddress common.Address `json:"megapoolAddress"` + ValidatorPubkeys []rptypes.ValidatorPubkey `json:"validatorPubkeys"` + GasInfo rocketpool.GasInfo `json:"gasInfo"` +} + +type NodeDepositsResponse struct { + Status string `json:"status"` + Error string `json:"error"` + TxHash common.Hash `json:"txHash"` + ValidatorPubkeys []rptypes.ValidatorPubkey `json:"validatorPubkeys"` + ScrubPeriod time.Duration `json:"scrubPeriod"` +} + type CanCreateVacantMinipoolResponse struct { Status string `json:"status"` Error string `json:"error"` From ceea2ec4bff6b295abc630fac542be3a21ab1252 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:56:36 -0300 Subject: [PATCH 2/4] wip --- rocketpool/api/node/commands.go | 78 +++++++++++---------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/rocketpool/api/node/commands.go b/rocketpool/api/node/commands.go index 16a23cc21..8e6796fcd 100644 --- a/rocketpool/api/node/commands.go +++ b/rocketpool/api/node/commands.go @@ -1,8 +1,6 @@ package node import ( - "fmt" - "github.com/urfave/cli" "github.com/rocket-pool/smartnode/shared/utils/api" @@ -939,14 +937,14 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { { Name: "can-deposit", Usage: "Check whether the node can make a deposit. Optionally specify count to check multiple deposits.", - UsageText: "rocketpool api node can-deposit amount min-fee salt use-express-ticket [count]", + UsageText: "rocketpool api node can-deposit amount min-fee salt use-express-ticket count", Action: func(c *cli.Context) error { - // Validate args - count is optional - argCount := c.NArg() - if argCount < 4 || argCount > 5 { - return fmt.Errorf("Invalid argument count. Expected 4 or 5 arguments, got %d", argCount) + // Validate args + if err := cliutils.ValidateArgCount(c, 5); err != nil { + return err } + amountWei, err := cliutils.ValidatePositiveWeiAmount("deposit amount", c.Args().Get(0)) if err != nil { return err @@ -961,30 +959,18 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { return err } - useExpressTicketString := c.Args().Get(3) - useExpressTicket, err := cliutils.ValidateBool("use-express-ticket", useExpressTicketString) + useExpressTicket, err := cliutils.ValidateBool("use-express-ticket", c.Args().Get(3)) if err != nil { return err } - // Check if count is provided - var count uint64 = 1 - if argCount == 5 { - count, err = cliutils.ValidateUint("count", c.Args().Get(4)) - if err != nil { - return err - } - if count == 0 { - return fmt.Errorf("Count must be greater than 0") - } + count, err := cliutils.ValidateUint("count", c.Args().Get(4)) + if err != nil { + return err } // Run - if count == 1 { - api.PrintResponse(canNodeDeposit(c, amountWei, minNodeFee, salt, useExpressTicket)) - } else { - api.PrintResponse(canNodeDeposits(c, count, amountWei, minNodeFee, salt, useExpressTicket)) - } + api.PrintResponse(canNodeDeposits(c, count, amountWei, minNodeFee, salt, useExpressTicket)) return nil }, @@ -993,14 +979,14 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { Name: "deposit", Aliases: []string{"d"}, Usage: "Make a deposit and create a minipool, or just make and sign the transaction (when submit = false). Optionally specify count to make multiple deposits.", - UsageText: "rocketpool api node deposit amount min-node-fee salt use-credit-balance use-express-ticket submit [count]", + UsageText: "rocketpool api node deposit amount min-node-fee salt use-credit-balance use-express-ticket submit count", Action: func(c *cli.Context) error { - // Validate args - count is optional - argCount := c.NArg() - if argCount < 6 || argCount > 7 { - return fmt.Errorf("Invalid argument count. Expected 6 or 7 arguments, got %d", argCount) + // Validate args + if err := cliutils.ValidateArgCount(c, 7); err != nil { + return err } + amountWei, err := cliutils.ValidatePositiveWeiAmount("deposit amount", c.Args().Get(0)) if err != nil { return err @@ -1033,32 +1019,18 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { } // Check if count is provided - var count uint64 = 1 - if argCount == 7 { - count, err = cliutils.ValidateUint("count", c.Args().Get(6)) - if err != nil { - return err - } - if count == 0 { - return fmt.Errorf("Count must be greater than 0") - } - } - - // Run - if count == 1 { - response, err := nodeDeposit(c, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) - if submit { - api.PrintResponse(response, err) - } // else nodeDeposit already printed the encoded transaction - return nil - } else { - response, err := nodeDeposits(c, count, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) - if submit { - api.PrintResponse(response, err) - } // else nodeDeposits already printed the encoded transaction - return nil + count, err := cliutils.ValidateUint("count", c.Args().Get(6)) + if err != nil { + return err } + // Run + response, err := nodeDeposits(c, count, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) + if submit { + api.PrintResponse(response, err) + } // else nodeDeposits already printed the encoded transaction + return nil + }, }, From e5de0219de3bb3f3a6e89af0fe6369f01bd48ba4 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+fornax2@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:32:35 -0300 Subject: [PATCH 3/4] Apply an upper limit on validator count to fit the tx gas limit --- rocketpool-cli/megapool/deposit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocketpool-cli/megapool/deposit.go b/rocketpool-cli/megapool/deposit.go index 6d6995f95..9b47e8c25 100644 --- a/rocketpool-cli/megapool/deposit.go +++ b/rocketpool-cli/megapool/deposit.go @@ -104,8 +104,8 @@ func nodeMegapoolDeposit(c *cli.Context) error { count := c.Uint64("count") // If the count was not provided, prompt the user for the number of deposits - for count == 0 { - countStr := prompt.Prompt("How many validators would you like to create?", "^\\d+$", "Invalid number.") + for count == 0 || count > 35 { + countStr := prompt.Prompt("How many validators would you like to create? (max: 35)", "^\\d+$", "Invalid number.") count, err = strconv.ParseUint(countStr, 10, 64) if err != nil { fmt.Println("Invalid number. Please try again.") From 22b5f51343aafcce266aaf42c3d62c0f1aa7a2f4 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:43:42 -0300 Subject: [PATCH 4/4] Add maxCounbt --- rocketpool-cli/megapool/deposit.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/rocketpool-cli/megapool/deposit.go b/rocketpool-cli/megapool/deposit.go index 9b47e8c25..cf6ccdc66 100644 --- a/rocketpool-cli/megapool/deposit.go +++ b/rocketpool-cli/megapool/deposit.go @@ -19,13 +19,11 @@ import ( // Config const ( - colorReset string = "\033[0m" - colorRed string = "\033[31m" - colorGreen string = "\033[32m" - colorYellow string = "\033[33m" - smoothingPoolLink string = "https://docs.rocketpool.net/guides/redstone/whats-new.html#smoothing-pool" - signallingAddressLink string = "https://docs.rocketpool.net/guides/houston/participate#setting-your-snapshot-signalling-address" - maxAlertItems int = 3 + colorReset string = "\033[0m" + colorRed string = "\033[31m" + colorGreen string = "\033[32m" + colorYellow string = "\033[33m" + maxCount uint64 = 35 ) func nodeMegapoolDeposit(c *cli.Context) error { @@ -104,8 +102,8 @@ func nodeMegapoolDeposit(c *cli.Context) error { count := c.Uint64("count") // If the count was not provided, prompt the user for the number of deposits - for count == 0 || count > 35 { - countStr := prompt.Prompt("How many validators would you like to create? (max: 35)", "^\\d+$", "Invalid number.") + for count == 0 || count > maxCount { + countStr := prompt.Prompt(fmt.Sprintf("How many validators would you like to create? (max: %d)", maxCount), "^\\d+$", "Invalid number.") count, err = strconv.ParseUint(countStr, 10, 64) if err != nil { fmt.Println("Invalid number. Please try again.")