Add 24h volume to scorecards (#274)
### Summary This pull request adds volume metrics to influxdb. Also, it adds the 24h volume metric to `GET /api/v1/scorecards`. Tracking issues: https://github.com/wormhole-foundation/wormhole-explorer/issues/221, https://github.com/wormhole-foundation/wormhole-explorer/issues/280 ### Changes: * The `parser` service no longer generates metrics for influxdb. All metrics-related code was removed from that service, that code was moved to the analytics service instead. * New volume metrics were added to the analytics service. * The notional cache was modified to use token names (i.e.: ticker symbols) as keys instead of chain IDs. * The notional cache reader was moved to the `common/client/cache` package. * A little bit of duplicated code between the cache reader and writer was removed. * A 24h volume metric was added to `GET /api/v1/scorecards`. * A dictionary that stores token metadata was added under `common/domain/tokenbridge.go`. More tokens will be added to it in the near future.
This commit is contained in:
parent
fd51f0a819
commit
d9d49ec919
|
@ -1 +1,3 @@
|
||||||
__debug_bin
|
__debug_bin
|
||||||
|
.env
|
||||||
|
analytic
|
||||||
|
|
|
@ -59,7 +59,10 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a metrics instance
|
// create a metrics instance
|
||||||
metric := metric.New(influxCli, config.InfluxOrganization, config.InfluxBucket, logger)
|
metric, err := metric.New(rootCtx, influxCli, config, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to create metrics instance", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
// create and start a consumer.
|
// create and start a consumer.
|
||||||
vaaConsumeFunc := newVAAConsume(rootCtx, config, logger)
|
vaaConsumeFunc := newVAAConsume(rootCtx, config, logger)
|
||||||
|
|
|
@ -24,6 +24,8 @@ type Configuration struct {
|
||||||
InfluxBucket string `env:"INFLUX_BUCKET"`
|
InfluxBucket string `env:"INFLUX_BUCKET"`
|
||||||
PprofEnabled bool `env:"PPROF_ENABLED,default=false"`
|
PprofEnabled bool `env:"PPROF_ENABLED,default=false"`
|
||||||
P2pNetwork string `env:"P2P_NETWORK,required"`
|
P2pNetwork string `env:"P2P_NETWORK,required"`
|
||||||
|
CacheURL string `env:"CACHE_URL"`
|
||||||
|
CacheChannel string `env:"CACHE_CHANNEL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a configuration with the values from .env file and environment variables.
|
// New creates a configuration with the values from .env file and environment variables.
|
||||||
|
|
|
@ -2,31 +2,88 @@ package metric
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
"github.com/wormhole-foundation/wormhole-explorer/analytic/config"
|
||||||
|
wormscanCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
|
||||||
|
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
||||||
|
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metric definition.
|
// Metric definition.
|
||||||
type Metric struct {
|
type Metric struct {
|
||||||
influxCli influxdb2.Client
|
influxCli influxdb2.Client
|
||||||
writeApi api.WriteAPIBlocking
|
writeApi api.WriteAPIBlocking
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
notionalCache wormscanNotionalCache.NotionalLocalCacheReadable
|
||||||
}
|
}
|
||||||
|
|
||||||
// New create a new *Metric.
|
// New create a new *Metric.
|
||||||
func New(influxCli influxdb2.Client, organization, bucket string, logger *zap.Logger) *Metric {
|
func New(
|
||||||
writeAPI := influxCli.WriteAPIBlocking(organization, bucket)
|
ctx context.Context,
|
||||||
return &Metric{influxCli: influxCli, writeApi: writeAPI, logger: logger}
|
influxCli influxdb2.Client,
|
||||||
|
cfg *config.Configuration,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) (*Metric, error) {
|
||||||
|
|
||||||
|
writeAPI := influxCli.WriteAPIBlocking(cfg.InfluxOrganization, cfg.InfluxBucket)
|
||||||
|
|
||||||
|
_, notionalCache, err := newCache(ctx, cfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Metric{
|
||||||
|
influxCli: influxCli,
|
||||||
|
writeApi: writeAPI,
|
||||||
|
logger: logger,
|
||||||
|
notionalCache: notionalCache,
|
||||||
|
}
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCache(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg *config.Configuration,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) (wormscanCache.CacheReadable, wormscanNotionalCache.NotionalLocalCacheReadable, error) {
|
||||||
|
|
||||||
|
// use a distributed cache and for notional a pubsub to sync local cache.
|
||||||
|
redisClient := redis.NewClient(&redis.Options{Addr: cfg.CacheURL})
|
||||||
|
|
||||||
|
// get cache client
|
||||||
|
cacheClient, err := wormscanCache.NewCacheClient(redisClient, true /*enabled*/, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create cache client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get notional cache client and init load to local cache
|
||||||
|
notionalCache, err := wormscanNotionalCache.NewNotionalCache(ctx, redisClient, cfg.CacheChannel, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create notional cache client: %w", err)
|
||||||
|
}
|
||||||
|
notionalCache.Init(ctx)
|
||||||
|
|
||||||
|
return cacheClient, notionalCache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push implement MetricPushFunc definition.
|
// Push implement MetricPushFunc definition.
|
||||||
func (m *Metric) Push(ctx context.Context, vaa *vaa.VAA) error {
|
func (m *Metric) Push(ctx context.Context, vaa *sdk.VAA) error {
|
||||||
return m.vaaCountMeasurement(ctx, vaa)
|
|
||||||
|
err1 := m.vaaCountMeasurement(ctx, vaa)
|
||||||
|
err2 := m.volumeMeasurement(ctx, vaa)
|
||||||
|
|
||||||
|
//TODO if we had go 1.20, we could just use `errors.Join(err1, err2)` here.
|
||||||
|
return fmt.Errorf("err1=%w, err2=%w", err1, err2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close influx client.
|
// Close influx client.
|
||||||
|
@ -34,19 +91,19 @@ func (m *Metric) Close() {
|
||||||
m.influxCli.Close()
|
m.influxCli.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// vaaCountMeasurement handle the push of metric point for measurement vaa_count.
|
// vaaCountMeasurement creates a new point for the `vaa_count` measurement.
|
||||||
func (m *Metric) vaaCountMeasurement(ctx context.Context, vaa *vaa.VAA) error {
|
func (m *Metric) vaaCountMeasurement(ctx context.Context, vaa *sdk.VAA) error {
|
||||||
|
|
||||||
measurement := "vaa_count"
|
const measurement = "vaa_count"
|
||||||
|
|
||||||
// Create a new point for the `vaa_count` measurement.
|
// Create a new point
|
||||||
point := influxdb2.
|
point := influxdb2.
|
||||||
NewPointWithMeasurement(measurement).
|
NewPointWithMeasurement(measurement).
|
||||||
AddTag("chain_id", strconv.Itoa(int(vaa.EmitterChain))).
|
AddTag("chain_id", strconv.Itoa(int(vaa.EmitterChain))).
|
||||||
AddField("count", 1).
|
AddField("count", 1).
|
||||||
SetTime(vaa.Timestamp.Add(time.Nanosecond * time.Duration(vaa.Sequence)))
|
SetTime(vaa.Timestamp.Add(time.Nanosecond * time.Duration(vaa.Sequence)))
|
||||||
|
|
||||||
// Write the point to influx.
|
// Write the point to influx
|
||||||
err := m.writeApi.WritePoint(ctx, point)
|
err := m.writeApi.WritePoint(ctx, point)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Error("failed to write metric",
|
m.logger.Error("failed to write metric",
|
||||||
|
@ -59,3 +116,111 @@ func (m *Metric) vaaCountMeasurement(ctx context.Context, vaa *vaa.VAA) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// volumeMeasurement creates a new point for the `vaa_volume` measurement.
|
||||||
|
func (m *Metric) volumeMeasurement(ctx context.Context, vaa *sdk.VAA) error {
|
||||||
|
|
||||||
|
const measurement = "vaa_volume"
|
||||||
|
|
||||||
|
// Decode the VAA payload
|
||||||
|
//
|
||||||
|
// If the VAA didn't come from the portal token bridge, we just skip it.
|
||||||
|
payload, err := sdk.DecodeTransferPayloadHdr(vaa.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the token metadata
|
||||||
|
//
|
||||||
|
// This is complementary data about the token that is not present in the VAA itself.
|
||||||
|
tokenMeta, ok := domain.GetTokenMetadata(payload.OriginChain, "0x"+payload.OriginAddress.String())
|
||||||
|
if !ok {
|
||||||
|
m.logger.Warn("found no token metadata for VAA",
|
||||||
|
zap.String("vaaId", vaa.MessageID()),
|
||||||
|
zap.String("tokenAddress", payload.OriginAddress.String()),
|
||||||
|
zap.Uint16("tokenChain", uint16(payload.OriginChain)),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the amount to 8 decimals
|
||||||
|
amount := payload.Amount
|
||||||
|
if tokenMeta.Decimals < 8 {
|
||||||
|
|
||||||
|
// factor = 10 ^ (8 - tokenMeta.Decimals)
|
||||||
|
var factor big.Int
|
||||||
|
factor.Exp(big.NewInt(10), big.NewInt(int64(8-tokenMeta.Decimals)), nil)
|
||||||
|
|
||||||
|
amount = amount.Mul(amount, &factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to obtain the token notional value from the cache
|
||||||
|
notional, err := m.notionalCache.Get(tokenMeta.UnderlyingSymbol)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("failed to obtain notional for this token",
|
||||||
|
zap.String("vaaId", vaa.MessageID()),
|
||||||
|
zap.String("tokenAddress", payload.OriginAddress.String()),
|
||||||
|
zap.Uint16("tokenChain", uint16(payload.OriginChain)),
|
||||||
|
zap.Any("tokenMetadata", tokenMeta),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the notional value to an integer with an implicit precision of 8 decimals
|
||||||
|
notionalBigInt, err := floatToBigInt(notional.NotionalUsd)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the volume, with an implicit precision of 8 decimals
|
||||||
|
var volume big.Int
|
||||||
|
volume.Mul(amount, notionalBigInt)
|
||||||
|
volume.Div(&volume, big.NewInt(1e8))
|
||||||
|
|
||||||
|
m.logger.Info("Pushing volume metrics",
|
||||||
|
zap.String("vaaId", vaa.MessageID()),
|
||||||
|
zap.String("amount", amount.String()),
|
||||||
|
zap.String("notional", notionalBigInt.String()),
|
||||||
|
zap.String("volume", volume.String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a data point with volume-related fields
|
||||||
|
//
|
||||||
|
// We're converting big integers to int64 because influxdb doesn't support bigint/numeric types.
|
||||||
|
point := influxdb2.NewPointWithMeasurement(measurement).
|
||||||
|
AddTag("chain_source_id", fmt.Sprintf("%d", payload.OriginChain)).
|
||||||
|
AddTag("chain_destination_id", fmt.Sprintf("%d", payload.TargetChain)).
|
||||||
|
AddTag("app_id", domain.AppIdPortalTokenBridge).
|
||||||
|
AddField("amount", amount.Int64()).
|
||||||
|
AddField("notional", notionalBigInt.Int64()).
|
||||||
|
AddField("volume", volume.Int64()).
|
||||||
|
SetTime(vaa.Timestamp)
|
||||||
|
|
||||||
|
// Write the point to influx
|
||||||
|
err = m.writeApi.WritePoint(ctx, point)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toInt converts a float64 into a big.Int with 8 decimals of implicit precision.
|
||||||
|
//
|
||||||
|
// If we ever upgrade the notional cache to store prices as big integers,
|
||||||
|
// this gnarly function won't be needed anymore.
|
||||||
|
func floatToBigInt(f float64) (*big.Int, error) {
|
||||||
|
|
||||||
|
integral, frac := math.Modf(f)
|
||||||
|
|
||||||
|
strIntegral := strconv.FormatFloat(integral, 'f', 0, 64)
|
||||||
|
strFrac := fmt.Sprintf("%.8f", frac)[2:]
|
||||||
|
|
||||||
|
i, err := strconv.ParseInt(strIntegral+strFrac, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return big.NewInt(i), nil
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ type Scorecards struct {
|
||||||
|
|
||||||
// Number of VAAs emitted in the last 24 hours (does not include Pyth messages).
|
// Number of VAAs emitted in the last 24 hours (does not include Pyth messages).
|
||||||
TxCount24h string
|
TxCount24h string
|
||||||
|
|
||||||
|
// Volume transferred through the token bridge in the last 24 hours, in USD.
|
||||||
|
Volume24h string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlobalTransactionDoc struct {
|
type GlobalTransactionDoc struct {
|
||||||
|
|
|
@ -60,6 +60,16 @@ from(bucket: "%s")
|
||||||
|> count()
|
|> count()
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const queryTemplateVolume24h = `
|
||||||
|
from(bucket: "%s")
|
||||||
|
|> range(start: -24h)
|
||||||
|
|> filter(fn: (r) => r._measurement == "vaa_volume")
|
||||||
|
|> filter(fn:(r) => r._field == "volume")
|
||||||
|
|> drop(columns: ["_measurement", "app_id", "chain_destination_id", "chain_source_id", "symbol"])
|
||||||
|
|> sum(column: "_value")
|
||||||
|
|> toString()
|
||||||
|
`
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
influxCli influxdb2.Client
|
influxCli influxdb2.Client
|
||||||
queryAPI api.QueryAPI
|
queryAPI api.QueryAPI
|
||||||
|
@ -119,20 +129,28 @@ func (r *Repository) buildFindVolumeQuery(q *ChainActivityQuery) string {
|
||||||
|
|
||||||
func (r *Repository) GetScorecards(ctx context.Context) (*Scorecards, error) {
|
func (r *Repository) GetScorecards(ctx context.Context) (*Scorecards, error) {
|
||||||
|
|
||||||
totalTxCount, err := r.getTotalTxCount(ctx)
|
//TODO the underlying query in this code is not using pre-summarized data.
|
||||||
if err != nil {
|
// We should fix that before re-enabling the metric.
|
||||||
r.logger.Error("failed to query total transaction count", zap.Error(err))
|
//totalTxCount, err := r.getTotalTxCount(ctx)
|
||||||
}
|
//if err != nil {
|
||||||
|
// return nil, fmt.Errorf("failed to query all-time tx count")
|
||||||
|
//}
|
||||||
|
|
||||||
txCount24h, err := r.getTxCount24h(ctx)
|
txCount24h, err := r.getTxCount24h(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query 24h transactions: %w", err)
|
return nil, fmt.Errorf("failed to query 24h transactions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume24h, err := r.getVolume24h(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query 24h volume: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// build the result and return
|
// build the result and return
|
||||||
scorecards := Scorecards{
|
scorecards := Scorecards{
|
||||||
TotalTxCount: totalTxCount,
|
//TotalTxCount: totalTxCount,
|
||||||
TxCount24h: txCount24h,
|
TxCount24h: txCount24h,
|
||||||
|
Volume24h: volume24h,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &scorecards, nil
|
return &scorecards, nil
|
||||||
|
@ -194,6 +212,44 @@ func (r *Repository) getTxCount24h(ctx context.Context) (string, error) {
|
||||||
return fmt.Sprint(row.Value), nil
|
return fmt.Sprint(row.Value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) getVolume24h(ctx context.Context) (string, error) {
|
||||||
|
|
||||||
|
// query 24h volume
|
||||||
|
query := fmt.Sprintf(queryTemplateVolume24h, r.bucket)
|
||||||
|
result, err := r.queryAPI.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("failed to query 24h volume", zap.Error(err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result.Err() != nil {
|
||||||
|
r.logger.Error("24h volume query result has errors", zap.Error(err))
|
||||||
|
return "", result.Err()
|
||||||
|
}
|
||||||
|
if !result.Next() {
|
||||||
|
return "", errors.New("expected at least one record in 24h volume query result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserialize the row returned
|
||||||
|
row := struct {
|
||||||
|
Value string `mapstructure:"_value"`
|
||||||
|
}{}
|
||||||
|
if err := mapstructure.Decode(result.Record().Values(), &row); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode 24h volume count query response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is less than 1 USD un volume, round it down to 0 to make math simpler in the next step
|
||||||
|
l := len(row.Value)
|
||||||
|
if l < 9 {
|
||||||
|
return "0.00000000", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn the integer amount into a decimal.
|
||||||
|
// The number always has 8 decimals, so we just need to insert a dot 8 digits from the end.
|
||||||
|
volume := row.Value[:l-8] + "." + row.Value[l-8:]
|
||||||
|
|
||||||
|
return volume, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetTransactionCount get the last transactions.
|
// GetTransactionCount get the last transactions.
|
||||||
func (r *Repository) GetTransactionCount(ctx context.Context, q *TransactionCountQuery) ([]TransactionCountResult, error) {
|
func (r *Repository) GetTransactionCount(ctx context.Context, q *TransactionCountQuery) ([]TransactionCountResult, error) {
|
||||||
query := r.buildLastTrxQuery(q)
|
query := r.buildLastTrxQuery(q)
|
||||||
|
|
|
@ -2,15 +2,15 @@ package vaa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/internal/cache"
|
|
||||||
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
|
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
|
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/response"
|
"github.com/wormhole-foundation/wormhole-explorer/api/response"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/types"
|
"github.com/wormhole-foundation/wormhole-explorer/api/types"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
@ -24,7 +24,14 @@ type Service struct {
|
||||||
|
|
||||||
// NewService creates a new VAA Service.
|
// NewService creates a new VAA Service.
|
||||||
func NewService(r *Repository, getCacheFunc cache.CacheGetFunc, logger *zap.Logger) *Service {
|
func NewService(r *Repository, getCacheFunc cache.CacheGetFunc, logger *zap.Logger) *Service {
|
||||||
return &Service{repo: r, getCacheFunc: getCacheFunc, logger: logger.With(zap.String("module", "VaaService"))}
|
|
||||||
|
s := Service{
|
||||||
|
repo: r,
|
||||||
|
getCacheFunc: getCacheFunc,
|
||||||
|
logger: logger.With(zap.String("module", "VaaService")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAllParams passes input data to the function `FindAll`.
|
// FindAllParams passes input data to the function `FindAll`.
|
||||||
|
@ -188,10 +195,12 @@ func (s *Service) discardVaaNotIndexed(ctx context.Context, chain vaa.ChainID, e
|
||||||
key := fmt.Sprintf("%s:%d:%s", "wormscan:vaa-max-sequence", chain, emitter.Hex())
|
key := fmt.Sprintf("%s:%d:%s", "wormscan:vaa-max-sequence", chain, emitter.Hex())
|
||||||
sequence, err := s.getCacheFunc(ctx, key)
|
sequence, err := s.getCacheFunc(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errs.ErrInternalError) {
|
if errors.Is(err, cache.ErrInternal) {
|
||||||
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
||||||
s.logger.Error("error getting value from cache",
|
s.logger.Error("encountered an internal error while getting value from cache",
|
||||||
zap.Error(err), zap.String("requestID", requestID))
|
zap.Error(err),
|
||||||
|
zap.String("requestID", requestID),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,6 @@ import (
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
"github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa"
|
||||||
wormscanCache "github.com/wormhole-foundation/wormhole-explorer/api/internal/cache"
|
|
||||||
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/api/internal/cache/notional"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/internal/config"
|
"github.com/wormhole-foundation/wormhole-explorer/api/internal/config"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/internal/db"
|
"github.com/wormhole-foundation/wormhole-explorer/api/internal/db"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/middleware"
|
"github.com/wormhole-foundation/wormhole-explorer/api/middleware"
|
||||||
|
@ -37,6 +35,8 @@ import (
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian"
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/guardian"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan"
|
"github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan"
|
||||||
rpcApi "github.com/wormhole-foundation/wormhole-explorer/api/rpc"
|
rpcApi "github.com/wormhole-foundation/wormhole-explorer/api/rpc"
|
||||||
|
wormscanCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
|
||||||
|
wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||||
xlogger "github.com/wormhole-foundation/wormhole-explorer/common/logger"
|
xlogger "github.com/wormhole-foundation/wormhole-explorer/common/logger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
|
@ -70,13 +70,15 @@ func (c *Controller) GetScorecards(ctx *fiber.Ctx) error {
|
||||||
// Query indicators from the database
|
// Query indicators from the database
|
||||||
scorecards, err := c.srv.GetScorecards(ctx.Context())
|
scorecards, err := c.srv.GetScorecards(ctx.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.logger.Error("failed to get scorecards", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert indicators to the response model
|
// Convert indicators to the response model
|
||||||
response := ScorecardsResponse{
|
response := ScorecardsResponse{
|
||||||
TxCount24h: scorecards.TxCount24h,
|
|
||||||
TotalTxCount: scorecards.TotalTxCount,
|
TotalTxCount: scorecards.TotalTxCount,
|
||||||
|
TxCount24h: scorecards.TxCount24h,
|
||||||
|
Volume24h: scorecards.Volume24h,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.JSON(response)
|
return ctx.JSON(response)
|
||||||
|
|
|
@ -24,18 +24,19 @@ type ChainActivity struct {
|
||||||
type ScorecardsResponse struct {
|
type ScorecardsResponse struct {
|
||||||
//TODO: we don't have the data for these fields yet, uncomment as the data becomes available.
|
//TODO: we don't have the data for these fields yet, uncomment as the data becomes available.
|
||||||
|
|
||||||
//TVL string `json:"tvl"`
|
// Number of VAAs emitted in the last 24 hours (includes Pyth messages).
|
||||||
|
//Messages24h string `json:"24h_messages"`
|
||||||
//TotalVolume string `json:"total_volume"`
|
|
||||||
|
|
||||||
// Number of VAAs emitted since the creation of the network (does not include Pyth messages)
|
// Number of VAAs emitted since the creation of the network (does not include Pyth messages)
|
||||||
TotalTxCount string `json:"total_tx_count,omitempty"`
|
TotalTxCount string `json:"total_tx_count,omitempty"`
|
||||||
|
|
||||||
//Volume24h string `json:"24h_volume"`
|
//TotalVolume string `json:"total_volume"`
|
||||||
|
|
||||||
|
//TVL string `json:"tvl"`
|
||||||
|
|
||||||
// Number of VAAs emitted in the last 24 hours (does not include Pyth messages).
|
// Number of VAAs emitted in the last 24 hours (does not include Pyth messages).
|
||||||
TxCount24h string `json:"24h_tx_count"`
|
TxCount24h string `json:"24h_tx_count"`
|
||||||
|
|
||||||
// Number of VAAs emitted in the last 24 hours (includes Pyth messages).
|
// Volume transferred through the token bridge in the last 24 hours, in USD.
|
||||||
//Messages24h string `json:"24h_messages"`
|
Volume24h string `json:"24h_volume"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrCacheNotEnabled = errors.New("CACHE NOT ENABLED")
|
var (
|
||||||
|
ErrCacheNotEnabled = errors.New("CACHE NOT ENABLED")
|
||||||
|
ErrNotFound = errors.New("KEY NOT FOUND IN CACHE")
|
||||||
|
ErrInternal = errors.New("INTERNAL CACHE ERROR")
|
||||||
|
)
|
||||||
|
|
||||||
// CacheClient redis cache client.
|
// CacheClient redis cache client.
|
||||||
type CacheClient struct {
|
type CacheClient struct {
|
||||||
|
@ -52,9 +55,9 @@ func (c *CacheClient) Get(ctx context.Context, key string) (string, error) {
|
||||||
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
requestID := fmt.Sprintf("%v", ctx.Value("requestid"))
|
||||||
c.logger.Error("key does not exist in cache",
|
c.logger.Error("key does not exist in cache",
|
||||||
zap.Error(err), zap.String("key", key), zap.String("requestID", requestID))
|
zap.Error(err), zap.String("key", key), zap.String("requestID", requestID))
|
||||||
return "", errs.ErrNotFound
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
return "", errs.ErrInternalError
|
return "", ErrInternal
|
||||||
}
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
|
@ -2,8 +2,6 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DummyCacheClient dummy cache client.
|
// DummyCacheClient dummy cache client.
|
||||||
|
@ -18,7 +16,7 @@ func NewDummyCacheClient() *DummyCacheClient {
|
||||||
// Get get method is a dummy method that always does not find the cache.
|
// Get get method is a dummy method that always does not find the cache.
|
||||||
// Use this Get function when run development enviroment
|
// Use this Get function when run development enviroment
|
||||||
func (d *DummyCacheClient) Get(ctx context.Context, key string) (string, error) {
|
func (d *DummyCacheClient) Get(ctx context.Context, key string) (string, error) {
|
||||||
return "", errs.ErrNotFound
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dummy cache client.
|
// Close dummy cache client.
|
|
@ -9,13 +9,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
wormscanNotionalCacheKeyRegex = "*WORMSCAN:NOTIONAL:CHAIN_ID:*"
|
|
||||||
wormscanNotionalUpdated = "NOTIONAL_UPDATED"
|
wormscanNotionalUpdated = "NOTIONAL_UPDATED"
|
||||||
|
wormscanNotionalCacheKeyRegex = "*WORMSCAN:NOTIONAL:SYMBOL:*"
|
||||||
|
KeyFormatString = "WORMSCAN:NOTIONAL:SYMBOL:%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -25,16 +25,23 @@ var (
|
||||||
|
|
||||||
// NotionalLocalCacheReadable is the interface for notional local cache.
|
// NotionalLocalCacheReadable is the interface for notional local cache.
|
||||||
type NotionalLocalCacheReadable interface {
|
type NotionalLocalCacheReadable interface {
|
||||||
Get(chainID vaa.ChainID) (NotionalCacheField, error)
|
Get(symbol string) (PriceData, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotionalCacheField is the notional value of assets in cache.
|
// PriceData is the notional value of assets in cache.
|
||||||
type NotionalCacheField struct {
|
type PriceData struct {
|
||||||
NotionalUsd float64 `json:"notional_usd"`
|
NotionalUsd float64 `json:"notional_usd"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||||
|
//
|
||||||
|
// This function is used when the notional job writes data to redis.
|
||||||
|
func (p PriceData) MarshalBinary() ([]byte, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
// NotionalCacheClient redis cache client.
|
// NotionalCacheClient redis cache client.
|
||||||
type NotionalCache struct {
|
type NotionalCache struct {
|
||||||
client *redis.Client
|
client *redis.Client
|
||||||
|
@ -87,7 +94,7 @@ func (c *NotionalCache) loadCache(ctx context.Context) error {
|
||||||
|
|
||||||
// Get notional value from keys
|
// Get notional value from keys
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
var field NotionalCacheField
|
var field PriceData
|
||||||
value, err := c.client.Get(ctx, key).Result()
|
value, err := c.client.Get(ctx, key).Result()
|
||||||
json.Unmarshal([]byte(value), &field)
|
json.Unmarshal([]byte(value), &field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -125,11 +132,11 @@ func (c *NotionalCache) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get notional cache value.
|
// Get notional cache value.
|
||||||
func (c *NotionalCache) Get(chainID vaa.ChainID) (NotionalCacheField, error) {
|
func (c *NotionalCache) Get(symbol string) (PriceData, error) {
|
||||||
var notional NotionalCacheField
|
var notional PriceData
|
||||||
|
|
||||||
// get notional cache key
|
// get notional cache key
|
||||||
key := fmt.Sprintf("WORMSCAN:NOTIONAL:CHAIN_ID:%d", chainID)
|
key := fmt.Sprintf(KeyFormatString, symbol)
|
||||||
|
|
||||||
// get notional cache value
|
// get notional cache value
|
||||||
field, ok := c.notionalMap.Load(key)
|
field, ok := c.notionalMap.Load(key)
|
||||||
|
@ -138,11 +145,11 @@ func (c *NotionalCache) Get(chainID vaa.ChainID) (NotionalCacheField, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert any field to NotionalCacheField
|
// convert any field to NotionalCacheField
|
||||||
notional, ok = field.(NotionalCacheField)
|
notional, ok = field.(PriceData)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.logger.Error("invalid notional cache field",
|
c.logger.Error("invalid notional cache field",
|
||||||
zap.Any("field", field),
|
zap.Any("field", field),
|
||||||
zap.Any("chainID", chainID))
|
zap.String("symbol", symbol))
|
||||||
return notional, ErrInvalidCacheField
|
return notional, ErrInvalidCacheField
|
||||||
}
|
}
|
||||||
return notional, nil
|
return notional, nil
|
|
@ -1,9 +1,5 @@
|
||||||
package notional
|
package notional
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DummyNotionalCache is a dummy notional cache.
|
// DummyNotionalCache is a dummy notional cache.
|
||||||
type DummyNotionalCache struct {
|
type DummyNotionalCache struct {
|
||||||
}
|
}
|
||||||
|
@ -14,8 +10,8 @@ func NewDummyNotionalCache() *DummyNotionalCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get get notional cache value.
|
// Get get notional cache value.
|
||||||
func (c *DummyNotionalCache) Get(chainID vaa.ChainID) (NotionalCacheField, error) {
|
func (c *DummyNotionalCache) Get(symbol string) (PriceData, error) {
|
||||||
return NotionalCacheField{}, nil
|
return PriceData{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the dummy cache.
|
// Close the dummy cache.
|
|
@ -0,0 +1,68 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenMetadata contains information about a token supported by Portal Token Bridge.
|
||||||
|
type TokenMetadata struct {
|
||||||
|
// UnderlyingSymbol is the name that crypto exchanges use to list the underlying asset represented by this token.
|
||||||
|
// For example, the underlying symbol of the token "WFTM (wrapped fantom)" is "FTM".
|
||||||
|
UnderlyingSymbol string
|
||||||
|
Decimals uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenMetadata returns information about a token identified by the pair (tokenChain, tokenAddr).
|
||||||
|
func GetTokenMetadata(tokenChain sdk.ChainID, tokenAddr string) (*TokenMetadata, bool) {
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%d-%s", tokenChain, tokenAddr)
|
||||||
|
|
||||||
|
result, ok := tokenMetadata[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The variable `result` is a copy of the value in the map,
|
||||||
|
// so we can safely return it without worrying about it being modified.
|
||||||
|
return &result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenMetadata contains information about some of the tokens supported by Portal Token Bridge.
|
||||||
|
//
|
||||||
|
// The map is indexed by "<tokenChain>-<tokenAddress>", which you can find on Token Bridge transfer payloads.
|
||||||
|
var tokenMetadata = map[string]TokenMetadata{
|
||||||
|
// ETH - Ether (Portal)
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// * https://api.staging.wormscan.io/api/v1/vaas/1/ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5/288088?parsedPayload=true
|
||||||
|
"2-0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
|
||||||
|
UnderlyingSymbol: "ETH",
|
||||||
|
Decimals: 8,
|
||||||
|
},
|
||||||
|
// UST (Wormhole)
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// * https://api.staging.wormscan.io/api/v1/vaas/2/0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585/111492?parsedPayload=true
|
||||||
|
"3-0x0100000000000000000000000000000000000000000000000000000075757364": {
|
||||||
|
UnderlyingSymbol: "UST",
|
||||||
|
Decimals: 8,
|
||||||
|
},
|
||||||
|
// Binance-Peg BSC-USD
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// * https://api.staging.wormscan.io/api/v1/vaas/4/000000000000000000000000b6f6d86a8f9879a9c87f643768d9efc38c1da6e7/242342?parsedPayload=true
|
||||||
|
"4-0x00000000000000000000000055d398326f99059ff775485246999027b3197955": {
|
||||||
|
UnderlyingSymbol: "BUSD",
|
||||||
|
Decimals: 8,
|
||||||
|
},
|
||||||
|
// WFTM - Wrapped Fantom
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// * https://api.staging.wormscan.io/api/v1/vaas/10/0000000000000000000000007c9fc5741288cdfdd83ceb07f3ea7e22618d79d2/25144?parsedPayload=true
|
||||||
|
"10-0x00000000000000000000000021be370d5312f44cb42ce377bc9b8a0cef1a4c83": {
|
||||||
|
UnderlyingSymbol: "FTM",
|
||||||
|
Decimals: 8,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
jobs/main
|
|
@ -82,7 +82,12 @@ func GetChainIDs(p2pNetwork string) []string {
|
||||||
"optimism",
|
"optimism",
|
||||||
"xpla",
|
"xpla",
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"base-protocol"}
|
"base-protocol",
|
||||||
|
"tether",
|
||||||
|
"usd-coin",
|
||||||
|
"binance-usd",
|
||||||
|
"terrausd-wormhole",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: define chains ids for testnet.
|
// TODO: define chains ids for testnet.
|
||||||
return []string{}
|
return []string{}
|
||||||
|
|
|
@ -2,18 +2,16 @@
|
||||||
package notional
|
package notional
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis"
|
"github.com/go-redis/redis"
|
||||||
|
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/jobs/internal/coingecko"
|
"github.com/wormhole-foundation/wormhole-explorer/jobs/internal/coingecko"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NotionalCacheKey is the cache key for notional value by chainID
|
type Symbol string
|
||||||
const NotionalCacheKey = "WORMSCAN:NOTIONAL:CHAIN_ID:%d"
|
|
||||||
|
|
||||||
// NotionalJob is the job to get the notional value of assets.
|
// NotionalJob is the job to get the notional value of assets.
|
||||||
type NotionalJob struct {
|
type NotionalJob struct {
|
||||||
|
@ -52,7 +50,7 @@ func (j *NotionalJob) Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert notionals with coingecko assets ids to notionals with wormhole chainIDs.
|
// convert notionals with coingecko assets ids to notionals with wormhole chainIDs.
|
||||||
notionals := convertToWormholeChainIDs(coingeckoNotionals)
|
notionals := convertToSymbols(coingeckoNotionals)
|
||||||
|
|
||||||
// save notional value of assets in cache.
|
// save notional value of assets in cache.
|
||||||
err = j.updateNotionalCache(notionals)
|
err = j.updateNotionalCache(notionals)
|
||||||
|
@ -75,138 +73,101 @@ func (j *NotionalJob) Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateNotionalCache updates the notional value of assets in cache.
|
// updateNotionalCache updates the notional value of assets in cache.
|
||||||
func (j *NotionalJob) updateNotionalCache(notionals map[vaa.ChainID]NotionalCacheField) error {
|
func (j *NotionalJob) updateNotionalCache(notionals map[Symbol]notional.PriceData) error {
|
||||||
for chainID, notional := range notionals {
|
|
||||||
key := fmt.Sprintf(NotionalCacheKey, chainID)
|
for chainID, n := range notionals {
|
||||||
err := j.cacheClient.Set(key, notional, 0).Err()
|
key := fmt.Sprintf(notional.KeyFormatString, chainID)
|
||||||
|
err := j.cacheClient.Set(key, n, 0).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotionalCacheField is the notional value of assets in cache.
|
// convertToSymbols converts the coingecko response into a symbol map
|
||||||
type NotionalCacheField struct {
|
//
|
||||||
NotionalUsd float64 `json:"notional_usd"`
|
// The returned map has symbols as keys, and price data as the values.
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
func convertToSymbols(m map[string]coingecko.NotionalUSD) map[Symbol]notional.PriceData {
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
w := make(map[Symbol]notional.PriceData, len(m))
|
||||||
func (n NotionalCacheField) MarshalBinary() ([]byte, error) {
|
|
||||||
return json.Marshal(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertToWormholeChainIDs converts the coingecko chain ids to wormhole chain ids.
|
|
||||||
func convertToWormholeChainIDs(m map[string]coingecko.NotionalUSD) map[vaa.ChainID]NotionalCacheField {
|
|
||||||
w := make(map[vaa.ChainID]NotionalCacheField, len(m))
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
|
|
||||||
|
// Do not update the dictionary when the token price is nil
|
||||||
|
if v.Price == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbol Symbol
|
||||||
|
|
||||||
switch k {
|
switch k {
|
||||||
case "solana":
|
case "solana":
|
||||||
if v.Price != nil {
|
symbol = "SOL"
|
||||||
w[vaa.ChainIDSolana] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "ethereum":
|
case "ethereum":
|
||||||
if v.Price != nil {
|
symbol = "ETH"
|
||||||
w[vaa.ChainIDEthereum] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "terra-luna":
|
case "terra-luna":
|
||||||
if v.Price != nil {
|
symbol = "LUNC"
|
||||||
w[vaa.ChainIDTerra] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "binancecoin":
|
case "binancecoin":
|
||||||
if v.Price != nil {
|
symbol = "BNB"
|
||||||
w[vaa.ChainIDBSC] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "matic-network":
|
case "matic-network":
|
||||||
if v.Price != nil {
|
symbol = "MATIC"
|
||||||
w[vaa.ChainIDPolygon] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "avalanche-2":
|
case "avalanche-2":
|
||||||
if v.Price != nil {
|
symbol = "AVAX"
|
||||||
w[vaa.ChainIDAvalanche] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "oasis-network":
|
case "oasis-network":
|
||||||
if v.Price != nil {
|
symbol = "ROSE"
|
||||||
w[vaa.ChainIDOasis] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "algorand":
|
case "algorand":
|
||||||
if v.Price != nil {
|
symbol = "ALGO"
|
||||||
w[vaa.ChainIDAlgorand] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "aurora":
|
case "aurora":
|
||||||
if v.Price != nil {
|
symbol = "AURORA"
|
||||||
w[vaa.ChainIDAurora] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "fantom":
|
case "fantom":
|
||||||
if v.Price != nil {
|
symbol = "FTM"
|
||||||
w[vaa.ChainIDFantom] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "karura":
|
case "karura":
|
||||||
if v.Price != nil {
|
symbol = "KAR"
|
||||||
w[vaa.ChainIDKarura] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "acala":
|
case "acala":
|
||||||
if v.Price != nil {
|
symbol = "ACA"
|
||||||
w[vaa.ChainIDAcala] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "klay-token":
|
case "klay-token":
|
||||||
if v.Price != nil {
|
symbol = "KLAY"
|
||||||
w[vaa.ChainIDKlaytn] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "celo":
|
case "celo":
|
||||||
if v.Price != nil {
|
symbol = "CELO"
|
||||||
w[vaa.ChainIDCelo] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "near":
|
case "near":
|
||||||
if v.Price != nil {
|
symbol = "NEAR"
|
||||||
w[vaa.ChainIDNear] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "moonbeam":
|
case "moonbeam":
|
||||||
if v.Price != nil {
|
symbol = "GLMR"
|
||||||
w[vaa.ChainIDMoonbeam] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "neon":
|
case "neon":
|
||||||
if v.Price != nil {
|
symbol = "NEON"
|
||||||
w[vaa.ChainIDNeon] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "terra-luna-2":
|
case "terra-luna-2":
|
||||||
if v.Price != nil {
|
symbol = "LUNA"
|
||||||
w[vaa.ChainIDTerra2] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "injective-protocol":
|
case "injective-protocol":
|
||||||
if v.Price != nil {
|
symbol = "INJ"
|
||||||
w[vaa.ChainIDInjective] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "aptos":
|
case "aptos":
|
||||||
if v.Price != nil {
|
symbol = "APT"
|
||||||
w[vaa.ChainIDAptos] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "sui":
|
case "sui":
|
||||||
if v.Price != nil {
|
symbol = "SUI"
|
||||||
w[vaa.ChainIDSui] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "arbitrum":
|
case "arbitrum":
|
||||||
if v.Price != nil {
|
symbol = "ARB"
|
||||||
w[vaa.ChainIDArbitrum] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "optimism":
|
case "optimism":
|
||||||
if v.Price != nil {
|
symbol = "OP"
|
||||||
w[vaa.ChainIDOptimism] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "xpla":
|
case "xpla":
|
||||||
if v.Price != nil {
|
symbol = "XPLA"
|
||||||
w[vaa.ChainIDXpla] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "bitcoin":
|
case "bitcoin":
|
||||||
if v.Price != nil {
|
symbol = "BTC"
|
||||||
w[vaa.ChainIDBtc] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
|
||||||
}
|
|
||||||
case "base-protocol":
|
case "base-protocol":
|
||||||
if v.Price != nil {
|
symbol = "BASE"
|
||||||
w[vaa.ChainIDBase] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now}
|
case "tether":
|
||||||
}
|
symbol = "USDT"
|
||||||
|
case "usd-coin":
|
||||||
|
symbol = "USDC"
|
||||||
|
case "binance-usd":
|
||||||
|
symbol = "BUSD"
|
||||||
|
case "terrausd-wormhole":
|
||||||
|
symbol = "UST"
|
||||||
|
}
|
||||||
|
|
||||||
|
if symbol != "" {
|
||||||
|
w[symbol] = notional.PriceData{NotionalUsd: *v.Price, UpdatedAt: now}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/http/infrastructure"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/http/infrastructure"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/internal/db"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/internal/db"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/internal/sqs"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/internal/sqs"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/metrics"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/parser"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/parser"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/processor"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/processor"
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/queue"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/queue"
|
||||||
|
@ -66,9 +65,7 @@ func main() {
|
||||||
repository := parser.NewRepository(db.Database, logger)
|
repository := parser.NewRepository(db.Database, logger)
|
||||||
|
|
||||||
//create a processor
|
//create a processor
|
||||||
influxCli := newInfluxClient(config.InfluxUrl, config.InfluxToken)
|
processor := processor.New(repository, logger)
|
||||||
metrics := metrics.New(influxCli, config.InfluxOrg, config.InfluxBucket, logger)
|
|
||||||
processor := processor.New(repository, metrics, logger)
|
|
||||||
|
|
||||||
// create and start a consumer
|
// create and start a consumer
|
||||||
consumer := consumer.New(vaaConsumeFunc, processor.Process, parserVAAAPIClient, logger)
|
consumer := consumer.New(vaaConsumeFunc, processor.Process, parserVAAAPIClient, logger)
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
|
||||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
const measurement = "vaa_volume"
|
|
||||||
|
|
||||||
// Metric definition.
|
|
||||||
type Metrics struct {
|
|
||||||
influxCli influxdb2.Client
|
|
||||||
writeApi api.WriteAPIBlocking
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
type Volume struct {
|
|
||||||
ChainSourceID uint16
|
|
||||||
ChainDestinationID uint16
|
|
||||||
Value uint64
|
|
||||||
Timestamp time.Time
|
|
||||||
AppID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New create a new *Metric
|
|
||||||
func New(influxCli influxdb2.Client, organization, bucket string, logger *zap.Logger) *Metrics {
|
|
||||||
writeAPI := influxCli.WriteAPIBlocking(organization, bucket)
|
|
||||||
return &Metrics{influxCli: influxCli, writeApi: writeAPI, logger: logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metrics) PushVolume(ctx context.Context, v *Volume) error {
|
|
||||||
point := influxdb2.NewPointWithMeasurement(measurement).
|
|
||||||
AddTag("chain_source_id", fmt.Sprintf("%d", v.ChainSourceID)).
|
|
||||||
AddTag("chain_destination_id", fmt.Sprintf("%d", v.ChainDestinationID)).
|
|
||||||
AddField("volume", v.Value).
|
|
||||||
AddField("app_id", v.AppID).
|
|
||||||
SetTime(v.Timestamp)
|
|
||||||
|
|
||||||
// write point to influx
|
|
||||||
err := m.writeApi.WritePoint(ctx, point)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -3,9 +3,6 @@ package processor
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/metrics"
|
|
||||||
"github.com/wormhole-foundation/wormhole-explorer/parser/parser"
|
"github.com/wormhole-foundation/wormhole-explorer/parser/parser"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
@ -24,14 +21,12 @@ type portalTokenBridgePayload struct {
|
||||||
|
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
repository *parser.Repository
|
repository *parser.Repository
|
||||||
metrics *metrics.Metrics
|
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(repository *parser.Repository, metrics *metrics.Metrics, logger *zap.Logger) *Processor {
|
func New(repository *parser.Repository, logger *zap.Logger) *Processor {
|
||||||
return &Processor{
|
return &Processor{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
metrics: metrics,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,38 +41,6 @@ func (p *Processor) Process(ctx context.Context, vaaParsed *parser.ParsedVaaUpda
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Info("Vaa save in repository", zap.String("id", vaaParsed.ID))
|
p.logger.Info("parsed VAA was successfully persisted", zap.String("id", vaaParsed.ID))
|
||||||
|
|
||||||
if vaaParsed.AppID == domain.AppIdPortalTokenBridge {
|
|
||||||
input, ok := vaaParsed.Result.(map[string]interface{})
|
|
||||||
if ok {
|
|
||||||
var result portalTokenBridgePayload
|
|
||||||
err := mapstructure.Decode(input, &result)
|
|
||||||
if err != nil {
|
|
||||||
p.logger.Warn("Decoding map to payload struct", zap.String("id", vaaParsed.ID), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if result.PayloadType == transferPayloadType || result.PayloadType == transferWithPayloadPayloadType {
|
|
||||||
if result.Amount == nil || result.ToChainID == nil {
|
|
||||||
p.logger.Warn("amount or toChain are empty", zap.String("id", vaaParsed.ID), zap.Any("payload", input))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
metric := &metrics.Volume{
|
|
||||||
ChainSourceID: vaaParsed.EmitterChain,
|
|
||||||
ChainDestinationID: *result.ToChainID,
|
|
||||||
Value: *result.Amount,
|
|
||||||
Timestamp: vaaParsed.Timestamp,
|
|
||||||
AppID: vaaParsed.AppID,
|
|
||||||
}
|
|
||||||
err := p.metrics.PushVolume(ctx, metric)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.logger.Warn("Casting parsed vaa to map", zap.String("id", vaaParsed.ID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue