synapsecns/sanguine

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

Summary

Maintainability
C
1 day
Test Coverage
package parser

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "strings"
    "time"

    "github.com/jpillora/backoff"
    "github.com/synapsecns/sanguine/services/explorer/consumer/parser/tokendata"

    "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/fastbridge"
    "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"
    rfqTypes "github.com/synapsecns/sanguine/services/explorer/types/fastbridge"
)

const ethCoinGeckoID = "ethereum"

// RFQParser parsers all rfq logs.
type RFQParser struct {
    // consumerDB is the database to store parsed data in
    consumerDB db.ConsumerDB
    // Filterer is the message Filterer we use to parse events
    Filterer *fastbridge.FastBridgeFilterer
    // messageAddress is the address of the message
    rfqAddress common.Address
    // consumerFetcher is the Fetcher for sender and timestamp
    consumerFetcher fetcher.ScribeFetcher
    // rfqService is the rfq service for getting token symbol information
    rfqService fetcher.RFQService
    // 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
}

// NewRFQParser creates a new RFQParser.
func NewRFQParser(consumerDB db.ConsumerDB, rfqAddress common.Address, consumerFetcher fetcher.ScribeFetcher, rfqService fetcher.RFQService, tokenDataService tokendata.Service, tokenPriceService tokenprice.Service, fromAPI bool) (*RFQParser, error) {
    filterer, err := fastbridge.NewFastBridgeFilterer(rfqAddress, nil)
    if err != nil {
        return nil, fmt.Errorf("could not create %T: %w", fastbridge.FastBridgeFilterer{}, err)
    }
    return &RFQParser{consumerDB, filterer, rfqAddress, consumerFetcher, rfqService, tokenDataService, tokenPriceService, fromAPI}, nil
}

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

