Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/subtivity #52

Merged
merged 20 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
11026ef
Added aggregation endpoints
Pelotfr Nov 13, 2023
1354a17
Added /uaw endpoint
Pelotfr Nov 14, 2023
9e7ba1d
Added supported chains as store (periodically updated)
Pelotfr Nov 15, 2023
ba4a2a0
Added chain parameter verification
Pelotfr Nov 15, 2023
c428f7b
Simplified queries and tests
Pelotfr Nov 15, 2023
df69f33
Added /uaw/history endpoint
Pelotfr Nov 15, 2023
98aef4f
fixed endpoint name and unix date to seconds
Pelotfr Nov 15, 2023
faccf5d
enum time range filter and results split by chain
Pelotfr Nov 16, 2023
23403f1
Renamed /uaw/history into /uaw and removed previous /uaw endpoint
Pelotfr Nov 17, 2023
332b0d3
Refactored parameter verification and defaults
Pelotfr Nov 17, 2023
b56f3d9
Added multi select + history logic to agg endpoints
Pelotfr Nov 21, 2023
e7aa833
added normalized data format and grouped similar queries in one
Pelotfr Nov 21, 2023
bba20f9
Removed multi-select for aggregate functions, defined new standard re…
Pelotfr Nov 21, 2023
6be9bd7
Updated docs and endpoints to use new standard response and simpler q…
Pelotfr Nov 21, 2023
6441d18
Changed from blocks to module_hashes for faster result
Pelotfr Nov 22, 2023
a3277f2
Default agg to sum and simplified checks
Pelotfr Nov 22, 2023
f2443cc
moved uaw request handling into aggregate, added some comments
Pelotfr Nov 22, 2023
5eab96a
Fixed description
Pelotfr Nov 22, 2023
e18d74e
Merge branch 'main' into feature/subtivity
Pelotfr Nov 22, 2023
0f0f201
Revert "Merge branch 'main' into feature/subtivity"
Pelotfr Nov 22, 2023
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
|-------------------------------------------|-----------------------|
| GET `/chains` | Available `chains`
| GET `/block` | Get block by `block_number`, `block_id` or `timestamp`
| GET `/trace_calls` | Get aggregate of trace_calls filtered by `chain`, `timestamp` or `block_number`
| GET `/transaction_traces` | Get aggregate of transaction_traces filtered by `chain`, `timestamp` or `block_number`
| GET `/uaw` | Get unique active wallets filtered by `chain` and `date`
| GET `/uaw/history` | Get daily unique active wallets for previous given number of days filtered by `chain`
| GET `/trace_calls` | Get aggregate of trace_calls for given time range filtered by `chain`
| GET `/transaction_traces` | Get aggregate of transaction_traces for given time range filtered by `chain`
| GET `/uaw` | Get daily unique active wallets for given time range filtered by `chain`
| GET `/health` | Health check
| GET `/metrics` | Prometheus metrics
| GET `/openapi` | [OpenAPI v3 JSON](https://spec.openapis.org/oas/v3.0.0)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "substreams-clock-api",
"description": "Block & Timestamps API",
"version": "1.4.0",
"version": "1.4.1",
"homepage": "https://github.com/pinax-network/substreams-clock-api",
"license": "MIT",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions src/clickhouse/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ class ClickhouseStore {
// Fetch data initially
this.fetchData();

// Set up a timer to fetch data periodically (e.g., every 1 hour)
// Fetch periodically
setInterval(() => {
this.fetchData();
}, 10000); // 3600000 milliseconds = 1 hour
}, 10000); // in milliseconds
}

