[WORMSCAN-API-1225] Add new endpoint for fetching x-chain-activity tops data (#1342)

* add 2 new tasks for collecting chain activity every day and hour

* making progress

* change query 2

* add query by month and year

* changes on task

* more changes

* change to 1d

* add 1d

* fix query

* adjust queryies

* change the way the query is executed

* changes on query

* making more progress

* fix per year query

* add a second group of tasks for downsampling

* add app_id

* update swagger docs

* optimize new tasks

* fix W

* fix W

* start using the new measurement

* change endpoint signature

* update endpoint name

* fix indents

* code review changes

* remove unnecessary break
This commit is contained in:
Mariano 2024-04-26 15:03:02 -03:00 committed by GitHub
parent 9d280f7d5f
commit ce1b7707fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 955 additions and 22 deletions

View File

@ -0,0 +1,40 @@
import "date"
runTask = (start,stop,srcBucket,destBucket,destMeasurement) => {
data = from(bucket: srcBucket)
|> range(start: start,stop: stop)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r.version == "v2")
|> filter(fn: (r) => r._field == "volume" and r._value > 0)
|> drop(columns:["destination_chain","app_id","token_chain","token_address","version","_measurement","_time"])
|> rename(columns: {_start: "_time"})
|> group(columns: ["emitter_chain","_time"])
vols = data
|> sum(column: "_value")
|> set(key: "_field", value: "volume")
|> set(key: "to", value: string(v:stop))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
return data
|> count(column: "_value")
|> set(key: "_field", value: "count")
|> set(key: "to", value: string(v:stop))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
}
bucketInfinite = "wormscan"
destMeasurement = "emitter_chain_activity_1d"
stop = date.truncate(t: now(),unit: 1d)
start = date.sub(d: 1d, from: stop)
option task = {
name: "calculate chain activity per emitter every day",
every: 1d,
}
runTask(start:start, stop: stop, srcBucket: bucketInfinite, destBucket: bucketInfinite, destMeasurement: destMeasurement)

View File

@ -0,0 +1,40 @@
import "date"
runTask = (start,stop,srcBucket,destBucket,destMeasurement) => {
data = from(bucket: srcBucket)
|> range(start: start,stop: stop)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r.version == "v2")
|> filter(fn: (r) => r._field == "volume" and r._value > 0)
|> drop(columns:["destination_chain","app_id","token_chain","token_address","version","_measurement","_time"])
|> rename(columns: {_start: "_time"})
|> group(columns: ["emitter_chain","_time"])
vols = data
|> sum(column: "_value")
|> set(key: "_field", value: "volume")
|> set(key: "to", value: string(v:stop))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
return data
|> count(column: "_value")
|> set(key: "_field", value: "count")
|> set(key: "to", value: string(v:stop))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
}
bucketInfinite = "wormscan"
destMeasurement = "emitter_chain_activity_1h"
stop = date.truncate(t: now(),unit: 1h)
start = date.sub(d: 1h, from: stop)
option task = {
name: "calculate chain activity per emitter every hour",
every: 1h,
}
runTask(start:start, stop: stop, srcBucket: bucketInfinite, destBucket: bucketInfinite, destMeasurement: destMeasurement)

View File

@ -0,0 +1,37 @@
import "date"
runTask = (start,stop,srcBucket,destBucket,destMeasurement) => {
data = from(bucket: srcBucket)
|> range(start: start,stop: stop)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r._field == "volume")
|> group(columns: ["emitter_chain", "destination_chain", "app_id"])
data
|> sum(column: "_value")
|> set(key: "_field", value: "volume")
|> map(fn: (r) => ({ r with _time: start }))
|> set(key: "to", value: string(v:date.add(d: 1d, to: start)))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
return data
|> count(column: "_value")
|> set(key: "_field", value: "count")
|> map(fn: (r) => ({ r with _time: start }))
|> set(key: "to", value: string(v:date.add(d: 1d, to: start)))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
}
bucketInfinite = "wormscan"
destMeasurement = "chain_activity_1d"
stop = date.truncate(t: now(),unit: 24h)
start = date.sub(d: 1d, from: stop)
option task = {
name: "calculate chain activity every day",
every: 1d,
}
runTask(start:start, stop: stop, srcBucket: bucketInfinite, destBucket: bucketInfinite, destMeasurement: destMeasurement)

