diff --git a/index.ts b/index.ts index d083749..d3448ff 100644 --- a/index.ts +++ b/index.ts @@ -69,7 +69,7 @@ function AntelopeTokenAPI() { const createUsageEndpoint = (endpoint: UsageEndpoints) => app.get( // Hono using different syntax than OpenAPI for path parameters // `/{path_param}` (OpenAPI) VS `/:path_param` (Hono) - endpoint.replace(/{([^}]+)}/, ":$1"), + endpoint.replace(/{([^}]+)}/g, ":$1"), async (ctx: Context) => { const result = EndpointByMethod["get"][endpoint].parameters.safeParse({ query: ctx.req.query(), @@ -92,13 +92,13 @@ function AntelopeTokenAPI() { } ); - createUsageEndpoint("/balance"); // TODO: Maybe separate `block_num`/`timestamp` queries with path parameters (additional response schemas) - createUsageEndpoint("/head"); - createUsageEndpoint("/holders"); - createUsageEndpoint("/supply"); // TODO: Same as `balance`` - createUsageEndpoint("/tokens"); - createUsageEndpoint("/transfers"); // TODO: Redefine `block_range` params - createUsageEndpoint("/transfers/{trx_id}"); + createUsageEndpoint("/{chain}/balance"); + createUsageEndpoint("/chains"); + createUsageEndpoint("/{chain}/holders"); + createUsageEndpoint("/{chain}/supply"); + createUsageEndpoint("/{chain}/tokens"); + createUsageEndpoint("/{chain}/transfers"); + createUsageEndpoint("/{chain}/transfers/{trx_id}"); app.notFound((ctx: Context) => APIErrorResponse(ctx, 404, "route_not_found", `Path not found: ${ctx.req.method} ${ctx.req.path}`)); diff --git a/src/clickhouse/client.ts b/src/clickhouse/client.ts index 17d484f..b443e51 100644 --- a/src/clickhouse/client.ts +++ b/src/clickhouse/client.ts @@ -3,7 +3,6 @@ import { ping } from "./ping.js"; import { APP_NAME, config } from "../config.js"; // TODO: Check how to abort previous queries if haven't returned yet -// TODO: Make client connect to all DB instances const client = createClient({ ...config, clickhouse_settings: { diff --git a/src/types/api.ts b/src/types/api.ts index 6a05ce2..21e2d31 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,4 +1,4 @@ -import { ZodArray, ZodBigInt, ZodBoolean, ZodDate, ZodDefault, ZodNumber, ZodOptional, ZodType, ZodTypeAny, ZodUndefined, ZodUnion, z } from "zod"; +import { ZodArray, ZodBigInt, ZodBoolean, ZodDate, ZodDefault, ZodLiteral, ZodNumber, ZodOptional, ZodType, ZodTypeAny, ZodUndefined, ZodUnion, z } from "zod"; import { EndpointByMethod, type GetEndpoints } from './zod.gen.js'; import { config } from "../config.js"; @@ -25,7 +25,7 @@ export function fixEndpointParametersCoercion() { if (EndpointByMethod["get"][endpoint as UsageEndpoints].parameters.shape) { Object.values(EndpointByMethod["get"][endpoint as UsageEndpoints].parameters.shape).map(p => p.shape).forEach( // `p` can be query or path parameters - (p) => Object.keys(p).forEach( + (p) => Object.keys(p).filter(k => k !== "chain").forEach( (key, _) => { let zod_type = p[key] as ZodTypeAny; let underlying_zod_type: ZodTypeAny; diff --git a/src/types/zod.gen.ts b/src/types/zod.gen.ts index c668b6e..2cca130 100644 --- a/src/types/zod.gen.ts +++ b/src/types/zod.gen.ts @@ -89,6 +89,9 @@ export const Supply = z.object({ supply_delta: z.number(), }); +export type SupportedChains = z.infer; +export const SupportedChains = z.union([z.literal("eos"), z.literal("wax")]); + export type Transfer = z.infer; export const Transfer = z.object({ trx_id: z.string(), @@ -112,30 +115,10 @@ export const Version = z.object({ commit: z.string(), }); -export type get_Usage_balance = typeof get_Usage_balance; -export const get_Usage_balance = { +export type get_Usage_chains = typeof get_Usage_chains; +export const get_Usage_chains = { method: z.literal("GET"), - path: z.literal("/balance"), - parameters: z.object({ - query: z.object({ - block_num: z.union([z.number(), z.undefined()]), - contract: z.union([z.string(), z.undefined()]), - symcode: z.union([z.string(), z.undefined()]), - account: z.string(), - limit: z.union([z.number(), z.undefined()]), - page: z.union([z.number(), z.undefined()]), - }), - }), - response: z.object({ - data: z.array(BalanceChange), - meta: ResponseMetadata, - }), -}; - -export type get_Usage_head = typeof get_Usage_head; -export const get_Usage_head = { - method: z.literal("GET"), - path: z.literal("/head"), + path: z.literal("/chains"), parameters: z.object({ query: z.object({ limit: z.number().optional(), @@ -160,10 +143,57 @@ export const get_Monitoring_health = { response: z.string(), }; +export type get_Monitoring_metrics = typeof get_Monitoring_metrics; +export const get_Monitoring_metrics = { + method: z.literal("GET"), + path: z.literal("/metrics"), + parameters: z.never(), + response: z.string(), +}; + +export type get_Docs_openapi = typeof get_Docs_openapi; +export const get_Docs_openapi = { + method: z.literal("GET"), + path: z.literal("/openapi"), + parameters: z.never(), + response: z.unknown(), +}; + +export type get_Docs_version = typeof get_Docs_version; +export const get_Docs_version = { + method: z.literal("GET"), + path: z.literal("/version"), + parameters: z.never(), + response: Version, +}; + +export type get_Usage_balance = typeof get_Usage_balance; +export const get_Usage_balance = { + method: z.literal("GET"), + path: z.literal("/{chain}/balance"), + parameters: z.object({ + query: z.object({ + block_num: z.union([z.number(), z.undefined()]), + contract: z.union([z.string(), z.undefined()]), + symcode: z.union([z.string(), z.undefined()]), + account: z.string(), + limit: z.union([z.number(), z.undefined()]), + page: z.union([z.number(), z.undefined()]), + }), + path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), + }), + }), + response: z.object({ + data: z.array(BalanceChange), + meta: ResponseMetadata, + }), +}; + export type get_Usage_holders = typeof get_Usage_holders; export const get_Usage_holders = { method: z.literal("GET"), - path: z.literal("/holders"), + path: z.literal("/{chain}/holders"), parameters: z.object({ query: z.object({ contract: z.string(), @@ -171,6 +201,9 @@ export const get_Usage_holders = { limit: z.union([z.number(), z.undefined()]), page: z.union([z.number(), z.undefined()]), }), + path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), + }), }), response: z.object({ data: z.array(Holder), @@ -178,26 +211,10 @@ export const get_Usage_holders = { }), }; -export type get_Monitoring_metrics = typeof get_Monitoring_metrics; -export const get_Monitoring_metrics = { - method: z.literal("GET"), - path: z.literal("/metrics"), - parameters: z.never(), - response: z.string(), -}; - -export type get_Docs_openapi = typeof get_Docs_openapi; -export const get_Docs_openapi = { - method: z.literal("GET"), - path: z.literal("/openapi"), - parameters: z.never(), - response: z.unknown(), -}; - export type get_Usage_supply = typeof get_Usage_supply; export const get_Usage_supply = { method: z.literal("GET"), - path: z.literal("/supply"), + path: z.literal("/{chain}/supply"), parameters: z.object({ query: z.object({ block_num: z.union([z.number(), z.undefined()]), @@ -207,6 +224,9 @@ export const get_Usage_supply = { limit: z.union([z.number(), z.undefined()]), page: z.union([z.number(), z.undefined()]), }), + path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), + }), }), response: z.object({ data: z.array(Supply), @@ -217,12 +237,15 @@ export const get_Usage_supply = { export type get_Usage_tokens = typeof get_Usage_tokens; export const get_Usage_tokens = { method: z.literal("GET"), - path: z.literal("/tokens"), + path: z.literal("/{chain}/tokens"), parameters: z.object({ query: z.object({ limit: z.number().optional(), page: z.number().optional(), }), + path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), + }), }), response: z.object({ data: z.array(Supply), @@ -233,7 +256,7 @@ export const get_Usage_tokens = { export type get_Usage_transfers = typeof get_Usage_transfers; export const get_Usage_transfers = { method: z.literal("GET"), - path: z.literal("/transfers"), + path: z.literal("/{chain}/transfers"), parameters: z.object({ query: z.object({ block_range: z.array(z.number()).optional(), @@ -244,6 +267,9 @@ export const get_Usage_transfers = { limit: z.number().optional(), page: z.number().optional(), }), + path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), + }), }), response: z.object({ data: z.array(Transfer), @@ -254,13 +280,14 @@ export const get_Usage_transfers = { export type get_Usage_transfer = typeof get_Usage_transfer; export const get_Usage_transfer = { method: z.literal("GET"), - path: z.literal("/transfers/{trx_id}"), + path: z.literal("/{chain}/transfers/{trx_id}"), parameters: z.object({ query: z.object({ limit: z.number().optional(), page: z.number().optional(), }), path: z.object({ + chain: z.union([z.literal("eos"), z.literal("wax")]), trx_id: z.string(), }), }), @@ -270,28 +297,20 @@ export const get_Usage_transfer = { }), }; -export type get_Docs_version = typeof get_Docs_version; -export const get_Docs_version = { - method: z.literal("GET"), - path: z.literal("/version"), - parameters: z.never(), - response: Version, -}; - // export const EndpointByMethod = { get: { - "/balance": get_Usage_balance, - "/head": get_Usage_head, + "/chains": get_Usage_chains, "/health": get_Monitoring_health, - "/holders": get_Usage_holders, "/metrics": get_Monitoring_metrics, "/openapi": get_Docs_openapi, - "/supply": get_Usage_supply, - "/tokens": get_Usage_tokens, - "/transfers": get_Usage_transfers, - "/transfers/{trx_id}": get_Usage_transfer, "/version": get_Docs_version, + "/{chain}/balance": get_Usage_balance, + "/{chain}/holders": get_Usage_holders, + "/{chain}/supply": get_Usage_supply, + "/{chain}/tokens": get_Usage_tokens, + "/{chain}/transfers": get_Usage_transfers, + "/{chain}/transfers/{trx_id}": get_Usage_transfer, }, }; export type EndpointByMethod = typeof EndpointByMethod; diff --git a/src/typespec/openapi3.tsp b/src/typespec/openapi3.tsp index a776d82..5111946 100644 --- a/src/typespec/openapi3.tsp +++ b/src/typespec/openapi3.tsp @@ -70,6 +70,11 @@ model UsageResponse { meta: ResponseMetadata; } +enum SupportedChains { + EOS: "eos", + WAX: "wax" +} + // Alias will *not* be present in the OpenAPI components. // This also helps preventing self-references in generated `components` for codegen to work properly. alias APIResponse = T | APIError; @@ -89,9 +94,10 @@ interface Usage { @returns Array of balances. */ @summary("Token balance") - @route("/balance") + @route("/{chain}/balance") @get balance( + @path chain: SupportedChains, @query block_num?: BlockInfo.block_num, @query contract?: TokenIdentifier.contract, @query symcode?: TokenIdentifier.symcode, @@ -100,13 +106,13 @@ interface Usage { ): APIResponse>; /** - Information about the current head block in the database. + List of available Antelope chains and corresponding latest block for which data is available. @returns Array of block information. */ - @summary("Head block information") - @route("/head") + @summary("Chains and latest block available") + @route("/chains") @get - head(...PaginationQueryParams): APIResponse>; @@ -115,9 +121,10 @@ interface Usage { @returns Array of accounts. */ @summary("Token holders") - @route("/holders") + @route("/{chain}/holders") @get holders( + @path chain: SupportedChains, @query contract: TokenIdentifier.contract, @query symcode: TokenIdentifier.symcode, ...PaginationQueryParams, @@ -128,9 +135,10 @@ interface Usage { @returns Array of supplies. */ @summary("Token supply") - @route("/supply") + @route("/{chain}/supply") @get supply( + @path chain: SupportedChains, @query block_num?: BlockInfo.block_num, @query issuer?: Supply.issuer, @query contract: TokenIdentifier.contract, @@ -143,23 +151,26 @@ interface Usage { @returns Array of supplies. */ @summary("Tokens") - @route("/tokens") + @route("/{chain}/tokens") @get - tokens(...PaginationQueryParams): APIResponse>; + tokens( + @path chain: SupportedChains, + ...PaginationQueryParams + ): APIResponse>; /** All transfers related to a token. @returns Array of transfers. */ @summary("Token transfers") - @route("/transfers") + @route("/{chain}/transfers") @get transfers( + @path chain: SupportedChains, @query({ format: "csv", }) block_range?: BlockInfo.block_num[], - @query from?: Transfer.from, @query to?: Transfer.to, @query contract?: TokenIdentifier.contract, @@ -172,9 +183,10 @@ interface Usage { @returns Array of transfers. */ @summary("Token transfer") - @route("/transfers/{trx_id}") + @route("/{chain}/transfers/{trx_id}") @get transfer( + @path chain: SupportedChains, @path trx_id: Models.TraceInformation.trx_id, ...PaginationQueryParams, ): APIResponse>; diff --git a/src/usage.ts b/src/usage.ts index d57ba8c..508db19 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -3,6 +3,8 @@ import { APIErrorResponse } from "./utils.js"; import type { Context } from "hono"; import type { AdditionalQueryParams, EndpointReturnTypes, UsageEndpoints, UsageResponse, ValidUserParams } from "./types/api.js"; +import { config } from "./config.js"; +import client from "./clickhouse/client.js"; export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, user_params: ValidUserParams) { type EndpointElementReturnType = EndpointReturnTypes[number]; @@ -17,7 +19,7 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use let filters = ""; // Don't add `limit` and `block_range` to WHERE clause - for (const k of Object.keys(query_params).filter(k => k !== "limit" && k !== "block_range")) + for (const k of Object.keys(query_params).filter(k => k !== "limit" && k !== "block_range" && k !== "chain")) filters += ` (${k} = {${k}: String}) AND`; filters = filters.substring(0, filters.lastIndexOf(' ')); // Remove last item ` AND` @@ -26,31 +28,39 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use let query = ""; let additional_query_params: AdditionalQueryParams = {}; - if (endpoint == "/balance" || endpoint == "/supply") { + let database = config.database; + + if (endpoint !== "/chains") { + // TODO: Document required database setup + const q = query_params as ValidUserParams; + database = `${q.chain}_tokens_v1` + } + + if (endpoint == "/{chain}/balance" || endpoint == "/{chain}/supply") { // Need to narrow the type of `query_params` explicitly to access properties based on endpoint value // See https://github.com/microsoft/TypeScript/issues/33014 const q = query_params as ValidUserParams; if (q.block_num) { query += `SELECT *` - + ` FROM ${endpoint == "/balance" ? "balance_change_events" : "supply_change_events"}`; + + ` FROM ${endpoint == "/{chain}/balance" ? `${database}.balance_change_events` : `${database}.supply_change_events`}`; query += ` ${filters} ORDER BY action_index DESC`; query_params.limit = 1; } else { query += `SELECT *, updated_at_block_num AS block_num, updated_at_timestamp AS timestamp` - + ` FROM ${endpoint == "/balance" ? "account_balances" : "token_supplies"}` + + ` FROM ${endpoint == "/{chain}/balance" ? `${database}.account_balances` : `${database}.token_supplies`}` + ` FINAL`; query += ` ${filters} ORDER BY block_num DESC`; } - } else if (endpoint == "/transfers") { + } else if (endpoint == "/{chain}/transfers") { query += `SELECT * FROM `; const q = query_params as ValidUserParams; if (q.block_range) { - query += `transfers_block_num`; + query += `${database}.transfers_block_num`; console.log(q.block_range); if (q.block_range[0] && q.block_range[1]) { filters += "AND (block_num >= {min_block: int} AND block_num <= {max_block: int})" @@ -69,24 +79,30 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use "((from = {from: String}) OR (to = {to: String}))", ) - query += `transfers_from`; + query += `${database}.transfers_from`; } else if (q.to) { - query += `transfers_to`; + query += `${database}.transfers_to`; } else if (q.contract || q.symcode) { - query += `transfers_contract`; + query += `${database}.transfers_contract`; } else { - query += `transfers_block_num`; + query += `${database}.transfers_block_num`; } query += ` ${filters} ORDER BY block_num DESC`; - } else if (endpoint == "/holders") { - query += `SELECT account, value FROM (SELECT account, MAX(updated_at_block_num) AS last_updated FROM eos_tokens_v1.account_balances ${filters} GROUP BY account) AS x INNER JOIN eos_tokens_v1.account_balances AS y ON y.account = x.account AND y.updated_at_block_num = x.last_updated ${filters} ORDER BY value DESC`; - } else if (endpoint == "/head") { - query += `SELECT MAX(block_num) as block_num FROM cursors ${filters} GROUP BY id`; - } else if (endpoint == "/transfers/{trx_id}") { - query += `SELECT * FROM transfer_events ${filters} ORDER BY action_index`; - } else if (endpoint == "/tokens") { - query += `SELECT *, updated_at_block_num AS block_num FROM eos_tokens_v1.token_supplies FINAL ${filters} ORDER BY block_num DESC`; + } else if (endpoint == "/{chain}/holders") { + query += `SELECT account, value FROM (SELECT account, MAX(updated_at_block_num) AS last_updated FROM ${database}.account_balances ${filters} GROUP BY account) AS x INNER JOIN ${database}.account_balances AS y ON y.account = x.account AND y.updated_at_block_num = x.last_updated ${filters} ORDER BY value DESC`; + } else if (endpoint == "/chains") { + // TODO: More flexible to account for different chains ? + query += + `SELECT 'wax' as chain, MAX(block_num) as block_num` + + ` FROM wax_tokens_v1.cursors GROUP BY id` + + ` UNION ALL` + + ` SELECT 'eos' as chain, MAX(block_num) as block_num` + + ` FROM eos_tokens_v1.cursors GROUP BY id` + } else if (endpoint == "/{chain}/transfers/{trx_id}") { + query += `SELECT * FROM ${database}.transfer_events ${filters} ORDER BY action_index`; + } else if (endpoint == "/{chain}/tokens") { + query += `SELECT *, updated_at_block_num AS block_num FROM ${database}.token_supplies FINAL ${filters} ORDER BY block_num DESC`; } query += " LIMIT {limit: int}"; diff --git a/tsp-output/@typespec/openapi3/openapi.json b/tsp-output/@typespec/openapi3/openapi.json index d7f6aa5..76ec6dd 100644 --- a/tsp-output/@typespec/openapi3/openapi.json +++ b/tsp-output/@typespec/openapi3/openapi.json @@ -21,48 +21,15 @@ } ], "paths": { - "/balance": { + "/chains": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_balance", - "summary": "Token balance", - "description": "Balances of an account.", + "operationId": "Usage_chains", + "summary": "Chains and latest block available", + "description": "List of available Antelope chains and corresponding latest block for which data is available.", "parameters": [ - { - "name": "block_num", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "uint64" - } - }, - { - "name": "contract", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "symcode", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "account", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "limit", "in": "query", @@ -86,7 +53,7 @@ ], "responses": { "200": { - "description": "Array of balances.", + "description": "Array of block information.", "content": { "application/json": { "schema": { @@ -99,7 +66,16 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BalanceChange" + "type": "object", + "properties": { + "block_num": { + "type": "integer", + "format": "uint64" + } + }, + "required": [ + "block_num" + ] } }, "meta": { @@ -123,67 +99,79 @@ } } }, - "/head": { + "/health": { "get": { "tags": [ - "Usage" + "Monitoring" ], - "operationId": "Usage_head", - "summary": "Head block information", - "description": "Information about the current head block in the database.", - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "uint64", - "default": 10 + "operationId": "Monitoring_health", + "summary": "Health check", + "description": "Checks database connection.", + "parameters": [], + "responses": { + "200": { + "description": "OK or APIError.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } } }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "uint64", - "default": 1 + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } } } + } + } + }, + "/metrics": { + "get": { + "tags": [ + "Monitoring" ], + "operationId": "Monitoring_metrics", + "summary": "Prometheus metrics", + "description": "Prometheus metrics.", + "parameters": [], "responses": { "200": { - "description": "Array of block information.", + "description": "Metrics as text.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/openapi": { + "get": { + "tags": [ + "Docs" + ], + "operationId": "Docs_openapi", + "summary": "OpenAPI JSON spec", + "description": "Reflection endpoint to return OpenAPI JSON spec. Also used by Swagger to generate the frontpage.", + "parameters": [], + "responses": { + "200": { + "description": "The OpenAPI JSON spec", "content": { "application/json": { "schema": { "type": "object", - "required": [ - "data", - "meta" - ], - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "block_num": { - "type": "integer", - "format": "uint64" - } - }, - "required": [ - "block_num" - ] - } - }, - "meta": { - "$ref": "#/components/schemas/ResponseMetadata" - } - } + "additionalProperties": {} } } } @@ -201,22 +189,22 @@ } } }, - "/health": { + "/version": { "get": { "tags": [ - "Monitoring" + "Docs" ], - "operationId": "Monitoring_health", - "summary": "Health check", - "description": "Checks database connection.", + "operationId": "Docs_version", + "summary": "API version", + "description": "API version and Git short commit hash.", "parameters": [], "responses": { "200": { - "description": "OK or APIError.", + "description": "The API version and commit hash.", "content": { "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/Version" } } } @@ -234,19 +222,36 @@ } } }, - "/holders": { + "/{chain}/balance": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_holders", - "summary": "Token holders", - "description": "List of holders of a token.", + "operationId": "Usage_balance", + "summary": "Token balance", + "description": "Balances of an account.", "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, + { + "name": "block_num", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, { "name": "contract", "in": "query", - "required": true, + "required": false, "schema": { "type": "string" } @@ -254,6 +259,14 @@ { "name": "symcode", "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "account", + "in": "query", "required": true, "schema": { "type": "string" @@ -282,7 +295,7 @@ ], "responses": { "200": { - "description": "Array of accounts.", + "description": "Array of balances.", "content": { "application/json": { "schema": { @@ -295,7 +308,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Holder" + "$ref": "#/components/schemas/BalanceChange" } }, "meta": { @@ -319,46 +332,82 @@ } } }, - "/metrics": { + "/{chain}/holders": { "get": { "tags": [ - "Monitoring" + "Usage" ], - "operationId": "Monitoring_metrics", - "summary": "Prometheus metrics", - "description": "Prometheus metrics.", - "parameters": [], - "responses": { - "200": { - "description": "Metrics as text.", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } + "operationId": "Usage_holders", + "summary": "Token holders", + "description": "List of holders of a token.", + "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, + { + "name": "contract", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "symcode", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 10 + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 1 } } - } - } - }, - "/openapi": { - "get": { - "tags": [ - "Docs" ], - "operationId": "Docs_openapi", - "summary": "OpenAPI JSON spec", - "description": "Reflection endpoint to return OpenAPI JSON spec. Also used by Swagger to generate the frontpage.", - "parameters": [], "responses": { "200": { - "description": "The OpenAPI JSON spec", + "description": "Array of accounts.", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": {} + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Holder" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } } } } @@ -376,7 +425,7 @@ } } }, - "/supply": { + "/{chain}/supply": { "get": { "tags": [ "Usage" @@ -385,6 +434,14 @@ "summary": "Token supply", "description": "Total supply for a token.", "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, { "name": "block_num", "in": "query", @@ -478,7 +535,7 @@ } } }, - "/tokens": { + "/{chain}/tokens": { "get": { "tags": [ "Usage" @@ -487,6 +544,14 @@ "summary": "Tokens", "description": "List of available tokens.", "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, { "name": "limit", "in": "query", @@ -547,7 +612,7 @@ } } }, - "/transfers": { + "/{chain}/transfers": { "get": { "tags": [ "Usage" @@ -556,6 +621,14 @@ "summary": "Token transfers", "description": "All transfers related to a token.", "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, { "name": "block_range", "in": "query", @@ -662,7 +735,7 @@ } } }, - "/transfers/{trx_id}": { + "/{chain}/transfers/{trx_id}": { "get": { "tags": [ "Usage" @@ -671,6 +744,14 @@ "summary": "Token transfer", "description": "Specific transfer related to a token.", "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, { "name": "trx_id", "in": "path", @@ -738,39 +819,6 @@ } } } - }, - "/version": { - "get": { - "tags": [ - "Docs" - ], - "operationId": "Docs_version", - "summary": "API version", - "description": "API version and Git short commit hash.", - "parameters": [], - "responses": { - "200": { - "description": "The API version and commit hash.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Version" - } - } - } - }, - "default": { - "description": "An unexpected error response.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIError" - } - } - } - } - } - } } }, "components": { @@ -1044,6 +1092,13 @@ } } }, + "SupportedChains": { + "type": "string", + "enum": [ + "eos", + "wax" + ] + }, "Transfer": { "type": "object", "required": [