synapsecns/sanguine

View on GitHub
services/explorer/consumer/parser/cctpparser.go

Summary

Maintainability
C
7 hrs
Test Coverage
package parser

import (
    "context"
    "database/sql"
    "fmt"
    "github.com/jpillora/backoff"
    "github.com/synapsecns/sanguine/services/explorer/consumer/parser/tokendata"
    "time"

    "github.com/ethereum/go-ethereum/common"
    ethTypes "github.com/ethereum/go-ethereum/core/types"
    "github.com/synapsecns/sanguine/services/explorer/consumer/fetcher"
    "github.com/synapsecns/sanguine/services/explorer/consumer/fetcher/tokenprice"
    "github.com/synapsecns/sanguine/services/explorer/contracts/cctp"
    "github.com/synapsecns/sanguine/services/explorer/db"
    model "github.com/synapsecns/sanguine/services/explorer/db/sql"
    bridgeTypes "github.com/synapsecns/sanguine/services/explorer/types/bridge"
    cctpTypes "github.com/synapsecns/sanguine/services/explorer/types/cctp"
)

// CCTPParser parses cctp logs.
type CCTPParser struct {
    // consumerDB is the database to store parsed data in
    consumerDB db.ConsumerDB
    // Filterer is the message Filterer we use to parse events
    Filterer *cctp.SynapseCCTPFilterer
    // messageAddress is the address of the message
    cctpAddress common.Address
    // consumerFetcher is the Fetcher for sender and timestamp
    consumerFetcher fetcher.ScribeFetcher
    // cctpService is the cctp service for getting token symbol information
    cctpService fetcher.CCTPService
    // tokenDataService contains the token data service/cache
    tokenDataService tokendata.Service
    // tokenPriceService contains the token price service/cache
    tokenPriceService tokenprice.Service
    // fromAPI is true if the parser is being called from the API.
    fromAPI bool
}

const usdcCoinGeckoID = "usd-coin"
const usdcDecimals = 6

// NewCCTPParser creates a new parser for a cctp event.
func NewCCTPParser(consumerDB db.ConsumerDB, cctpAddress common.Address, consumerFetcher fetcher.ScribeFetcher, cctpService fetcher.CCTPService, tokenDataService tokendata.Service, tokenPriceService tokenprice.Service, fromAPI bool) (*CCTPParser, error) {
    filterer, err := cctp.NewSynapseCCTPFilterer(cctpAddress, nil)
    if err != nil {
        return nil, fmt.Errorf("could not create %T: %w", cctp.SynapseCCTPFilterer{}, err)
    }
    return &CCTPParser{consumerDB, filterer, cctpAddress, consumerFetcher, cctpService, tokenDataService, tokenPriceService, fromAPI}, nil
}

// ParserType returns the type of parser.
func (c *CCTPParser) ParserType() string {
    return "cctp"
}

// ParseLog log converts an eth log to a cctp event type.
func (c *CCTPParser) ParseLog(log ethTypes.Log, chainID uint32) (*model.CCTPEvent, cctpTypes.EventLog, error) {
    logTopic := log.Topics[0]
    iFace, err := func(log ethTypes.Log) (cctpTypes.EventLog, error) {
        switch logTopic {
        case cctp.Topic(cctpTypes.CircleRequestSentEvent):
            iFace, err := c.Filterer.ParseCircleRequestSent(log)
            if err != nil {
                return nil, fmt.Errorf("could not parse circle request sent : %w", err)
            }
            return iFace, nil
        case cctp.Topic(cctpTypes.CircleRequestFulfilledEvent):
            iFace, err := c.Filterer.ParseCircleRequestFulfilled(log)
            if err != nil {
                return nil, fmt.Errorf("could not parse circle request fulfilled: %w", err)
            }
            return iFace, nil

        default:
            logger.Warnf("ErrUnknownTopic in cctp: %s %s chain: %d address: %s", log.TxHash, logTopic.String(), chainID, log.Address.Hex())

            return nil, fmt.Errorf(ErrUnknownTopic)
        }
    }(log)

    if err != nil {
        // Switch failed.

        return nil, nil, err
    }
    if iFace == nil {
        // Unknown topic.
        return nil, nil, fmt.Errorf("unknwn topic")
    }

    // Populate cctp event type so following operations can mature the event data.
    cctpEvent := eventToCCTPEvent(iFace, chainID)
    return &cctpEvent, iFace, nil
}

