From 3df48d72887cc69f8c67be061fc1c0675e5f7a0a Mon Sep 17 00:00:00 2001 From: Fornax <23104993+fornax2@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:09:19 -0300 Subject: [PATCH] Add option to send all tokens --- rocketpool-cli/node/commands.go | 18 ++++-- rocketpool-cli/node/send.go | 111 +++++++++++++++++++++++++++++++- rocketpool/api/node/commands.go | 2 +- shared/services/gas/gas.go | 9 +++ 4 files changed, 133 insertions(+), 7 deletions(-) diff --git a/rocketpool-cli/node/commands.go b/rocketpool-cli/node/commands.go index dfa8cf9fc..d57f4dab1 100644 --- a/rocketpool-cli/node/commands.go +++ b/rocketpool-cli/node/commands.go @@ -661,7 +661,7 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { { Name: "send", Aliases: []string{"n"}, - Usage: "Send ETH or tokens from the node account to an address. ENS names supported. can be 'rpl', 'eth', 'fsrpl' (for the old RPL v1 token), 'reth', or the address of an arbitrary token you want to send (including the 0x prefix).", + Usage: "Send ETH or tokens from the node account to an address. ENS names supported. Use 'all' as the amount to send the entire balance. can be 'rpl', 'eth', 'fsrpl' (for the old RPL v1 token), 'reth', or the address of an arbitrary token you want to send (including the 0x prefix).", UsageText: "rocketpool node send [options] amount token to", Flags: []cli.Flag{ cli.BoolFlag{ @@ -675,17 +675,25 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { if err := cliutils.ValidateArgCount(c, 3); err != nil { return err } - amount, err := cliutils.ValidatePositiveEthAmount("send amount", c.Args().Get(0)) - if err != nil { - return err + + amountStr := c.Args().Get(0) + sendAll := strings.EqualFold(amountStr, "all") + var amount float64 + if !sendAll { + var err error + amount, err = cliutils.ValidatePositiveEthAmount("send amount", amountStr) + if err != nil { + return err + } } + token, err := cliutils.ValidateTokenType("token type", c.Args().Get(1)) if err != nil { return err } // Run - return nodeSend(c, amount, token, c.Args().Get(2)) + return nodeSend(c, amount, sendAll, token, c.Args().Get(2)) }, }, diff --git a/rocketpool-cli/node/send.go b/rocketpool-cli/node/send.go index 6bbb849b2..a4dd66cd1 100644 --- a/rocketpool-cli/node/send.go +++ b/rocketpool-cli/node/send.go @@ -13,7 +13,7 @@ import ( "github.com/rocket-pool/smartnode/shared/utils/cli/prompt" ) -func nodeSend(c *cli.Context, amountRaw float64, token string, toAddressOrENS string) error { +func nodeSend(c *cli.Context, amountRaw float64, sendAll bool, token string, toAddressOrENS string) error { // Get RP client rp, err := rocketpool.NewClientFromCtx(c).WithReady() @@ -40,6 +40,11 @@ func nodeSend(c *cli.Context, amountRaw float64, token string, toAddressOrENS st toAddressString = toAddress.Hex() } + // Handle "send all" mode + if sendAll { + return nodeSendAll(c, rp, token, toAddress, toAddressString) + } + // Check tokens can be sent canSend, err := rp.CanNodeSend(amountRaw, token, toAddress) if err != nil { @@ -110,3 +115,107 @@ func nodeSend(c *cli.Context, amountRaw float64, token string, toAddressOrENS st return nil } + +// nodeSendAll sends the entire balance of the specified token to the recipient. +// For ETH, it reserves enough to cover the estimated maximum gas cost. +func nodeSendAll(c *cli.Context, rp *rocketpool.Client, token string, toAddress common.Address, toAddressString string) error { + + // Query balance and gas info using a zero amount + canSend, err := rp.CanNodeSend(0, token, toAddress) + if err != nil { + return err + } + + if canSend.Balance <= 0 { + if strings.HasPrefix(token, "0x") { + fmt.Printf("The node's balance of %s (%s) is zero, nothing to send.\n", canSend.TokenSymbol, token) + } else { + fmt.Printf("The node's %s balance is zero, nothing to send.\n", token) + } + return nil + } + + tokenString := fmt.Sprintf("%s (%s)", canSend.TokenSymbol, token) + amountRaw := canSend.Balance + + if strings.EqualFold(token, "eth") { + fmt.Printf("Node balance: %.8f ETH\n", canSend.Balance) + fmt.Printf("For sending all ETH, we need to estimate the gas costs first.\n") + // For ETH, determine gas settings first so we can subtract the gas cost from the balance. + // This may prompt the user to select a gas price. + g, err := gas.GetMaxFeeAndLimit(canSend.GasInfo, rp, c.Bool("yes")) + if err != nil { + return err + } + + gasCost := g.GetMaxGasCostEth(canSend.GasInfo) + amountRaw = canSend.Balance - gasCost + + if amountRaw <= 0 { + fmt.Printf("The node's ETH balance (%.8f ETH) is not enough to cover the gas cost (%.8f ETH).\n", canSend.Balance, gasCost) + return nil + } + + fmt.Printf("Node balance: %.8f ETH\n", canSend.Balance) + fmt.Printf("Gas reserve: %.8f ETH\n", gasCost) + fmt.Printf("Send amount: %.8f ETH\n\n", amountRaw) + + if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("Are you sure you want to send %.8f ETH to %s? This action cannot be undone!", amountRaw, toAddressString))) { + fmt.Println("Cancelled.") + return nil + } + + // Assign the gas settings + g.Assign(rp) + } else { + // For non-ETH tokens, confirm first, then assign gas settings + if strings.HasPrefix(token, "0x") { + fmt.Printf("Token address: %s\n", token) + fmt.Printf("Token name: %s\n", canSend.TokenName) + fmt.Printf("Token symbol: %s\n", canSend.TokenSymbol) + fmt.Printf("Node balance: %.8f %s\n\n", canSend.Balance, canSend.TokenSymbol) + fmt.Printf("%sWARNING: Please confirm that the above token is the one you intend to send before confirming below!%s\n\n", colorYellow, colorReset) + + if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("Are you sure you want to send all %.8f of %s to %s? This action cannot be undone!", amountRaw, tokenString, toAddressString))) { + fmt.Println("Cancelled.") + return nil + } + } else { + fmt.Printf("Node balance: %.8f %s\n\n", canSend.Balance, token) + if !(c.Bool("yes") || prompt.Confirm(fmt.Sprintf("Are you sure you want to send all %.8f %s to %s? This action cannot be undone!", amountRaw, token, toAddressString))) { + fmt.Println("Cancelled.") + return nil + } + } + + // Assign max fees + err = gas.AssignMaxFeeAndLimit(canSend.GasInfo, rp, c.Bool("yes")) + if err != nil { + return err + } + } + + // Send tokens + response, err := rp.NodeSend(amountRaw, token, toAddress) + if err != nil { + return err + } + + if strings.HasPrefix(token, "0x") { + fmt.Printf("Sending %.8f of %s to %s...\n", amountRaw, tokenString, toAddressString) + } else { + fmt.Printf("Sending %.8f %s to %s...\n", amountRaw, token, toAddressString) + } + cliutils.PrintTransactionHash(rp, response.TxHash) + if _, err = rp.WaitForTransaction(response.TxHash); err != nil { + return err + } + + // Log & return + if strings.HasPrefix(token, "0x") { + fmt.Printf("Successfully sent %.6f of %s to %s.\n", amountRaw, tokenString, toAddressString) + } else { + fmt.Printf("Successfully sent %.6f %s to %s.\n", amountRaw, token, toAddressString) + } + return nil +} diff --git a/rocketpool/api/node/commands.go b/rocketpool/api/node/commands.go index 1b59cd751..ab3e6bdbb 100644 --- a/rocketpool/api/node/commands.go +++ b/rocketpool/api/node/commands.go @@ -1042,7 +1042,7 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { if err := cliutils.ValidateArgCount(c, 3); err != nil { return err } - amountRaw, err := cliutils.ValidatePositiveEthAmount("send amount", c.Args().Get(0)) + amountRaw, err := cliutils.ValidateEthAmount("send amount", c.Args().Get(0)) if err != nil { return err } diff --git a/shared/services/gas/gas.go b/shared/services/gas/gas.go index ce84ebbb0..c2ac6bf9d 100644 --- a/shared/services/gas/gas.go +++ b/shared/services/gas/gas.go @@ -42,6 +42,15 @@ func (g *Gas) Assign(rp *rpsvc.Client) { return } +// GetMaxGasCostEth returns the maximum possible gas cost in ETH for the given gas info, +func (g *Gas) GetMaxGasCostEth(gasInfo rocketpool.GasInfo) float64 { + limit := uint64(float64(gasInfo.EstGasLimit) * 1.1) + if g.gasLimit != 0 { + limit = g.gasLimit + } + return g.maxFeeGwei / eth.WeiPerGwei * float64(limit) +} + func GetMaxFeeAndLimit(gasInfo rocketpool.GasInfo, rp *rpsvc.Client, headless bool) (Gas, error) { cfg, isNew, err := rp.LoadConfig()