// ParseLog log converts an eth log to a rfq event type.
//
//nolint:dupl
func (p *RFQParser) ParseLog(log ethTypes.Log, chainID uint32) (*model.RFQEvent, rfqTypes.EventLog, error) {
    logTopic := log.Topics[0]
    iFace, err := func(log ethTypes.Log) (rfqTypes.EventLog, error) {
        // Get the topic hash safely
        bridgeRequestedTopic, err := fastbridge.Topic(rfqTypes.BridgeRequestedEvent)
        if err == nil && logTopic == bridgeRequestedTopic {
            return p.Filterer.ParseBridgeRequested(log)
        }

        bridgeRelayedTopic, err := fastbridge.Topic(rfqTypes.BridgeRelayedEvent)
        if err == nil && logTopic == bridgeRelayedTopic {
            return p.Filterer.ParseBridgeRelayed(log)
        }
        bridgeProofProvidedTopic, err := fastbridge.Topic(rfqTypes.BridgeProvenEvent)
        if err == nil && logTopic == bridgeProofProvidedTopic {
            return p.Filterer.ParseBridgeProofProvided(log)
        }
        bridgeDepositClaimedTopic, err := fastbridge.Topic(rfqTypes.BridgeClaimedEvent)
        if err == nil && logTopic == bridgeDepositClaimedTopic {
            return p.Filterer.ParseBridgeDepositClaimed(log)
        }
        bridgeDepositRefundedTopic, err := fastbridge.Topic(rfqTypes.BridgeRefundedEvent)
        if err == nil && logTopic == bridgeDepositRefundedTopic {
            return p.Filterer.ParseBridgeDepositRefunded(log)
        }

        logger.Warnf("ErrUnknownTopic in rfq: %s %s chain: %d address: %s",
            log.TxHash, logTopic.String(), chainID, log.Address.Hex())
        return nil, errors.New("unknown topic")
    }(log)

    if err != nil {
        // Switch failed.

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

    // Populate rfq event type so following operations can mature the event data.
    rfqEvent := eventToRFQEvent(iFace, chainID)
    return &rfqEvent, iFace, nil
}

// MatureLogs takes a rfq event and adds data to them.
func (p *RFQParser) MatureLogs(ctx context.Context, rfqEvent *model.RFQEvent, iFace rfqTypes.EventLog, chainID uint32) (interface{}, error) {
    // Get timestamp from consumer
    timeStamp, err := p.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 rfqEvent.
    // This logic will have to be generalized as we support more tokens (we need to programatically find coingecko id based on token address)
    timeStampBig := uint64(*timeStamp)
    rfqEvent.TimeStamp = &timeStampBig

    var curCoinGeckoID string
    tokenAddressStr := common.HexToAddress(rfqEvent.OriginToken).Hex()
    const ethAddress = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"

    switch {
    case strings.EqualFold(tokenAddressStr, ethAddress) || strings.EqualFold(tokenAddressStr, "0x2170Ed0880ac9A755fd29B2688956BD959F933F8"):
        rfqEvent.TokenSymbol = "ETH"
        rfqEvent.TokenDecimal = new(uint8)
        *rfqEvent.TokenDecimal = 18
        curCoinGeckoID = ethCoinGeckoID
    case strings.EqualFold(tokenAddressStr, "0x2cFc85d8E48F8EAB294be644d9E25C3030863003") || strings.EqualFold(tokenAddressStr, "0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1"):
        rfqEvent.TokenSymbol = "WLD"
        rfqEvent.TokenDecimal = new(uint8)
        *rfqEvent.TokenDecimal = 18
        curCoinGeckoID = "worldcoin-wld"
    default:
        rfqEvent.TokenSymbol = "USDC"
        rfqEvent.TokenDecimal = new(uint8)
        *rfqEvent.TokenDecimal = 6
        curCoinGeckoID = usdcCoinGeckoID
    }
    // find the price data for that specific token
    p.applyPriceData(ctx, rfqEvent, curCoinGeckoID)

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

    return rfqEvent, nil
}

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

// applyPriceData applies price data to the rfq event, setting USD values.
func (p *RFQParser) applyPriceData(ctx context.Context, rfqEvent *model.RFQEvent, coinGeckoID string) {
    tokenPrice := p.tokenPriceService.GetPriceData(ctx, int(*rfqEvent.TimeStamp), coinGeckoID)
    if tokenPrice == nil {
        logger.Warnf("RFQ could not get token price for coingeckotoken; assuming price of 1:  %s txhash %s %d", coinGeckoID, rfqEvent.TxHash, rfqEvent.TimeStamp)
        one := 1.0
        tokenPrice = &one
    }
    // We can maybe hardcode this to be the integer of the event type if the second item is incorrect.
    if rfqEvent.EventType == rfqTypes.BridgeRequestedEvent.Int() {
        amountUSD := GetAmountUSD(rfqEvent.OriginAmount, *rfqEvent.TokenDecimal, tokenPrice)
        if amountUSD != nil {
            logger.Warnf("RFQ GetAmountUSD properly found the token price for coingecko token: %s", coinGeckoID)
            rfqEvent.AmountUSD = *amountUSD
        }
    } else if rfqEvent.EventType == rfqTypes.BridgeRelayedEvent.Int() {
        amountUSD := GetAmountUSD(rfqEvent.DestinationAmount, *rfqEvent.TokenDecimal, tokenPrice)
        if amountUSD != nil {
            logger.Warnf("RFQ GetAmountUSD properly found the token price for coingecko token: %s", coinGeckoID)
            rfqEvent.AmountUSD = *amountUSD
        }
    }
}

// eventToRFQEvent stores a message event.
func eventToRFQEvent(event rfqTypes.EventLog, chainID uint32) model.RFQEvent {
    transactionID := event.GetTransactionID()

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

    var formattedChainGas uint8
    if event.GetSendChainGas() != nil {
        formattedChainGas = 1
    } else {
        formattedChainGas = 0
    }

    return model.RFQEvent{
        InsertTime:      uint64(time.Now().UnixNano()),
        ChainID:         chainID,
        TxHash:          event.GetTxHash().String(),
        ContractAddress: event.GetContractAddress().String(),
        BlockNumber:     event.GetBlockNumber(),
        EventType:       event.GetEventType().Int(),
        EventIndex:      event.GetEventIndex(),
        TransactionID:   common.Bytes2Hex(transactionID[:]),

        Recipient:          ToNullString(event.GetTo()),
        Sender:             ToNullString(event.GetSender()),
        Relayer:            ToNullString(event.GetRelayer()),
        FormattedRequest:   formattedRequest,
        OriginChainID:      event.GetOriginChainID(),
        DestinationChainID: event.GetDestChainID(),
        OriginToken:        event.GetOriginToken().String(),
        DestinationToken:   event.GetDestToken().String(),
        OriginAmount:       event.GetOriginAmount(),
        DestinationAmount:  event.GetDestAmount(),
        ChainGas:           formattedChainGas,
        ChainGasAmount:     event.GetChainGasAmount(),
    }
}

func rfqEventToBridgeEvent(rfqEvent model.RFQEvent) model.BridgeEvent {
    // Only convert BridgeRequestedEvent and BridgeRelayedEvent to bridge events
    // Exclude BridgeDepositRefunded, BridgeProofProvided, and BridgeDepositClaimed
    eventType := rfqEvent.EventType
    if eventType != rfqTypes.BridgeRequestedEvent.Int() &&
        eventType != rfqTypes.BridgeRelayedEvent.Int() {
        return model.BridgeEvent{}
    }

    bridgeType := bridgeTypes.BridgeRequestedEvent
    token := rfqEvent.OriginToken
    amount := rfqEvent.OriginAmount
    destinationKappa := rfqEvent.TransactionID

    var kappa *string
    if rfqEvent.EventType == rfqTypes.BridgeRelayedEvent.Int() {
        bridgeType = bridgeTypes.BridgeRelayedEvent
        destinationKappa = ""
        kappa = &rfqEvent.TransactionID
        token = rfqEvent.DestinationToken
        amount = rfqEvent.DestinationAmount
    }

    // Adjust sender and recipient based on null values
    sender := rfqEvent.Sender.String
    recipient := rfqEvent.Recipient
    if sender == "" {
        sender = recipient.String
    } else if recipient.String == "" {
        recipient = sql.NullString{Valid: true, String: sender}
    }

    return model.BridgeEvent{
        InsertTime:       rfqEvent.InsertTime,
        ContractAddress:  rfqEvent.ContractAddress,
        ChainID:          rfqEvent.ChainID,
        EventType:        bridgeType.Int(),
        BlockNumber:      rfqEvent.BlockNumber,
        TxHash:           rfqEvent.TxHash,
        Token:            token,
        Amount:           amount,
        EventIndex:       rfqEvent.EventIndex,
        DestinationKappa: destinationKappa,
        Sender:           sender,

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

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

func (p *RFQParser) 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 rfq converted bridge event", ctx.Err())
        case <-time.After(timeout):
            err := p.consumerDB.StoreEvent(ctx, &bridgeEvent)
            if err != nil {
                timeout = b.Duration()
                continue
            }
            return nil
        }
    }
}