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..40559e907 100644 --- a/rocketpool-cli/megapool/commands.go +++ b/rocketpool-cli/megapool/commands.go @@ -30,16 +30,22 @@ 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", Usage: "Automatically confirm deposit", }, - cli.BoolFlag{ - Name: "use-express-ticket, e", - Usage: "Use an express ticket to create a new validator", + cli.Int64Flag{ + Name: "express-tickets, e", + Usage: "Number of express tickets to use", + Value: -1, + }, + cli.UintFlag{ + Name: "count, c", + Usage: "Number of deposits to make", + Value: 0, }, }, Action: func(c *cli.Context) error { diff --git a/rocketpool-cli/megapool/deposit.go b/rocketpool-cli/megapool/deposit.go index d4ac04114..882bbb4eb 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" @@ -18,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 { @@ -64,26 +63,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 var expressTicketCount uint64 var queueDetails api.GetQueueDetailsResponse @@ -118,7 +97,19 @@ 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 || 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.") + continue + } + } + + 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 } @@ -127,20 +118,19 @@ func nodeMegapoolDeposit(c *cli.Context) error { fmt.Printf("There are %d validator(s) on the standard queue.\n", queueDetails.StandardLength) fmt.Printf("The express queue rate is %d.\n\n", queueDetails.ExpressRate) - if c.Bool("use-express-ticket") { - if expressTicketCount > 0 { - useExpressTicket = true - } else { - fmt.Println("You do not have any express tickets available.") - return nil + expressTickets := c.Int64("express-tickets") + if expressTickets >= 0 { + if expressTicketCount < uint64(expressTickets) { + expressTickets = int64(expressTicketCount) } - } else { - if expressTicketCount > 0 { - 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?") { - useExpressTicket = true + } + if expressTicketCount > 0 && expressTickets < 0 { + // Prompt for the number of express tickets to use + for expressTickets == -1 || uint64(expressTickets) > expressTicketCount { + expressTicketsStr := prompt.Prompt(fmt.Sprintf("How many express tickets would you like to use? (max: %d)", expressTicketCount), "^\\d+$", "Invalid number.") + expressTickets, err = strconv.ParseInt(expressTicketsStr, 10, 64) + if err != nil { + fmt.Println("Invalid number. Please try again.") } } } @@ -149,12 +139,12 @@ func nodeMegapoolDeposit(c *cli.Context) error { minNodeFee := 0.0 // Check deposit can be made - canDeposit, err := rp.CanNodeDeposit(amountWei, minNodeFee, big.NewInt(0), useExpressTicket) + canDeposit, err := rp.CanNodeDeposits(count, amountWei, minNodeFee, big.NewInt(0), uint64(expressTickets)) 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.NodeHasDebt { fmt.Println("The node has debt. You must repay the debt before creating a new validator. Use the `rocketpool megapool repay-debt` command to repay the debt.") } @@ -165,7 +155,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.") @@ -177,12 +171,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 { @@ -192,6 +187,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.") @@ -227,24 +223,27 @@ 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) + // Make deposit(s) + + response, err := rp.NodeDeposits(count, amountWei, minNodeFee, big.NewInt(0), useCreditBalance, uint64(expressTickets), true) if err != nil { return err } - - // Log and wait for the megapool validator deposit - fmt.Printf("Creating megapool validator...\n") + // Log and wait for the megapool validator deposits + fmt.Printf("Creating %d megapool validators ...\n", count) cliutils.PrintTransactionHash(rp, response.TxHash) _, err = rp.WaitForTransaction(response.TxHash) if err != nil { @@ -252,15 +251,21 @@ func nodeMegapoolDeposit(c *cli.Context) error { } // 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.Printf("The node deposit of %.6f ETH each (%.6f ETH total) was 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 response.ValidatorPubkeys { + fmt.Printf(" %d. %s\n", i+1, pubkey.Hex()) + } + fmt.Println() - 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.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 transaction use `rocketpool service logs node`") + fmt.Println("To monitor the stake transactions use `rocketpool service logs node`") return nil diff --git a/rocketpool-cli/node/deposit.go b/rocketpool-cli/node/deposit.go index 327a3cc29..d7bf2e7df 100644 --- a/rocketpool-cli/node/deposit.go +++ b/rocketpool-cli/node/deposit.go @@ -1,25 +1,16 @@ package node import ( - "crypto/rand" "fmt" - "math/big" - "strconv" - "github.com/rocket-pool/smartnode/bindings/utils/eth" "github.com/urfave/cli" - "github.com/rocket-pool/smartnode/shared/services/gas" "github.com/rocket-pool/smartnode/shared/services/rocketpool" - cliutils "github.com/rocket-pool/smartnode/shared/utils/cli" - "github.com/rocket-pool/smartnode/shared/utils/cli/prompt" - "github.com/rocket-pool/smartnode/shared/utils/math" ) // Config const ( defaultMaxNodeFeeSlippage = 0.01 // 1% below current network fee - depositWarningMessage = "NOTE: By creating a new minipool, your node will automatically initialize voting power to itself. If you would like to delegate your on-chain voting power, you should run the command `rocketpool pdao initialize-voting` before creating a new minipool." ) func nodeDeposit(c *cli.Context) error { @@ -31,21 +22,6 @@ func nodeDeposit(c *cli.Context) error { } defer rp.Close() - // Make sure ETH2 is on the correct chain - depositContractInfo, err := rp.DepositContractInfo() - if err != nil { - return err - } - if depositContractInfo.RPNetwork != depositContractInfo.BeaconNetwork || - depositContractInfo.RPDepositContract != depositContractInfo.BeaconDepositContract { - cliutils.PrintDepositMismatchError( - depositContractInfo.RPNetwork, - depositContractInfo.BeaconNetwork, - depositContractInfo.RPDepositContract, - depositContractInfo.BeaconDepositContract) - return nil - } - saturnDeployed, err := rp.IsSaturnDeployed() if err != nil { return err @@ -56,450 +32,6 @@ func nodeDeposit(c *cli.Context) error { return nil } - fmt.Println("Your eth2 client is on the correct network.") - fmt.Println() - - // Get the node's registration status - smoothie, err := rp.NodeGetSmoothingPoolRegistrationStatus() - if err != nil { - return err - } - - if !smoothie.NodeRegistered { - fmt.Println("Your node is not opted into the smoothing pool.") - } else { - fmt.Println("Your node is currently opted into the smoothing pool.") - } - fmt.Println() - - // Post a warning about ETH only minipools - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: We’re excited to announce that newly launched Saturn 0 minipools will feature a commission structure ranging from 5%% to 14%%.\n\n- 5%% base commission\n- 5%% dynamic commission boost until the 5th reward snapshot after Saturn 1\n- Up to 4%% boost for staked RPL valued at ≥10%% of borrowed ETH\n- Smoothing pool participation is required to benefit from dynamic commission\n\nNewly launched minipools with no RPL staked receive 10%% commission while newly launched minipools with ≥10%% of borrowed ETH staked receive 14%% commission.\n\nTo learn more about Saturn 0 and how it affects newly launched minipools, visit: https://rpips.rocketpool.net/tokenomics-explainers/005-rework-prelude%s\nWould you like to continue?", colorYellow, colorReset))) { - fmt.Println("Cancelled.") - return nil - } - - // Post a final warning about the dynamic commission boost - if !smoothie.NodeRegistered { - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sWARNING: Your node is not opted into the smoothing pool, which means newly launched minipools will not benefit from the 5-9%% dynamic commission boost. You can join the smoothing pool using: 'rocketpool node join-smoothing-pool'.\n%sAre you sure you'd like to continue without opting into the smoothing pool?", colorRed, colorReset))) { - fmt.Println("Cancelled.") - 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 minipool.\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 minipool, 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 - } - - // Get deposit amount - var amount float64 - - if c.String("amount") != "" { - // Parse amount - depositAmount, err := strconv.ParseFloat(c.String("amount"), 64) - if err != nil { - return fmt.Errorf("Invalid deposit amount '%s': %w", c.String("amount"), err) - } - amount = depositAmount - } else { - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: You are about to make an 8 ETH deposit.%s\nWould you like to continue?", colorYellow, colorReset))) { - fmt.Println("Cancelled.") - return nil - } - amount = 8 - } - - amountWei := eth.EthToWei(amount) - - // Get network node fees - nodeFees, err := rp.NodeFee() - if err != nil { - return err - } - - // Get minimum node fee - var minNodeFee float64 - if c.String("max-slippage") == "auto" { - - // Use default max slippage - minNodeFee = nodeFees.NodeFee - defaultMaxNodeFeeSlippage - if minNodeFee < nodeFees.MinNodeFee { - minNodeFee = nodeFees.MinNodeFee - } - - } else if c.String("max-slippage") != "" { - - // Parse max slippage - maxNodeFeeSlippagePerc, err := strconv.ParseFloat(c.String("max-slippage"), 64) - if err != nil { - return fmt.Errorf("Invalid maximum commission rate slippage '%s': %w", c.String("max-slippage"), err) - } - maxNodeFeeSlippage := maxNodeFeeSlippagePerc / 100 - - // Calculate min node fee - minNodeFee = nodeFees.NodeFee - maxNodeFeeSlippage - if minNodeFee < nodeFees.MinNodeFee { - minNodeFee = nodeFees.MinNodeFee - } - - } else { - - // Prompt for min node fee - if nodeFees.MinNodeFee == nodeFees.MaxNodeFee { - fmt.Printf("Your minipool will use the current base commission rate of %.2f%%.\n", nodeFees.MinNodeFee*100) - minNodeFee = nodeFees.MinNodeFee - } else { - minNodeFee = promptMinNodeFee(nodeFees.NodeFee, nodeFees.MinNodeFee) - } - - } - - // Get minipool salt - var salt *big.Int - if c.String("salt") != "" { - var success bool - salt, success = big.NewInt(0).SetString(c.String("salt"), 0) - if !success { - return fmt.Errorf("Invalid minipool salt: %s", c.String("salt")) - } - } else { - buffer := make([]byte, 32) - _, err = rand.Read(buffer) - if err != nil { - return fmt.Errorf("Error generating random salt: %w", err) - } - salt = big.NewInt(0).SetBytes(buffer) - } - - // Check deposit can be made - canDeposit, err := rp.CanNodeDeposit(amountWei, minNodeFee, salt, false) - if err != nil { - return err - } - if !canDeposit.CanDeposit { - fmt.Println("Cannot make node deposit:") - 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) - } - 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 minipool with a %.1f ETH bond.", nodeBalance, creditBalance, amount) - } - if canDeposit.InvalidAmount { - fmt.Println("The deposit amount is invalid.") - } - if canDeposit.DepositDisabled { - fmt.Println("Node deposits are currently disabled.") - } - return nil - } - - useCreditBalance := false - 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) - 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 { - fmt.Printf("This deposit will use %.6f ETH from your credit balance plus ETH staked on your behalf and will not require any ETH from your node.\n\n", amount) - } - } else { - fmt.Printf("%sNOTE: Your credit balance *cannot* currently be used to create a new minipool; 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 minipool 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.") - return nil - } - - if c.String("salt") != "" { - fmt.Printf("Using custom salt %s, your minipool address will be %s.\n\n", c.String("salt"), canDeposit.MinipoolAddress.Hex()) - } - - // Check to see if eth2 is synced - colorReset := "\033[0m" - colorRed := "\033[31m" - colorYellow := "\033[33m" - syncResponse, err := rp.NodeSync() - if err != nil { - fmt.Printf("%s**WARNING**: Can't verify the sync status of your consensus client.\nYOU WILL LOSE ETH if your minipool is activated before it is fully synced.\n"+ - "Reason: %s\n%s", colorRed, err, colorReset) - } else { - if syncResponse.BcStatus.PrimaryClientStatus.IsSynced { - fmt.Printf("Your consensus client is synced, you may safely create a minipool.\n") - } else if syncResponse.BcStatus.FallbackEnabled { - if syncResponse.BcStatus.FallbackClientStatus.IsSynced { - fmt.Printf("Your fallback consensus client is synced, you may safely create a minipool.\n") - } else { - fmt.Printf("%s**WARNING**: neither your primary nor fallback consensus clients are fully synced.\nYOU WILL LOSE ETH if your minipool is activated before they are fully synced.\n%s", colorRed, colorReset) - } - } else { - fmt.Printf("%s**WARNING**: your primary consensus client is either not fully synced or offline and you do not have a fallback client configured.\nYOU WILL LOSE ETH if your minipool is activated before it is fully synced.\n%s", colorRed, colorReset) - } - } - - // Assign max fees - err = gas.AssignMaxFeeAndLimit(canDeposit.GasInfo, rp, c.Bool("yes")) - if err != nil { - return err - } - - // Prompt for confirmation - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf( - "You are about to deposit %.6f ETH to create a minipool with a minimum possible commission rate of %f%%.\n"+ - "%sARE YOU SURE YOU WANT TO DO THIS? Exiting this minipool and retrieving your capital cannot be done until your minipool has been *active* on the Beacon Chain for 256 epochs (approx. 27 hours).%s\n", - math.RoundDown(eth.WeiToEth(amountWei), 6), - minNodeFee*100, - colorYellow, - colorReset))) { - fmt.Println("Cancelled.") - return nil - } - - // Make deposit - response, err := rp.NodeDeposit(amountWei, minNodeFee, salt, useCreditBalance, false, true) - if err != nil { - return err - } - - // Log and wait for the minipool address - fmt.Printf("Creating minipool...\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("Your new minipool's address is: %s\n", response.MinipoolAddress) - fmt.Printf("The validator pubkey is: %s\n\n", response.ValidatorPubkey.Hex()) - - fmt.Println("Your minipool is now in Initialized status.") - fmt.Println("Once the remaining ETH has been assigned to your minipool from the staking pool, it will move to Prelaunch status.") - fmt.Printf("After that, it will move to Staking status once %s have passed.\n", response.ScrubPeriod) - fmt.Println("You can watch its progress using `rocketpool service logs node`.") - - return nil - -} - -func nodeMegapoolDeposit(c *cli.Context) error { - - // Get RP client - rp, err := rocketpool.NewClientFromCtx(c).WithReady() - if err != nil { - return err - } - defer rp.Close() - - // Make sure ETH2 is on the correct chain - depositContractInfo, err := rp.DepositContractInfo() - if err != nil { - return err - } - if depositContractInfo.RPNetwork != depositContractInfo.BeaconNetwork || - depositContractInfo.RPDepositContract != depositContractInfo.BeaconDepositContract { - cliutils.PrintDepositMismatchError( - depositContractInfo.RPNetwork, - depositContractInfo.BeaconNetwork, - depositContractInfo.RPDepositContract, - depositContractInfo.BeaconDepositContract) - return nil - } - - fmt.Println("Your eth2 client is on the correct network.") - fmt.Println() - - /* - // 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 - } - */ - // Get deposit amount - var amount float64 - - if c.String("amount") != "" { - // Parse amount - depositAmount, err := strconv.ParseFloat(c.String("amount"), 64) - if err != nil { - return fmt.Errorf("Invalid deposit amount '%s': %w", c.String("amount"), err) - } - amount = depositAmount - } else { - if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("%sNOTE: You are about to make a 4 ETH deposit.%s\nWould you like to continue?", colorYellow, colorReset))) { - fmt.Println("Cancelled.") - return nil - } - amount = 4 - } - - useExpressTicket := false - - expressTicketCount, err := rp.GetExpressTicketCount() - if err != nil { - return err - } - - if c.Bool("use-express-ticket") { - if expressTicketCount.Count > 0 { - useExpressTicket = true - } else { - fmt.Println("You do not have any express tickets available.") - return nil - } - } else { - if expressTicketCount.Count > 0 { - fmt.Printf("You have %d express tickets available.", expressTicketCount.Count) - fmt.Println() - // Prompt for confirmation - if c.Bool("yes") || prompt.Confirm("Would you like to use an express ticket?") { - useExpressTicket = true - } - } - } - - amountWei := eth.EthToWei(amount) - minNodeFee := 0.0 - - // Check deposit can be made - canDeposit, err := rp.CanNodeDeposit(amountWei, minNodeFee, big.NewInt(0), useExpressTicket) - if err != nil { - return err - } - if !canDeposit.CanDeposit { - fmt.Println("Cannot make node deposit:") - 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) - } - 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 canDeposit.InvalidAmount { - fmt.Println("The deposit amount is invalid.") - } - if canDeposit.DepositDisabled { - fmt.Println("Node deposits are currently disabled.") - } - return nil - } - - useCreditBalance := false - 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) - 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 { - fmt.Printf("This deposit will use %.6f ETH from your credit balance plus ETH staked on your behalf and will not require any ETH from your node.\n\n", amount) - } - } else { - 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.") - return nil - } - - // Check to see if eth2 is synced - colorReset := "\033[0m" - colorRed := "\033[31m" - colorYellow := "\033[33m" - syncResponse, err := rp.NodeSync() - if err != nil { - fmt.Printf("%s**WARNING**: Can't verify the sync status of your consensus client.\nYOU WILL LOSE ETH if your megapool validator is activated before it is fully synced.\n"+ - "Reason: %s\n%s", colorRed, err, colorReset) - } else { - if syncResponse.BcStatus.PrimaryClientStatus.IsSynced { - fmt.Printf("Your consensus client is synced, you may safely create a megapool validator.\n") - } else if syncResponse.BcStatus.FallbackEnabled { - if syncResponse.BcStatus.FallbackClientStatus.IsSynced { - fmt.Printf("Your fallback consensus client is synced, you may safely create a megapool validator.\n") - } else { - fmt.Printf("%s**WARNING**: neither your primary nor fallback consensus clients are fully synced.\nYOU WILL LOSE ETH if your megapool validator is activated before they are fully synced.\n%s", colorRed, colorReset) - } - } else { - fmt.Printf("%s**WARNING**: your primary consensus client is either not fully synced or offline and you do not have a fallback client configured.\nYOU WILL LOSE ETH if your megapool validator is activated before it is fully synced.\n%s", colorRed, colorReset) - } - } - - // Assign max fees - err = gas.AssignMaxFeeAndLimit(canDeposit.GasInfo, rp, c.Bool("yes")) - if err != nil { - return err - } - - // 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? You can exit this validator while it waits in the queue using `rocketpool megapool exit-queue`.%s\n", - math.RoundDown(eth.WeiToEth(amountWei), 6), - 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 - } - - // 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()) - - fmt.Println("The new megapool validator is now in Initialized status.") - fmt.Println("Once the remaining ETH has been assigned to your megapool validator from the staking pool, it will move to Prelaunch status.") - fmt.Printf("After that, it will move to Staking status once %s have passed.\n", response.ScrubPeriod) - fmt.Println("You can watch its progress using `rocketpool service logs node`.") - + fmt.Println("The minipool queue is closed in anticipation of Saturn 1 (launching Feb 18, 2026), when users will be able to create Megapools. See details here: https://saturn.rocketpool.net/") return nil - } diff --git a/rocketpool/api/megapool/status.go b/rocketpool/api/megapool/status.go index 9ac32a8f8..fc600357c 100644 --- a/rocketpool/api/megapool/status.go +++ b/rocketpool/api/megapool/status.go @@ -255,5 +255,4 @@ func getValidatorMapAndBalances(c *cli.Context) (*api.MegapoolValidatorMapAndRew // Return response return &response, nil - } diff --git a/rocketpool/api/node/commands.go b/rocketpool/api/node/commands.go index 67eb04aaf..1b9612b08 100644 --- a/rocketpool/api/node/commands.go +++ b/rocketpool/api/node/commands.go @@ -936,14 +936,15 @@ 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 express-tickets count", Action: func(c *cli.Context) error { // Validate args - if err := cliutils.ValidateArgCount(c, 4); err != nil { + 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 @@ -958,14 +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) + expressTickets, err := cliutils.ValidateUint("express-tickets", c.Args().Get(3)) + if err != nil { + return err + } + + count, err := cliutils.ValidateUint("count", c.Args().Get(4)) if err != nil { return err } // Run - api.PrintResponse(canNodeDeposit(c, amountWei, minNodeFee, salt, useExpressTicket)) + api.PrintResponse(canNodeDeposits(c, count, amountWei, minNodeFee, salt, int64(expressTickets))) return nil }, @@ -973,14 +978,15 @@ 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 express-tickets submit count", Action: func(c *cli.Context) error { // Validate args - if err := cliutils.ValidateArgCount(c, 6); err != nil { + 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 @@ -1002,8 +1008,7 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { return err } - useExpressTicketString := c.Args().Get(4) - useExpressTicket, err := cliutils.ValidateBool("use-express-ticket", useExpressTicketString) + expressTickets, err := cliutils.ValidateUint("express-tickets", c.Args().Get(4)) if err != nil { return err } @@ -1012,15 +1017,17 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { return err } + // Check if count is provided + count, err := cliutils.ValidateUint("count", c.Args().Get(6)) if err != nil { return err } // Run - response, err := nodeDeposit(c, amountWei, minNodeFee, salt, useCreditBalance, useExpressTicket, submit) + response, err := nodeDeposits(c, count, amountWei, minNodeFee, salt, useCreditBalance, int64(expressTickets), submit) if submit { api.PrintResponse(response, err) - } // else nodeDeposit already printed the encoded transaction + } // else nodeDeposits already printed the encoded transaction return nil }, diff --git a/rocketpool/api/node/deposit.go b/rocketpool/api/node/deposit.go index 96c38ae67..6f88a14c8 100644 --- a/rocketpool/api/node/deposit.go +++ b/rocketpool/api/node/deposit.go @@ -311,6 +311,236 @@ 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, expressTicketsRequested int64) (*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 creditBalanceWei *big.Int + var expressTicketCount uint64 + + // Check credit balance + wg1.Go(func() error { + creditBalanceWei, err = node.GetNodeUsableCreditAndBalance(rp, nodeAccount.Address, nil) + if err == nil { + response.CreditBalance = creditBalanceWei + } + 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 the express ticket count + wg1.Go(func() error { + var err error + expressTicketCount, err = node.GetExpressTicketCount(rp, nodeAccount.Address, nil) + return err + }) + if saturnDeployed { + // Check whether the node has debt + wg1.Go(func() error { + // Load the megapool contract + + megapoolAddress, err := megapool.GetMegapoolExpectedAddress(rp, nodeAccount.Address, nil) + if err != nil { + return err + } + + // Check whether the megapool is deployed + deployed, err := megapool.GetMegapoolDeployed(rp, nodeAccount.Address, nil) + if err != nil { + return err + } + if !deployed { + return nil + } + + mp, err := megapool.NewMegaPoolV1(rp, megapoolAddress, nil) + if err != nil { + return err + } + hasDebt, err := mp.GetDebt(nil) + if err == nil { + response.NodeHasDebt = hasDebt.Cmp(big.NewInt(0)) > 0 + } + 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.CanUseCredit = creditBalanceWei.Cmp(totalAmountWei) >= 0 + + // Update response + response.CanDeposit = !(response.InsufficientBalance || response.InvalidAmount || response.DepositDisabled || response.NodeHasDebt) + 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 = totalAmountWei + } + + // 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 + deposits := 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 + } + expressTicketsRequested = min(expressTicketsRequested, int64(expressTicketCount)) + for i := uint64(0); i < count; i++ { + + // 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 + deposits = append(deposits, node.NodeDeposit{ + BondAmount: amountWei, + UseExpressTicket: expressTicketsRequested > 0, + ValidatorPubkey: pubKey[:], + ValidatorSignature: signature[:], + DepositDataRoot: depositDataRoot, + }) + + // Store the pubkey in the response + response.ValidatorPubkeys[i] = pubKey + expressTicketsRequested-- + } + + // Ensure count is valid + if count == 0 { + return nil, fmt.Errorf("count must be greater than 0") + } + + gasInfo, err := node.EstimateDepositMultiGas(rp, deposits, 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 @@ -542,6 +772,215 @@ 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, expressTicketsRequested int64, 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 + } + + expressTicketCount, err := node.GetExpressTicketCount(rp, nodeAccount.Address, nil) + if err != nil { + return nil, err + } + + // Get the withdrawal credentials + withdrawalCredentials := services.CalculateMegapoolWithdrawalCredentials(megapoolAddress) + + // Get the node's credit and ETH staked on behalf balance + creditBalanceWei, err := node.GetNodeCreditAndBalance(rp, nodeAccount.Address, nil) + if err != nil { + return nil, err + } + + // 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))) + + // Set the value to the total amount needed// Get how much credit to use + if useCreditBalance { + remainingAmount := big.NewInt(0).Sub(totalAmountWei, creditBalanceWei) + 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 = totalAmountWei + } + + // Create validator keys and deposit data for all deposits + depositAmount := uint64(1e9) // 1 ETH in gwei + deposits := make([]node.NodeDeposit, count) + response.ValidatorPubkeys = make([]rptypes.ValidatorPubkey, count) + + expressTicketsRequested = min(expressTicketsRequested, int64(expressTicketCount)) + 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(), + ) + } + + deposits[i] = node.NodeDeposit{ + BondAmount: amountWei, + UseExpressTicket: expressTicketsRequested > 0, + ValidatorPubkey: pubKey[:], + ValidatorSignature: signature[:], + DepositDataRoot: depositDataRoot, + } + + response.ValidatorPubkeys[i] = pubKey + expressTicketsRequested-- + } + + // 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 + 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..97d4fff0f 100644 --- a/shared/services/rocketpool/node.go +++ b/shared/services/rocketpool/node.go @@ -697,34 +697,34 @@ func (c *Client) NodeWithdrawCredit(amountWei *big.Int) (api.NodeWithdrawCreditR return response, nil } -// Check whether the node can make a deposit -func (c *Client) CanNodeDeposit(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", amountWei.String(), minFee, salt.String(), useExpressTicket)) +// Check whether the node can make multiple deposits +func (c *Client) CanNodeDeposits(count uint64, amountWei *big.Int, minFee float64, salt *big.Int, expressTickets uint64) (api.CanNodeDepositsResponse, error) { + responseBytes, err := c.callAPI(fmt.Sprintf("node can-deposit %s %f %s %d %d", amountWei.String(), minFee, salt.String(), expressTickets, count)) if err != nil { - return api.CanNodeDepositResponse{}, fmt.Errorf("Could not get can node deposit status: %w", err) + return api.CanNodeDepositsResponse{}, fmt.Errorf("Could not get can node deposits status: %w", err) } - var response api.CanNodeDepositResponse + var response api.CanNodeDepositsResponse if err := json.Unmarshal(responseBytes, &response); err != nil { - return api.CanNodeDepositResponse{}, fmt.Errorf("Could not decode can node deposit response: %w", err) + return api.CanNodeDepositsResponse{}, fmt.Errorf("Could not decode can node deposits response: %w", err) } if response.Error != "" { - return api.CanNodeDepositResponse{}, fmt.Errorf("Could not get can node deposit status: %s", response.Error) + return api.CanNodeDepositsResponse{}, 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)) +// Make multiple node deposits +func (c *Client) NodeDeposits(count uint64, amountWei *big.Int, minFee float64, salt *big.Int, useCreditBalance bool, expressTickets uint64, submit bool) (api.NodeDepositsResponse, error) { + responseBytes, err := c.callAPI(fmt.Sprintf("node deposit %s %f %s %t %d %t %d", amountWei.String(), minFee, salt.String(), useCreditBalance, expressTickets, submit, count)) if err != nil { - return api.NodeDepositResponse{}, fmt.Errorf("Could not make node deposit: %w", err) + return api.NodeDepositsResponse{}, fmt.Errorf("Could not make node deposits: %w", err) } - var response api.NodeDepositResponse + var response api.NodeDepositsResponse if err := json.Unmarshal(responseBytes, &response); err != nil { - return api.NodeDepositResponse{}, fmt.Errorf("Could not decode node deposit response: %w", err) + return api.NodeDepositsResponse{}, fmt.Errorf("Could not decode node deposits response: %w", err) } if response.Error != "" { - return api.NodeDepositResponse{}, fmt.Errorf("Could not make node deposit: %s", response.Error) + return api.NodeDepositsResponse{}, fmt.Errorf("Could not make node deposits: %s", response.Error) } return response, nil } diff --git a/shared/types/api/node.go b/shared/types/api/node.go index 343968daa..cd627d763 100644 --- a/shared/types/api/node.go +++ b/shared/types/api/node.go @@ -428,22 +428,23 @@ 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"` - NodeHasDebt bool `json:"nodeHasDebt"` - 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"` + NodeHasDebt bool `json:"nodeHasDebt"` + 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"` @@ -454,6 +455,34 @@ 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"` + NodeHasDebt bool `json:"nodeHasDebt"` + 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"`