wormhole-explorer/api/handlers/protocols/repository.go

370 lines
12 KiB
Go

package protocols
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/query"
"github.com/mitchellh/mapstructure"
"github.com/wormhole-foundation/wormhole-explorer/common/dbconsts"
"go.uber.org/zap"
"time"
)
// QueryCoreProtocolTotalStartOfDay Query template for core protocols (cctp and portal_token_bridge) to fetch total values till the start of current day
const QueryCoreProtocolTotalStartOfDay = `
import "date"
startOfCurrentDay = date.truncate(t: now(), unit: 1d)
data = from(bucket: "%s")
|> range(start: 1970-01-01T00:00:00Z,stop:startOfCurrentDay)
|> filter(fn: (r) => r._measurement == "%s" and r.app_id == "%s")
tvt = data
|> filter(fn : (r) => r._field == "total_value_transferred")
|> group()
|> sum()
|> set(key:"_field",value:"total_value_transferred")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
totalMsgs = data
|> filter(fn : (r) => r._field == "total_messages")
|> group()
|> sum()
|> set(key:"_field",value:"total_messages")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
union(tables:[tvt,totalMsgs])
|> set(key:"_time",value:string(v:startOfCurrentDay))
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> set(key:"app_id",value:"%s")
`
// QueryCoreProtocolDeltaSinceStartOfDay calculate delta since the beginning of current day
const QueryCoreProtocolDeltaSinceStartOfDay = `
import "date"
import "types"
ts = date.truncate(t: now(), unit: 1h)
startOfDay = date.truncate(t: now(), unit: 1d)
data = from(bucket: "%s")
|> range(start: startOfDay,stop:ts)
|> filter(fn: (r) => r._measurement == "%s" and r.app_id == "%s")
tvt = data
|> filter(fn : (r) => r._field == "total_value_transferred")
|> group()
|> sum()
|> set(key:"_field",value:"total_value_transferred")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
totalMsgs = data
|> filter(fn : (r) => r._field == "total_messages")
|> group()
|> sum()
|> set(key:"_field",value:"total_messages")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
union(tables:[tvt,totalMsgs])
|> set(key:"_time",value:string(v:startOfDay))
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> set(key:"app_id",value:"%s")
`
// QueryCoreProtocolDeltaLastDay calculate last day delta
const QueryCoreProtocolDeltaLastDay = `
import "date"
import "types"
ts = date.truncate(t: now(), unit: 1h)
yesterday = date.sub(d: 1d, from: ts)
data = from(bucket: "%s")
|> range(start: yesterday,stop:ts)
|> filter(fn: (r) => r._measurement == "%s" and r.app_id == "%s")
tvt = data
|> filter(fn : (r) => r._field == "total_value_transferred")
|> group()
|> sum()
|> set(key:"_field",value:"total_value_transferred")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
totalMsgs = data
|> filter(fn : (r) => r._field == "total_messages")
|> group()
|> sum()
|> set(key:"_field",value:"total_messages")
|> map(fn: (r) => ({r with _value: int(v: r._value)}))
union(tables:[tvt,totalMsgs])
|> set(key:"_time",value:string(v:yesterday))
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> set(key:"app_id",value:"%s")
`
const QueryTemplateProtocolStatsLastDay = `
from(bucket: "%s")
|> range(start: %s, stop: %s)
|> filter(fn: (r) => r._measurement == "%s" and r.protocol == "%s")
|> first()
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
`
const QueryTemplateProtocolStats = `
data = from(bucket: "%s")
|> range(start: -2d)
|> filter(fn: (r) => r._measurement == "%s" and r.protocol == "%s")
totalMsg = data
|> filter(fn: (r) => r._field == "total_messages")
|> sort(columns:["_time"],desc:false)
|> last()
tvl = data
|> filter(fn: (r) => r._field == "total_value_locked")
|> sort(columns:["_time"],desc:false)
|> last()
volume = data
|> filter(fn: (r) => r._field == "volume")
|> sort(columns:["_time"],desc:false)
|> last()
union(tables:[totalMsg,tvl,volume])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
`
const QueryTemplateProtocolActivity = `
data =
from(bucket: "%s")
|> range(start: %s)
|> filter(fn: (r) => r._measurement == "%s" and r.protocol == "%s")
tvs = data
|> filter(fn: (r) => r._field == "total_value_secure")
|> cumulativeSum()
|> last()
tvt = data
|> filter(fn: (r) => r._field == "total_value_transferred")
|> cumulativeSum()
|> last()
volume = data
|> filter(fn: (r) => r._field == "volume")
|> sort(columns:["_time"],desc:false)
|> cumulativeSum()
|> last()
txs = data
|> filter(fn: (r) => r._field == "txs")
|> sort(columns:["_time"],desc:false)
|> cumulativeSum()
|> last()
union(tables:[tvs,tvt,volume,txs])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
`
type Repository struct {
queryAPI QueryDoer
logger *zap.Logger
bucketInfinite string
bucket30d string
coreProtocolMeasurement map[string]struct {
Daily string
Hourly string
}
}
type rowStat struct {
Protocol string `mapstructure:"protocol"`
TotalMessages uint64 `mapstructure:"total_messages"`
TotalValueLocked float64 `mapstructure:"total_value_locked"`
Volume float64 `mapstructure:"volume"`
Time time.Time `mapstructure:"_time"`
}
type intRowStat struct {
Protocol string `mapstructure:"app_id"`
TotalMessages uint64 `mapstructure:"total_messages"`
TotalValueTransferred uint64 `mapstructure:"total_value_transferred"`
}
type intStats struct {
Latest intRowStat
DeltaLast24hr intRowStat
}
type rowActivity struct {
Protocol string `mapstructure:"protocol"`
Time time.Time `mapstructure:"_time"`
TotalUsd float64 `mapstructure:"total_usd"`
TotalValueTransferred float64 `mapstructure:"total_value_transferred"`
TotalValueSecure float64 `mapstructure:"total_value_secure"`
Txs uint64 `mapstructure:"txs"`
}
type stats struct {
Latest rowStat
Last24 rowStat
}
type QueryDoer interface {
Query(ctx context.Context, query string) (QueryResult, error)
}
type queryApiWrapper struct {
qApi api.QueryAPI
}
type QueryResult interface {
Next() bool
Record() *query.FluxRecord
Err() error
Close() error
}
func WrapQueryAPI(qApi api.QueryAPI) QueryDoer {
return &queryApiWrapper{qApi: qApi}
}
func NewRepository(qApi QueryDoer, bucketInfinite, bucket30d string, logger *zap.Logger) *Repository {
return &Repository{
queryAPI: qApi,
bucketInfinite: bucketInfinite,
bucket30d: bucket30d,
logger: logger,
coreProtocolMeasurement: map[string]struct {
Daily string
Hourly string
}{
CCTP: {Daily: dbconsts.CctpStatsMeasurementDaily, Hourly: dbconsts.CctpStatsMeasurementHourly},
PortalTokenBridge: {Daily: dbconsts.TokenBridgeStatsMeasurementDaily, Hourly: dbconsts.TokenBridgeStatsMeasurementHourly},
},
}
}
func (q *queryApiWrapper) Query(ctx context.Context, query string) (QueryResult, error) {
return q.qApi.Query(ctx, query)
}
func (r *Repository) getProtocolStats(ctx context.Context, protocol string) (rowStat, error) {
q := fmt.Sprintf(QueryTemplateProtocolStats, r.bucket30d, dbconsts.ProtocolsStatsMeasurementHourly, protocol)
statsData, err := fetchSingleRecordData[rowStat](r.logger, r.queryAPI, ctx, q, protocol)
if err != nil {
r.logger.Error("error fetching latest daily stats", zap.Error(err))
return rowStat{}, err
}
return rowStat{
Protocol: protocol,
TotalMessages: statsData.TotalMessages,
TotalValueLocked: statsData.TotalValueLocked,
Volume: statsData.Volume,
}, nil
}
func (r *Repository) getProtocolStatsLastDay(ctx context.Context, protocol string) (rowStat, error) {
to := time.Now().UTC().Truncate(24 * time.Hour)
from := to.Add(-24 * time.Hour)
q := fmt.Sprintf(QueryTemplateProtocolStatsLastDay, r.bucket30d, from.Format(time.RFC3339), to.Format(time.RFC3339), dbconsts.ProtocolsStatsMeasurementHourly, protocol)
lastDayData, err := fetchSingleRecordData[rowStat](r.logger, r.queryAPI, ctx, q, protocol)
if err != nil {
r.logger.Error("error fetching last day stats", zap.Error(err))
return rowStat{}, err
}
return lastDayData, nil
}
func (r *Repository) getProtocolActivity(ctx context.Context, protocol string) (rowActivity, error) {
q := fmt.Sprintf(QueryTemplateProtocolActivity, r.bucketInfinite, "1970-01-01T00:00:00Z", dbconsts.ProtocolsActivityMeasurementDaily, protocol)
activityDaily, err := fetchSingleRecordData[rowActivity](r.logger, r.queryAPI, ctx, q, protocol)
if err != nil {
r.logger.Error("error fetching latest daily activity", zap.Error(err))
return rowActivity{}, err
}
q = fmt.Sprintf(QueryTemplateProtocolActivity, r.bucket30d, activityDaily.Time.Format(time.RFC3339), dbconsts.ProtocolsActivityMeasurementHourly, protocol)
activityHourly, err := fetchSingleRecordData[rowActivity](r.logger, r.queryAPI, ctx, q, protocol)
return rowActivity{
Protocol: protocol,
Txs: activityDaily.Txs + activityHourly.Txs,
TotalUsd: activityDaily.TotalUsd + activityHourly.TotalUsd,
TotalValueTransferred: activityDaily.TotalValueTransferred + activityHourly.TotalValueTransferred,
TotalValueSecure: activityDaily.TotalValueSecure + activityHourly.TotalValueSecure,
}, nil
}
// returns latest and last 24 hr for core protocols (cctp and portal_token_bridge)
func (r *Repository) getCoreProtocolStats(ctx context.Context, protocol string) (intStats, error) {
// calculate total values till the start of current day
totalTillCurrentDayQuery := fmt.Sprintf(QueryCoreProtocolTotalStartOfDay, r.bucketInfinite, r.coreProtocolMeasurement[protocol].Daily, protocol, protocol)
totalsUntilToday, err := fetchSingleRecordData[intRowStat](r.logger, r.queryAPI, ctx, totalTillCurrentDayQuery, protocol)
if err != nil {
return intStats{}, err
}
// calculate delta since the beginning of current day
q2 := fmt.Sprintf(QueryCoreProtocolDeltaSinceStartOfDay, r.bucket30d, r.coreProtocolMeasurement[protocol].Hourly, protocol, protocol)
currentDayStats, errCD := fetchSingleRecordData[intRowStat](r.logger, r.queryAPI, ctx, q2, protocol)
if errCD != nil {
return intStats{}, errCD
}
latestTotal := intRowStat{
Protocol: protocol,
TotalMessages: totalsUntilToday.TotalMessages + currentDayStats.TotalMessages,
TotalValueTransferred: totalsUntilToday.TotalValueTransferred + currentDayStats.TotalValueTransferred,
}
result := intStats{
Latest: latestTotal,
}
// calculate last day delta
q3 := fmt.Sprintf(QueryCoreProtocolDeltaLastDay, r.bucket30d, r.coreProtocolMeasurement[protocol].Hourly, protocol, protocol)
deltaYesterdayStats, errQ3 := fetchSingleRecordData[intRowStat](r.logger, r.queryAPI, ctx, q3, protocol)
if errQ3 != nil {
return result, errQ3
}
result.DeltaLast24hr = deltaYesterdayStats
return result, nil
}
func fetchSingleRecordData[T any](logger *zap.Logger, queryAPI QueryDoer, ctx context.Context, query, protocol string) (T, error) {
var res T
result, err := queryAPI.Query(ctx, query)
if err != nil {
logger.Error("error executing query to fetch data", zap.Error(err), zap.String("protocol", protocol), zap.String("query", query))
return res, err
}
defer result.Close()
if !result.Next() {
if result.Err() != nil {
logger.Error("error reading query response", zap.Error(result.Err()), zap.String("protocol", protocol), zap.String("query", query))
return res, result.Err()
}
logger.Info("empty query response", zap.String("protocol", protocol), zap.String("query", query))
return res, err
}
err = mapstructure.Decode(result.Record().Values(), &res)
return res, err
}