synapsecns/sanguine

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

Summary

Maintainability
F
3 days
Test Coverage
package parser

import (
    "context"
    "database/sql"
    "fmt"
    "math/big"
    "time"

    "github.com/synapsecns/sanguine/services/explorer/consumer/fetcher/tokenprice"

    "github.com/synapsecns/sanguine/services/explorer/consumer/parser/tokendata"
    "golang.org/x/sync/errgroup"

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

// BridgeParser parses events from the bridge contract.
type BridgeParser struct {
    // consumerDB is the database to store parsed data in.
    consumerDB db.ConsumerDB
    // Filterer is the bridge Filterer we use to parse events.
    Filterer *bridge.SynapseBridgeFilterer
    // Filterer is the bridge Filterer we use to parse events.
    FiltererV1 *bridgev1.SynapseBridgeFilterer
    // bridgeAddress is the address of the bridge.
    bridgeAddress common.Address
    // tokenDataService contains the token data service/cache
    tokenDataService tokendata.Service
    // tokenPriceService contains the token price service/cache
    tokenPriceService tokenprice.Service
    // consumerFetcher is the ScribeFetcher for sender and timestamp.
    consumerFetcher fetcher.ScribeFetcher
    // coinGeckoIDs is the mapping of token id to coin gecko ID
    coinGeckoIDs map[string]string
    // fromAPI is true if the parser is being called from the API.
    fromAPI bool
}

const noTokenID = "NO_TOKEN"
const noPrice = "NO_PRICE"

// TODO these parsers need a custom struct with config with the services.

// NewBridgeParser creates a new parser for a given bridge.
func NewBridgeParser(consumerDB db.ConsumerDB, bridgeAddress common.Address, tokenDataService tokendata.Service, consumerFetcher fetcher.ScribeFetcher, tokenPriceService tokenprice.Service, fromAPI bool) (*BridgeParser, error) {
    filterer, err := bridge.NewSynapseBridgeFilterer(bridgeAddress, nil)
    if err != nil {
        return nil, fmt.Errorf("could not create %T: %w", bridge.SynapseBridgeFilterer{}, err)
    }

    // Old bridge contract to filter all events across all times.
    filtererV1, err := bridgev1.NewSynapseBridgeFilterer(bridgeAddress, nil)
    if err != nil {
        return nil, fmt.Errorf("could not create %T: %w", bridgev1.SynapseBridgeFilterer{}, err)
    }

    idCoinGeckoIDs, err := ParseYaml(static.GetTokenIDToCoingekoConfig())
    if err != nil {
        return nil, fmt.Errorf("could not open yaml file: %w", err)
    }

    return &BridgeParser{
        consumerDB:        consumerDB,
        Filterer:          filterer,
        FiltererV1:        filtererV1,
        bridgeAddress:     bridgeAddress,
        tokenDataService:  tokenDataService,
        tokenPriceService: tokenPriceService,
        consumerFetcher:   consumerFetcher,
        coinGeckoIDs:      idCoinGeckoIDs,
        fromAPI:           fromAPI,
    }, nil
}

// EventType returns the event type of a bridge log.
func (p *BridgeParser) EventType(log ethTypes.Log) (_ bridgeTypes.EventType, ok bool) {
    for _, logTopic := range log.Topics {
        eventType := bridge.EventTypeFromTopic(logTopic)
        if eventType == nil {
            continue
        }

        return *eventType, true
    }

    // Return an unknown event to avoid cases where user failed to check the event type.
    return bridgeTypes.EventType(len(bridgeTypes.AllEventTypes()) + 2), false
}

// eventToBridgeEvent stores a bridge event.
func eventToBridgeEvent(event bridgeTypes.EventLog, chainID uint32) model.BridgeEvent {
    var recipient sql.NullString

    if event.GetRecipient() != nil {
        recipient.Valid = true
        recipient.String = event.GetRecipient().String()
    } else {
        recipient.Valid = false
    }

    var recipientBytes sql.NullString

    if event.GetRecipientBytes() != nil {
        recipientBytes.Valid = true
        recipientBytes.String = common.Bytes2Hex(event.GetRecipientBytes()[:])
    } else {
        recipientBytes.Valid = false
    }

    var destinationChainID *big.Int
    var destinationKappa string
    if event.GetDestinationChainID() != nil {
        destinationChainID = big.NewInt(int64(event.GetDestinationChainID().Uint64()))
        destinationKappa = crypto.Keccak256Hash([]byte(event.GetTxHash().String())).String()[2:]
    }

    var tokenIndexFrom *big.Int

    if event.GetTokenIndexFrom() != nil {
        tokenIndexFrom = big.NewInt(int64(*event.GetTokenIndexFrom()))
    }

    var tokenIndexTo *big.Int

    if event.GetTokenIndexTo() != nil {
        tokenIndexTo = big.NewInt(int64(*event.GetTokenIndexTo()))
    }

    var swapSuccess *big.Int

    if event.GetSwapSuccess() != nil {
        swapSuccess = big.NewInt(int64(*BoolToUint8(event.GetSwapSuccess())))
    }

    var swapTokenIndex *big.Int

    if event.GetSwapTokenIndex() != nil {
        swapTokenIndex = big.NewInt(int64(*event.GetSwapTokenIndex()))
    }

    var kappa sql.NullString

    if event.GetKappa() != nil {
        kappa.Valid = true
        kappa.String = common.Bytes2Hex(event.GetKappa()[:])
    } else {
        kappa.Valid = false
    }
    // For event type 2 (TokenWithdraw), Amount is calculated as GetAmount() - GetFee()
    // For all other event types, Amount is simply GetAmount(), which directly pulls the amount from the event
    amount := event.GetAmount()
    if event.GetEventType().Int() == 2 { // Event Type 2 is WithdrawEvent
        amount = new(big.Int).Sub(event.GetAmount(), event.GetFee())
    }

    return model.BridgeEvent{
        InsertTime:         uint64(time.Now().UnixNano()),
        ContractAddress:    event.GetContractAddress().String(),
        ChainID:            chainID,
        EventType:          event.GetEventType().Int(),
        BlockNumber:        event.GetBlockNumber(),
        TxHash:             event.GetTxHash().String(),
        Amount:             amount,
        EventIndex:         event.GetEventIndex(),
        DestinationKappa:   destinationKappa,
        Sender:             "",
        Recipient:          recipient,
        RecipientBytes:     recipientBytes,
        DestinationChainID: destinationChainID,
        Token:              event.GetToken().String(),
        Fee:                event.GetFee(),
        Kappa:              kappa,
        TokenIndexFrom:     tokenIndexFrom,
        TokenIndexTo:       tokenIndexTo,
        MinDy:              event.GetMinDy(),
        Deadline:           event.GetDeadline(),
        SwapSuccess:        swapSuccess,
        SwapTokenIndex:     swapTokenIndex,
        SwapMinAmount:      event.GetSwapMinAmount(),
        SwapDeadline:       event.GetSwapDeadline(),

        // Placeholders for further data maturation of this event.
        TimeStamp:    nil,
        AmountUSD:    nil,
        FeeUSD:       nil,
        TokenDecimal: nil,
        TokenSymbol:  sql.NullString{},
    }
}

// ParserType returns the type of parser.
func (p *BridgeParser) ParserType() string {
    return "bridge"
}

// Parse parses the bridge logs and returns a model that can be stored.
func (p *BridgeParser) Parse(ctx context.Context, log ethTypes.Log, chainID uint32) (interface{}, error) {
    bridgeEvent, iFace, err := p.ParseLog(log, chainID)
    if err != nil {
        return nil, err
    }
    bridgeEventInterface, err := p.MatureLogs(ctx, bridgeEvent, iFace, chainID)
    if err != nil {
        return nil, err
    }
    return bridgeEventInterface, nil
}

// ParseLog parses the bridge logs and returns a model that can be stored.
//
// nolint:gocognit,cyclop
func (p *BridgeParser) ParseLog(log ethTypes.Log, chainID uint32) (*model.BridgeEvent, bridgeTypes.EventLog, error) {
    logTopic := log.Topics[0]

    iFace, err := func(log ethTypes.Log) (bridgeTypes.EventLog, error) {
        switch logTopic {
        case bridge.Topic(bridgeTypes.DepositEvent):
            iFace, err := p.Filterer.ParseTokenDeposit(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenDeposit(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse deposit: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse deposit")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.RedeemEvent):
            iFace, err := p.Filterer.ParseTokenRedeem(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenRedeem(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse redeem: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse redeem")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.WithdrawEvent):
            iFace, err := p.Filterer.ParseTokenWithdraw(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenWithdraw(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse withdraw: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse withdraw")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.MintEvent):
            iFace, err := p.Filterer.ParseTokenMint(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenMint(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse mint: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse mint")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.DepositAndSwapEvent):
            iFace, err := p.Filterer.ParseTokenDepositAndSwap(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenDepositAndSwap(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse deposit and swap: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse deposit and swap")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.MintAndSwapEvent):
            iFace, err := p.Filterer.ParseTokenMintAndSwap(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenMintAndSwap(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse mint and swap: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse mint and swap")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.RedeemAndSwapEvent):
            iFace, err := p.Filterer.ParseTokenRedeemAndSwap(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenRedeemAndSwap(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse redeem and swap: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse redeem and swap")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.RedeemAndRemoveEvent):
            iFace, err := p.Filterer.ParseTokenRedeemAndRemove(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenRedeemAndRemove(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse redeem and remove: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse redeem and remove")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.WithdrawAndRemoveEvent):
            iFace, err := p.Filterer.ParseTokenWithdrawAndRemove(log)
            if err != nil {
                iFaceV1, err := p.FiltererV1.ParseTokenWithdrawAndRemove(log)
                if err != nil {
                    return nil, fmt.Errorf("could not parse withdraw and remove: %w", err)
                }

                logger.Warnf("used v1 bridge contract to parse withdraw and remove")

                return iFaceV1, nil
            }

            return iFace, nil
        case bridge.Topic(bridgeTypes.RedeemV2Event):
            iFace, err := p.Filterer.ParseTokenRedeemV2(log)
            if err != nil {
                return nil, fmt.Errorf("could not parse redeem v2: %w", err)
            }

            return iFace, nil
        default:
            logger.Warnf("ErrUnknownTopic in bridge: %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
    }
    bridgeEvent := eventToBridgeEvent(iFace, chainID)

    return &bridgeEvent, iFace, nil
}

// MatureLogs takes a bridge event and matures it by fetching the sender and timestamp from the API and more.
//
// nolint:gocognit,cyclop
func (p *BridgeParser) MatureLogs(ctx context.Context, bridgeEvent *model.BridgeEvent, iFace bridgeTypes.EventLog, chainID uint32) (interface{}, error) {
    g, groupCtx := errgroup.WithContext(ctx)
    var err error
    var sender *string
    var timeStamp *uint64
    g.Go(func() error {
        if p.fromAPI {
            rawTimeStamp, err := p.consumerFetcher.FetchBlockTime(groupCtx, int(chainID), int(bridgeEvent.BlockNumber))
            if err != nil {
                return fmt.Errorf("could not get timestamp, sender on chain %d and tx %s from tx %w", chainID, iFace.GetTxHash().String(), err)
            }
            uint64TimeStamp := uint64(*rawTimeStamp)
            timeStamp = &uint64TimeStamp
            senderStr := "" // empty for bridge watcher/api parser
            sender = &senderStr
            return nil
        }
        timeStamp, sender, err = p.consumerFetcher.FetchTx(groupCtx, iFace.GetTxHash().String(), int(chainID), int(bridgeEvent.BlockNumber))
        if err != nil {
            return fmt.Errorf("could not get timestamp, sender on chain %d and tx %s from tx %w", chainID, iFace.GetTxHash().String(), err)
        }
        return nil
    })

    var tokenData tokendata.ImmutableTokenData
    g.Go(func() error {
        // Get Token from BridgeConfig data (for getting token decimal but use this for anything else).
        tokenData, err = p.tokenDataService.GetTokenData(groupCtx, chainID, iFace.GetToken())
        if err != nil {
            return fmt.Errorf("could not parse get token from bridge config event: %w", err)
        }

        return nil
    })

    err = g.Wait()
    if err != nil {
        return nil, fmt.Errorf("could not parse bridge event: %w", err)
    }
    if *timeStamp == 0 {
        logger.Errorf("empty block time: chain: %d address %s", chainID, bridgeEvent.ContractAddress)
        return nil, fmt.Errorf("empty block time: chain: %d address %s", chainID, bridgeEvent.ContractAddress)
    }

    bridgeEvent.TimeStamp = timeStamp
    bridgeEvent.Sender = *sender

    if tokenData.TokenID() == fetcher.NoTokenID {
        logger.Errorf("could not get token data token id chain: %d address %s", chainID, bridgeEvent.ContractAddress)
        return bridgeEvent, nil
    }

    realDecimals := tokenData.Decimals()
    realID := tokenData.TokenID()
    bridgeEvent.TokenDecimal = &realDecimals

    // Add the price of the token at the block the event occurred using coin gecko (to bridgeEvent).
    coinGeckoID := p.coinGeckoIDs[tokenData.TokenID()]
    if coinGeckoID == "" {
        logger.Warnf("BRIDGE - EMPTY TOKEN ID: %s, TokenID: %s", p.coinGeckoIDs[tokenData.TokenID()], tokenData.TokenID())
    }

    // Add TokenSymbol to bridgeEvent.
    bridgeEvent.TokenSymbol = ToNullString(&realID)
    var tokenPrice *float64
    // takes into account an empty bridge token id and for tokens that were bridged before price trackers (coin gecko) had price data.
    if coinGeckoID != "" && !(coinGeckoID == "xjewel" && *timeStamp < 1649030400) && !(coinGeckoID == "synapse-2" && *timeStamp < 1630281600) && !(coinGeckoID == "governance-ohm" && *timeStamp < 1638316800) && !(coinGeckoID == "highstreet" && *timeStamp < 1634263200) {
        tokenPrice = p.tokenPriceService.GetPriceData(ctx, int(*timeStamp), coinGeckoID)
        if tokenPrice == nil && coinGeckoID != noTokenID && coinGeckoID != noPrice {
            return nil, fmt.Errorf("BRIDGE could not get token price for coingeckotoken:  %s chain: %d txhash %s %d", coinGeckoID, chainID, bridgeEvent.TxHash, bridgeEvent.TimeStamp)
        }
    }

    if tokenPrice != nil {
        bridgeEvent.AmountUSD = GetAmountUSD(bridgeEvent.Amount, tokenData.Decimals(), tokenPrice)
        if iFace.GetFee() != nil {
            bridgeEvent.FeeUSD = GetAmountUSD(bridgeEvent.Fee, tokenData.Decimals(), tokenPrice)
        }
    }
    return bridgeEvent, nil
}