Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions analyzer/aggregate_stats/aggregate_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,36 @@ func (a *aggregateStatsAnalyzer) aggregateStatsWorker(ctx context.Context) {
statsComputations = append(statsComputations, sc)
}

// Compute daily total accounts for all layers.
// This is a simple count of all accounts in the layer, updated once per day.
for _, layer := range statsLayers {
sc := &statsComputation{
target: a.target,
name: "total_accounts" + "_" + layer,
layer: layer,
outputTable: "stats.total_accounts",
outputColumn: "total_accounts",
windowSize: 24 * time.Hour,
windowStep: 24 * time.Hour,
}
if layer == layerConsensus {
// Consensus layer queries.
sc.statsQuery = QueryConsensusTotalAccounts
sc.latestAvailableDataTs = func(ctx context.Context, target storage.TargetStorage) (*time.Time, error) {
var latestBlockTs *time.Time
return latestBlockTs, target.QueryRow(ctx, QueryLatestConsensusBlockTime).Scan(&latestBlockTs)
}
} else {
// Runtime layer queries.
sc.statsQuery = QueryRuntimeTotalAccounts
sc.latestAvailableDataTs = func(ctx context.Context, target storage.TargetStorage) (*time.Time, error) {
var latestBlockTs *time.Time
return latestBlockTs, target.QueryRow(ctx, QueryLatestRuntimeBlockTime, sc.layer).Scan(&latestBlockTs)
}
}
statsComputations = append(statsComputations, sc)
}

