cbfb4770fd
Signed-off-by: Jakub Sztandera <kubuxu@protocol.ai>
265 lines
7.2 KiB
Go
265 lines
7.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/Kubuxu/imtui"
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
"github.com/filecoin-project/go-state-types/big"
|
|
"github.com/filecoin-project/lotus/api"
|
|
"github.com/filecoin-project/lotus/build"
|
|
types "github.com/filecoin-project/lotus/chain/types"
|
|
"github.com/gdamore/tcell/v2"
|
|
cid "github.com/ipfs/go-cid"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
func InteractiveSend(ctx context.Context, cctx *cli.Context, srv ServicesAPI,
|
|
proto *api.MessagePrototype) (*types.SignedMessage, error) {
|
|
|
|
msg, checks, err := srv.PublishMessage(ctx, proto, cctx.Bool("force") || cctx.Bool("force-send"))
|
|
printer := cctx.App.Writer
|
|
if xerrors.Is(err, ErrCheckFailed) {
|
|
if !cctx.Bool("interactive") {
|
|
fmt.Fprintf(printer, "Following checks have failed:\n")
|
|
printChecks(printer, checks, proto.Message.Cid())
|
|
} else {
|
|
proto, err = resolveChecks(ctx, srv, cctx.App.Writer, proto, checks)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("from UI: %w", err)
|
|
}
|
|
|
|
msg, _, err = srv.PublishMessage(ctx, proto, true)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("publishing message: %w", err)
|
|
}
|
|
|
|
return msg, nil
|
|
}
|
|
|
|
var interactiveSolves = map[api.CheckStatusCode]bool{
|
|
api.CheckStatusMessageMinBaseFee: true,
|
|
api.CheckStatusMessageBaseFee: true,
|
|
api.CheckStatusMessageBaseFeeLowerBound: true,
|
|
api.CheckStatusMessageBaseFeeUpperBound: true,
|
|
}
|
|
|
|
func baseFeeFromHints(hint map[string]interface{}) big.Int {
|
|
bHint, ok := hint["baseFee"]
|
|
if !ok {
|
|
return big.Zero()
|
|
}
|
|
bHintS, ok := bHint.(string)
|
|
if !ok {
|
|
return big.Zero()
|
|
}
|
|
|
|
var err error
|
|
baseFee, err := big.FromString(bHintS)
|
|
if err != nil {
|
|
return big.Zero()
|
|
}
|
|
return baseFee
|
|
}
|
|
|
|
func resolveChecks(ctx context.Context, s ServicesAPI, printer io.Writer,
|
|
proto *api.MessagePrototype, checkGroups [][]api.MessageCheckStatus,
|
|
) (*api.MessagePrototype, error) {
|
|
|
|
fmt.Fprintf(printer, "Following checks have failed:\n")
|
|
printChecks(printer, checkGroups, proto.Message.Cid())
|
|
|
|
if feeCapBad, baseFee := isFeeCapProblem(checkGroups, proto.Message.Cid()); feeCapBad {
|
|
fmt.Fprintf(printer, "Fee of the message can be adjusted\n")
|
|
if askUser(printer, "Do you wish to do that? [Yes/no]: ", true) {
|
|
var err error
|
|
proto, err = runFeeCapAdjustmentUI(proto, baseFee)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
checks, err := s.RunChecksForPrototype(ctx, proto)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Fprintf(printer, "Following checks still failed:\n")
|
|
printChecks(printer, checks, proto.Message.Cid())
|
|
}
|
|
|
|
if !askUser(printer, "Do you wish to send this message? [yes/No]: ", false) {
|
|
return nil, ErrAbortedByUser
|
|
}
|
|
return proto, nil
|
|
}
|
|
|
|
var ErrAbortedByUser = errors.New("aborted by user")
|
|
|
|
func printChecks(printer io.Writer, checkGroups [][]api.MessageCheckStatus, protoCid cid.Cid) {
|
|
for _, checks := range checkGroups {
|
|
for _, c := range checks {
|
|
if c.OK {
|
|
continue
|
|
}
|
|
aboutProto := c.Cid.Equals(protoCid)
|
|
msgName := "current"
|
|
if !aboutProto {
|
|
msgName = c.Cid.String()
|
|
}
|
|
fmt.Fprintf(printer, "%s message failed a check %s: %s\n", msgName, c.Code, c.Err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func askUser(printer io.Writer, q string, def bool) bool {
|
|
var resp string
|
|
fmt.Fprint(printer, q)
|
|
fmt.Scanln(&resp)
|
|
resp = strings.ToLower(resp)
|
|
if len(resp) == 0 {
|
|
return def
|
|
}
|
|
return resp[0] == 'y'
|
|
}
|
|
|
|
func isFeeCapProblem(checkGroups [][]api.MessageCheckStatus, protoCid cid.Cid) (bool, big.Int) {
|
|
baseFee := big.Zero()
|
|
yes := false
|
|
for _, checks := range checkGroups {
|
|
for _, c := range checks {
|
|
if c.OK {
|
|
continue
|
|
}
|
|
aboutProto := c.Cid.Equals(protoCid)
|
|
if aboutProto && interactiveSolves[c.Code] {
|
|
yes = true
|
|
if baseFee.IsZero() {
|
|
baseFee = baseFeeFromHints(c.Hint)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if baseFee.IsZero() {
|
|
// this will only be the case if failing check is: MessageMinBaseFee
|
|
baseFee = big.NewInt(build.MinimumBaseFee)
|
|
}
|
|
|
|
return yes, baseFee
|
|
}
|
|
|
|
func runFeeCapAdjustmentUI(proto *api.MessagePrototype, baseFee abi.TokenAmount) (*api.MessagePrototype, error) {
|
|
t, err := imtui.NewTui()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
maxFee := big.Mul(proto.Message.GasFeeCap, big.NewInt(proto.Message.GasLimit))
|
|
send := false
|
|
t.PushScene(feeUI(baseFee, proto.Message.GasLimit, &maxFee, &send))
|
|
|
|
err = t.Run()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !send {
|
|
return nil, fmt.Errorf("aborted by user")
|
|
}
|
|
|
|
proto.Message.GasFeeCap = big.Div(maxFee, big.NewInt(proto.Message.GasLimit))
|
|
|
|
return proto, nil
|
|
}
|
|
|
|
func feeUI(baseFee abi.TokenAmount, gasLimit int64, maxFee *abi.TokenAmount, send *bool) func(*imtui.Tui) error {
|
|
orignalMaxFee := *maxFee
|
|
required := big.Mul(baseFee, big.NewInt(gasLimit))
|
|
safe := big.Mul(required, big.NewInt(10))
|
|
|
|
price := fmt.Sprintf("%s", types.FIL(*maxFee).Unitless())
|
|
|
|
return func(t *imtui.Tui) error {
|
|
if t.CurrentKey != nil {
|
|
if t.CurrentKey.Key() == tcell.KeyRune {
|
|
pF, err := types.ParseFIL(price)
|
|
switch t.CurrentKey.Rune() {
|
|
case 's', 'S':
|
|
price = types.FIL(safe).Unitless()
|
|
case '+':
|
|
if err == nil {
|
|
p := big.Mul(big.Int(pF), types.NewInt(11))
|
|
p = big.Div(p, types.NewInt(10))
|
|
price = fmt.Sprintf("%s", types.FIL(p).Unitless())
|
|
}
|
|
case '-':
|
|
if err == nil {
|
|
p := big.Mul(big.Int(pF), types.NewInt(10))
|
|
p = big.Div(p, types.NewInt(11))
|
|
price = fmt.Sprintf("%s", types.FIL(p).Unitless())
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
|
|
if t.CurrentKey.Key() == tcell.KeyEnter {
|
|
*send = true
|
|
t.PopScene()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
defS := tcell.StyleDefault
|
|
|
|
row := 0
|
|
t.Label(0, row, "Fee of the message is too low.", defS)
|
|
row++
|
|
|
|
t.Label(0, row, fmt.Sprintf("Your configured maximum fee is: %s FIL",
|
|
types.FIL(orignalMaxFee).Unitless()), defS)
|
|
row++
|
|
t.Label(0, row, fmt.Sprintf("Required maximum fee for the message: %s FIL",
|
|
types.FIL(required).Unitless()), defS)
|
|
row++
|
|
w := t.Label(0, row, fmt.Sprintf("Safe maximum fee for the message: %s FIL",
|
|
types.FIL(safe).Unitless()), defS)
|
|
t.Label(w, row, " Press S to use it", defS)
|
|
row++
|
|
|
|
w = t.Label(0, row, "Current Maximum Fee: ", defS)
|
|
|
|
w += t.EditFieldFiltered(w, row, 14, &price, imtui.FilterDecimal, defS.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack))
|
|
|
|
w += t.Label(w, row, " FIL", defS)
|
|
|
|
pF, err := types.ParseFIL(price)
|
|
*maxFee = abi.TokenAmount(pF)
|
|
if err != nil {
|
|
w += t.Label(w, row, " invalid price", defS.Foreground(tcell.ColorMaroon).Bold(true))
|
|
} else if maxFee.GreaterThanEqual(safe) {
|
|
w += t.Label(w, row, " SAFE", defS.Foreground(tcell.ColorDarkGreen).Bold(true))
|
|
} else if maxFee.GreaterThanEqual(required) {
|
|
w += t.Label(w, row, " low", defS.Foreground(tcell.ColorYellow).Bold(true))
|
|
over := big.Div(big.Mul(*maxFee, big.NewInt(100)), required)
|
|
w += t.Label(w, row,
|
|
fmt.Sprintf(" %.1fx over the minimum", float64(over.Int64())/100.0), defS)
|
|
} else {
|
|
w += t.Label(w, row, " too low", defS.Foreground(tcell.ColorRed).Bold(true))
|
|
}
|
|
row += 2
|
|
|
|
t.Label(0, row, fmt.Sprintf("Current Base Fee is: %s", types.FIL(baseFee).Nano()), defS)
|
|
row++
|
|
t.Label(0, row, fmt.Sprintf("Resulting FeeCap is: %s",
|
|
types.FIL(big.Div(*maxFee, big.NewInt(gasLimit))).Nano()), defS)
|
|
row++
|
|
t.Label(0, row, "You can use '+' and '-' to adjust the fee.", defS)
|
|
|
|
return nil
|
|
}
|
|
}
|