services/explorer/consumer/parser/bridgeparser.go
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
}