View File

@ -0,0 +1,40 @@
import "date"
runTask = (start,stop,srcBucket,destBucket,destMeasurement) => {
data = from(bucket: srcBucket)
|> range(start: start,stop: stop)
|> filter(fn: (r) => r._measurement == "vaa_volume_v2" and r._field == "volume")
|> group(columns: ["emitter_chain", "destination_chain", "app_id"])
data
|> sum(column: "_value")
|> set(key: "_field", value: "volume")
|> map(fn: (r) => ({ r with _time: start }))
|> set(key: "to", value: string(v:date.add(d: 1h, to: start)))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
return data
|> count(column: "_value")
|> set(key: "_field", value: "count")
|> map(fn: (r) => ({ r with _time: start }))
|> set(key: "to", value: string(v:date.add(d: 1h, to: start)))
|> set(key: "_measurement", value: destMeasurement)
|> to(bucket: destBucket)
}
bucketInfinite = "wormscan"
destMeasurement = "chain_activity_1h"
stop = date.truncate(t: now(),unit: 1h)
start = date.sub(d: 1h, from: stop)
option task = {
name: "calculate chain activity every hour",
every: 1h,
}
runTask(start:start, stop: stop, srcBucket: bucketInfinite, destBucket: bucketInfinite, destMeasurement: destMeasurement)

View File

