diff --git a/api/spec/v1.yaml b/api/spec/v1.yaml index fa3e05482..a42c58f69 100644 --- a/api/spec/v1.yaml +++ b/api/spec/v1.yaml @@ -804,13 +804,22 @@ paths: $ref: '#/components/schemas/RuntimeTokenList' <<: *common_error_responses - /consensus/stats/tx_volume: + /{layer}/stats/tx_volume: get: - summary: Returns the consensus layer transaction volume at daily granularity + summary: | + Returns a timeline of the transaction volume at the chosen granularity, + for either consensus or one of the paratimes. parameters: - *limit - *offset - *bucket_size_seconds + - in: path + name: layer + required: true + schema: + $ref: '#/components/schemas/Layer' + description: | + The layer for which to return the transaction volume timeline. responses: '200': description: | @@ -823,6 +832,10 @@ paths: components: schemas: + Layer: + type: string + enum: [consensus, emerald] + Status: type: object required: [latest_chain_id, latest_block, latest_update] diff --git a/api/v1/strict_server.go b/api/v1/strict_server.go index 316a3e175..cbb3c5f4c 100644 --- a/api/v1/strict_server.go +++ b/api/v1/strict_server.go @@ -174,12 +174,17 @@ func (srv *StrictServerImpl) GetConsensusProposalsProposalIdVotes(ctx context.Co return apiTypes.GetConsensusProposalsProposalIdVotes200JSONResponse(*votes), nil } -func (srv *StrictServerImpl) GetConsensusStatsTxVolume(ctx context.Context, request apiTypes.GetConsensusStatsTxVolumeRequestObject) (apiTypes.GetConsensusStatsTxVolumeResponseObject, error) { - volumeList, err := srv.dbClient.TxVolumes(ctx, request.Params) +func (srv *StrictServerImpl) GetLayerStatsTxVolume(ctx context.Context, request apiTypes.GetLayerStatsTxVolumeRequestObject) (apiTypes.GetLayerStatsTxVolumeResponseObject, error) { + // Additional param validation. + if !request.Layer.IsValid() { + return nil, &apiTypes.InvalidParamFormatError{ParamName: "layer", Err: fmt.Errorf("not a valid enum value: %s", request.Layer)} + } + + volumeList, err := srv.dbClient.TxVolumes(ctx, request.Layer, request.Params) if err != nil { return nil, err } - return apiTypes.GetConsensusStatsTxVolume200JSONResponse(*volumeList), nil + return apiTypes.GetLayerStatsTxVolume200JSONResponse(*volumeList), nil } func (srv *StrictServerImpl) GetConsensusTransactions(ctx context.Context, request apiTypes.GetConsensusTransactionsRequestObject) (apiTypes.GetConsensusTransactionsResponseObject, error) { diff --git a/api/v1/types/util.go b/api/v1/types/util.go index 92eb16f23..082fcde23 100644 --- a/api/v1/types/util.go +++ b/api/v1/types/util.go @@ -25,3 +25,12 @@ func (c ConsensusEventType) IsValid() bool { return false } } + +func (c Layer) IsValid() bool { + switch c { + case LayerConsensus, LayerEmerald: + return true + default: + return false + } +} diff --git a/storage/client/client.go b/storage/client/client.go index 8e954ca79..df8728193 100644 --- a/storage/client/client.go +++ b/storage/client/client.go @@ -1455,12 +1455,8 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetEmerald } // TxVolumes returns a list of transaction volumes per time bucket. -func (c *StorageClient) TxVolumes(ctx context.Context, p apiTypes.GetConsensusStatsTxVolumeParams) (*TxVolumeList, error) { - if p.BucketSizeSeconds == nil { - return nil, fmt.Errorf("bucket_size_seconds default was not applied") // double-check middleware's job - } - - qf := NewQueryFactory(strcase.ToSnake(c.chainID), "") +func (c *StorageClient) TxVolumes(ctx context.Context, layer apiTypes.Layer, p apiTypes.GetLayerStatsTxVolumeParams) (*TxVolumeList, error) { + qf := NewQueryFactory(strcase.ToSnake(c.chainID), string(layer)) var query string if *p.BucketSizeSeconds == 300 { query = qf.FineTxVolumesQuery() @@ -1473,6 +1469,7 @@ func (c *StorageClient) TxVolumes(ctx context.Context, p apiTypes.GetConsensusSt rows, err := c.db.Query( ctx, query, + layer, p.Limit, p.Offset, ) diff --git a/storage/client/queries.go b/storage/client/queries.go index a2b445510..5635bde4d 100644 --- a/storage/client/queries.go +++ b/storage/client/queries.go @@ -357,21 +357,23 @@ func (qf QueryFactory) RuntimeTokensQuery() string { func (qf QueryFactory) FineTxVolumesQuery() string { return ` SELECT window_start, tx_volume - FROM stats.min5_tx_volume + FROM stats.min5_tx_volume + WHERE layer = $1::text ORDER BY window_start DESC - LIMIT $1::bigint - OFFSET $2::bigint + LIMIT $2::bigint + OFFSET $3::bigint ` } func (qf QueryFactory) TxVolumesQuery() string { return ` SELECT window_start, tx_volume - FROM stats.daily_tx_volume + FROM stats.daily_tx_volume + WHERE layer = $1::text ORDER BY window_start DESC - LIMIT $1::bigint - OFFSET $2::bigint + LIMIT $2::bigint + OFFSET $3::bigint ` } diff --git a/storage/migrations/03_agg_stats.up.sql b/storage/migrations/03_agg_stats.up.sql index a0689dc24..3241e8ae5 100644 --- a/storage/migrations/03_agg_stats.up.sql +++ b/storage/migrations/03_agg_stats.up.sql @@ -18,20 +18,32 @@ GRANT EXECUTE ON FUNCTION floor_5min TO PUBLIC; -- NOTE: This materialized view is NOT refreshed every 5 minutes due to computational cost. CREATE MATERIALIZED VIEW stats.min5_tx_volume AS SELECT + 'consensus' AS layer, floor_5min(b.time) AS window_start, COUNT(*) AS tx_volume FROM oasis_3.blocks AS b - INNER JOIN oasis_3.transactions AS t ON b.height = t.block - GROUP BY 1; + JOIN oasis_3.transactions AS t ON b.height = t.block + GROUP BY 2 + + UNION ALL + + SELECT + 'emerald' AS layer, + floor_5min(b.timestamp) AS window_start, + COUNT(*) AS tx_volume + FROM oasis_3.emerald_rounds AS b + JOIN oasis_3.emerald_transactions AS t ON b.round = t.round + GROUP BY 2; -- daily_tx_volume stores the number of transactions per day -- at the consensus layer. CREATE MATERIALIZED VIEW stats.daily_tx_volume AS SELECT + layer, date_trunc ( 'day', sub.window_start ) AS window_start, SUM(sub.tx_volume) AS tx_volume FROM stats.min5_tx_volume AS sub - GROUP BY 1; + GROUP BY 1, 2; -- Grant others read-only use. This does NOT apply to future tables in the schema.