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 }