synapsecns/sanguine

View on GitHub
services/scribe/backend/backend.go

Summary

Maintainability
A
0 mins
Test Coverage
package backend

import (
    "context"
    "fmt"
    "math/big"

    "github.com/benbjohnson/immutable"
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/lmittmann/w3/module/eth"
    "github.com/lmittmann/w3/w3types"
    "github.com/synapsecns/sanguine/core/metrics"
    "github.com/synapsecns/sanguine/ethergo/client"
    "github.com/synapsecns/sanguine/ethergo/util"
    "golang.org/x/exp/constraints"
)

// ScribeBackend is the set of functions that the scribe needs from a client.
type ScribeBackend interface {
    // ChainID gets the chain id from the rpc server.
    ChainID(ctx context.Context) (*big.Int, error)
    // BlockNumber gets the latest block number.
    BlockNumber(ctx context.Context) (uint64, error)
    // HeaderByNumber returns the block header with the given block number.
    HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
    // BatchWithContext batches multiple
    BatchWithContext(ctx context.Context, calls ...w3types.Caller) error
}

// DialBackend returns a scribe backend.
func DialBackend(ctx context.Context, url string, handler metrics.Handler) (ScribeBackend, error) {
    //nolint:wrapcheck
    return client.DialBackend(ctx, url, handler)
}

// GetLogsInRange gets all logs in a range with a single batch request
// in successful cases an immutable list is returned, otherwise an error is returned.
func GetLogsInRange(ctx context.Context, backend ScribeBackend, contractAddresses []common.Address, expectedChainID uint64, chunks []*util.Chunk, topics [][]common.Hash) (*immutable.List[*[]types.Log], error) {
    calls := make([]w3types.Caller, len(chunks)+2)
    results := make([][]types.Log, len(chunks))
    chainID := new(uint64)
    calls[0] = eth.ChainID().Returns(chainID)

    maxHeight := new(big.Int)
    calls[1] = eth.BlockNumber().Returns(maxHeight)

    for i := 0; i < len(chunks); i++ {
        startBlock := chunks[i].StartBlock
        endBlock := chunks[i].EndBlock

        // handle desc iterator
        if startBlock.Uint64() > endBlock.Uint64() {
            startBlock = chunks[i].EndBlock
            endBlock = chunks[i].StartBlock
        }
        filter := ethereum.FilterQuery{
            FromBlock: startBlock,
            ToBlock:   endBlock,
            Addresses: contractAddresses,
            Topics:    topics,
        }
        calls[i+2] = eth.Logs(filter).Returns(&results[i])
    }

    // for logging purposes
    startHeight := chunks[0].StartBlock.Uint64()
    endHeight := chunks[len(chunks)-1].EndBlock.Uint64()
    if err := backend.BatchWithContext(ctx, calls...); err != nil {
        return nil, fmt.Errorf("could not fetch logs in range %d to %d: %w", startHeight, endHeight, err)
    }
    if expectedChainID != *chainID {
        return nil, fmt.Errorf("could not fetch logs in range %d to %d: Incorrect RPC used for expected chainID: %d, RPC used chainID %d instead", startHeight, endHeight, expectedChainID, chainID)
    }
    if endHeight > maxHeight.Uint64() {
        return nil, fmt.Errorf("could not fetch logs in range %d to %d: Block not available past max height: %d", startHeight, endHeight, maxHeight.Uint64())
    }

    // use an immutable list for additional safety to the caller, don't allocate until batch returns successfully
    res := immutable.NewListBuilder[*[]types.Log]()
    for _, result := range results {
        logChunk := result
        if len(result) > 0 {
            res.Append(&logChunk)
        }
    }

    return res.List(), nil
}

// BlockHashesInRange gets all block hashes in a range with a single batch request
// in successful cases an immutable map is returned of [height->hash], otherwise an error is returned.
func BlockHashesInRange(ctx context.Context, backend ScribeBackend, startHeight uint64, endHeight uint64) (*immutable.Map[uint64, string], error) {
    // performance impact will be negligible here because of external constraints on blocksize
    blocks := MakeRange(startHeight, endHeight)
    bulkSize := len(blocks)
    calls := make([]w3types.Caller, bulkSize)
    results := make([]types.Header, bulkSize)

    for i, blockNumber := range blocks {
        calls[i] = eth.HeaderByNumber(new(big.Int).SetUint64(blockNumber)).Returns(&results[i])
    }

    if err := backend.BatchWithContext(ctx, calls...); err != nil {
        return nil, fmt.Errorf("could not fetch blocks in range %d to %d: %w", startHeight, endHeight, err)
    }

    // use an immutable map for additional safety to the caller, don't allocate until batch returns successfully
    res := immutable.NewMapBuilder[uint64, string](nil)
    for _, result := range results {
        res.Set(result.Number.Uint64(), result.Hash().String())
    }

    return res.Map(), nil
}

// MakeRange returns a range of integers from min to max inclusive.
func MakeRange[T constraints.Integer](min, max T) []T {
    a := make([]T, max-min+1)
    for i := range a {
        a[i] = min + T(i)
    }
    return a
}