diff --git a/api/docs/docs.go b/api/docs/docs.go index 5dd542ba..0f877d8c 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -923,6 +923,26 @@ const docTemplate = `{ } } }, + "/api/v1/scorecards": { + "get": { + "description": "Returns a list of KPIs for Wormhole.", + "tags": [ + "Wormscan" + ], + "operationId": "get-scorecards", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.ScorecardsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/": { "get": { "description": "Returns all VAAs. Output is paginated and can also be be sorted.", @@ -2306,6 +2326,9 @@ const docTemplate = `{ } } }, + "transactions.ScorecardsResponse": { + "type": "object" + }, "transactions.TransactionCountResult": { "type": "object", "properties": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index f1d7cfd6..809a9c3e 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -916,6 +916,26 @@ } } }, + "/api/v1/scorecards": { + "get": { + "description": "Returns a list of KPIs for Wormhole.", + "tags": [ + "Wormscan" + ], + "operationId": "get-scorecards", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.ScorecardsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/": { "get": { "description": "Returns all VAAs. Output is paginated and can also be be sorted.", @@ -2299,6 +2319,9 @@ } } }, + "transactions.ScorecardsResponse": { + "type": "object" + }, "transactions.TransactionCountResult": { "type": "object", "properties": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 5800aa87..bbf78b91 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -482,6 +482,8 @@ definitions: volume: type: number type: object + transactions.ScorecardsResponse: + type: object transactions.TransactionCountResult: properties: count: @@ -1216,6 +1218,19 @@ paths: description: Internal Server Error tags: - Wormscan + /api/v1/scorecards: + get: + description: Returns a list of KPIs for Wormhole. + operationId: get-scorecards + responses: + "200": + description: OK + schema: + $ref: '#/definitions/transactions.ScorecardsResponse' + "500": + description: Internal Server Error + tags: + - Wormscan /api/v1/vaas/: get: description: Returns all VAAs. Output is paginated and can also be be sorted. diff --git a/api/handlers/transactions/model.go b/api/handlers/transactions/model.go index fd856d82..456bfc96 100644 --- a/api/handlers/transactions/model.go +++ b/api/handlers/transactions/model.go @@ -7,6 +7,14 @@ import ( "github.com/wormhole-foundation/wormhole/sdk/vaa" ) +type Scorecards struct { + // Number of VAAs emitted since the creation of the network (does not include Pyth messages) + TotalTxCount string + + // Number of VAAs emitted in the last 24 hours (does not include Pyth messages). + TxCount24h string +} + type GlobalTransactionDoc struct { ID string `bson:"_id" json:"id"` OriginTx *OriginTx `bson:"originTx" json:"originTx"` diff --git a/api/handlers/transactions/repository.go b/api/handlers/transactions/repository.go index e41e672a..bfcbcab5 100644 --- a/api/handlers/transactions/repository.go +++ b/api/handlers/transactions/repository.go @@ -43,6 +43,21 @@ from(bucket: "%s") |> map(fn:(r) => ( {_time: r._time, count: r._value})) ` +const queryTemplateTotalTxCount = ` +from(bucket: "%s") + |> range(start: 2018-01-01T00:00:00Z) + |> filter(fn: (r) => r._field == "total_vaa_count") + |> last() +` + +const queryTemplateTxCount24h = ` +from(bucket: "%s") + |> range(start: -24h) + |> filter(fn: (r) => r._measurement == "vaa_count") + |> group(columns: ["_measurement"]) + |> count() +` + type Repository struct { influxCli influxdb2.Client queryAPI api.QueryAPI @@ -100,6 +115,83 @@ func (r *Repository) buildFindVolumeQuery(q *ChainActivityQuery) string { return fmt.Sprintf(queryTemplate, r.bucket, start, stop, operation) } +func (r *Repository) GetScorecards(ctx context.Context) (*Scorecards, error) { + + totalTxCount, err := r.getTotalTxCount(ctx) + if err != nil { + r.logger.Error("failed to query total transaction count", zap.Error(err)) + } + + txCount24h, err := r.getTxCount24h(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query 24h transactions: %w", err) + } + + // build the result and return + scorecards := Scorecards{ + TotalTxCount: totalTxCount, + TxCount24h: txCount24h, + } + + return &scorecards, nil +} + +func (r *Repository) getTotalTxCount(ctx context.Context) (string, error) { + + // query 24h transactions + query := fmt.Sprintf(queryTemplateTotalTxCount, r.bucket) + result, err := r.queryAPI.Query(ctx, query) + if err != nil { + r.logger.Error("failed to query total transaction count", zap.Error(err)) + return "", err + } + if result.Err() != nil { + r.logger.Error("total transaction count query result has errors", zap.Error(err)) + return "", result.Err() + } + if !result.Next() { + return "", errors.New("expected at least one record in total transaction count query result") + } + + // deserialize the row returned + row := struct { + Value uint64 `mapstructure:"_value"` + }{} + if err := mapstructure.Decode(result.Record().Values(), &row); err != nil { + return "", fmt.Errorf("failed to decode total transaction count query response: %w", err) + } + + return fmt.Sprint(row.Value), nil +} + +func (r *Repository) getTxCount24h(ctx context.Context) (string, error) { + + // query 24h transactions + query := fmt.Sprintf(queryTemplateTxCount24h, r.bucket) + result, err := r.queryAPI.Query(ctx, query) + if err != nil { + r.logger.Error("failed to query 24h transactions", zap.Error(err)) + return "", err + } + if result.Err() != nil { + r.logger.Error("24h transactions query result has errors", zap.Error(err)) + return "", result.Err() + } + if !result.Next() { + return "", errors.New("expected at least one record in 24h transactions query result") + } + + // deserialize the row returned + row := struct { + Value uint64 `mapstructure:"_value"` + }{} + if err := mapstructure.Decode(result.Record().Values(), &row); err != nil { + return "", fmt.Errorf("failed to decode 24h transaction count query response: %w", err) + } + + return fmt.Sprint(row.Value), nil +} + // GetTransactionCount get the last transactions. func (r *Repository) GetTransactionCount(ctx context.Context, q *TransactionCountQuery) ([]TransactionCountResult, error) { query := r.buildLastTrxQuery(q) diff --git a/api/handlers/transactions/service.go b/api/handlers/transactions/service.go index 607e6a9a..60483120 100644 --- a/api/handlers/transactions/service.go +++ b/api/handlers/transactions/service.go @@ -24,6 +24,10 @@ func (s *Service) GetTransactionCount(ctx context.Context, q *TransactionCountQu return s.repo.GetTransactionCount(ctx, q) } +func (s *Service) GetScorecards(ctx context.Context) (*Scorecards, error) { + return s.repo.GetScorecards(ctx) +} + // GetChainActivity get chain activity. func (s *Service) GetChainActivity(ctx context.Context, q *ChainActivityQuery) ([]ChainActivityResult, error) { return s.repo.FindChainActivity(ctx, q) diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go index 4331937a..c9ea25ea 100644 --- a/api/routes/wormscan/routes.go +++ b/api/routes/wormscan/routes.go @@ -63,9 +63,10 @@ func RegisterRoutes( api.Get("/address/:id", addressCtrl.FindById) // analytics - api.Get("/last-txs", transactionCtrl.GetLastTransactions) - api.Get("/x-chain-activity", transactionCtrl.GetChainActivity) api.Get("/global-tx/:chain/:emitter/:sequence", transactionCtrl.FindGlobalTransactionByID) + api.Get("/last-txs", transactionCtrl.GetLastTransactions) + api.Get("/scorecards", transactionCtrl.GetScorecards) + api.Get("/x-chain-activity", transactionCtrl.GetChainActivity) // vaas resource vaas := api.Group("/vaas") diff --git a/api/routes/wormscan/transactions/controller.go b/api/routes/wormscan/transactions/controller.go index de368ba7..235e668f 100644 --- a/api/routes/wormscan/transactions/controller.go +++ b/api/routes/wormscan/transactions/controller.go @@ -58,6 +58,30 @@ func (c *Controller) GetLastTransactions(ctx *fiber.Ctx) error { return ctx.JSON(lastTrx) } +// GetScorecards godoc +// @Description Returns a list of KPIs for Wormhole. +// @Tags Wormscan +// @ID get-scorecards +// @Success 200 {object} ScorecardsResponse +// @Failure 500 +// @Router /api/v1/scorecards [get] +func (c *Controller) GetScorecards(ctx *fiber.Ctx) error { + + // Query indicators from the database + scorecards, err := c.srv.GetScorecards(ctx.Context()) + if err != nil { + return err + } + + // Convert indicators to the response model + response := ScorecardsResponse{ + TxCount24h: scorecards.TxCount24h, + TotalTxCount: scorecards.TotalTxCount, + } + + return ctx.JSON(response) +} + // GetChainActivity godoc // @Description Returns a list of tx by source chain and destination chain. // @Tags Wormscan diff --git a/api/routes/wormscan/transactions/response.go b/api/routes/wormscan/transactions/response.go index 377f3ba1..cf0e28dd 100644 --- a/api/routes/wormscan/transactions/response.go +++ b/api/routes/wormscan/transactions/response.go @@ -19,3 +19,23 @@ type Destination struct { type ChainActivity struct { Txs []Tx `json:"txs"` } + +// ScorecardsResponse is the response model for the endpoint `GET /api/v1/scorecards`. +type ScorecardsResponse struct { + //TODO: we don't have the data for these fields yet, uncomment as the data becomes available. + + //TVL string `json:"tvl"` + + //TotalVolume string `json:"total_volume"` + + // Number of VAAs emitted since the creation of the network (does not include Pyth messages) + TotalTxCount string `json:"total_tx_count,omitempty"` + + //Volume24h string `json:"24h_volume"` + + // Number of VAAs emitted in the last 24 hours (does not include Pyth messages). + TxCount24h string `json:"24h_tx_count"` + + // Number of VAAs emitted in the last 24 hours (includes Pyth messages). + //Messages24h string `json:"24h_messages"` +}