services/scribe/db/datastore/sql/base/lastindexed.go
package base
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"github.com/ethereum/go-ethereum/common"
"github.com/synapsecns/sanguine/core/metrics"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"gorm.io/gorm/clause"
)
const lastIndexedLivefillKey = "0x0000000000000000000000000000000000000000"
// StoreLastIndexed stores the last indexed block number for a contract.
// It updates the value if there is a previous last indexed value, and creates a new
// entry if there's no previous value.
func (s Store) StoreLastIndexed(parentCtx context.Context, contractAddress common.Address, chainID uint32, blockNumber uint64, livefillAtHead bool) (err error) {
ctx, span := s.metrics.Tracer().Start(parentCtx, "StoreLastIndexed", trace.WithAttributes(
attribute.String("contractAddress", contractAddress.String()),
attribute.Int("chainID", int(chainID)),
attribute.Int("blockNumber", int(blockNumber)),
))
defer func() {
metrics.EndSpanWithErr(span, err)
}()
address := contractAddress.String()
if livefillAtHead {
address = lastIndexedLivefillKey
}
dbTx := s.DB().WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: ContractAddressFieldName}, {Name: ChainIDFieldName}},
DoUpdates: clause.AssignmentColumns([]string{BlockNumberFieldName}),
Where: clause.Where{
Exprs: []clause.Expression{
clause.And(
clause.Where{
Exprs: []clause.Expression{
clause.Eq{
Column: clause.Column{Name: ContractAddressFieldName},
Value: address,
},
clause.Eq{
Column: clause.Column{Name: ChainIDFieldName},
Value: chainID,
},
},
},
clause.Lt{
Column: clause.Column{Name: BlockNumberFieldName},
Value: blockNumber,
},
),
},
},
}).
Create(&LastIndexedInfo{
ContractAddress: address,
ChainID: chainID,
BlockNumber: blockNumber,
})
if dbTx.Error != nil {
return fmt.Errorf("could not update last indexed info: %w", dbTx.Error)
}
return nil
}
// RetrieveLastIndexed retrieves the last indexed block number for a contract.
func (s Store) RetrieveLastIndexed(ctx context.Context, contractAddress common.Address, chainID uint32, livefill bool) (uint64, error) {
entry := LastIndexedInfo{}
address := contractAddress.String()
if livefill {
address = lastIndexedLivefillKey
}
dbTx := s.DB().WithContext(ctx).
Model(&LastIndexedInfo{}).
Where(&LastIndexedInfo{
ContractAddress: address,
ChainID: chainID,
}).
First(&entry)
if dbTx.RowsAffected == 0 {
return 0, nil
}
if dbTx.Error != nil {
return 0, fmt.Errorf("could not retrieve last indexed info: %w", dbTx.Error)
}
return entry.BlockNumber, nil
}
// StoreLastIndexedMultiple stores the last indexed block numbers for numerous contracts.
func (s Store) StoreLastIndexedMultiple(parentCtx context.Context, contractAddresses []common.Address, chainID uint32, blockNumber uint64) error {
g, groupCtx := errgroup.WithContext(parentCtx)
for i := range contractAddresses {
index := i
g.Go(func() error {
err := s.StoreLastIndexed(groupCtx, contractAddresses[index], chainID, blockNumber, false)
if err != nil {
return fmt.Errorf("could not backfill: %w", err)
}
return nil
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("could not store last indexed: %w", err)
}
return nil
}
// RetrieveLastIndexedMultiple retrieves the last indexed block numbers for numerous contracts.
func (s Store) RetrieveLastIndexedMultiple(ctx context.Context, contractAddresses []common.Address, chainID uint32) (map[common.Address]uint64, error) {
var entries []LastIndexedInfo
addrStrings := make([]string, len(contractAddresses))
for i, addr := range contractAddresses {
addrStrings[i] = addr.String()
}
dbTx := s.DB().WithContext(ctx).
Model(&LastIndexedInfo{}).
Where("contract_address in ? AND chain_id = ?", addrStrings, chainID).
Find(&entries)
if dbTx.Error != nil {
return nil, fmt.Errorf("could not retrieve last indexed info: %w", dbTx.Error)
}
result := make(map[common.Address]uint64)
for _, addr := range contractAddresses {
result[addr] = 0
}
for _, entry := range entries {
addr := common.HexToAddress(entry.ContractAddress)
result[addr] = entry.BlockNumber
}
return result, nil
}