[API/ANALYTICS] Scorecards endpoint (#232)

### Summary

This pull request adds the `GET /api/v1/scorecards` endpoint, which is required by the wormscan frontend.

Most of the fields that this endpoint should return are being omitted because the data is not currently available on the backend. Those fields will be added iteratively as the data becomes available.

The current format of the response is:
```json
{
  "total_tx_count": "1300200",
  "24h_tx_count": "4200"
}
```

Tracking issue: https://github.com/wormhole-foundation/wormhole-explorer/issues/221

## Deployment details

In order to populate the `"total_tx_count"` metric, a task is needed in influxdb:

```
$ cat total-vaa-count.flux 
option task = {
	name: "Total number of emitted VAAs",
	every: 1m
}

from(bucket: "wormhole-explorer")
  |> range(start: 2018-01-01T00:00:00Z)
  |> filter(fn: (r) => r._measurement == "vaa_count")
  |> group(columns: ["_measurement"])
  |> set(key: "_measurement", value: "total_vaa_count")
  |> count()
  |> map(fn: (r) => ({r with _time: now()}))
  |> map(fn: (r) => ({r with _field: "total_vaa_count"}))
  |> to(bucket: "wormhole-explorer", org: "xlabs")

```
This commit is contained in:
agodnic 2023-04-20 16:01:10 -03:00 committed by GitHub
parent 18efc01460
commit c8aba636e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 212 additions and 2 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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.

View File

@ -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"`

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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"`
}