From 75429d3a5d8126e2a23cc44c5e73839eff02127f Mon Sep 17 00:00:00 2001 From: serfersac Date: Wed, 29 Apr 2026 14:41:40 +0000 Subject: [PATCH] feat: add transaction cost estimation and confirmation to cli wizards --- cmd/livepeer_cli/wizard.go | 21 +++++++++++++++++++ cmd/livepeer_cli/wizard_bond.go | 30 +++++++++++++++++++++++++++ cmd/livepeer_cli/wizard_rounds.go | 8 +++++++ cmd/livepeer_cli/wizard_token.go | 8 +++++++ cmd/livepeer_cli/wizard_transcoder.go | 18 ++++++++++++++++ 5 files changed, 85 insertions(+) diff --git a/cmd/livepeer_cli/wizard.go b/cmd/livepeer_cli/wizard.go index c033deef8f..f965e4bd18 100644 --- a/cmd/livepeer_cli/wizard.go +++ b/cmd/livepeer_cli/wizard.go @@ -305,6 +305,27 @@ func httpPostWithParamsHeaders(url string, val url.Values, headers map[string]st return "", false } +func (w *wizard) confirm(prompt string) bool { + fmt.Printf("%s [Y/n] ", prompt) + text := w.read() + if text != "Y" { + fmt.Println("Aborting.") + return false + } + return true +} + +func (w *wizard) printGasInfo(gasLimit *big.Int, gasPrice *big.Int) { + var cost *big.Float + if gasPrice != nil { + cost = new(big.Float).SetInt(new(big.Int).Mul(gasLimit, gasPrice)) + } else { + cost = new(big.Float).SetInt(new(big.Int).Mul(gasLimit, w.eth.GasPrice())) + } + fmt.Printf("Estimated TX cost: %v ETH\n", eth.ToETH(cost, big.NewInt(1))) +} + + defer resp.Body.Close() result, err := ioutil.ReadAll(resp.Body) if err != nil { diff --git a/cmd/livepeer_cli/wizard_bond.go b/cmd/livepeer_cli/wizard_bond.go index c9ecd6b381..8b73a20ec6 100644 --- a/cmd/livepeer_cli/wizard_bond.go +++ b/cmd/livepeer_cli/wizard_bond.go @@ -221,6 +221,12 @@ func (w *wizard) rebond() { } if dInfo.Status == "Unbonded" { + w.printGasInfo(big.NewInt(eth.BondGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to bond %v LPT to \"%s\"?", eth.FormatUnits(amount, "lpt"), tAddr.Hex())) { + return + } + + fmt.Printf("You are unbonded - you will need to choose an address to rebond to.\n") var toAddr common.Address @@ -260,6 +266,12 @@ func (w *wizard) unbond() { fmt.Printf("Current Bonded Amount: %v\n", eth.FormatUnits(dInfo.BondedAmount, "LPT")) fmt.Printf("Current Delegate: %v\n", dInfo.DelegateAddress.Hex()) + w.printGasInfo(big.NewInt(eth.RebondGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to rebond from unbonding lock %v?", unbondingLockID)) { + return + } + + fmt.Printf("Would you like to fully unbond? (y/n) - ") @@ -298,6 +310,12 @@ func (w *wizard) withdrawStake() { if err != nil { glog.Errorf("Error getting delegator info: %v", err) return + w.printGasInfo(big.NewInt(eth.UnbondGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to unbond %v LPT?", eth.FormatUnits(amount, "lpt"))) { + return + } + + } fmt.Printf("Current Bonded Amount: %v\n", eth.FormatUnits(dInfo.BondedAmount, "LPT")) @@ -324,6 +342,12 @@ func (w *wizard) withdrawStake() { } val := url.Values{ + w.printGasInfo(big.NewInt(eth.WithdrawStakeGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to withdraw stake from unbonding lock %v?", unbondingLockID)) { + return + } + + "unbondingLockId": {fmt.Sprintf("%v", strconv.FormatInt(unbondingLockID, 10))}, } @@ -336,6 +360,12 @@ func (w *wizard) withdrawFees() { glog.Errorf("Error getting delegator info: %v", err) return } + w.printGasInfo(big.NewInt(eth.WithdrawFeesGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to withdraw %v ETH in fees?", eth.FormatUnits(dInfo.PendingFees, "eth"))) { + return + } + + val := url.Values{ "amount": {fmt.Sprintf("%v", dInfo.PendingFees.String())}, diff --git a/cmd/livepeer_cli/wizard_rounds.go b/cmd/livepeer_cli/wizard_rounds.go index e4923c52d7..686fb8bed5 100644 --- a/cmd/livepeer_cli/wizard_rounds.go +++ b/cmd/livepeer_cli/wizard_rounds.go @@ -5,6 +5,8 @@ import ( "io/ioutil" "math/big" "net/http" + + "github.com/livepeer/go-livepeer/eth" ) func (w *wizard) currentRound() (*big.Int, error) { @@ -30,6 +32,12 @@ func (w *wizard) currentRound() (*big.Int, error) { return cr, nil } + w.printGasInfo(big.NewInt(eth.InitializeRoundGas), nil) + if !w.confirm("Are you sure you want to initialize the round?") { + return + } + + func (w *wizard) initializeRound() { httpPost(fmt.Sprintf("http://%v:%v/initializeRound", w.host, w.httpPort)) diff --git a/cmd/livepeer_cli/wizard_token.go b/cmd/livepeer_cli/wizard_token.go index 5da482c025..41136e13ec 100644 --- a/cmd/livepeer_cli/wizard_token.go +++ b/cmd/livepeer_cli/wizard_token.go @@ -2,8 +2,11 @@ package main import ( "fmt" + "math/big" "net/url" "strings" + + "github.com/livepeer/go-livepeer/eth" ) func (w *wizard) transferTokens() { @@ -14,6 +17,11 @@ func (w *wizard) transferTokens() { amount := w.readBigInt("Enter amount") + w.printGasInfo(big.NewInt(eth.TransferTokensGas), nil) + if !w.confirm(fmt.Sprintf("Are you sure you want to send %v LPTU to \"%s\"?", eth.FormatUnits(amount, "lpt"), to)) { + return + } + val := url.Values{ "to": {fmt.Sprintf("%v", to)}, "amount": {fmt.Sprintf("%v", amount.String())}, diff --git a/cmd/livepeer_cli/wizard_transcoder.go b/cmd/livepeer_cli/wizard_transcoder.go index 1c5f731ced..6e2a13bf67 100644 --- a/cmd/livepeer_cli/wizard_transcoder.go +++ b/cmd/livepeer_cli/wizard_transcoder.go @@ -205,6 +205,12 @@ func (w *wizard) getOrchestratorConfigFormValues() url.Values { "currency": {fmt.Sprintf("%v", currency)}, "pixelsPerUnit": {fmt.Sprintf("%v", pixelsPerUnit)}, "serviceURI": {fmt.Sprintf("%v", serviceURI)}, + w.printGasInfo(big.NewInt(eth.BondGas), nil) + if !w.confirm("Are you sure you want to activate an orchestrator?") { + return + } + + } } @@ -221,6 +227,12 @@ func (w *wizard) callReward() { if c.Cmp(t.LastRewardRound) == 0 { fmt.Printf("Reward for current round %v already called\n", c) + w.printGasInfo(big.NewInt(eth.SetOrchestratorConfigGas), nil) + if !w.confirm("Are you sure you want to set the orchestrator config?") { + return + } + + return } @@ -241,6 +253,12 @@ func (w *wizard) vote() { } return in, nil }) + w.printGasInfo(big.NewInt(eth.RewardGas), nil) + if !w.confirm("Are you sure you want to call reward?") { + return + } + + var ( confirm = "n"