@ -1760,6 +1760,73 @@ const docTemplate = `{
}
}
},
"/api/v1/x-chain-activity/tops": {
"get": {
"description": "Search, for a specific period of time, the number of transactions and the volume.",
"tags": [
"wormholescan"
],
"operationId": "x-chain-activity-tops",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 1d, 1mo and 1y",
"name": "timespan",
"in": "query",
"required": true
},
{
"type": "string",
"description": "From date, supported format 2006-01-02T15:04:05Z07:00",
"name": "from",
"in": "query",
"required": true
},
{
"type": "string",
"description": "To date, supported format 2006-01-02T15:04:05Z07:00",
"name": "to",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Search by appId",
"name": "appId",
"in": "query"
},
{
"type": "string",
"description": "Search by sourceChain",
"name": "sourceChain",
"in": "query"
},
{
"type": "string",
"description": "Search by targetChain",
"name": "targetChain",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/transactions.ChainActivityTopResult"
}
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/swagger.json": {
"get": {
"description": "Returns the swagger specification for this API.",
@ -3325,6 +3392,29 @@ const docTemplate = `{
}
}
},
"transactions.ChainActivityTopResult": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"destination_chain": {
"type": "string"
},
"emitter_chain": {
"type": "string"
},
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"volume": {
"type": "integer"
}
}
},
"transactions.ChainPair": {
"type": "object",
"properties": {
@ -3591,7 +3681,6 @@ const docTemplate = `{
14,
15,
16,
17,
18,
19,
20,
@ -3599,24 +3688,35 @@ const docTemplate = `{
22,
23,
24,
25,
26,
28,
29,
30,
32,
33,
34,
35,
36,
37,
38,
39,
3104,
4000,
4001,
4002,
4003,
4004,
4005,
4006,
4007,
4008,
10002,
10003,
10004,
10005,
10006
10006,
10007
],
"x-enum-varnames": [
"ChainIDUnset",
@ -3636,7 +3736,6 @@ const docTemplate = `{
"ChainIDCelo",
"ChainIDNear",
"ChainIDMoonbeam",
"ChainIDNeon",
"ChainIDTerra2",
"ChainIDInjective",
"ChainIDOsmosis",
@ -3644,24 +3743,35 @@ const docTemplate = `{
"ChainIDAptos",
"ChainIDArbitrum",
"ChainIDOptimism",
"ChainIDGnosis",
"ChainIDPythNet",
"ChainIDXpla",
"ChainIDBtc",
"ChainIDBase",
"ChainIDSei",
"ChainIDRootstock",
"ChainIDScroll",
"ChainIDMantle",
"ChainIDBlast",
"ChainIDXLayer",
"ChainIDLinea",
"ChainIDBerachain",
"ChainIDWormchain",
"ChainIDCosmoshub",
"ChainIDEvmos",
"ChainIDKujira",
"ChainIDNeutron",
"ChainIDCelestia",
"ChainIDStargaze",
"ChainIDSeda",
"ChainIDDymension",
"ChainIDProvenance",
"ChainIDSepolia",
"ChainIDArbitrumSepolia",
"ChainIDBaseSepolia",
"ChainIDOptimismSepolia",
"ChainIDHolesky"
"ChainIDHolesky",
"ChainIDPolygonSepolia"
]
},
"vaa.VaaDoc": {

View File

@ -1753,6 +1753,73 @@
}
}
},
"/api/v1/x-chain-activity/tops": {
"get": {
"description": "Search, for a specific period of time, the number of transactions and the volume.",
"tags": [
"wormholescan"
],
"operationId": "x-chain-activity-tops",
"parameters": [
{
"type": "string",
"description": "Time span, supported values: 1d, 1mo and 1y",
"name": "timespan",
"in": "query",
"required": true
},
{
"type": "string",
"description": "From date, supported format 2006-01-02T15:04:05Z07:00",
"name": "from",
"in": "query",
"required": true
},
{
"type": "string",
"description": "To date, supported format 2006-01-02T15:04:05Z07:00",
"name": "to",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Search by appId",
"name": "appId",
"in": "query"
},
{
"type": "string",
"description": "Search by sourceChain",
"name": "sourceChain",
"in": "query"
},
{
"type": "string",
"description": "Search by targetChain",
"name": "targetChain",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/transactions.ChainActivityTopResult"
}
}
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/swagger.json": {
"get": {
"description": "Returns the swagger specification for this API.",
@ -3318,6 +3385,29 @@
}
}
},
"transactions.ChainActivityTopResult": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"destination_chain": {
"type": "string"
},
"emitter_chain": {
"type": "string"
},
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"volume": {
"type": "integer"
}
}
},
"transactions.ChainPair": {
"type": "object",
"properties": {
@ -3584,7 +3674,6 @@
14,
15,
16,
17,
18,
19,
20,
@ -3592,24 +3681,35 @@
22,
23,
24,
25,
26,
28,
29,
30,
32,
33,
34,
35,
36,
37,
38,
39,
3104,
4000,
4001,
4002,
4003,
4004,
4005,
4006,
4007,
4008,
10002,
10003,
10004,
10005,
10006
10006,
10007
],
"x-enum-varnames": [
"ChainIDUnset",
@ -3629,7 +3729,6 @@
"ChainIDCelo",
"ChainIDNear",
"ChainIDMoonbeam",
"ChainIDNeon",
"ChainIDTerra2",
"ChainIDInjective",
"ChainIDOsmosis",
@ -3637,24 +3736,35 @@
"ChainIDAptos",
"ChainIDArbitrum",
"ChainIDOptimism",
"ChainIDGnosis",
"ChainIDPythNet",
"ChainIDXpla",
"ChainIDBtc",
"ChainIDBase",
"ChainIDSei",
"ChainIDRootstock",
"ChainIDScroll",
"ChainIDMantle",
"ChainIDBlast",
"ChainIDXLayer",
"ChainIDLinea",
"ChainIDBerachain",
"ChainIDWormchain",
"ChainIDCosmoshub",
"ChainIDEvmos",
"ChainIDKujira",
"ChainIDNeutron",
"ChainIDCelestia",
"ChainIDStargaze",
"ChainIDSeda",
"ChainIDDymension",
"ChainIDProvenance",
"ChainIDSepolia",
"ChainIDArbitrumSepolia",
"ChainIDBaseSepolia",
"ChainIDOptimismSepolia",
"ChainIDHolesky"
"ChainIDHolesky",
"ChainIDPolygonSepolia"
]
},
"vaa.VaaDoc": {

View File

@ -824,6 +824,21 @@ definitions:
$ref: '#/definitions/transactions.Tx'
type: array
type: object
transactions.ChainActivityTopResult:
properties:
count:
type: integer
destination_chain:
type: string
emitter_chain:
type: string
from:
type: string
to:
type: string
volume:
type: integer
type: object
transactions.ChainPair:
properties:
destinationChain:
@ -1012,7 +1027,6 @@ definitions:
- 14
- 15
- 16
- 17
- 18
- 19
- 20
@ -1020,24 +1034,35 @@ definitions:
- 22
- 23
- 24
- 25
- 26
- 28
- 29
- 30
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 3104
- 4000
- 4001
- 4002
- 4003
- 4004
- 4005
- 4006
- 4007
- 4008
- 10002
- 10003
- 10004
- 10005
- 10006
- 10007
type: integer
x-enum-varnames:
- ChainIDUnset
@ -1057,7 +1082,6 @@ definitions:
- ChainIDCelo
- ChainIDNear
- ChainIDMoonbeam
- ChainIDNeon
- ChainIDTerra2
- ChainIDInjective
- ChainIDOsmosis
@ -1065,24 +1089,35 @@ definitions:
- ChainIDAptos
- ChainIDArbitrum
- ChainIDOptimism
- ChainIDGnosis
- ChainIDPythNet
- ChainIDXpla
- ChainIDBtc
- ChainIDBase
- ChainIDSei
- ChainIDRootstock
- ChainIDScroll
- ChainIDMantle
- ChainIDBlast
- ChainIDXLayer
- ChainIDLinea
- ChainIDBerachain
- ChainIDWormchain
- ChainIDCosmoshub
- ChainIDEvmos
- ChainIDKujira
- ChainIDNeutron
- ChainIDCelestia
- ChainIDStargaze
- ChainIDSeda
- ChainIDDymension
- ChainIDProvenance
- ChainIDSepolia
- ChainIDArbitrumSepolia
- ChainIDBaseSepolia
- ChainIDOptimismSepolia
- ChainIDHolesky
- ChainIDPolygonSepolia
vaa.VaaDoc:
properties:
appId:
@ -2326,6 +2361,52 @@ paths:
description: Internal Server Error
tags:
- wormholescan
/api/v1/x-chain-activity/tops:
get:
description: Search, for a specific period of time, the number of transactions
and the volume.
operationId: x-chain-activity-tops
parameters:
- description: 'Time span, supported values: 1d, 1mo and 1y'
in: query
name: timespan
required: true
type: string
- description: From date, supported format 2006-01-02T15:04:05Z07:00
in: query
name: from
required: true
type: string
- description: To date, supported format 2006-01-02T15:04:05Z07:00
in: query
name: to
required: true
type: string
- description: Search by appId
in: query
name: appId
type: string
- description: Search by sourceChain
in: query
name: sourceChain
type: string
- description: Search by targetChain
in: query
name: targetChain
type: string
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/transactions.ChainActivityTopResult'
type: array
"400":
description: Bad Request
"500":
description: Internal Server Error
tags:
- wormholescan
/swagger.json:
get:
description: Returns the swagger specification for this API.

View File

@ -143,6 +143,17 @@ type ChainActivityResult struct {
Volume uint64 `mapstructure:"_value" json:"volume"`
}
type ChainActivityTopResult struct {
Time time.Time `json:"from" mapstructure:"_time"`
To string `json:"to" mapstructure:"to"`
ChainSourceID string `mapstructure:"emitter_chain" json:"emitter_chain"`
ChainDestinationID string `mapstructure:"destination_chain" json:"destination_chain,omitempty"`
Volume uint64 `mapstructure:"volume" json:"volume"`
Txs uint64 `mapstructure:"count" json:"count"`
}
type ChainActivityTopResults []ChainActivityTopResult
type ChainActivityTimeSpan string
const (
@ -202,3 +213,25 @@ type TransactionDto struct {
Payload map[string]interface{} `bson:"payload"`
StandardizedProperties map[string]interface{} `bson:"standardizedProperties"`
}
type ChainActivityTopsQuery struct {
SourceChain *sdk.ChainID `json:"source_chain"`
TargetChain *sdk.ChainID `json:"target_chain"`
AppId string `json:"app_id"`
From time.Time `json:"from"`
To time.Time `json:"to"`
Timespan Timespan `json:"timespan"`
}
type Timespan string
const (
Hour Timespan = "1h"
Day Timespan = "1d"
Month Timespan = "1mo"
Year Timespan = "1y"
)
func (t Timespan) IsValid() bool {
return t == Hour || t == Day || t == Month || t == Year
}

View File

@ -3,6 +3,7 @@ package transactions
import (
"context"
"fmt"
"github.com/valyala/fasthttp"
"strconv"
"strings"
"sync"
@ -1048,3 +1049,338 @@ func (r *Repository) ListTransactionsByAddress(
return documents, nil
}
func (r *Repository) FindChainActivityTops(ctx *fasthttp.RequestCtx, q ChainActivityTopsQuery) ([]ChainActivityTopResult, error) {
query := r.buildChainActivityQueryTops(q)
result, err := r.queryAPI.Query(ctx, query)
if err != nil {
return nil, err
}
if result.Err() != nil {
return nil, result.Err()
}
var response []ChainActivityTopResult
for result.Next() {
var row ChainActivityTopResult
if err = mapstructure.Decode(result.Record().Values(), &row); err != nil {
return nil, err
}
parsedTime, errTime := time.Parse(time.RFC3339Nano, row.To)
if errTime == nil {
row.To = parsedTime.Format(time.RFC3339)
}
response = append(response, row)
}
return response, nil
}
func (r *Repository) buildChainActivityQueryTops(q ChainActivityTopsQuery) string {
var start, stop string
switch q.Timespan {
case Hour:
start = q.From.Truncate(1 * time.Hour).UTC().Format(time.RFC3339)
stop = q.To.Truncate(1 * time.Hour).UTC().Format(time.RFC3339)
case Day:
start = q.From.Truncate(24 * time.Hour).UTC().Format(time.RFC3339)
stop = q.To.Truncate(24 * time.Hour).UTC().Format(time.RFC3339)
case Month:
start = time.Date(q.From.Year(), q.From.Month(), 1, 0, 0, 0, 0, q.From.Location()).UTC().Format(time.RFC3339)
stop = time.Date(q.To.Year(), q.To.Month(), 1, 0, 0, 0, 0, q.To.Location()).UTC().Format(time.RFC3339)
default:
start = time.Date(q.From.Year(), 1, 1, 0, 0, 0, 0, q.From.Location()).UTC().Format(time.RFC3339)
stop = time.Date(q.To.Year(), 1, 1, 0, 0, 0, 0, q.To.Location()).UTC().Format(time.RFC3339)
}
filterTargetChain := ""
if q.TargetChain != nil {
filterTargetChain = "|> filter(fn: (r) => r.destination_chain == \"" + strconv.Itoa(int(*q.TargetChain)) + "\")"
}
filterSourceChain := ""
if q.SourceChain != nil {
filterSourceChain = "|> filter(fn: (r) => r.emitter_chain == \"" + strconv.Itoa(int(*q.SourceChain)) + "\")"
}
filterAppId := ""
if q.AppId != "" {
filterAppId = "|> filter(fn: (r) => r.app_id == \"" + q.AppId + "\")"
}
if q.TargetChain == nil && q.AppId == "" {
return r.buildQueryChainActivityTopsByEmitter(q, start, stop, filterSourceChain)
}
var query string
switch q.Timespan {
case Hour:
query = r.buildQueryChainActivityHourly(start, stop, filterSourceChain, filterTargetChain, filterAppId)
case Day:
query = r.buildQueryChainActivityDaily(start, stop, filterSourceChain, filterTargetChain, filterAppId)
case Month:
query = r.buildQueryChainActivityMonthly(start, stop, filterSourceChain, filterTargetChain, filterAppId)
default:
query = r.buildQueryChainActivityYearly(start, stop, filterSourceChain, filterTargetChain, filterAppId)
}
return query
}
func (r *Repository) buildQueryChainActivityTopsByEmitter(q ChainActivityTopsQuery, start, stop, filterSourceChain string) string {
measurement := ""
switch q.Timespan {
case Hour:
measurement = "emitter_chain_activity_1h"
default:
measurement = "emitter_chain_activity_1d"
}
if q.Timespan == Hour || q.Timespan == Day {
query := `
import "date"
from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "%s")
%s
|> pivot(rowKey:["_time","emitter_chain"], columnKey: ["_field"], valueColumn: "_value")
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, measurement, filterSourceChain)
}
if q.Timespan == Month {
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "%s")
%s
|> drop(columns:["to"])
|> window(every: 1mo, period:1mo)
|> drop(columns:["_time"])
|> rename(columns: {_start: "_time"})
|> map(fn: (r) => ({r with to: string(v: r._stop)}))
vols = data
|> filter(fn: (r) => (r._field == "volume" and r._value > 0))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, measurement, filterSourceChain)
}
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "%s")
%s
|> drop(columns:["to"])
|> window(every: 1y, period:1y)
|> drop(columns:["_time"])
|> rename(columns: {_start: "_time"})
|> map(fn: (r) => ({r with to: string(v: r._stop)}))
vols = data
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, measurement, filterSourceChain)
}
func (r *Repository) buildQueryChainActivityHourly(start, stop, filterSourceChain, filterTargetChain, filterAppId string) string {
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "chain_activity_1h")
%s
%s
%s
|> drop(columns:["destination_chain"])
vols = data
|> filter(fn: (r) => (r._field == "volume" and r._value > 0))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.to == r.to and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, filterSourceChain, filterTargetChain, filterAppId)
}
func (r *Repository) buildQueryChainActivityDaily(start, stop, filterSourceChain, filterTargetChain, filterAppId string) string {
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "chain_activity_1d")
%s
%s
%s
|> drop(columns:["destination_chain"])
vols = data
|> filter(fn: (r) => (r._field == "volume" and r._value > 0))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.to == r.to and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, filterSourceChain, filterTargetChain, filterAppId)
}
func (r *Repository) buildQueryChainActivityMonthly(start, stop, filterSourceChain, filterTargetChain, filterAppId string) string {
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "chain_activity_1d")
%s
%s
%s
|> drop(columns:["destination_chain","to","app_id"])
|> window(every: 1mo, period:1mo)
|> drop(columns:["_time"])
|> rename(columns: {_start: "_time"})
|> map(fn: (r) => ({r with to: string(v: r._stop)}))
vols = data
|> filter(fn: (r) => (r._field == "volume" and r._value > 0))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, filterSourceChain, filterTargetChain, filterAppId)
}
func (r *Repository) buildQueryChainActivityYearly(start, stop, filterSourceChain, filterTargetChain, filterAppId string) string {
query := `
import "date"
import "join"
data = from(bucket: "%s")
|> range(start: %s,stop: %s)
|> filter(fn: (r) => r._measurement == "chain_activity_1d")
%s
%s
%s
|> drop(columns:["destination_chain","to","app_id"])
|> window(every: 1y, period:1y)
|> drop(columns:["_time"])
|> rename(columns: {_start: "_time"})
|> map(fn: (r) => ({r with to: string(v: r._stop)}))
vols = data
|> filter(fn: (r) => (r._field == "volume" and r._value > 0))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "volume"})
counts = data
|> filter(fn: (r) => (r._field == "count"))
|> group(columns:["_time","to","emitter_chain"])
|> sum()
|> rename(columns: {_value: "count"})
join.inner(
left: vols,
right: counts,
on: (l, r) => l._time == r._time and l.emitter_chain == r.emitter_chain,
as: (l, r) => ({l with count: r.count}),
)
|> group()
|> sort(columns:["emitter_chain","_time"],desc:false)
`
return fmt.Sprintf(query, r.bucketInfiniteRetention, start, stop, filterSourceChain, filterTargetChain, filterAppId)
}

View File

@ -2,12 +2,13 @@ package transactions
import (
"context"
errors "errors"
"fmt"
"github.com/valyala/fasthttp"
"strings"
"time"
"github.com/wormhole-foundation/wormhole-explorer/api/cacheable"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
errs "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/metrics"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination"
@ -34,6 +35,7 @@ const (
topAssetsByVolumeKey = "wormscan:top-assets-by-volume"
topChainPairsByNumTransfersKey = "wormscan:top-chain-pairs-by-num-transfers"
chainActivityKey = "wormscan:chain-activity"
chainActivityTopsKey = "wormscan:chain-activity-tops"
)
// NewService create a new Service.
@ -157,7 +159,7 @@ func (s *Service) GetTransactionByID(
return nil, err
}
if len(output) == 0 {
return nil, errors.ErrNotFound
return nil, errs.ErrNotFound
}
// Return matching document
@ -167,3 +169,44 @@ func (s *Service) GetTransactionByID(
func (s *Service) GetTokenProvider() *domain.TokenProvider {
return s.tokenProvider
}
func (s *Service) GetChainActivityTops(ctx *fasthttp.RequestCtx, q ChainActivityTopsQuery) (ChainActivityTopResults, error) {
timeDuration := q.To.Sub(q.From)
if q.Timespan == Hour && timeDuration > 15*24*time.Hour {
return nil, errors.New("time range is too large for hourly data. Max time range allowed: 15 days")
}
if q.Timespan == Day {
if timeDuration < 24*time.Hour {
return nil, errors.New("time range is too small for daily data. Min time range allowed: 2 day")
}
if timeDuration > 365*24*time.Hour {
return nil, errors.New("time range is too large for daily data. Max time range allowed: 1 year")
}
}
if q.Timespan == Month {
if timeDuration < 30*24*time.Hour {
return nil, errors.New("time range is too small for monthly data. Min time range allowed: 60 days")
}
if timeDuration > 10*365*24*time.Hour {
return nil, errors.New("time range is too large for monthly data. Max time range allowed: 1 year")
}
}
if q.Timespan == Year {
if timeDuration < 365*24*time.Hour {
return nil, errors.New("time range is too small for yearly data. Min time range allowed: 1 year")
}
if timeDuration > 10*365*24*time.Hour {
return nil, errors.New("time range is too large for yearly data. Max time range allowed: 10 year")
}
}
return s.repo.FindChainActivityTops(ctx, q)
}

View File

@ -61,10 +61,6 @@ func ExtractToChain(c *fiber.Ctx, l *zap.Logger) (*sdk.ChainID, error) {
return &result, nil
}
func ExtractChain(c *fiber.Ctx, l *zap.Logger) (*sdk.ChainID, error) {
return extractChainQueryParam(c, l, "chain")
}
func ExtractSourceChain(c *fiber.Ctx, l *zap.Logger) (*sdk.ChainID, error) {
return extractChainQueryParam(c, l, "sourceChain")
}
@ -74,12 +70,10 @@ func ExtractTargetChain(c *fiber.Ctx, l *zap.Logger) (*sdk.ChainID, error) {
}
func extractChainQueryParam(c *fiber.Ctx, l *zap.Logger, queryParam string) (*sdk.ChainID, error) {
param := c.Query(queryParam)
if param == "" {
return nil, nil
}
chain, err := strconv.ParseInt(param, 10, 16)
if err != nil {
requestID := fmt.Sprintf("%v", c.Locals("requestid"))
@ -90,7 +84,6 @@ func extractChainQueryParam(c *fiber.Ctx, l *zap.Logger, queryParam string) (*sd
return nil, response.NewInvalidParamError(c, "INVALID CHAIN VALUE", errors.WithStack(err))
}
result := sdk.ChainID(chain)
return &result, nil
}
@ -358,14 +351,13 @@ func ExtractTimeSpanAndSampleRate(c *fiber.Ctx, l *zap.Logger) (string, string,
return timeSpan, sampleRate, nil
}
func ExtractTime(c *fiber.Ctx, queryParam string) (*time.Time, error) {
func ExtractTime(c *fiber.Ctx, timeLayout, queryParam string) (*time.Time, error) {
// get the start_time from query params
date := c.Query(queryParam, "")
if date == "" {
return nil, nil
}
t, err := time.Parse("20060102T150405Z", date)
t, err := time.Parse(timeLayout, date)
if err != nil {
return nil, response.NewInvalidQueryParamError(c, fmt.Sprintf("INVALID <%s> QUERY PARAMETER", queryParam), nil)
}

View File

@ -85,6 +85,7 @@ func RegisterRoutes(
api.Get("/last-txs", transactionCtrl.GetLastTransactions)
api.Get("/scorecards", transactionCtrl.GetScorecards)
api.Get("/x-chain-activity", transactionCtrl.GetChainActivity)
api.Get("/x-chain-activity/tops", transactionCtrl.GetChainActivityTops)
api.Get("/top-assets-by-volume", transactionCtrl.GetTopAssets)
api.Get("/top-chain-pairs-by-num-transfers", transactionCtrl.GetTopChainPairs)
api.Get("token/:chain/:token_address", transactionCtrl.GetTokenByChainAndAddress)

View File

@ -2,6 +2,7 @@ package transactions
import (
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/shopspring/decimal"
@ -182,6 +183,75 @@ func (c *Controller) GetTopAssets(ctx *fiber.Ctx) error {
return ctx.JSON(response)
}
// GetChainActivityTops godoc
// @Description Search for a specific period of time the number of transactions and the volume.
// @Tags wormholescan
// @ID x-chain-activity-tops
// @Method Get
// @Param timespan query string true "Time span, supported values: 1d, 1mo and 1y"
// @Param from query string true "From date, supported format 2006-01-02T15:04:05Z07:00"
// @Param to query string true "To date, supported format 2006-01-02T15:04:05Z07:00"
// @Param appId query string false "Search by appId"
// @Param sourceChain query string false "Search by sourceChain"
// @Param targetChain query string false "Search by targetChain"
// @Success 200 {object} transactions.ChainActivityTopResults
// @Failure 400
// @Failure 500
// @Router /api/v1/x-chain-activity/tops [get]
func (c *Controller) GetChainActivityTops(ctx *fiber.Ctx) error {
sourceChain, err := middleware.ExtractSourceChain(ctx, c.logger)
if err != nil {
return err
}
targetChain, err := middleware.ExtractTargetChain(ctx, c.logger)
if err != nil {
return err
}
from, err := middleware.ExtractTime(ctx, time.RFC3339, "from")
if err != nil {
return err
}
to, err := middleware.ExtractTime(ctx, time.RFC3339, "to")
if err != nil {
return err
}
if from == nil || to == nil {
return response.NewInvalidParamError(ctx, "missing from/to query params ", nil)
}
payload := transactions.ChainActivityTopsQuery{
SourceChain: sourceChain,
TargetChain: targetChain,
From: *from,
To: *to,
AppId: middleware.ExtractAppId(ctx, c.logger),
Timespan: transactions.Timespan(ctx.Query("timespan")),
}
if !payload.Timespan.IsValid() {
return response.NewInvalidParamError(ctx, "invalid timespan", nil)
}
nowUTC := time.Now().UTC()
if nowUTC.Before(payload.To.UTC()) {
payload.To = nowUTC
}
if payload.To.Sub(payload.From) <= 0 {
return response.NewInvalidParamError(ctx, "invalid time range", nil)
}
// Get the chain activity.
activity, err := c.srv.GetChainActivityTops(ctx.Context(), payload)
if err != nil {
c.logger.Error("Error getting chain activity", zap.Error(err))
return err
}
return ctx.JSON(activity)
}
// GetChainActivity godoc
// @Description Returns a list of chain pairs by origin chain and destination chain.
// @Description The list could be rendered by notional or transaction count.