github.com/hungdoo/bot@v0.0.0-20240325145135-dd1f386f7b81/src/packages/command/tomb/command.go (about)

     1  package tomb
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math/big"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    11  	ethCommon "github.com/ethereum/go-ethereum/common"
    12  	"github.com/ethereum/go-ethereum/params"
    13  	"github.com/hungdoo/bot/src/common"
    14  	command "github.com/hungdoo/bot/src/packages/command/common"
    15  	"github.com/hungdoo/bot/src/packages/tombplus"
    16  )
    17  
    18  type TombCommand struct {
    19  	command.Command    `json:"command" bson:"command"`
    20  	Id                 string    `json:"-" bson:"_id,unique"`
    21  	Rpc                string    `json:"rpc" bson:"rpc"`
    22  	Contract           string    `json:"contract" bson:"contract"`
    23  	Up                 bool      `json:"up" bson:"up"`
    24  	PkIdx              int64     `json:"pkIdx" bson:"pkIdx"`
    25  	Key                string    `json:"key" bson:"key"`
    26  	SentTx             string    `json:"sent_tx" bson:"sent_tx"`
    27  	CurrentEpoch       int64     `json:"currentEpoch" bson:"currentEpoch"`
    28  	User               string    `json:"user"`
    29  	VoteEndTimestamp   time.Time `json:"voteEndTimestamp" bson:"voteEndTimestamp"`
    30  	NextEpochTimestamp time.Time `json:"nextEpochTimestamp" bson:"nextEpochTimestamp"`
    31  	MaxGas             int64     `json:"maxGas" bson:"maxGas"`
    32  }
    33  
    34  func (c TombCommand) MarshalJSON() ([]byte, error) {
    35  	return json.Marshal(&struct {
    36  		Name string `json:"name"`
    37  		Type string `json:"type"`
    38  		// Data     []string `json:"data"`
    39  		IdleTime           string `json:"idletime"`
    40  		Rpc                string `json:"rpc"`
    41  		Contract           string `json:"contract"`
    42  		Up                 bool   `json:"up"`
    43  		PkIdx              int64  `json:"pkIdx"`
    44  		Key                string `json:"key" bson:"key"`
    45  		SentTx             string `json:"sent_tx" bson:"sent_tx"`
    46  		VoteEndTimestamp   string `json:"voteEndTimestamp"`
    47  		NextEpochTimestamp string `json:"nextEpochTimestamp"`
    48  		User               string `json:"user"`
    49  		Command            string `json:"command"`
    50  	}{
    51  		Name: c.Name,
    52  		Type: c.Type.String(),
    53  		// Data:     c.Data,
    54  		IdleTime:           c.IdleTime.String(),
    55  		Rpc:                c.Rpc,
    56  		Contract:           c.Contract,
    57  		Up:                 c.Up,
    58  		PkIdx:              c.PkIdx,
    59  		Key:                c.Key,
    60  		SentTx:             c.SentTx,
    61  		User:               c.User,
    62  		VoteEndTimestamp:   c.VoteEndTimestamp.String(),
    63  		NextEpochTimestamp: c.NextEpochTimestamp.String(),
    64  		Command:            fmt.Sprintf("add tomb %s %s %s %v %v %v %v", c.Name, c.Rpc, c.Contract, c.Up, c.PkIdx, c.Key, c.MaxGas),
    65  	})
    66  }
    67  
    68  func (c *TombCommand) Validate(data []string) error { // rpc, contract, up, pkIdx, k
    69  	if len(data) < 6 {
    70  		return fmt.Errorf("invalid params: rpc, contract, up, pkIdx, k, maxGas[Gwei]")
    71  	}
    72  	return nil
    73  }
    74  
    75  func (c *TombCommand) SetData(newValue []string) (err error) {
    76  	if err = c.Validate(newValue); err != nil {
    77  		return err
    78  	}
    79  	// c.Data = newValue // TODO: refractor to store into DB dynamically
    80  	c.Rpc = newValue[0]
    81  	c.Contract = newValue[1]
    82  	c.Up, err = strconv.ParseBool(newValue[2])
    83  	if err != nil {
    84  		return err
    85  	}
    86  	c.PkIdx, err = strconv.ParseInt(newValue[3], 10, 64)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	c.Key = newValue[4]
    91  
    92  	pk, err := LoadSecrets(int(c.PkIdx), c.Key)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	user, err := tombplus.AddressFromPriKey(pk)
    97  	if err != nil {
    98  		return err
    99  	}
   100  	c.User = user.String()
   101  
   102  	newMaxGas, err := strconv.ParseInt(newValue[5], 10, 64)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	c.MaxGas = newMaxGas
   107  	return nil
   108  }
   109  
   110  func (c *TombCommand) Execute(mustReport bool, subcommand string) (string, *common.ErrorWithSeverity) {
   111  	contractAddress := ethCommon.HexToAddress(c.Contract)
   112  	cli, err := tombplus.GetClient(c.Rpc, contractAddress)
   113  	if err != nil {
   114  		return "", common.NewErrorWithSeverity(common.Error, err.Error())
   115  	}
   116  
   117  	switch subcommand {
   118  	case "stats":
   119  		if len(c.User) == 0 {
   120  			return "", common.NewErrorWithSeverity(common.Error, "cannot get stats of empty user address")
   121  		}
   122  		rewards := cli.GetRewards(ethCommon.HexToAddress(c.User))
   123  
   124  		return fmt.Sprintf("rewards: %v", rewards), nil
   125  
   126  	case "clear":
   127  		c.SentTx = ""
   128  		return "", nil
   129  
   130  	case "claim":
   131  		pk, err := LoadSecrets(int(c.PkIdx), c.Key)
   132  		if err != nil {
   133  			return "", common.NewErrorWithSeverity(common.Error, err.Error())
   134  		}
   135  		res, err2 := cli.Claim(pk, nil) // nil: use oracle gasPrice
   136  		if err2 != nil {
   137  			return "", err2
   138  		}
   139  		// c.SentTx = res.Hash().String() // no need to record claim tx
   140  		return res.Hash().String(), nil
   141  
   142  	default:
   143  		if len(c.SentTx) != 0 {
   144  			toCheck := c.SentTx
   145  			// Has pending tx
   146  			if err := cli.CheckResult(toCheck); err != nil {
   147  				return "", err
   148  			}
   149  			// tx successful, clear sent tx hash
   150  			c.SentTx = ""
   151  			c.VoteEndTimestamp = time.Time{}
   152  
   153  			return fmt.Sprintf("tx[%s] successful", toCheck), nil
   154  		}
   155  
   156  		if c.VoteEndTimestamp.IsZero() && c.NextEpochTimestamp.IsZero() { // new epoch setup
   157  			c.CurrentEpoch = cli.CurrentEpoch()
   158  			if len(c.User) == 0 {
   159  				return "", common.NewErrorWithSeverity(common.Critical, "cannot get stats of empty user address")
   160  			}
   161  			lastVotedEpoch, err := cli.GetUserLastedVoteEpochId(ethCommon.HexToAddress(c.User))
   162  			if err != nil {
   163  				return "", common.NewErrorWithSeverity(common.Error, err.Error())
   164  			}
   165  
   166  			if c.CurrentEpoch > 0 && c.CurrentEpoch > lastVotedEpoch.Int64() {
   167  				timestamps, err := cli.Tomb.GetEpochObservationTimestamps(&bind.CallOpts{}, big.NewInt(c.CurrentEpoch))
   168  				if err != nil {
   169  					return "", common.NewErrorWithSeverity(common.Info, err.Error())
   170  				}
   171  
   172  				nextVoteEndTimestamp := time.Unix(timestamps.ObservationStartTimestamp.Int64(), 0)
   173  				nextEpochTimestamp := time.Unix(timestamps.ObservationEndTimestamp.Int64(), 0)
   174  				c.NextEpochTimestamp = nextEpochTimestamp
   175  				c.VoteEndTimestamp = nextVoteEndTimestamp
   176  				return fmt.Sprintf("vote end timestamp set to %s", c.VoteEndTimestamp.String()), nil
   177  			}
   178  			if mustReport {
   179  				return fmt.Sprintf("already voted currentEpoch[%v]/last[%v]", c.CurrentEpoch, lastVotedEpoch), nil
   180  			}
   181  			return "", nil
   182  		} else if c.VoteEndTimestamp.After(time.Now()) && time.Until(c.VoteEndTimestamp) < 1*time.Hour {
   183  			// determine vote side up/down
   184  			data, err := cli.Tomb.UpcomingEpochData(&bind.CallOpts{}, big.NewInt(c.CurrentEpoch))
   185  			if err != nil {
   186  				return "", common.NewErrorWithSeverity(common.Info, err.Error())
   187  			}
   188  			var up bool
   189  			cmp := data.TshareVotesUp.Cmp(data.TshareVotesDown)
   190  			if cmp >= 1 {
   191  				up = true
   192  			} else if cmp <= -1 {
   193  				up = false
   194  			} else {
   195  				up = c.Up
   196  			}
   197  
   198  			pk, err := LoadSecrets(int(c.PkIdx), c.Key)
   199  			if err != nil {
   200  				return "", common.NewErrorWithSeverity(common.Error, err.Error())
   201  			}
   202  			res, errWithSeverity := cli.Flip(pk, new(big.Int).Mul(big.NewInt(c.MaxGas), big.NewInt(params.GWei)), up)
   203  			if errWithSeverity != nil {
   204  				return "", errWithSeverity
   205  			}
   206  			c.SentTx = res.Hash().String()
   207  
   208  			return fmt.Sprintf("tx[%s] sent. Voted Up:%v. Data: %+v", c.SentTx, up, data), nil
   209  		} else if c.VoteEndTimestamp.Before(time.Now()) && c.NextEpochTimestamp.After(time.Now()) {
   210  			if mustReport {
   211  				return "vote ended", nil
   212  			}
   213  			return "", nil
   214  		} else if c.NextEpochTimestamp.Before(time.Now()) {
   215  			c.VoteEndTimestamp = time.Time{}
   216  			c.NextEpochTimestamp = time.Time{}
   217  			return "new epoch started", nil
   218  		} else {
   219  			if mustReport {
   220  				return fmt.Sprintf("too early till vote end. %vm left", time.Until(c.VoteEndTimestamp).Minutes()), nil
   221  			}
   222  			return "", nil
   223  		}
   224  	}
   225  }