for {
// If batch limit was reached, or the batch timeout was reached, start the next iteration sooner.
var useCatchupTimeout bool
Expand Down
19 changes: 19 additions & 0 deletions analyzer/aggregate_stats/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,23 @@ const (
ORDER BY window_end DESC
LIMIT 1
`

// QueryConsensusTotalAccounts is the query to get the total number of accounts
// in the consensus layer.
// Note: The query uses standard parameters for consistency with other stats queries,
// but only counts total accounts regardless of time window.
QueryConsensusTotalAccounts = `
WITH dummy AS (SELECT $1::text, $2::timestamptz, $3::timestamptz) -- Dummy select for parameter compatibility.
SELECT COUNT(*)
FROM chain.accounts
`

// QueryRuntimeTotalAccounts is the query to get the total number of accounts
// in a runtime layer.
QueryRuntimeTotalAccounts = `
WITH dummy AS (SELECT $2::timestamptz, $3::timestamptz) -- Dummy select for parameter compatibility.
SELECT COUNT(*)
FROM chain.runtime_accounts
WHERE runtime = $1
`
)
52 changes: 52 additions & 0 deletions api/spec/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,31 @@ paths:
$ref: '#/components/schemas/ActiveAccountsList'
<<: *common_error_responses

/{layer}/stats/total_accounts:
get:
summary: |
Returns the total number of accounts for either consensus or one of the paratimes.
This is a daily snapshot of the total account count, updated once per day.
parameters:
- *limit
- *offset
- in: path
name: layer
required: true
schema:
allOf: [$ref: '#/components/schemas/Layer']
description: |
The layer for which to return the total accounts.
responses:
'200':
description: |
A JSON object containing a list of total account snapshots.
content:
application/json:
schema:
$ref: '#/components/schemas/TotalAccountsList'
<<: *common_error_responses

components:
schemas:
Layer:
Expand Down Expand Up @@ -4141,6 +4166,33 @@ components:
description: The number of active accounts for the 24hour window ending at window_end.
example: 420

TotalAccountsList:
type: object
required: [snapshots]
properties:
snapshots:
type: array
items:
allOf: [$ref: '#/components/schemas/TotalAccountsSnapshot']
description: The list of total account snapshots, ordered by date descending.
description: |
A list of daily total account snapshots showing account growth over time.

TotalAccountsSnapshot:
type: object
required: [date, total_accounts]
properties:
date:
type: string
format: date-time
description: The date of the snapshot.
example: *iso_timestamp_1
total_accounts:
type: integer
format: uint64
description: The total number of accounts as of this date.
example: 42000

RoflAppList:
allOf:
- $ref: '#/components/schemas/List'
Expand Down
13 changes: 13 additions & 0 deletions api/v1/strict_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ func (srv *StrictServerImpl) GetLayerStatsActiveAccounts(ctx context.Context, re
return apiTypes.GetLayerStatsActiveAccounts200JSONResponse(*activeAccountsList), nil
}

func (srv *StrictServerImpl) GetLayerStatsTotalAccounts(ctx context.Context, request apiTypes.GetLayerStatsTotalAccountsRequestObject) (apiTypes.GetLayerStatsTotalAccountsResponseObject, 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)}
}

totalAccountsList, err := srv.dbClient.TotalAccounts(ctx, request.Layer, request.Params)
if err != nil {
return nil, err
}
return apiTypes.GetLayerStatsTotalAccounts200JSONResponse(*totalAccountsList), nil
}

func (srv *StrictServerImpl) GetConsensusTransactions(ctx context.Context, request apiTypes.GetConsensusTransactionsRequestObject) (apiTypes.GetConsensusTransactionsResponseObject, error) {
txs, err := srv.dbClient.Transactions(ctx, request.Params, nil)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions storage/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3019,3 +3019,35 @@ func (c *StorageClient) DailyActiveAccounts(ctx context.Context, layer apiTypes.

return &ts, nil
}

// TotalAccounts returns a list of total account snapshots.
func (c *StorageClient) TotalAccounts(ctx context.Context, layer apiTypes.Layer, p apiTypes.GetLayerStatsTotalAccountsParams) (*TotalAccountsList, error) {
rows, err := c.db.Query(
ctx,
queries.TotalAccounts,
translateLayer(layer),
p.Limit,
p.Offset,
)
if err != nil {
return nil, wrapError(err)
}
defer rows.Close()

ts := TotalAccountsList{
Snapshots: []apiTypes.TotalAccountsSnapshot{},
}
for rows.Next() {
var t apiTypes.TotalAccountsSnapshot
if err := rows.Scan(
&t.Date,
&t.TotalAccounts,
); err != nil {
return nil, wrapError(err)
}
t.Date = t.Date.UTC() // Ensure UTC timestamp in response.
ts.Snapshots = append(ts.Snapshots, t)
}

return &ts, nil
}
18 changes: 18 additions & 0 deletions storage/client/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,24 @@ const (
window_end DESC
LIMIT $2::bigint
OFFSET $3::bigint`

// TotalAccounts returns the query for total account snapshots per layer.
TotalAccounts = `
SELECT window_end, total_accounts
FROM stats.total_accounts
WHERE layer = $1::text
ORDER BY
window_end DESC
LIMIT $2::bigint
OFFSET $3::bigint`

// LatestTotalAccounts returns the most recent total account count for a layer.
LatestTotalAccounts = `
SELECT window_end, total_accounts
FROM stats.total_accounts
WHERE layer = $1::text
ORDER BY window_end DESC
LIMIT 1`
)

func escapeLike(s string) string {
Expand Down
3 changes: 3 additions & 0 deletions storage/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ type TxVolume = api.TxVolume
// DailyActiveAccountsList is the storage response for GetDailyActiveAccounts.
type DailyActiveAccountsList = api.ActiveAccountsList

// TotalAccountsList is the storage response for GetTotalAccounts.
type TotalAccountsList = api.TotalAccountsList

// RoflAppList is the storage response for GetRuntimeRoflApps.
type RoflAppList = api.RoflAppList

Expand Down
18 changes: 18 additions & 0 deletions storage/migrations/54_total_accounts_stats.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Add stats table for tracking total number of accounts per layer.

BEGIN;

-- total_accounts stores the daily snapshot of total accounts per layer.
CREATE TABLE stats.total_accounts
(
layer TEXT NOT NULL,
window_end TIMESTAMP WITH TIME ZONE NOT NULL,
total_accounts UINT63 NOT NULL,

PRIMARY KEY (layer, window_end)
);

-- Grant read access.
GRANT SELECT ON stats.total_accounts TO PUBLIC;

COMMIT;
Loading