diff --git a/analytic/scripts/vaa_volume_24h.flux b/analytic/scripts/asset_volumes_24h.flux similarity index 65% rename from analytic/scripts/vaa_volume_24h.flux rename to analytic/scripts/asset_volumes_24h.flux index 73f71ba8..6b89f18a 100644 --- a/analytic/scripts/vaa_volume_24h.flux +++ b/analytic/scripts/asset_volumes_24h.flux @@ -8,12 +8,14 @@ option task = { start = date.sub(from: now(), d: 24h) stop = now() -from(bucket: "wormscan-24hours-mainnet-staging") +from(bucket: "wormscan") |> range(start: start, stop: stop) |> filter(fn: (r) => r["_measurement"] == "vaa_volume") |> filter(fn: (r) => r["_field"] == "volume") |> drop(columns: ["app_id", "destination_address", "destination_chain"]) + |> group(columns: ["emitter_chain", "token_address", "token_chain"]) |> sum(column: "_value") - |> set(key: "_measurement", value: "vaa_volume_24h") + |> set(key: "_measurement", value: "asset_volumes_24h") + |> set(key: "_field", value: "volume") |> map(fn: (r) => ({r with _time: start})) - |> to(bucket: "wormscan-30days-mainnet-staging") \ No newline at end of file + |> to(bucket: "wormscan-30days") \ No newline at end of file diff --git a/analytic/scripts/chain_pair_transfers_24h.flux b/analytic/scripts/chain_pair_transfers_24h.flux new file mode 100644 index 00000000..fa056c2d --- /dev/null +++ b/analytic/scripts/chain_pair_transfers_24h.flux @@ -0,0 +1,21 @@ +import "date" + +option task = { + name: "chain pair transfers with 24-hour granularity", + every: 24h, +} + +start = date.sub(from: now(), d: 24h) +stop = now() + +from(bucket: "wormscan") + |> range(start: start, stop: stop) + |> filter(fn: (r) => r["_measurement"] == "vaa_volume") + |> filter(fn: (r) => r["_field"] == "volume") + |> drop(columns: ["app_id", "destination_address", "token_address", "token_chain", "_field"]) + |> group(columns: ["emitter_chain", "destination_chain"]) + |> count(column: "_value") + |> set(key: "_measurement", value: "chain_pair_transfers_24h") + |> set(key: "_field", value: "num_transfers") + |> map(fn: (r) => ({r with _time: start})) + |> to(bucket: "wormscan-30days") \ No newline at end of file diff --git a/api/docs/docs.go b/api/docs/docs.go index 59ac6328..2619c4c5 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -943,6 +943,64 @@ const docTemplate = `{ } } }, + "/api/v1/top-assets-by-volume": { + "get": { + "description": "Returns a list of the (emitter_chain, asset) pairs with the most volume.", + "tags": [ + "Wormscan" + ], + "operationId": "get-top-assets-by-volume", + "parameters": [ + { + "type": "string", + "description": "Time span, supported values: 7d, 15d, 30d.", + "name": "timeSpan", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.TopAssetsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/v1/top-chain-pairs-by-num-transfers": { + "get": { + "description": "Returns a list of the (emitter_chain, destination_chain) pairs with the highest number of transfers.", + "tags": [ + "Wormscan" + ], + "operationId": "get-top-chain-pairs-by-num-transfers", + "parameters": [ + { + "type": "string", + "description": "Time span, supported values: 7d, 15d, 30d.", + "name": "timeSpan", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.TopChainPairsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/": { "get": { "description": "Returns all VAAs. Output is paginated and can also be be sorted.", @@ -2301,6 +2359,20 @@ const docTemplate = `{ } } }, + "transactions.AssetWithVolume": { + "type": "object", + "properties": { + "emitterChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "symbol": { + "type": "string" + }, + "volume": { + "type": "string" + } + } + }, "transactions.ChainActivity": { "type": "object", "properties": { @@ -2312,6 +2384,20 @@ const docTemplate = `{ } } }, + "transactions.ChainPair": { + "type": "object", + "properties": { + "destinationChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "emitterChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "numberOfTransfers": { + "type": "string" + } + } + }, "transactions.Destination": { "type": "object", "properties": { @@ -2333,12 +2419,38 @@ const docTemplate = `{ "description": "Number of VAAs emitted in the last 24 hours (does not include Pyth messages).", "type": "string" }, + "24h_volume": { + "description": "Volume transferred through the token bridge in the last 24 hours, in USD.", + "type": "string" + }, "total_tx_count": { "description": "Number of VAAs emitted since the creation of the network (does not include Pyth messages)", "type": "string" } } }, + "transactions.TopAssetsResponse": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/transactions.AssetWithVolume" + } + } + } + }, + "transactions.TopChainPairsResponse": { + "type": "object", + "properties": { + "chainPairs": { + "type": "array", + "items": { + "$ref": "#/definitions/transactions.ChainPair" + } + } + } + }, "transactions.TransactionCountResult": { "type": "object", "properties": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 42b97127..43c948b6 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -936,6 +936,64 @@ } } }, + "/api/v1/top-assets-by-volume": { + "get": { + "description": "Returns a list of the (emitter_chain, asset) pairs with the most volume.", + "tags": [ + "Wormscan" + ], + "operationId": "get-top-assets-by-volume", + "parameters": [ + { + "type": "string", + "description": "Time span, supported values: 7d, 15d, 30d.", + "name": "timeSpan", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.TopAssetsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/v1/top-chain-pairs-by-num-transfers": { + "get": { + "description": "Returns a list of the (emitter_chain, destination_chain) pairs with the highest number of transfers.", + "tags": [ + "Wormscan" + ], + "operationId": "get-top-chain-pairs-by-num-transfers", + "parameters": [ + { + "type": "string", + "description": "Time span, supported values: 7d, 15d, 30d.", + "name": "timeSpan", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/transactions.TopChainPairsResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/": { "get": { "description": "Returns all VAAs. Output is paginated and can also be be sorted.", @@ -2294,6 +2352,20 @@ } } }, + "transactions.AssetWithVolume": { + "type": "object", + "properties": { + "emitterChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "symbol": { + "type": "string" + }, + "volume": { + "type": "string" + } + } + }, "transactions.ChainActivity": { "type": "object", "properties": { @@ -2305,6 +2377,20 @@ } } }, + "transactions.ChainPair": { + "type": "object", + "properties": { + "destinationChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "emitterChain": { + "$ref": "#/definitions/vaa.ChainID" + }, + "numberOfTransfers": { + "type": "string" + } + } + }, "transactions.Destination": { "type": "object", "properties": { @@ -2326,12 +2412,38 @@ "description": "Number of VAAs emitted in the last 24 hours (does not include Pyth messages).", "type": "string" }, + "24h_volume": { + "description": "Volume transferred through the token bridge in the last 24 hours, in USD.", + "type": "string" + }, "total_tx_count": { "description": "Number of VAAs emitted since the creation of the network (does not include Pyth messages)", "type": "string" } } }, + "transactions.TopAssetsResponse": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/transactions.AssetWithVolume" + } + } + } + }, + "transactions.TopChainPairsResponse": { + "type": "object", + "properties": { + "chainPairs": { + "type": "array", + "items": { + "$ref": "#/definitions/transactions.ChainPair" + } + } + } + }, "transactions.TransactionCountResult": { "type": "object", "properties": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 7d1c5f07..56a41901 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -466,6 +466,15 @@ definitions: next: type: string type: object + transactions.AssetWithVolume: + properties: + emitterChain: + $ref: '#/definitions/vaa.ChainID' + symbol: + type: string + volume: + type: string + type: object transactions.ChainActivity: properties: txs: @@ -473,6 +482,15 @@ definitions: $ref: '#/definitions/transactions.Tx' type: array type: object + transactions.ChainPair: + properties: + destinationChain: + $ref: '#/definitions/vaa.ChainID' + emitterChain: + $ref: '#/definitions/vaa.ChainID' + numberOfTransfers: + type: string + type: object transactions.Destination: properties: chain: @@ -488,11 +506,29 @@ definitions: description: Number of VAAs emitted in the last 24 hours (does not include Pyth messages). type: string + 24h_volume: + description: Volume transferred through the token bridge in the last 24 hours, + in USD. + type: string total_tx_count: description: Number of VAAs emitted since the creation of the network (does not include Pyth messages) type: string type: object + transactions.TopAssetsResponse: + properties: + assets: + items: + $ref: '#/definitions/transactions.AssetWithVolume' + type: array + type: object + transactions.TopChainPairsResponse: + properties: + chainPairs: + items: + $ref: '#/definitions/transactions.ChainPair' + type: array + type: object transactions.TransactionCountResult: properties: count: @@ -1242,6 +1278,46 @@ paths: description: Internal Server Error tags: - Wormscan + /api/v1/top-assets-by-volume: + get: + description: Returns a list of the (emitter_chain, asset) pairs with the most + volume. + operationId: get-top-assets-by-volume + parameters: + - description: 'Time span, supported values: 7d, 15d, 30d.' + in: query + name: timeSpan + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/transactions.TopAssetsResponse' + "500": + description: Internal Server Error + tags: + - Wormscan + /api/v1/top-chain-pairs-by-num-transfers: + get: + description: Returns a list of the (emitter_chain, destination_chain) pairs + with the highest number of transfers. + operationId: get-top-chain-pairs-by-num-transfers + parameters: + - description: 'Time span, supported values: 7d, 15d, 30d.' + in: query + name: timeSpan + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/transactions.TopChainPairsResponse' + "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 93b4d014..5a3259ad 100644 --- a/api/handlers/transactions/model.go +++ b/api/handlers/transactions/model.go @@ -19,7 +19,7 @@ type Scorecards struct { Volume24h string } -// AssetDTO is used for the return value of the function `GetTopAssetsByVolume`. +// AssetDTO is used for the return value of the function `GetTopAssets`. type AssetDTO struct { EmitterChain sdk.ChainID TokenChain sdk.ChainID @@ -27,27 +27,34 @@ type AssetDTO struct { Volume string } -// TopAssetsTimerange is used as an input parameter for the function `GetTopAssetsByVolume`. -type TopAssetsTimerange string +// ChainPairDTO is used for the return value of the function `GetTopChainPairs`. +type ChainPairDTO struct { + EmitterChain sdk.ChainID + DestinationChain sdk.ChainID + NumberOfTransfers string +} + +// TopStatisticsTimeSpan is used as an input parameter for the functions `GetTopAssets` and `GetTopChainPairs`. +type TopStatisticsTimeSpan string const ( - TopAssetsTimerange7Days TopAssetsTimerange = "7d" - TopAssetsTimerange15Days TopAssetsTimerange = "15d" - TopAssetsTimerange30Days TopAssetsTimerange = "30d" + TimeSpan7Days TopStatisticsTimeSpan = "7d" + TimeSpan15Days TopStatisticsTimeSpan = "15d" + TimeSpan30Days TopStatisticsTimeSpan = "30d" ) -// NewTopAssetsTimerange parses a string and returns a `TopAssetsTimerange`. -func NewTopAssetsTimerange(s string) (*TopAssetsTimerange, error) { +// ParseTopStatisticsTimeSpan parses a string and returns a `TopAssetsTimeSpan`. +func ParseTopStatisticsTimeSpan(s string) (*TopStatisticsTimeSpan, error) { - if s == string(TopAssetsTimerange7Days) || - s == string(TopAssetsTimerange15Days) || - s == string(TopAssetsTimerange30Days) { + if s == string(TimeSpan7Days) || + s == string(TimeSpan15Days) || + s == string(TimeSpan30Days) { - tmp := TopAssetsTimerange(s) + tmp := TopStatisticsTimeSpan(s) return &tmp, nil } - return nil, fmt.Errorf("invalid timerange: %s", s) + return nil, fmt.Errorf("invalid time span: %s", s) } type GlobalTransactionDoc struct { diff --git a/api/handlers/transactions/repository.go b/api/handlers/transactions/repository.go index 3666bd1f..b940e71e 100644 --- a/api/handlers/transactions/repository.go +++ b/api/handlers/transactions/repository.go @@ -61,13 +61,13 @@ from(bucket: "%s") |> sum(column: "_value") ` -const queryTemplateTopAssetsByVolume = ` +const queryTemplateTopAssets = ` import "date" // Get historic volumes from the summarized metric. summarized = from(bucket: "%s") |> range(start: -%s) - |> filter(fn: (r) => r["_measurement"] == "vaa_volume_24h") + |> filter(fn: (r) => r["_measurement"] == "asset_volumes_24h") |> group(columns: ["emitter_chain", "token_address", "token_chain"]) // Get the current day's volume from the unsummarized metric. @@ -76,6 +76,7 @@ startOfDay = date.truncate(t: now(), unit: 1d) raw = from(bucket: "%s") |> range(start: startOfDay) |> filter(fn: (r) => r["_measurement"] == "vaa_volume") + |> filter(fn: (r) => r["_field"] == "volume") |> group(columns: ["emitter_chain", "token_address", "token_chain"]) // Merge all results, compute the sum, return the top 7 volumes. @@ -86,6 +87,36 @@ union(tables: [summarized, raw]) |> top(columns: ["_value"], n: 7) ` +const queryTemplateTopChainPairs = ` +import "date" + +// Get historic number of transfers from the summarized metric. +summarized = from(bucket: "%s") + |> range(start: -%s) + |> filter(fn: (r) => r["_measurement"] == "chain_pair_transfers_24h") + |> group(columns: ["emitter_chain", "destination_chain"]) + |> sum() + +// Get the current day's number of transfers from the unsummarized metric. +// This assumes that the summarization task runs exactly once per day at 00:00hs +startOfDay = date.truncate(t: now(), unit: 1d) +raw = from(bucket: "%s") + |> range(start: startOfDay) + |> filter(fn: (r) => r["_measurement"] == "vaa_volume") + |> filter(fn: (r) => r["_field"] == "volume") + |> group(columns: ["emitter_chain", "destination_chain"]) + |> drop(columns: ["app_id", "destination_address", "token_address", "token_chain", "_field"]) + |> group(columns: ["emitter_chain", "destination_chain"]) + |> count() + +// Merge all results, compute the sum, return the top 7 volumes. +union(tables: [summarized, raw]) + |> group(columns: ["emitter_chain", "destination_chain"]) + |> sum() + |> group() + |> top(columns: ["_value"], n: 7) +` + type Repository struct { influxCli influxdb2.Client queryAPI api.QueryAPI @@ -121,10 +152,10 @@ func NewRepository( return &r } -func (r *Repository) GetTopAssetsByVolume(ctx context.Context, timerange *TopAssetsTimerange) ([]AssetDTO, error) { +func (r *Repository) GetTopAssets(ctx context.Context, timeSpan *TopStatisticsTimeSpan) ([]AssetDTO, error) { // Submit the query to InfluxDB - query := fmt.Sprintf(queryTemplateTopAssetsByVolume, r.bucket30DaysRetention, *timerange, r.bucket24HoursRetention) + query := fmt.Sprintf(queryTemplateTopAssets, r.bucket30DaysRetention, *timeSpan, r.bucketInfiniteRetention) result, err := r.queryAPI.Query(ctx, query) if err != nil { return nil, err @@ -178,6 +209,61 @@ func (r *Repository) GetTopAssetsByVolume(ctx context.Context, timerange *TopAss return assets, nil } +func (r *Repository) GetTopChainPairs(ctx context.Context, timeSpan *TopStatisticsTimeSpan) ([]ChainPairDTO, error) { + + // Submit the query to InfluxDB + query := fmt.Sprintf(queryTemplateTopChainPairs, r.bucket30DaysRetention, *timeSpan, r.bucketInfiniteRetention) + result, err := r.queryAPI.Query(ctx, query) + if err != nil { + return nil, err + } + if result.Err() != nil { + return nil, result.Err() + } + + // Scan query results + type Row struct { + EmitterChain string `mapstructure:"emitter_chain"` + DestinationChain string `mapstructure:"destination_chain"` + NumberOfTransfers int64 `mapstructure:"_value"` + } + var rows []Row + for result.Next() { + var row Row + if err := mapstructure.Decode(result.Record().Values(), &row); err != nil { + return nil, err + } + rows = append(rows, row) + } + + // Convert the rows into the response model + var assets []ChainPairDTO + for i := range rows { + + // parse emitter chain + emitterChain, err := strconv.ParseUint(rows[i].EmitterChain, 10, 16) + if err != nil { + return nil, fmt.Errorf("failed to convert emitter chain field to uint16") + } + + // parse destination chain + destinationChain, err := strconv.ParseUint(rows[i].DestinationChain, 10, 16) + if err != nil { + return nil, fmt.Errorf("failed to convert destination chain field to uint16") + } + + // append the new item to the response + asset := ChainPairDTO{ + EmitterChain: sdk.ChainID(emitterChain), + DestinationChain: sdk.ChainID(destinationChain), + NumberOfTransfers: fmt.Sprintf("%d", rows[i].NumberOfTransfers), + } + assets = append(assets, asset) + } + + return assets, nil +} + // convertToDecimal converts an integer amount to a decimal string, with 8 decimals of precision. func convertToDecimal(amount int64) string { diff --git a/api/handlers/transactions/service.go b/api/handlers/transactions/service.go index 81b2eece..aaedeac3 100644 --- a/api/handlers/transactions/service.go +++ b/api/handlers/transactions/service.go @@ -28,8 +28,12 @@ func (s *Service) GetScorecards(ctx context.Context) (*Scorecards, error) { return s.repo.GetScorecards(ctx) } -func (s *Service) GetTopAssetsByVolume(ctx context.Context, timerange *TopAssetsTimerange) ([]AssetDTO, error) { - return s.repo.GetTopAssetsByVolume(ctx, timerange) +func (s *Service) GetTopAssets(ctx context.Context, timeSpan *TopStatisticsTimeSpan) ([]AssetDTO, error) { + return s.repo.GetTopAssets(ctx, timeSpan) +} + +func (s *Service) GetTopChainPairs(ctx context.Context, timeSpan *TopStatisticsTimeSpan) ([]ChainPairDTO, error) { + return s.repo.GetTopChainPairs(ctx, timeSpan) } // GetChainActivity get chain activity. diff --git a/api/middleware/extract_parameters.go b/api/middleware/extract_parameters.go index eb8f5225..98d92035 100644 --- a/api/middleware/extract_parameters.go +++ b/api/middleware/extract_parameters.go @@ -299,16 +299,20 @@ func ExtractIsNotional(ctx *fiber.Ctx) (bool, error) { return false, response.NewInvalidQueryParamError(ctx, "INVALID QUERY PARAMETER", nil) } -// ExtractTopAssetsTimerange parses the `timerange` parameter from the `GET /api/v1/top-assets-by-volume` endpoint. -func ExtractTopAssetsTimerange(ctx *fiber.Ctx) (*transactions.TopAssetsTimerange, error) { +// ExtractTopStatisticsTimeSpan parses the `timespan` parameter used on top statistics endpoints. +// +// The endpoints that accept this parameter are: +// * `GET /api/v1/top-assets-by-volume` +// * `GET /api/v1/top-chain-pairs-by-num-transfers` +func ExtractTopStatisticsTimeSpan(ctx *fiber.Ctx) (*transactions.TopStatisticsTimeSpan, error) { - s := ctx.Query("timerange") - timerange, err := transactions.NewTopAssetsTimerange(s) + s := ctx.Query("timeSpan") + timeSpan, err := transactions.ParseTopStatisticsTimeSpan(s) if err != nil { - return nil, response.NewInvalidQueryParamError(ctx, "INVALID QUERY PARAMETER", nil) + return nil, response.NewInvalidQueryParamError(ctx, "INVALID QUERY PARAMETER", nil) } - return timerange, nil + return timeSpan, nil } func ExtractTimeRange(ctx *fiber.Ctx) (*time.Time, *time.Time, error) { diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go index 2cc098c3..b200fff3 100644 --- a/api/routes/wormscan/routes.go +++ b/api/routes/wormscan/routes.go @@ -67,7 +67,8 @@ func RegisterRoutes( api.Get("/last-txs", transactionCtrl.GetLastTransactions) api.Get("/scorecards", transactionCtrl.GetScorecards) api.Get("/x-chain-activity", transactionCtrl.GetChainActivity) - api.Get("/top-assets-by-volume", transactionCtrl.GetTopAssetsByVolume) + api.Get("/top-assets-by-volume", transactionCtrl.GetTopAssets) + api.Get("/top-chain-pairs-by-num-transfers", transactionCtrl.GetTopChainPairs) // vaas resource vaas := api.Group("/vaas") diff --git a/api/routes/wormscan/transactions/controller.go b/api/routes/wormscan/transactions/controller.go index bbbc009c..1a45c186 100644 --- a/api/routes/wormscan/transactions/controller.go +++ b/api/routes/wormscan/transactions/controller.go @@ -85,30 +85,70 @@ func (c *Controller) GetScorecards(ctx *fiber.Ctx) error { return ctx.JSON(response) } -// GetTopAssetsByVolume godoc -// @Description Returns the list of (emitter_chain, asset) pairs with the most volume. +// GetTopChainPairs godoc +// @Description Returns a list of the (emitter_chain, destination_chain) pairs with the highest number of transfers. // @Tags Wormscan -// @ID get-top-assets-by-volume -// @Success 200 {object} TopAssetsByVolumeResponse +// @ID get-top-chain-pairs-by-num-transfers +// @Param timeSpan query string true "Time span, supported values: 7d, 15d, 30d." +// @Success 200 {object} TopChainPairsResponse // @Failure 500 -// @Router /api/v1/top-assets-by-volume [get] -func (c *Controller) GetTopAssetsByVolume(ctx *fiber.Ctx) error { +// @Router /api/v1/top-chain-pairs-by-num-transfers [get] +func (c *Controller) GetTopChainPairs(ctx *fiber.Ctx) error { // Extract query parameters - timerange, err := middleware.ExtractTopAssetsTimerange(ctx) + timeSpan, err := middleware.ExtractTopStatisticsTimeSpan(ctx) + if err != nil { + return err + } + + // Query chain pairs from the database + chainPairDTOs, err := c.srv.GetTopChainPairs(ctx.Context(), timeSpan) + if err != nil { + c.logger.Error("failed to get top chain pairs by number of transfers", zap.Error(err)) + return err + } + + // Convert DTOs to the response model + response := TopChainPairsResponse{ + ChainPairs: make([]ChainPair, 0, len(chainPairDTOs)), + } + for i := range chainPairDTOs { + chainPair := ChainPair{ + EmitterChain: chainPairDTOs[i].EmitterChain, + DestinationChain: chainPairDTOs[i].DestinationChain, + NumberOfTransfers: chainPairDTOs[i].NumberOfTransfers, + } + response.ChainPairs = append(response.ChainPairs, chainPair) + } + + return ctx.JSON(response) +} + +// GetTopAssets godoc +// @Description Returns a list of the (emitter_chain, asset) pairs with the most volume. +// @Tags Wormscan +// @ID get-top-assets-by-volume +// @Param timeSpan query string true "Time span, supported values: 7d, 15d, 30d." +// @Success 200 {object} TopAssetsResponse +// @Failure 500 +// @Router /api/v1/top-assets-by-volume [get] +func (c *Controller) GetTopAssets(ctx *fiber.Ctx) error { + + // Extract query parameters + timeSpan, err := middleware.ExtractTopStatisticsTimeSpan(ctx) if err != nil { return err } // Query assets from the database - assetDTOs, err := c.srv.GetTopAssetsByVolume(ctx.Context(), timerange) + assetDTOs, err := c.srv.GetTopAssets(ctx.Context(), timeSpan) if err != nil { c.logger.Error("failed to get top assets by volume", zap.Error(err)) return err } // Convert DTOs to the response model - response := TopAssetsByVolumeResponse{ + response := TopAssetsResponse{ Assets: make([]AssetWithVolume, 0, len(assetDTOs)), } for i := range assetDTOs { @@ -133,7 +173,6 @@ func (c *Controller) GetTopAssetsByVolume(ctx *fiber.Ctx) error { } return ctx.JSON(response) - } // GetChainActivity godoc diff --git a/api/routes/wormscan/transactions/response.go b/api/routes/wormscan/transactions/response.go index 1303ca62..9173fe1e 100644 --- a/api/routes/wormscan/transactions/response.go +++ b/api/routes/wormscan/transactions/response.go @@ -44,13 +44,24 @@ type ScorecardsResponse struct { Volume24h string `json:"24h_volume"` } -// TopAssetsByVolumeResponse is the "200 OK" response model for `GET /api/v1/top-assets-by-volume`. -type TopAssetsByVolumeResponse struct { +// TopAssetsResponse is the "200 OK" response model for `GET /api/v1/top-assets-by-volume`. +type TopAssetsResponse struct { Assets []AssetWithVolume `json:"assets"` } type AssetWithVolume struct { - EmitterChain sdk.ChainID `json:"emitterChain` + EmitterChain sdk.ChainID `json:"emitterChain"` Symbol string `json:"symbol"` Volume string `json:"volume"` } + +// TopChainPairsResponse is the "200 OK" response model for `GET /api/v1/top-chain-pairs-by-num-transfers`. +type TopChainPairsResponse struct { + ChainPairs []ChainPair `json:"chainPairs"` +} + +type ChainPair struct { + EmitterChain sdk.ChainID `json:"emitterChain"` + DestinationChain sdk.ChainID `json:"destinationChain"` + NumberOfTransfers string `json:"numberOfTransfers"` +} diff --git a/jobs/go.mod b/jobs/go.mod index 414ae501..8682ad26 100644 --- a/jobs/go.mod +++ b/jobs/go.mod @@ -6,15 +6,18 @@ require ( github.com/go-redis/redis v6.15.9+incompatible github.com/joho/godotenv v1.5.1 github.com/sethvargo/go-envconfig v0.9.0 - github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8 - github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0 + github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230512135429-25a675f99a4b + github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230426150516-e695fad0bed8 go.uber.org/zap v1.24.0 ) require ( github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/ethereum/go-ethereum v1.10.21 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/holiman/uint256 v1.2.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect diff --git a/jobs/go.sum b/jobs/go.sum index f10ea9d2..6f1b16fe 100644 --- a/jobs/go.sum +++ b/jobs/go.sum @@ -2,6 +2,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,6 +11,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/ethereum/go-ethereum v1.10.21 h1:5lqsEx92ZaZzRyOqBEXux4/UR06m296RGzN3ol3teJY= github.com/ethereum/go-ethereum v1.10.21/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -16,6 +20,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -56,8 +62,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8 h1:nJPjdHphY0JGPorg3GrGzIf8J4YR1eyTalxT7MzPIZg= github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8/go.mod h1:wySbOH0GO2dRhkTktCCCBnZ4FgNIpy3fL4hEbNMz5KI= +github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230512135429-25a675f99a4b h1:Z5ulKWN8mtUW4zX8nMl+oJeGAj0Wk2wRFLj7aDnGVoI= +github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230512135429-25a675f99a4b/go.mod h1:9N5u61eWAJ5CTFu4UF6gJ8FP2FiLbU39XCRMdYKxY/I= github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0 h1:uEJOLDlkpDxpShkCbFobYPd3MHZpNpkpt0+iyQnb9x4= github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0/go.mod h1:dE12DOucCq23gjGGGhtbyx41FBxuHxjpPvG+ArO+8t0= +github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230426150516-e695fad0bed8 h1:rrOyHd+H9a6Op1iUyZNCaI5v9D1syq8jDAYyX/2Q4L8= +github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230426150516-e695fad0bed8/go.mod h1:dE12DOucCq23gjGGGhtbyx41FBxuHxjpPvG+ArO+8t0= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=