Add top-100-corridors endpoint (#1065)

Improve handling token in volume metrics
This commit is contained in:
ftocal 2024-01-30 15:27:48 -03:00 committed by GitHub
parent 54c5d34a6e
commit e63a7a12e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 767 additions and 29 deletions

View File

@ -12,7 +12,22 @@ import (
"go.uber.org/zap"
)
var ErrUnknownToken = errors.New("token is unknown")
type UnknownTokenErr struct {
detail string
}
func (e *UnknownTokenErr) Error() string {
return fmt.Sprintf("unknown token. %s", e.detail)
}
func IsUnknownTokenErr(err error) bool {
switch err.(type) {
case *UnknownTokenErr:
return true
default:
return false
}
}
type TransferredToken struct {
AppId string
@ -62,20 +77,6 @@ func (r *TokenResolver) GetTransferredTokenByVaa(ctx context.Context, vaa *sdk.V
return nil, nil
}
// Decode the VAA payload
payload, err := sdk.DecodeTransferPayloadHdr(vaa.Payload)
if err == nil && payload.OriginChain != sdk.ChainIDUnset {
return &TransferredToken{
AppId: domain.AppIdPortalTokenBridge,
FromChain: vaa.EmitterChain,
ToChain: payload.TargetChain,
TokenAddress: payload.OriginAddress,
TokenChain: payload.OriginChain,
Amount: payload.Amount,
}, nil
}
// Parse the VAA with standarized properties
result, err := r.client.ParseVaaWithStandarizedProperties(vaa)
if err != nil {
@ -97,7 +98,7 @@ func (r *TokenResolver) GetTransferredTokenByVaa(ctx context.Context, vaa *sdk.V
r.logger.Debug("Creating transferred token",
zap.String("vaaId", vaa.MessageID()),
zap.Error(err))
return nil, ErrUnknownToken
return nil, &UnknownTokenErr{detail: err.Error()}
}
return token, err
@ -105,16 +106,16 @@ func (r *TokenResolver) GetTransferredTokenByVaa(ctx context.Context, vaa *sdk.V
func createToken(s parser.StandardizedProperties, emitterChain sdk.ChainID) (*TransferredToken, error) {
if s.TokenChain.String() == sdk.ChainIDUnset.String() {
return nil, errors.New("tokenChain is unset")
if !domain.ChainIdIsValid(s.TokenChain) {
return nil, fmt.Errorf("tokenChain is invalid: %d", s.TokenChain)
}
if s.ToChain.String() == sdk.ChainIDUnset.String() {
return nil, errors.New("toChain is unset")
if !domain.ChainIdIsValid(s.ToChain) {
return nil, fmt.Errorf("toChain is invalid: %d", s.ToChain)
}
if emitterChain.String() == sdk.ChainIDUnset.String() {
return nil, errors.New("emitterChain is unset")
if !domain.ChainIdIsValid(emitterChain) {
return nil, fmt.Errorf("emitterChain is invalid: %d", emitterChain)
}
if s.TokenAddress == "" {

View File

@ -96,7 +96,7 @@ func (m *Metric) Push(ctx context.Context, params *Params) error {
transferredToken, err := m.getTransferredTokenByVaa(ctx, params.Vaa)
if err != nil {
if err != token.ErrUnknownToken {
if !token.IsUnknownTokenErr(err) {
m.logger.Error("Failed to obtain transferred token for this VAA",
zap.String("trackId", params.TrackID),
zap.String("vaaId", params.Vaa.MessageID()),
@ -129,8 +129,8 @@ func (m *Metric) Push(ctx context.Context, params *Params) error {
)
} else {
m.logger.Warn("Cannot obtain transferred token for this VAA",
zap.Error(err),
zap.String("trackId", params.TrackID),
zap.String("vaaId", params.Vaa.MessageID()),
)
@ -145,6 +145,7 @@ func (m *Metric) Push(ctx context.Context, params *Params) error {
if params.Vaa.EmitterChain != sdk.ChainIDPythNet {
m.logger.Info("Transaction processed successfully",
zap.String("trackId", params.TrackID),
zap.Bool("isVaaSigned", isVaaSigned),
zap.String("vaaId", params.Vaa.MessageID()))
}

View File

@ -0,0 +1,39 @@
import "date"
option task = {
name: "top 100 corridors with 3-hour granularity",
every: 3h,
}
sourceBucket = "wormscan"
destinationBucket = "wormscan-24hours"
execution = date.truncate(t: now(), unit: 1h)
start7d = date.truncate(t: -7d, unit: 1h)
start2d = date.truncate(t: -2d, unit: 1h)
from(bucket: sourceBucket)
|> range(start: start7d)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r._field == "volume")
|> group(columns: ["emitter_chain", "destination_chain", "token_chain", "token_address"])
|> count(column: "_value")
|> group()
|> sort(desc:true)
|> limit(n:100)
|> map(fn: (r) => ({r with _time: execution}))
|> set(key: "_measurement", value: "top_100_corridors_7_days_3h_v2")
|> set(key: "_field", value: "count")
|> to(bucket: destinationBucket)
from(bucket: sourceBucket)
|> range(start: start2d)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r._field == "volume")
|> group(columns: ["emitter_chain", "destination_chain", "token_chain", "token_address"])
|> count(column: "_value")
|> group()
|> sort(desc:true)
|> limit(n:100)
|> map(fn: (r) => ({r with _time: execution}))
|> set(key: "_measurement", value: "top_100_corridors_2_days_3h_v2")
|> set(key: "_field", value: "count")
|> to(bucket: destinationBucket)

View File

@ -991,6 +991,37 @@ const docTemplate = `{
}
}
},
"/api/v1/top-100-corridors": {
"get": {
"description": "Returns a list of the top 100 tokens, sorted in descending order by the number of transactions.",
"tags": [
"wormholescan"
],
"operationId": "/api/v1/top-100-corridors",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 2d and 7d (default is 2d).",
"name": "timeSpan",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/stats.TopCorridorsResult"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/top-assets-by-volume": {
"get": {
"description": "Returns a list of emitter_chain and asset pairs with ordered by volume.\nThe volume is calculated using the notional price of the symbol at the day the VAA was emitted.",
@ -1049,6 +1080,37 @@ const docTemplate = `{
}
}
},
"/api/v1/top-symbols-by-volume": {
"get": {
"description": "Returns a list of symbols by origin chain and tokens.\nThe volume is calculated using the notional price of the symbol at the day the VAA was emitted.",
"tags": [
"wormholescan"
],
"operationId": "top-symbols-by-volume",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 7d, 15d and 30d (default is 7d).",
"name": "timeSpan",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/stats.TopSymbolByVolumeResult"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/transactions/": {
"get": {
"description": "Returns transactions. Output is paginated.",
@ -2410,11 +2472,20 @@ const docTemplate = `{
"relays.DeliveryReponse": {
"type": "object",
"properties": {
"budget": {
"type": "string"
},
"execution": {
"$ref": "#/definitions/relays.ResultExecutionResponse"
},
"maxRefund": {
"type": "string"
},
"relayGasUsed": {
"type": "integer"
},
"targetChainDecimals": {
"type": "integer"
}
}
},
@ -2485,6 +2556,9 @@ const docTemplate = `{
"instructions": {
"$ref": "#/definitions/relays.InstructionsResponse"
},
"maxAttempts": {
"type": "integer"
},
"toTxHash": {
"type": "string"
}
@ -2717,6 +2791,88 @@ const docTemplate = `{
}
}
},
"stats.TokenResult": {
"type": "object",
"properties": {
"emitter_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"token_address": {
"type": "string"
},
"token_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"txs": {
"type": "number"
},
"volume": {
"type": "number"
}
}
},
"stats.TopCorridor": {
"type": "object",
"properties": {
"emitter_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"target_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"token_address": {
"type": "string"
},
"token_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"txs": {
"type": "integer"
}
}
},
"stats.TopCorridorsResult": {
"type": "object",
"properties": {
"corridors": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TopCorridor"
}
}
}
},
"stats.TopSymbolByVolumeResult": {
"type": "object",
"properties": {
"symbols": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TopSymbolResult"
}
}
}
},
"stats.TopSymbolResult": {
"type": "object",
"properties": {
"symbol": {
"type": "string"
},
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TokenResult"
}
},
"txs": {
"type": "number"
},
"volume": {
"type": "number"
}
}
},
"transactions.AssetWithVolume": {
"type": "object",
"properties": {
@ -3025,6 +3181,7 @@ const docTemplate = `{
17,
18,
19,
20,
21,
22,
23,
@ -3034,8 +3191,19 @@ const docTemplate = `{
29,
30,
32,
34,
35,
3104,
10002
4000,
4001,
4002,
4003,
4004,
10002,
10003,
10004,
10005,
10006
],
"x-enum-varnames": [
"ChainIDUnset",
@ -3058,6 +3226,7 @@ const docTemplate = `{
"ChainIDNeon",
"ChainIDTerra2",
"ChainIDInjective",
"ChainIDOsmosis",
"ChainIDSui",
"ChainIDAptos",
"ChainIDArbitrum",
@ -3067,8 +3236,19 @@ const docTemplate = `{
"ChainIDBtc",
"ChainIDBase",
"ChainIDSei",
"ChainIDScroll",
"ChainIDMantle",
"ChainIDWormchain",
"ChainIDSepolia"
"ChainIDCosmoshub",
"ChainIDEvmos",
"ChainIDKujira",
"ChainIDNeutron",
"ChainIDCelestia",
"ChainIDSepolia",
"ChainIDArbitrumSepolia",
"ChainIDBaseSepolia",
"ChainIDOptimismSepolia",
"ChainIDHolesky"
]
},
"vaa.VaaDoc": {

View File

@ -984,6 +984,37 @@
}
}
},
"/api/v1/top-100-corridors": {
"get": {
"description": "Returns a list of the top 100 tokens, sorted in descending order by the number of transactions.",
"tags": [
"wormholescan"
],
"operationId": "/api/v1/top-100-corridors",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 2d and 7d (default is 2d).",
"name": "timeSpan",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/stats.TopCorridorsResult"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/top-assets-by-volume": {
"get": {
"description": "Returns a list of emitter_chain and asset pairs with ordered by volume.\nThe volume is calculated using the notional price of the symbol at the day the VAA was emitted.",
@ -1042,6 +1073,37 @@
}
}
},
"/api/v1/top-symbols-by-volume": {
"get": {
"description": "Returns a list of symbols by origin chain and tokens.\nThe volume is calculated using the notional price of the symbol at the day the VAA was emitted.",
"tags": [
"wormholescan"
],
"operationId": "top-symbols-by-volume",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 7d, 15d and 30d (default is 7d).",
"name": "timeSpan",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/stats.TopSymbolByVolumeResult"
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/transactions/": {
"get": {
"description": "Returns transactions. Output is paginated.",
@ -2403,11 +2465,20 @@
"relays.DeliveryReponse": {
"type": "object",
"properties": {
"budget": {
"type": "string"
},
"execution": {
"$ref": "#/definitions/relays.ResultExecutionResponse"
},
"maxRefund": {
"type": "string"
},
"relayGasUsed": {
"type": "integer"
},
"targetChainDecimals": {
"type": "integer"
}
}
},
@ -2478,6 +2549,9 @@
"instructions": {
"$ref": "#/definitions/relays.InstructionsResponse"
},
"maxAttempts": {
"type": "integer"
},
"toTxHash": {
"type": "string"
}
@ -2710,6 +2784,88 @@
}
}
},
"stats.TokenResult": {
"type": "object",
"properties": {
"emitter_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"token_address": {
"type": "string"
},
"token_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"txs": {
"type": "number"
},
"volume": {
"type": "number"
}
}
},
"stats.TopCorridor": {
"type": "object",
"properties": {
"emitter_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"target_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"token_address": {
"type": "string"
},
"token_chain": {
"$ref": "#/definitions/vaa.ChainID"
},
"txs": {
"type": "integer"
}
}
},
"stats.TopCorridorsResult": {
"type": "object",
"properties": {
"corridors": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TopCorridor"
}
}
}
},
"stats.TopSymbolByVolumeResult": {
"type": "object",
"properties": {
"symbols": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TopSymbolResult"
}
}
}
},
"stats.TopSymbolResult": {
"type": "object",
"properties": {
"symbol": {
"type": "string"
},
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/stats.TokenResult"
}
},
"txs": {
"type": "number"
},
"volume": {
"type": "number"
}
}
},
"transactions.AssetWithVolume": {
"type": "object",
"properties": {
@ -3018,6 +3174,7 @@
17,
18,
19,
20,
21,
22,
23,
@ -3027,8 +3184,19 @@
29,
30,
32,
34,
35,
3104,
10002
4000,
4001,
4002,
4003,
4004,
10002,
10003,
10004,
10005,
10006
],
"x-enum-varnames": [
"ChainIDUnset",
@ -3051,6 +3219,7 @@
"ChainIDNeon",
"ChainIDTerra2",
"ChainIDInjective",
"ChainIDOsmosis",
"ChainIDSui",
"ChainIDAptos",
"ChainIDArbitrum",
@ -3060,8 +3229,19 @@
"ChainIDBtc",
"ChainIDBase",
"ChainIDSei",
"ChainIDScroll",
"ChainIDMantle",
"ChainIDWormchain",
"ChainIDSepolia"
"ChainIDCosmoshub",
"ChainIDEvmos",
"ChainIDKujira",
"ChainIDNeutron",
"ChainIDCelestia",
"ChainIDSepolia",
"ChainIDArbitrumSepolia",
"ChainIDBaseSepolia",
"ChainIDOptimismSepolia",
"ChainIDHolesky"
]
},
"vaa.VaaDoc": {

View File

@ -398,10 +398,16 @@ definitions:
type: object
relays.DeliveryReponse:
properties:
budget:
type: string
execution:
$ref: '#/definitions/relays.ResultExecutionResponse'
maxRefund:
type: string
relayGasUsed:
type: integer
targetChainDecimals:
type: integer
type: object
relays.InstructionsResponse:
properties:
@ -447,6 +453,8 @@ definitions:
type: string
instructions:
$ref: '#/definitions/relays.InstructionsResponse'
maxAttempts:
type: integer
toTxHash:
type: string
type: object
@ -596,6 +604,59 @@ definitions:
next:
type: string
type: object
stats.TokenResult:
properties:
emitter_chain:
$ref: '#/definitions/vaa.ChainID'
token_address:
type: string
token_chain:
$ref: '#/definitions/vaa.ChainID'
txs:
type: number
volume:
type: number
type: object
stats.TopCorridor:
properties:
emitter_chain:
$ref: '#/definitions/vaa.ChainID'
target_chain:
$ref: '#/definitions/vaa.ChainID'
token_address:
type: string
token_chain:
$ref: '#/definitions/vaa.ChainID'
txs:
type: integer
type: object
stats.TopCorridorsResult:
properties:
corridors:
items:
$ref: '#/definitions/stats.TopCorridor'
type: array
type: object
stats.TopSymbolByVolumeResult:
properties:
symbols:
items:
$ref: '#/definitions/stats.TopSymbolResult'
type: array
type: object
stats.TopSymbolResult:
properties:
symbol:
type: string
tokens:
items:
$ref: '#/definitions/stats.TokenResult'
type: array
txs:
type: number
volume:
type: number
type: object
transactions.AssetWithVolume:
properties:
emitterChain:
@ -811,6 +872,7 @@ definitions:
- 17
- 18
- 19
- 20
- 21
- 22
- 23
@ -820,8 +882,19 @@ definitions:
- 29
- 30
- 32
- 34
- 35
- 3104
- 4000
- 4001
- 4002
- 4003
- 4004
- 10002
- 10003
- 10004
- 10005
- 10006
type: integer
x-enum-varnames:
- ChainIDUnset
@ -844,6 +917,7 @@ definitions:
- ChainIDNeon
- ChainIDTerra2
- ChainIDInjective
- ChainIDOsmosis
- ChainIDSui
- ChainIDAptos
- ChainIDArbitrum
@ -853,8 +927,19 @@ definitions:
- ChainIDBtc
- ChainIDBase
- ChainIDSei
- ChainIDScroll
- ChainIDMantle
- ChainIDWormchain
- ChainIDCosmoshub
- ChainIDEvmos
- ChainIDKujira
- ChainIDNeutron
- ChainIDCelestia
- ChainIDSepolia
- ChainIDArbitrumSepolia
- ChainIDBaseSepolia
- ChainIDOptimismSepolia
- ChainIDHolesky
vaa.VaaDoc:
properties:
appId:
@ -1575,6 +1660,27 @@ paths:
description: Not Found
tags:
- wormholescan
/api/v1/top-100-corridors:
get:
description: Returns a list of the top 100 tokens, sorted in descending order
by the number of transactions.
operationId: /api/v1/top-100-corridors
parameters:
- description: 'Time span, supported values: 2d and 7d (default is 2d).'
in: query
name: timeSpan
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/stats.TopCorridorsResult'
"400":
description: Bad Request
"500":
description: Internal Server Error
tags:
- wormholescan
/api/v1/top-assets-by-volume:
get:
description: |-
@ -1616,6 +1722,28 @@ paths:
description: Internal Server Error
tags:
- wormholescan
/api/v1/top-symbols-by-volume:
get:
description: |-
Returns a list of symbols by origin chain and tokens.
The volume is calculated using the notional price of the symbol at the day the VAA was emitted.
operationId: top-symbols-by-volume
parameters:
- description: 'Time span, supported values: 7d, 15d and 30d (default is 7d).'
in: query
name: timeSpan
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/stats.TopSymbolByVolumeResult'
"400":
description: Bad Request
"500":
description: Internal Server Error
tags:
- wormholescan
/api/v1/transactions/:
get:
description: Returns transactions. Output is paginated.

View File

@ -17,3 +17,16 @@ func buildSymbolWithAssets(bucket string, t time.Time, measurement string) strin
start := t.Truncate(time.Hour * 24).Format(time.RFC3339Nano)
return fmt.Sprintf(queryTemplateSymbolWithAssets, bucket, start, measurement)
}
const queryTemplateTopCorridors = `
from(bucket: "%s")
|> range(start: %s)
|> filter(fn: (r) => r._measurement == "%s" and r._field == "count")
|> last()
|> group()
`
func buildTopCorridors(bucket string, t time.Time, measurement string) string {
start := t.Truncate(time.Hour * 24).Format(time.RFC3339Nano)
return fmt.Sprintf(queryTemplateTopCorridors, bucket, start, measurement)
}

View File

@ -128,3 +128,90 @@ func (r *Repository) GetSymbolWithAssets(ctx context.Context, timeSpan SymbolWit
return values, nil
}
func (r *Repository) GetTopCorridores(ctx context.Context, timeSpan TopCorridorsTimeSpan) ([]TopCorridorsDTO, error) {
var measurement string
switch timeSpan {
case TimeSpan7DaysTopCorridors:
measurement = "top_100_corridors_7_days_3h_v2"
case TimeSpan2DaysTopCorridors:
measurement = "top_100_corridors_2_days_3h_v2"
default:
measurement = "top_100_corridors_2_days_3h_v2"
}
query := buildTopCorridors(r.bucket24HoursRetention, time.Now(), measurement)
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"`
TokenChain string `mapstructure:"token_chain"`
TokenAddress string `mapstructure:"token_address"`
Txs uint64 `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 values []TopCorridorsDTO
for _, row := range rows {
// parse emitter chain
emitterChain, err := strconv.ParseUint(row.EmitterChain, 10, 16)
if err != nil {
return nil, fmt.Errorf("failed to convert emitter chain field to uint16. %v", err)
}
// parse emitter chain
destinationChain, err := strconv.ParseUint(row.DestinationChain, 10, 16)
if err != nil {
return nil, fmt.Errorf("failed to convert destination chain field to uint16. %v", err)
}
// parse token chain
tokenChain, err := strconv.ParseUint(row.TokenChain, 10, 16)
if err != nil {
return nil, fmt.Errorf("failed to convert token chain field to uint16. %v", err)
}
// append the new item to the response
value := TopCorridorsDTO{
EmitterChainID: sdk.ChainID(emitterChain),
DestinationChainID: sdk.ChainID(destinationChain),
TokenChainID: sdk.ChainID(tokenChain),
TokenAddress: row.TokenAddress,
Txs: row.Txs,
}
// do not include invalid chain IDs in the response
if !domain.ChainIdIsValid(value.EmitterChainID) || !domain.ChainIdIsValid(value.DestinationChainID) {
r.logger.Warn("Invalid chain ID in top corridors",
zap.Uint16("emitter_chain", uint16(value.EmitterChainID)),
zap.Uint16("destination_chain", uint16(value.DestinationChainID)),
zap.Uint16("token_chain", uint16(value.TokenChainID)),
zap.String("token_address", value.TokenAddress),
zap.Uint64("txs", value.Txs),
)
continue
}
values = append(values, value)
}
return values, nil
}

View File

@ -20,7 +20,8 @@ type Service struct {
}
const (
topSymbolsByVolumeKey = "wormscan:top-assets-symbol-by-volume"
topSymbolsByVolumeKey = "wormscan:top-assets-symbol-by-volume"
topCorridorsByCountKey = "wormscan:top-corridors-by-count"
)
// NewService create a new Service.
@ -36,3 +37,12 @@ func (s *Service) GetSymbolWithAssets(ctx context.Context, ts SymbolWithAssetsTi
return s.repo.GetSymbolWithAssets(ctx, ts)
})
}
func (s *Service) GetTopCorridors(ctx context.Context, ts TopCorridorsTimeSpan) ([]TopCorridorsDTO, error) {
key := topCorridorsByCountKey
key = fmt.Sprintf("%s:%s", key, ts)
return cacheable.GetOrLoad(ctx, s.logger, s.cache, s.expiration, key, s.metrics,
func() ([]TopCorridorsDTO, error) {
return s.repo.GetTopCorridores(ctx, ts)
})
}

View File

@ -9,11 +9,15 @@ import (
// SymbolWithAssetsTimeSpan is used as an input parameter for the functions `GetTopAssets` and `GetTopChainPairs`.
type SymbolWithAssetsTimeSpan string
type TopCorridorsTimeSpan string
const (
TimeSpan7Days SymbolWithAssetsTimeSpan = "7d"
TimeSpan15Days SymbolWithAssetsTimeSpan = "15d"
TimeSpan30Days SymbolWithAssetsTimeSpan = "30d"
TimeSpan2DaysTopCorridors TopCorridorsTimeSpan = "2d"
TimeSpan7DaysTopCorridors TopCorridorsTimeSpan = "7d"
)
// ParseSymbolsWithAssetsTimeSpan parses a string and returns a `SymbolsWithAssetsTimeSpan`.
@ -38,3 +42,22 @@ type SymbolWithAssetDTO struct {
Volume decimal.Decimal
Txs decimal.Decimal
}
func ParseTopCorridorsTimeSpan(s string) (*TopCorridorsTimeSpan, error) {
if s == string(TimeSpan2DaysTopCorridors) ||
s == string(TimeSpan7DaysTopCorridors) {
tmp := TopCorridorsTimeSpan(s)
return &tmp, nil
}
return nil, fmt.Errorf("invalid time span: %s", s)
}
type TopCorridorsDTO struct {
EmitterChainID sdk.ChainID
DestinationChainID sdk.ChainID
TokenChainID sdk.ChainID
TokenAddress string
Txs uint64
}

View File

@ -406,3 +406,17 @@ func ExtractSymbolWithAssetsTimeSpan(ctx *fiber.Ctx) (*stats.SymbolWithAssetsTim
return timeSpan, nil
}
func ExtractTopCorridorsTimeSpan(ctx *fiber.Ctx) (*stats.TopCorridorsTimeSpan, error) {
defaultTimeSpan := stats.TimeSpan2DaysTopCorridors
s := ctx.Query("timeSpan")
if s == "" {
return &defaultTimeSpan, nil
}
timeSpan, err := stats.ParseTopCorridorsTimeSpan(s)
if err != nil {
return nil, response.NewInvalidQueryParamError(ctx, "INVALID <timeSpan> QUERY PARAMETER", nil)
}
return timeSpan, nil
}

View File

@ -89,6 +89,7 @@ func RegisterRoutes(
// stats custom endpoints
api.Get("/top-symbols-by-volume", statsCrtl.GetTopSymbolsByVolume)
api.Get("/top-100-corridors", statsCrtl.GetTopCorridors)
// operations resource
operations := api.Group("/operations")

View File

@ -93,3 +93,52 @@ func (c *Controller) createTopSymbolsByVolumeResult(assets []stats.SymbolWithAss
}
return values, nil
}
// GetTop100Corridors godoc
// @Description Returns a list of the top 100 tokens, sorted in descending order by the number of transactions.
// @Tags wormholescan
// @ID /api/v1/top-100-corridors
// @Param timeSpan query string false "Time span, supported values: 2d and 7d (default is 2d)."
// @Success 200 {object} stats.TopCorridorsResult
// @Failure 400
// @Failure 500
// @Router /api/v1/top-100-corridors [get]
func (c *Controller) GetTopCorridors(ctx *fiber.Ctx) error {
timeSpan, err := middleware.ExtractTopCorridorsTimeSpan(ctx)
if err != nil {
return err
}
// Get the chain activity.
corridors, err := c.srv.GetTopCorridors(ctx.Context(), *timeSpan)
if err != nil {
c.logger.Error("Error getting symbol with assets", zap.Error(err))
return err
}
result := createTop100CorridorsResult(corridors)
return ctx.JSON(TopCorridorsResult{Corridors: result})
}
func createTop100CorridorsResult(corridors []stats.TopCorridorsDTO) []*TopCorridor {
result := make([]*TopCorridor, 0, len(corridors))
for _, c := range corridors {
result = append(result, &TopCorridor{
EmitterChainID: c.EmitterChainID,
TargetChainID: c.DestinationChainID,
TokenChainID: c.TokenChainID,
TokenAddress: c.TokenAddress,
Txs: c.Txs,
})
}
sort.Slice(result[:], func(i, j int) bool {
return result[i].Txs > result[j].Txs
})
if len(result) >= 100 {
return result[:100]
}
return result
}

View File

@ -23,3 +23,15 @@ type TokenResult struct {
type TopSymbolByVolumeResult struct {
Symbols []*TopSymbolResult `json:"symbols"`
}
type TopCorridorsResult struct {
Corridors []*TopCorridor `json:"corridors"`
}
type TopCorridor struct {
EmitterChainID sdk.ChainID `json:"emitter_chain"`
TargetChainID sdk.ChainID `json:"target_chain"`
TokenChainID sdk.ChainID `json:"token_chain"`
TokenAddress string `json:"token_address"`
Txs uint64 `json:"txs"`
}