// MatureLogs takes a cctp event and adds data to them.
func (c *CCTPParser) MatureLogs(ctx context.Context, cctpEvent *model.CCTPEvent, iFace cctpTypes.EventLog, chainID uint32) (interface{}, error) {
    // Get timestamp from consumer
    timeStamp, err := c.consumerFetcher.FetchBlockTime(ctx, int(chainID), int(iFace.GetBlockNumber()))
    if err != nil {
        return nil, fmt.Errorf("could not get block time: %w", err)
    }

    // If we have a timestamp, populate the following attributes of cctpEvent.
    timeStampBig := uint64(*timeStamp)
    cctpEvent.TimeStamp = &timeStampBig

    tokenData, err := c.tokenDataService.GetCCTPTokenData(ctx, chainID, common.HexToAddress(cctpEvent.Token), c.cctpService)
    if err != nil {
        logger.Errorf("could not get token data: %v", err)
        return nil, fmt.Errorf("could not get pool token data: %w", err)
    }
    decimals := uint8(usdcDecimals)
    // Hotfix
    if chainID == 8453 && (cctpEvent.Token == "0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93" || cctpEvent.Token == "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb") {
        decimals = 18
    }
    cctpEvent.TokenSymbol = tokenData.TokenID()
    if cctpEvent.Token == "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" {
        decimals = 18
        cctpEvent.TokenSymbol = "DAI"
    }
    if chainID == 10 && (cctpEvent.Token == "0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9") {
        decimals = 18
        cctpEvent.TokenSymbol = "sUSD"
    }
    cctpEvent.TokenDecimal = &decimals
    c.applyPriceData(ctx, cctpEvent, usdcCoinGeckoID)

    // Would store into bridge database with a new goroutine but saw unreliable storage of events w/parent context cancellation.
    bridgeEvent := cctpEventToBridgeEvent(*cctpEvent)
    if c.fromAPI {
        return bridgeEvent, nil
    }
    err = c.storeBridgeEvent(ctx, bridgeEvent)
    if err != nil {
        logger.Errorf("could not store cctp event into bridge database: %v", err)
    }

    return cctpEvent, nil
}

// Parse parses the cctp logs.
//
// nolint:gocognit,cyclop,dupl
func (c *CCTPParser) Parse(ctx context.Context, log ethTypes.Log, chainID uint32) (interface{}, error) {
    cctpEvent, iFace, err := c.ParseLog(log, chainID)
    if err != nil {
        return nil, fmt.Errorf("could not parse cctp event: %w", err)
    }
    bridgeEventInterface, err := c.MatureLogs(ctx, cctpEvent, iFace, chainID)
    if err != nil {
        return nil, fmt.Errorf("could not mature cctp event: %w", err)
    }
    return bridgeEventInterface, nil
}

// applyPriceData applies price data to the cctp event, setting USD values.
func (c *CCTPParser) applyPriceData(ctx context.Context, cctpEvent *model.CCTPEvent, coinGeckoID string) {
    tokenPrice := c.tokenPriceService.GetPriceData(ctx, int(*cctpEvent.TimeStamp), coinGeckoID)
    if tokenPrice == nil {
        logger.Warnf("CCTP could not get token price for coingeckotoken; assuming price of 1:  %s txhash %s %d", coinGeckoID, cctpEvent.TxHash, cctpEvent.TimeStamp)
        one := 1.0
        tokenPrice = &one
    }

    if cctpEvent.Amount != nil {
        amountUSD := GetAmountUSD(cctpEvent.Amount, *cctpEvent.TokenDecimal, tokenPrice)
        if amountUSD != nil {
            cctpEvent.AmountUSD = *amountUSD
        }
    }
    if cctpEvent.Fee != nil {
        cctpEvent.FeeUSD = GetAmountUSD(cctpEvent.Fee, *cctpEvent.TokenDecimal, tokenPrice)
    }
}

// eventToCCTPEvent stores a message event.
func eventToCCTPEvent(event cctpTypes.EventLog, chainID uint32) model.CCTPEvent {
    requestID := event.GetRequestID()

    var formattedRequest sql.NullString
    if event.GetFormattedRequest() != nil {
        formattedRequest.Valid = true
        formattedRequest.String = common.Bytes2Hex(*event.GetFormattedRequest())
    } else {
        formattedRequest.Valid = false
    }

    return model.CCTPEvent{
        InsertTime:      uint64(time.Now().UnixNano()),
        ChainID:         chainID,
        TxHash:          event.GetTxHash().String(),
        ContractAddress: event.GetContractAddress().String(),
        BlockNumber:     event.GetBlockNumber(),
        EventType:       event.GetEventType().Int(),
        RequestID:       common.Bytes2Hex(requestID[:]),

        Token:              event.GetToken(),
        Amount:             event.GetAmount(),
        EventIndex:         event.GetEventIndex(),
        OriginChainID:      event.GetOriginChainID(),
        DestinationChainID: event.GetDestinationChainID(),
        Sender:             ToNullString(event.GetSender()),
        Nonce:              ToNullInt64(event.GetNonce()),
        MintToken:          ToNullString(event.GetMintToken()),
        RequestVersion:     ToNullInt32(event.GetRequestVersion()),
        FormattedRequest:   formattedRequest,
        Recipient:          ToNullString(event.GetRecipient()),
        Fee:                event.GetFee(),
    }
}

func cctpEventToBridgeEvent(cctpEvent model.CCTPEvent) model.BridgeEvent {
    bridgeType := bridgeTypes.CircleRequestSentEvent

    destinationKappa := cctpEvent.RequestID
    var kappa *string
    if cctpEvent.EventType == cctpTypes.CircleRequestFulfilledEvent.Int() {
        bridgeType = bridgeTypes.CircleRequestFulfilledEvent
        destinationKappa = ""
        kappa = &cctpEvent.RequestID
    }
    return model.BridgeEvent{
        InsertTime:       cctpEvent.InsertTime,
        ContractAddress:  cctpEvent.ContractAddress,
        ChainID:          cctpEvent.ChainID,
        EventType:        bridgeType.Int(),
        BlockNumber:      cctpEvent.BlockNumber,
        TxHash:           cctpEvent.TxHash,
        Token:            cctpEvent.Token,
        Amount:           cctpEvent.Amount,
        EventIndex:       cctpEvent.EventIndex,
        DestinationKappa: destinationKappa,
        Sender:           cctpEvent.Sender.String,

        Recipient:          cctpEvent.Recipient,
        RecipientBytes:     sql.NullString{},
        DestinationChainID: cctpEvent.DestinationChainID,
        Fee:                cctpEvent.Fee,
        Kappa:              ToNullString(kappa),
        TokenIndexFrom:     nil,
        TokenIndexTo:       nil,
        MinDy:              nil,
        Deadline:           nil,

        SwapSuccess:    nil,
        SwapTokenIndex: nil,
        SwapMinAmount:  nil,
        SwapDeadline:   nil,
        AmountUSD:      &cctpEvent.AmountUSD,
        FeeUSD:         cctpEvent.FeeUSD,
        TokenDecimal:   cctpEvent.TokenDecimal,
        TokenSymbol:    ToNullString(&cctpEvent.TokenSymbol),
        TimeStamp:      cctpEvent.TimeStamp,
    }
}

func (c *CCTPParser) storeBridgeEvent(ctx context.Context, bridgeEvent model.BridgeEvent) error {
    b := &backoff.Backoff{
        Factor: 2,
        Jitter: true,
        Min:    1 * time.Second,
        Max:    300 * time.Second,
    }

    timeout := time.Duration(0)
    for {
        select {
        case <-ctx.Done():
            return fmt.Errorf("%w while retrying store cctp converted bridge event", ctx.Err())
        case <-time.After(timeout):
            err := c.consumerDB.StoreEvent(ctx, &bridgeEvent)
            if err != nil {
                timeout = b.Duration()
                continue
            }
            return nil
        }
    }
}