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  }