Skip to content

Commit

Permalink
[API/ANALYTICS] Scorecards endpoint (#232)
Browse files Browse the repository at this point in the history
### 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: #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")

```
  • Loading branch information
agodnic committed Apr 20, 2023
1 parent 18efc01 commit c8aba63
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 1 deletion.
23 changes: 23 additions & 0 deletions api/docs/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions api/docs/swagger.json
Expand Up @@ -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.",
Expand Down Expand Up @@ -2299,6 +2319,9 @@
}
}
},
"transactions.ScorecardsResponse": {
"type": "object"
},
"transactions.TransactionCountResult": {
"type": "object",
"properties": {
Expand Down
15 changes: 15 additions & 0 deletions api/docs/swagger.yaml
Expand Up @@ -482,6 +482,8 @@ definitions:
volume:
type: number
type: object
transactions.ScorecardsResponse:
type: object
transactions.TransactionCountResult:
properties:
count:
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions api/handlers/transactions/model.go
Expand Up @@ -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"`
Expand Down
92 changes: 92 additions & 0 deletions api/handlers/transactions/repository.go
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions api/handlers/transactions/service.go
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion api/routes/wormscan/routes.go
Expand Up @@ -63,9 +63,10 @@ func RegisterRoutes(
api.Get("/address/:id", addressCtrl.FindById)

// analytics
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)
api.Get("/global-tx/:chain/:emitter/:sequence", transactionCtrl.FindGlobalTransactionByID)

// vaas resource
vaas := api.Group("/vaas")
Expand Down
24 changes: 24 additions & 0 deletions api/routes/wormscan/transactions/controller.go
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions api/routes/wormscan/transactions/response.go
Expand Up @@ -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"`
}

0 comments on commit c8aba63

Please sign in to comment.