services/explorer/db/sql/reader.go
package sql
import (
"context"
"fmt"
"github.com/benbjohnson/immutable"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/services/explorer/graphql/server/graph/model"
)
/*╔══════════════════════════════════════════════════════════════════════╗*\
▏*║ Generic Read Functions ║*▕
\*╚══════════════════════════════════════════════════════════════════════╝*/
// GetUint64 gets a uint64 from a given query.
func (s *Store) GetUint64(ctx context.Context, query string) (uint64, error) {
var res int64
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return 0, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return uint64(res), nil
}
// GetFloat64 gets a float64 from a given query.
func (s *Store) GetFloat64(ctx context.Context, query string) (float64, error) {
var res float64
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return 0, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetString gets a string from a given query.
func (s *Store) GetString(ctx context.Context, query string) (string, error) {
var res string
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return "", fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetStringArray returns a string array for a given query.
func (s *Store) GetStringArray(ctx context.Context, query string) ([]string, error) {
var res []string
dbTx := s.db.WithContext(ctx).Raw(query + " SETTINGS readonly=1").Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetBridgeEvent returns a bridge event.
func (s *Store) GetBridgeEvent(ctx context.Context, query string) (*BridgeEvent, error) {
var res BridgeEvent
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return &res, nil
}
// GetMVBridgeEvent gets a bridge event from the materialized view table.
func (s *Store) GetMVBridgeEvent(ctx context.Context, query string) (*HybridBridgeEvent, error) {
var res HybridBridgeEvent
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return &res, nil
}
// GetBridgeEvents returns bridge events.
func (s *Store) GetBridgeEvents(ctx context.Context, query string) ([]BridgeEvent, error) {
var res []BridgeEvent
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetAllBridgeEvents returns bridge events.
func (s *Store) GetAllBridgeEvents(ctx context.Context, query string) ([]HybridBridgeEvent, error) {
var res []HybridBridgeEvent
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetDailyTotals returns bridge events.
func (s *Store) GetDailyTotals(ctx context.Context, query string) ([]*model.DateResultByChain, error) {
var res []*model.DateResultByChain
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetAllMessageBusEvents returns message bus events.
func (s *Store) GetAllMessageBusEvents(ctx context.Context, query string) ([]HybridMessageBusEvent, error) {
var res []HybridMessageBusEvent
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read message bus event: %w", dbTx.Error)
}
return res, nil
}
// GetTxCounts returns Tx counts.
func (s *Store) GetTxCounts(ctx context.Context, query string) ([]*model.TransactionCountResult, error) {
var res []*model.TransactionCountResult
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetTokenCounts returns Tx counts.
func (s *Store) GetTokenCounts(ctx context.Context, query string) ([]*model.TokenCountResult, error) {
var res []*model.TokenCountResult
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetRankedChainsByVolume gets ranked chains by volume.
func (s *Store) GetRankedChainsByVolume(ctx context.Context, query string) ([]*model.VolumeByChainID, error) {
var res []*model.VolumeByChainID
dbTx := s.db.WithContext(ctx).Raw(query).Find(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
return res, nil
}
// GetDateResults returns the dya by day data.
func (s *Store) GetDateResults(ctx context.Context, query string) ([]*model.DateResult, error) {
var res []*model.DateResult
dbTx := s.db.WithContext(ctx).Raw(query + " SETTINGS readonly=1").Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get date results: %w", dbTx.Error)
}
return res, nil
}
// GetAddressData returns the address data.
func (s *Store) GetAddressData(ctx context.Context, query string) (float64, float64, int, error) {
type addressData struct {
VolumeTotal float64 `gorm:"column:volumeTotal"`
FeeTotal float64 `gorm:"column:feeTotal"`
TxTotal int `gorm:"column:txTotal"`
}
var res addressData
// var test map[string]interface{}
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&res)
if dbTx.Error != nil {
return 0, 0, 0, fmt.Errorf("failed to get address data: %w", dbTx.Error)
}
return res.VolumeTotal, res.FeeTotal, res.TxTotal, nil
}
// GetAddressChainRanking ranks chains by volume for a given address.
func (s *Store) GetAddressChainRanking(ctx context.Context, query string) ([]*model.AddressChainRanking, error) {
var res []*model.AddressChainRanking
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get address chain ranking: %w", dbTx.Error)
}
return res, nil
}
// GetAddressDailyData gets daily data (number of txs_ for a given address.
func (s *Store) GetAddressDailyData(ctx context.Context, query string) ([]*model.AddressDailyCount, error) {
var res []*model.AddressDailyCount
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get address daily data: %w", dbTx.Error)
}
return res, nil
}
// GetAddressRanking gets AddressRanking for a given query.
func (s *Store) GetAddressRanking(ctx context.Context, query string) ([]*model.AddressRanking, error) {
var res []*model.AddressRanking
dbTx := s.db.WithContext(ctx).Raw(query + " SETTINGS readonly=1").Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to read bridge event: %w", dbTx.Error)
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
// GetLastStoredBlock returns the last stored block for a given chainID and contract.
func (s *Store) GetLastStoredBlock(ctx context.Context, chainID uint32, contract string) (uint64, error) {
query := fmt.Sprintf("SELECT %s FROM last_blocks WHERE %s = %d AND %s = '%s' ORDER BY %s DESC LIMIT 1", BlockNumberFieldName, ChainIDFieldName, chainID, ContractAddressFieldName, contract, BlockNumberFieldName)
lastBlock, err := s.GetUint64(ctx, query)
if err != nil {
return 0, fmt.Errorf("failed to get last block: %w", err)
}
return lastBlock, nil
}
// GetLeaderboard gets the bridge leaderboard.
func (s *Store) GetLeaderboard(ctx context.Context, query string) ([]*model.Leaderboard, error) {
var res []*model.Leaderboard
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get leaderboard: %w", dbTx.Error)
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
// GetPendingByChain gets the bridge leaderboard by chain.
// returns chainid, count
// TODO: test this.
func (s *Store) GetPendingByChain(ctx context.Context) (res *immutable.Map[int, int], err error) {
const query = `SELECT
toInt64(destination_chain_id) as destination_chain_id,
toInt64(COUNTDistinct(destination_kappa)) AS distinct_count
FROM bridge_events
WHERE destination_kappa NOT IN (
SELECT kappa
FROM bridge_events
WHERE kappa != ''
)
GROUP BY destination_chain_id`
type PendingByChain struct {
DestinationChainID *int64 `gorm:"column:destination_chain_id"`
DistinctCount *int64 `gorm:"column:distinct_count"`
}
var pending []*PendingByChain
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&pending)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get pending by chain: %w", dbTx.Error)
}
builder := immutable.NewMapBuilder[int, int](nil)
for _, kvPair := range pending {
builder.Set(int(*kvPair.DestinationChainID), int(*kvPair.DistinctCount))
}
return builder.Map(), nil
}
func (s *Store) GetBlockHeights(ctx context.Context, query string, contractTypeMap map[string]model.ContractType) ([]*model.BlockHeight, error) {
var res []*LastBlock
dbTx := s.db.WithContext(ctx).Raw(query).Scan(&res)
if dbTx.Error != nil {
return nil, fmt.Errorf("failed to get block heights: %w", dbTx.Error)
}
if len(res) == 0 {
return nil, nil
}
var formatted []*model.BlockHeight
for _, block := range res {
chainID := int(block.ChainID)
blockNumber := int(block.BlockNumber)
formatted = append(formatted, &model.BlockHeight{
ChainID: &chainID,
Type: core.PtrTo(contractTypeMap[block.ContractAddress]),
BlockNumber: &blockNumber,
})
}
return formatted, nil
}