private fetchData() {
this.ChainsPromise = client
.query({ query: "SELECT DISTINCT chain FROM blocks", format: "JSONEachRow" })
.query({ query: "SELECT DISTINCT chain FROM module_hashes", format: "JSONEachRow" })
.then((response) => response.json<Array<{ chain: string }>>())
.then((chains) => chains.map(({ chain }) => chain))
.catch(() => []);
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const DEFAULT_MAX_LIMIT = 500;
export const DEFAULT_VERBOSE = false;
export const APP_NAME = pkg.name;
export const DEFAULT_SORT_BY = "DESC";
export const DEFAULT_AGGREGATE_FUNCTION = "count";
export const DEFAULT_AGGREGATE_FUNCTION = "sum";

// parse command line options
const opts = program
Expand Down
5 changes: 1 addition & 4 deletions src/fetch/GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import health from "./health.js";
import chains from "./chains.js";
import block from "./block.js";
import aggregate from "./aggregate.js";
import uaw from "./uaw.js";
import history from "./history.js";
import * as prometheus from "../prometheus.js";
import { logger } from "../logger.js";
import swaggerHtml from "../../swagger/index.html"
Expand All @@ -24,8 +22,7 @@ export default async function (req: Request) {
if ( pathname === "/block" ) return block(req);
if ( pathname === "/trace_calls" ) return aggregate(req, pathname);
if ( pathname === "/transaction_traces" ) return aggregate(req, pathname);
if ( pathname === "/uaw" ) return uaw(req);
if ( pathname === "/uaw/history" ) return history(req);
if ( pathname === "/uaw" ) return aggregate(req, pathname);
logger.warn(`Not found: ${pathname}`);
prometheus.request_error.inc({pathname, status: 404});
return NotFound;
Expand Down
13 changes: 9 additions & 4 deletions src/fetch/aggregate.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { makeQuery } from "../clickhouse/makeQuery.js";
import { logger } from "../logger.js";
import { getAggregate } from "../queries.js";
import { NormalizedHistoryData, getAggregate } from "../queries.js";
import * as prometheus from "../prometheus.js";
import { BadRequest, toJSON } from "./cors.js";
import { verifyParameters } from "../utils.js";
import { parseNormalized, verifyParameters } from "../utils.js";

// endpoint for aggregates (trace_calls, transaction_traces, uaw)
export default async function (req: Request, pathname: string) {
// verify some crucial parameters beforehand
const parametersResult = await verifyParameters(req);
if(parametersResult instanceof Response) {
return parametersResult;
}
try {
const { searchParams } = new URL(req.url);
logger.info({searchParams: Object.fromEntries(Array.from(searchParams))});
// creates the query for requested aggregate column based on pathname
const query = getAggregate(searchParams, pathname.replace("/", ""));
const response = await makeQuery<number>(query)
return toJSON(response.data);
const response = await makeQuery<NormalizedHistoryData>(query)
// formats the response into daily intervals
const formatted = parseNormalized(response.data, 86400);
return toJSON(formatted);
} catch (e: any) {
logger.error(e);
prometheus.request_error.inc({pathname: pathname, status: 400});
Expand Down
25 changes: 0 additions & 25 deletions src/fetch/history.ts

This file was deleted.

117 changes: 17 additions & 100 deletions src/fetch/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import pkg from "../../package.json" assert { type: "json" };
import { OpenApiBuilder, SchemaObject, ExampleObject, ParameterObject } from "openapi3-ts/oas31";
import { config } from "../config.js";
import { store } from "../clickhouse/stores.js";
import { getBlock, getAggregate, getUAWFromDate, getUAWHistory, UAWHistory } from "../queries.js";
import { getBlock, getAggregate, NormalizedHistoryData } from "../queries.js";
import { registry } from "../prometheus.js";
import { makeQuery } from "../clickhouse/makeQuery.js";
import { parseUAWResponse } from "../utils.js";
import { parseNormalized } from "../utils.js";

const TAGS = {
MONITORING: "Monitoring",
Expand All @@ -16,10 +16,9 @@ const TAGS = {
} as const;

const block_example = (await makeQuery(await getBlock( new URLSearchParams({limit: "2"})))).data;
const trace_calls_example = (await makeQuery(getAggregate( new URLSearchParams({aggregate_function: "count", chain: "wax"}), "trace_calls"))).data;
const transaction_traces_example = (await makeQuery(getAggregate( new URLSearchParams({aggregate_function: "count", chain: "wax"}), "transaction_traces"))).data;
const uaw_example = (await makeQuery(getUAWFromDate( new URLSearchParams({chain: "wax", date: "2023-09-06"})))).data;
const history_example = parseUAWResponse((await makeQuery<UAWHistory>(getUAWHistory( new URLSearchParams({chain: "eos", range: "7"})))).data);
const trace_calls_example = parseNormalized((await makeQuery<NormalizedHistoryData>(getAggregate( new URLSearchParams({aggregate_function: "count", chain: "wax"}), "trace_calls"))).data, 86400);
const transaction_traces_example = parseNormalized((await makeQuery<NormalizedHistoryData>(getAggregate( new URLSearchParams({aggregate_function: "count", chain: "wax"}), "transaction_traces"))).data, 86400);
const uaw_example = parseNormalized((await makeQuery<NormalizedHistoryData>(getAggregate( new URLSearchParams({chain: "eos", range: "24h"}), "uaw"))).data, 86400);

const timestampSchema: SchemaObject = { anyOf: [
{type: "number"},
Expand Down Expand Up @@ -154,7 +153,7 @@ export default new OpenApiBuilder()
get: {
tags: [TAGS.USAGE],
summary: "Get aggregate of trace_calls",
description: "Get aggregate of trace_calls filtered by `chain`, `timestamp` or `block_number`",
description: "Get aggregate of trace_calls for given time range filtered by `chain`",
parameters: [
{
name: "aggregate_function",
Expand All @@ -171,39 +170,12 @@ export default new OpenApiBuilder()
schema: {enum: await store.chains},
},
{
name: 'timestamp',
in: 'query',
description: 'Filter by exact timestamp',
required: false,
schema: timestampSchema,
examples: timestampExamples,
},
{
name: "block_number",
description: "Filter by Block number (ex: 18399498)",
name: "range",
in: "query",
description: "Time range to query (ex: 24h)",
required: false,
schema: { type: "number" },
},
...["greater_or_equals_by_timestamp", "greater_by_timestamp", "less_or_equals_by_timestamp", "less_by_timestamp"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: timestampSchema,
examples: timestampExamples,
} as ParameterObject
}),
...["greater_or_equals_by_block_number", "greater_by_block_number", "less_or_equals_by_block_number", "less_by_block_number"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: { type: "number" },
} as ParameterObject
}),
schema: { enum: ["24h", "7d", "30d", "90d", "1y", "all"] },
}
],
responses: {
200: { description: "Aggregate of sales", content: { "text/plain": { example: trace_calls_example} } },
Expand All @@ -215,7 +187,7 @@ export default new OpenApiBuilder()
get: {
tags: [TAGS.USAGE],
summary: "Get aggregate of transaction_traces",
description: "Get aggregate of transaction_traces filtered by `chain`, `timestamp` or `block_number`",
description: "Get aggregate of transaction_traces for given time range filtered by `chain`",
parameters: [
{
name: "aggregate_function",
Expand All @@ -232,39 +204,12 @@ export default new OpenApiBuilder()
schema: {enum: await store.chains},
},
{
name: 'timestamp',
in: 'query',
description: 'Filter by exact timestamp',
required: false,
schema: timestampSchema,
examples: timestampExamples,
},
{
name: "block_number",
description: "Filter by Block number (ex: 18399498)",
name: "range",
in: "query",
description: "Time range to query (ex: 24h)",
required: false,
schema: { type: "number" },
},
...["greater_or_equals_by_timestamp", "greater_by_timestamp", "less_or_equals_by_timestamp", "less_by_timestamp"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: timestampSchema,
examples: timestampExamples,
} as ParameterObject
}),
...["greater_or_equals_by_block_number", "greater_by_block_number", "less_or_equals_by_block_number", "less_by_block_number"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: { type: "number" },
} as ParameterObject
}),
schema: { enum: ["24h", "7d", "30d", "90d", "1y", "all"] },
}
],
responses: {
200: { description: "Aggregate of sales", content: { "text/plain": { example: transaction_traces_example} } },
Expand All @@ -273,34 +218,6 @@ export default new OpenApiBuilder()
},
})
.addPath("/uaw", {
get: {
tags: [TAGS.USAGE],
summary: "Get unique active wallets",
description: "Get unique active wallets filtered by `chain` and `date`",
parameters: [
{
name: "chain",
in: "query",
description: "Filter by chain name",
required: false,
schema: {enum: await store.chains},
},
{
name: "date",
description: "Filter by date (ex: 2023-09-06)",
in: "query",
required: false,
schema: DateSchema,
examples: DateExamples,
},
],
responses: {
200: { description: "Unique active wallets", content: { "text/plain": { example: uaw_example} } },
400: { description: "Bad request", content: { "text/plain": { example: "Bad request", schema: { type: "string" } } }, },
},
},
})
.addPath("/uaw/history", {
get: {
tags: [TAGS.USAGE],
summary: "Get daily unique active wallets",
Expand All @@ -316,13 +233,13 @@ export default new OpenApiBuilder()
{
name: "range",
in: "query",
description: "Time range to query (ex: 7d)",
description: "Time range to query (ex: 24h)",
required: false,
schema: { enum: ["24h", "7d", "30d", "90d", "1y", "all"] },
}
],
responses: {
200: { description: "Daily active wallets", content: { "text/plain": { example: history_example} } },
200: { description: "Daily active wallets", content: { "text/plain": { example: uaw_example} } },
400: { description: "Bad request", content: { "text/plain": { example: "Bad request", schema: { type: "string" } } }, },
},
},
Expand Down
24 changes: 0 additions & 24 deletions src/fetch/uaw.ts

This file was deleted.

32 changes: 5 additions & 27 deletions src/queries.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, jest, mock, test } from "bun:test";
import { createBlockQuery, getBlock, getAggregate, getUAWFromDate, getUAWHistory } from "./queries.js";
import { createBlockQuery, getBlock, getAggregate } from "./queries.js";
import { store } from "./clickhouse/stores.js";

