Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions rocketpool-cli/node/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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. <token> 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. <token> 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{
Expand All @@ -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))

},
},
Expand Down
111 changes: 110 additions & 1 deletion rocketpool-cli/node/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion rocketpool/api/node/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 9 additions & 0 deletions shared/services/gas/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading