github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/modules/blockchain/contract.go (about) 1 package blockchain 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "math/big" 8 "sort" 9 "time" 10 11 "github.com/ethereum/go-ethereum" 12 "github.com/ethereum/go-ethereum/common" 13 ethtypes "github.com/ethereum/go-ethereum/core/types" 14 "github.com/pkg/errors" 15 16 "github.com/machinefi/w3bstream/pkg/depends/conf/logger" 17 "github.com/machinefi/w3bstream/pkg/depends/kit/logr" 18 "github.com/machinefi/w3bstream/pkg/depends/kit/sqlx" 19 "github.com/machinefi/w3bstream/pkg/depends/kit/sqlx/builder" 20 "github.com/machinefi/w3bstream/pkg/depends/kit/sqlx/datatypes" 21 "github.com/machinefi/w3bstream/pkg/models" 22 "github.com/machinefi/w3bstream/pkg/types" 23 ) 24 25 type contract struct { 26 *monitor 27 listInterval time.Duration 28 blockInterval uint64 29 } 30 31 // a group of models.ContractLog, will list the chain and update current height togither 32 type listChainGroup struct { 33 toBlock uint64 34 cs []*models.ContractLog 35 } 36 37 func (t *contract) run(ctx context.Context) { 38 ticker := time.NewTicker(t.listInterval) 39 defer ticker.Stop() 40 41 for range ticker.C { 42 t.do(ctx) 43 } 44 } 45 46 func (t *contract) do(ctx context.Context) { 47 ctx, l := logger.NewSpanContext(ctx, "bc.contract.do") 48 defer l.End() 49 50 d := types.MustMonitorDBExecutorFromContext(ctx) 51 m := &models.ContractLog{} 52 53 cs, err := m.List(d, builder.And( 54 builder.Or( 55 m.ColBlockCurrent().Lt(m.ColBlockEnd()), 56 m.ColBlockEnd().Eq(0), 57 ), 58 m.ColPaused().Eq(datatypes.FALSE), 59 )) 60 if err != nil { 61 l.Error(errors.Wrap(err, "list contractlog db failed")) 62 return 63 } 64 65 gs, err := t.getListChainGroups(ctx, cs) 66 if err != nil { 67 l.Error(errors.Wrap(err, "get lister units failed")) 68 return 69 } 70 71 for _, g := range gs { 72 toBlock, err := t.listChainAndSendEvent(ctx, g) 73 if err != nil { 74 l.Error(errors.Wrap(err, "list chain and send event failed")) 75 continue 76 } 77 78 if err := sqlx.NewTasks(d).With( 79 func(d sqlx.DBExecutor) error { 80 for _, c := range g.cs { 81 c.BlockCurrent = toBlock + 1 82 if c.BlockEnd > 0 && c.BlockCurrent >= c.BlockEnd { 83 c.Uniq = c.ContractLogID 84 } 85 if err := c.UpdateByID(d); err != nil { 86 return err 87 } 88 } 89 return nil 90 }, 91 ).Do(); err != nil { 92 l.Error(errors.Wrap(err, "update contractlog db failed")) 93 } 94 } 95 } 96 97 func (t *contract) getListChainGroups(ctx context.Context, cs []models.ContractLog) ([]*listChainGroup, error) { 98 ctx, l := logr.Start(ctx, "bc.contract.getListChainGroups") 99 defer l.End() 100 101 us := t.groupContractLog(cs) 102 t.pruneListChainGroups(us) 103 if err := t.setToBlock(ctx, us); err != nil { 104 return nil, err 105 } 106 return us, nil 107 } 108 109 // projectName + chainID -> contractLog list 110 func (t *contract) groupContractLog(cs []models.ContractLog) []*listChainGroup { 111 groups := make(map[string][]*models.ContractLog) 112 113 for i := range cs { 114 key := fmt.Sprintf("%s_%d", cs[i].ProjectName, cs[i].ChainID) 115 groups[key] = append(groups[key], &cs[i]) 116 } 117 118 ret := []*listChainGroup{} 119 for _, cs := range groups { 120 ret = append(ret, &listChainGroup{ 121 cs: cs, 122 }) 123 } 124 return ret 125 } 126 127 func (t *contract) pruneListChainGroups(gs []*listChainGroup) { 128 for _, g := range gs { 129 sort.SliceStable(g.cs, func(i, j int) bool { 130 return g.cs[i].BlockCurrent < g.cs[j].BlockCurrent 131 }) 132 133 if g.cs[0].BlockCurrent == g.cs[len(g.cs)-1].BlockCurrent { 134 continue 135 } 136 for i := range g.cs { 137 if i == 0 { 138 continue 139 } 140 if g.cs[i].BlockCurrent != g.cs[i-1].BlockCurrent { 141 g.toBlock = g.cs[i].BlockCurrent - 1 142 g.cs = g.cs[:i] 143 break 144 } 145 } 146 } 147 } 148 149 func (t *contract) setToBlock(ctx context.Context, gs []*listChainGroup) error { 150 ctx, l := logr.Start(ctx, "bc.contract.setToBlock") 151 defer l.End() 152 153 ethcli := types.MustETHClientConfigFromContext(ctx) 154 155 for _, g := range gs { 156 c := g.cs[0] 157 158 cli, ok := ethcli.Clients[uint32(c.ChainID)] 159 if !ok { 160 err := errors.New("blockchain not exist") 161 l.WithValues("chainID", c.ChainID).Error(err) 162 return err 163 } 164 165 currHeight, err := cli.BlockNumber(context.Background()) 166 if err != nil { 167 l.Error(errors.Wrap(err, "get blockchain current height failed")) 168 return err 169 } 170 171 to := c.BlockCurrent + t.blockInterval 172 if to > currHeight { 173 to = currHeight 174 } 175 for _, c := range g.cs { 176 if c.BlockEnd > 0 && to > c.BlockEnd { 177 to = c.BlockEnd 178 } 179 } 180 if g.toBlock == 0 { 181 g.toBlock = to 182 } 183 if g.toBlock > to { 184 g.toBlock = to 185 } 186 } 187 return nil 188 } 189 190 func (t *contract) listChainAndSendEvent(ctx context.Context, g *listChainGroup) (uint64, error) { 191 ctx, l := logr.Start(ctx, "bc.contract.listChainAndSendEvent") 192 defer l.End() 193 194 ethcli := types.MustETHClientConfigFromContext(ctx) 195 196 c := g.cs[0] 197 198 l = l.WithValues("chainID", c.ChainID, "projectName", c.ProjectName) 199 200 cli, ok := ethcli.Clients[uint32(c.ChainID)] 201 if !ok { 202 err := errors.New("blockchain not exist") 203 l.Error(err) 204 return 0, err 205 } 206 207 from, to := c.BlockCurrent, g.toBlock 208 209 if from > to { 210 // l.WithValues("from block", from, "to block", to).Debug("no new block") 211 return to, nil 212 } 213 // l.WithValues("from block", from, "to block", to).Debug("find new block") 214 215 as, mas := t.getAddresses(g.cs) 216 ts, mts := t.getTopic(g.cs) 217 query := ethereum.FilterQuery{ 218 FromBlock: big.NewInt(int64(from)), 219 ToBlock: big.NewInt(int64(to)), 220 Addresses: as, 221 Topics: ts, 222 } 223 logs, err := cli.FilterLogs(context.Background(), query) 224 if err != nil { 225 l.Error(errors.Wrap(err, "filter event logs failed")) 226 return 0, err 227 } 228 for i := range logs { 229 cs := t.getExpectedContractLogs(&logs[i], mas, mts) 230 if len(cs) == 0 { 231 err := errors.New("cannot find expected contract log") 232 l.Error(err) 233 return 0, err 234 } 235 236 data, err := logs[i].MarshalJSON() 237 if err != nil { 238 return 0, err 239 } 240 for _, c := range cs { 241 if err := t.sendEvent(ctx, data, c.ProjectName, c.EventType); err != nil { 242 return 0, err 243 } 244 } 245 } 246 return to, nil 247 } 248 249 func (t *contract) getExpectedContractLogs(log *ethtypes.Log, mas map[*models.ContractLog]common.Address, mts map[*models.ContractLog][]*common.Hash) []*models.ContractLog { 250 res := []*models.ContractLog{} 251 252 for c, addr := range mas { 253 if bytes.Equal(addr.Bytes(), log.Address.Bytes()) { 254 ts := mts[c] 255 256 for i, contractLogTopic := range ts { 257 if contractLogTopic == nil { 258 continue 259 } 260 if len(log.Topics) > i && bytes.Equal(log.Topics[i].Bytes(), contractLogTopic.Bytes()) { 261 continue 262 } 263 goto Next 264 } 265 res = append(res, c) 266 } 267 Next: 268 } 269 return res 270 } 271 272 func (t *contract) getAddresses(cs []*models.ContractLog) ([]common.Address, map[*models.ContractLog]common.Address) { 273 as := []common.Address{} 274 mas := make(map[*models.ContractLog]common.Address) 275 for _, c := range cs { 276 a := common.HexToAddress(c.ContractAddress) 277 as = append(as, a) 278 mas[c] = a 279 } 280 return as, mas 281 } 282 283 func (t *contract) getTopic(cs []*models.ContractLog) ([][]common.Hash, map[*models.ContractLog][]*common.Hash) { 284 res := make([][]common.Hash, 4) 285 mres := make(map[*models.ContractLog][]*common.Hash) 286 287 for _, c := range cs { 288 h0 := t.parseTopic(c.Topic0) 289 mres[c] = append(mres[c], h0) 290 if h0 != nil { 291 res[0] = append(res[0], *h0) 292 } 293 294 h1 := t.parseTopic(c.Topic1) 295 mres[c] = append(mres[c], h1) 296 if h1 != nil { 297 res[1] = append(res[1], *h1) 298 } 299 300 h2 := t.parseTopic(c.Topic2) 301 mres[c] = append(mres[c], h2) 302 if h2 != nil { 303 res[2] = append(res[2], *h2) 304 } 305 306 h3 := t.parseTopic(c.Topic3) 307 mres[c] = append(mres[c], h3) 308 if h3 != nil { 309 res[3] = append(res[3], *h3) 310 } 311 312 } 313 314 if len(res[3]) == 0 { 315 res = res[:3] 316 if len(res[2]) == 0 { 317 res = res[:2] 318 if len(res[1]) == 0 { 319 res = res[:1] 320 if len(res[0]) == 0 { 321 res = res[:0] 322 } 323 } 324 } 325 } 326 return res, mres 327 } 328 329 func (t *contract) parseTopic(tStr string) *common.Hash { 330 if tStr == "" { 331 return nil 332 } 333 h := common.HexToHash(tStr) 334 return &h 335 }