// Mock supported chains data to prevent DB query
Expand Down Expand Up @@ -29,33 +29,11 @@ test("getBlock", async () => {
});

test("getAggregate", async () => {
const singleChainQuery = new URLSearchParams({ chain: "wax"});
expect(getAggregate(singleChainQuery, "trace_calls"))
.toBe(`SELECT chain, count(trace_calls) FROM BlockStats WHERE (chain == 'wax') GROUP BY chain`);

expect(getAggregate(new URLSearchParams(), "transaction_traces"))
.toBe(`SELECT chain, count(transaction_traces) FROM BlockStats GROUP BY chain`);
});

test("getUAWFromDate", async () => {
const singleChainQuery = new URLSearchParams({ chain: "wax", date: "2023-09-06" });
expect(getUAWFromDate(singleChainQuery))
.toBe(`SELECT chain, count(distinct uaw) FROM BlockStats ARRAY JOIN uaw WHERE (toUnixTimestamp(DATE(timestamp)) == toUnixTimestamp(DATE(1693958400)) AND chain == 'wax') GROUP BY chain`);

expect(getUAWFromDate(new URLSearchParams({ date: "2023-09-06" })))
.toBe(`SELECT chain, count(distinct uaw) FROM BlockStats ARRAY JOIN uaw WHERE (toUnixTimestamp(DATE(timestamp)) == toUnixTimestamp(DATE(1693958400))) GROUP BY chain`);
});

test("getUAWHistory", async () => {
const date_of_query = Math.floor(Number(new Date().setHours(0,0,0,0)) / 1000);
const datetime_of_query = Math.floor(Number(new Date()) / 1000);
expect(getAggregate(new URLSearchParams({ chain: "wax" }), "trace_calls"))
.toBe(`SELECT chain, toUnixTimestamp(DATE(timestamp)) as timestamp, sum(trace_calls) as value FROM BlockStats WHERE (timestamp BETWEEN ${datetime_of_query} - 3600 * 24 AND ${datetime_of_query} AND chain == 'wax') GROUP BY chain, timestamp ORDER BY timestamp ASC`);

expect(getUAWHistory(new URLSearchParams({ chain: "eos", range: "7d" })))
.toBe(`SELECT chain, toUnixTimestamp(DATE(timestamp)) as day, count(distinct uaw) as UAW FROM BlockStats ARRAY JOIN uaw WHERE (timestamp BETWEEN ${date_of_query} - 86400 * 7 AND ${date_of_query} AND chain == 'eos') GROUP BY chain, day ORDER BY day ASC`);

expect(getUAWHistory(new URLSearchParams({ range: "24h" })))
.toBe(`SELECT chain, toUnixTimestamp(DATE(timestamp)) as day, count(distinct uaw) as UAW FROM BlockStats ARRAY JOIN uaw WHERE (timestamp BETWEEN ${datetime_of_query} - 3600 * 24 AND ${datetime_of_query}) GROUP BY chain, day ORDER BY day ASC`);

expect(getUAWHistory(new URLSearchParams({ range: "1y" })))
.toBe(`SELECT chain, toUnixTimestamp(DATE(timestamp)) as day, count(distinct uaw) as UAW FROM BlockStats ARRAY JOIN uaw WHERE (timestamp BETWEEN ${date_of_query} - 31536000 * 1 AND ${date_of_query}) GROUP BY chain, day ORDER BY day ASC`);
expect(getAggregate(new URLSearchParams({ range: "7d"}), "transaction_traces"))
.toBe(`SELECT chain, toUnixTimestamp(DATE(timestamp)) as timestamp, sum(transaction_traces) as value FROM BlockStats WHERE (timestamp BETWEEN ${date_of_query} - 86400 * 7 AND ${date_of_query}) GROUP BY chain, timestamp ORDER BY timestamp ASC`);
});
Loading
Loading