diff --git a/README.md b/README.md index f2058d6..1acfb21 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,14 @@ | GET
`text/plain` | `/health` | Checks database connection | | GET
`text/plain` | `/metrics` | [Prometheus](https://prometheus.io/) metrics | +## GraphQL + +Go to `/graphql` for a GraphIQL interface. + ## Requirements - [ClickHouse](clickhouse.com/), databases should follow a `{chain}_tokens_{version}` naming scheme. Database tables can be setup using the [`schema.sql`](./schema.sql) definitions created by the [`create_schema.sh`](./create_schema.sh) script. -- A [Substream sink](https://substreams.streamingfast.io/reference-and-specs/glossary#sink) for loading data into ClickHouse. We recommend [Substreams Sink ClickHouse](https://github.com/pinax-network/substreams-sink-clickhouse/) or [Substreams Sink SQL](https://github.com/pinax-network/substreams-sink-sql). You should use the generated [`protobuf` files](tsp-output/@typespec/protobuf) to build your substream. This Token API makes use of the [`substreams-antelope-tokens`](https://github.com/pinax-network/substreams-antelope-tokens/) substream. +- A [Substream sink](https://substreams.streamingfast.io/reference-and-specs/glossary#sink) for loading data into ClickHouse. We recommend [Substreams Sink ClickHouse](https://github.com/pinax-network/substreams-sink-clickhouse/) or [Substreams Sink SQL](https://github.com/pinax-network/substreams-sink-sql). You should use the generated [`protobuf` files](static/@typespec/protobuf) to build your substream. This Token API makes use of the [`substreams-antelope-tokens`](https://github.com/pinax-network/substreams-antelope-tokens/) substream. ### API stack architecture diff --git a/bun.lockb b/bun.lockb index 0c19d78..005f82c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.ts b/index.ts index e7c4f4b..cf4c687 100644 --- a/index.ts +++ b/index.ts @@ -1,21 +1,22 @@ -import client from './src/clickhouse/client.js'; -import openapi from "./tsp-output/@typespec/openapi3/openapi.json"; - -import { Hono } from "hono"; +import { Hono, type Context } from "hono"; +import { type RootResolver, graphqlServer } from '@hono/graphql-server'; +import { buildSchema } from 'graphql'; import { z } from 'zod'; -import { paths } from './src/types/zod.gen.js'; + +import client from './src/clickhouse/client.js'; +import openapi from "./static/@typespec/openapi3/openapi.json"; +import * as prometheus from './src/prometheus.js'; import { APP_VERSION } from "./src/config.js"; import { logger } from './src/logger.js'; -import * as prometheus from './src/prometheus.js'; import { makeUsageQuery } from "./src/usage.js"; import { APIErrorResponse } from "./src/utils.js"; +import { usageOperationsToEndpointsMap, type EndpointReturnTypes, type UsageEndpoints, type ValidPathParams, type ValidUserParams } from "./src/types/api.js"; +import { paths } from './src/types/zod.gen.js'; -import type { Context } from "hono"; -import type { EndpointReturnTypes, UsageEndpoints, ValidPathParams, ValidUserParams } from "./src/types/api.js"; - -function AntelopeTokenAPI() { +async function AntelopeTokenAPI() { const app = new Hono(); + // Tracking all incoming requests app.use(async (ctx: Context, next) => { const pathname = ctx.req.path; logger.trace(`Incoming request: [${pathname}]`); @@ -24,6 +25,10 @@ function AntelopeTokenAPI() { await next(); }); + // --------------- + // --- Swagger --- + // --------------- + app.get( "/", async (_) => new Response(Bun.file("./swagger/index.html")) @@ -34,6 +39,10 @@ function AntelopeTokenAPI() { async (_) => new Response(Bun.file("./swagger/favicon.ico")) ); + // ------------ + // --- Docs --- + // ------------ + app.get( "/openapi", async (ctx: Context) => ctx.json<{ [key: string]: EndpointReturnTypes<"/openapi">; }, 200>(openapi) @@ -44,6 +53,10 @@ function AntelopeTokenAPI() { async (ctx: Context) => ctx.json, 200>(APP_VERSION) ); + // ------------------ + // --- Monitoring --- + // ------------------ + app.get( "/health", async (ctx: Context) => { @@ -62,6 +75,10 @@ function AntelopeTokenAPI() { async (_) => new Response(await prometheus.registry.metrics(), { headers: { "Content-Type": prometheus.registry.contentType } }) ); + // -------------------------- + // --- REST API endpoints --- + // -------------------------- + const createUsageEndpoint = (endpoint: UsageEndpoints) => app.get( // Hono using different syntax than OpenAPI for path parameters // `/{path_param}` (OpenAPI) VS `/:path_param` (Hono) @@ -88,17 +105,57 @@ function AntelopeTokenAPI() { } ); - createUsageEndpoint("/{chain}/balance"); - createUsageEndpoint("/chains"); - createUsageEndpoint("/{chain}/holders"); - createUsageEndpoint("/{chain}/supply"); - createUsageEndpoint("/{chain}/tokens"); - createUsageEndpoint("/{chain}/transfers"); - createUsageEndpoint("/{chain}/transfers/{trx_id}"); + // Create all API endpoints interacting with DB + Object.values(usageOperationsToEndpointsMap).forEach(e => createUsageEndpoint(e)); + + // ------------------------ + // --- GraphQL endpoint --- + // ------------------------ + + const schema = buildSchema(await Bun.file("./static/@openapi-to-graphql/graphql/schema.graphql").text()); + const rootResolver: RootResolver = async (ctx?: Context) => { + if (ctx) { + const createGraphQLUsageResolver = (endpoint: UsageEndpoints) => + async (args: ValidUserParams) => await (await makeUsageQuery(ctx, endpoint, { ...args })).json(); + + return Object.keys(usageOperationsToEndpointsMap).reduce( + // SQL queries endpoints + (resolver, op) => Object.assign( + resolver, + { + [op]: createGraphQLUsageResolver(usageOperationsToEndpointsMap[op] as UsageEndpoints) + } + ), + // Other endpoints + { + health: async () => { + const response = await client.ping(); + return response.success ? "OK" : `[500] bad_database_response: ${response.error.message}`; + }, + openapi: () => openapi, + metrics: async () => await prometheus.registry.getMetricsAsJSON(), + version: () => APP_VERSION + } + ); + } + }; + + app.use( + '/graphql', + graphqlServer({ + schema, + rootResolver, + graphiql: true, // if `true`, presents GraphiQL when the GraphQL endpoint is loaded in a browser. + }) + ); + + // ------------- + // --- Miscs --- + // ------------- app.notFound((ctx: Context) => APIErrorResponse(ctx, 404, "route_not_found", `Path not found: ${ctx.req.method} ${ctx.req.path}`)); return app; } -export default AntelopeTokenAPI(); \ No newline at end of file +export default await AntelopeTokenAPI(); \ No newline at end of file diff --git a/kubb.config.ts b/kubb.config.ts index 676765e..27d84bc 100644 --- a/kubb.config.ts +++ b/kubb.config.ts @@ -5,7 +5,7 @@ export default defineConfig(() => { return { root: '.', input: { - path: './tsp-output/@typespec/openapi3/openapi.json', + path: './static/@typespec/openapi3/openapi.json', }, output: { path: './src/types' diff --git a/package.json b/package.json index f8eefca..6e6da75 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "antelope-token-api", "description": "Token balances, supply and transfers from the Antelope blockchains", - "version": "4.0.0", + "version": "5.0.0", "homepage": "https://github.com/pinax-network/antelope-token-api", "license": "MIT", "authors": [ @@ -18,6 +18,7 @@ ], "dependencies": { "@clickhouse/client-web": "latest", + "@hono/graphql-server": "^0.5.0", "@kubb/cli": "^2.23.3", "@kubb/core": "^2.23.3", "@kubb/plugin-oas": "^2.23.3", @@ -37,7 +38,7 @@ "lint": "export APP_VERSION=$(git rev-parse --short HEAD) && bun run tsc --noEmit --skipLibCheck --pretty", "start": "export APP_VERSION=$(git rev-parse --short HEAD) && bun index.ts", "test": "bun test --coverage", - "types": "bun run tsp compile ./src/typespec && bun run kubb", + "types": "bun run tsp compile ./src/typespec --output-dir static && bun run openapi-to-graphql ./static/@typespec/openapi3/openapi.json --save static/@openapi-to-graphql/graphql/schema.graphql --simpleNames --singularNames && bun run kubb", "types:check": "bun run tsp compile ./src/typespec --no-emit --pretty --warn-as-error", "types:format": "bun run tsp format src/typespec/**/*.tsp", "types:watch": "bun run tsp compile ./src/typespec --watch --pretty --warn-as-error" @@ -45,10 +46,11 @@ "type": "module", "devDependencies": { "@typespec/compiler": "latest", + "@typespec/openapi": "latest", "@typespec/openapi3": "latest", "@typespec/protobuf": "latest", - "@typespec/openapi": "latest", "bun-types": "latest", + "openapi-to-graphql-cli": "^3.0.7", "typescript": "latest" }, "prettier": { diff --git a/src/types/README.md b/src/types/README.md index 91b19eb..c8695ca 100644 --- a/src/types/README.md +++ b/src/types/README.md @@ -1,9 +1,8 @@ ### `zod.gen.ts` -> [!WARNING] -> **DO NOT EDIT**: Auto-generated [Zod](https://zod.dev/) schemas definitions from the [OpenAPI3](../tsp-output/@typespec/openapi3/openapi.json) specification using [`Kubb`](https://kubb.dev). - -Use `bun run types` to run the code generation for Zod schemas. +> [!CAUTION] +> Auto-generated [Zod](https://zod.dev/) schemas definitions from the [OpenAPI3](../static/@typespec/openapi3/openapi.json) specification using [`Kubb`](https://kubb.dev). **DO NOT EDIT MANUALLY**. +> Use `bun run types` to run the code generation. ### `api.ts` diff --git a/src/types/api.ts b/src/types/api.ts index 45eb67b..42b36ea 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { paths } from './zod.gen.js'; +import { operations, paths } from './zod.gen.js'; type GetEndpoints = typeof paths; export type EndpointReturnTypes = z.infer; @@ -21,3 +21,14 @@ export type ValidUserParams = EndpointParameters ex export type AdditionalQueryParams = { offset?: number; min_block?: number; max_block?: number; }; // Allow any valid parameters from the endpoint to be used as SQL query parameters export type ValidQueryParams = ValidUserParams & AdditionalQueryParams; + +// Map stripped operations name (e.g. `Usage_transfers` stripped to `transfers`) to endpoint paths (e.g. `/{chain}/transfers`) +// This is used to map GraphQL operations to REST endpoints +export const usageOperationsToEndpointsMap = Object.entries(operations).filter(([k, _]) => k.startsWith("Usage")).reduce( + (o, [k, v]) => Object.assign( + o, + { + [k.split('_')[1] as string]: Object.entries(paths).find(([k_, v_]) => v_.get === v)?.[0] + } + ), {} +) as { [key in string]: UsageEndpoints }; \ No newline at end of file diff --git a/src/types/zod.gen.ts b/src/types/zod.gen.ts index 1cb1e1e..8a52176 100644 --- a/src/types/zod.gen.ts +++ b/src/types/zod.gen.ts @@ -5,7 +5,7 @@ export const apiErrorSchema = z.object({ "status": z.union([z.literal(500), z.li export type ApiErrorSchema = z.infer; -export const balanceChangeSchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.number(), "account": z.coerce.string(), "balance": z.coerce.string(), "balance_delta": z.coerce.number() }); +export const balanceChangeSchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.string(), "account": z.coerce.string(), "balance": z.coerce.string(), "balance_delta": z.coerce.number() }); export type BalanceChangeSchema = z.infer; @@ -25,15 +25,15 @@ export const responseMetadataSchema = z.object({ "statistics": z.lazy(() => quer export type ResponseMetadataSchema = z.infer; -export const supplySchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.number(), "issuer": z.coerce.string(), "max_supply": z.coerce.string(), "supply": z.coerce.string(), "supply_delta": z.coerce.number() }); +export const supplySchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.string(), "issuer": z.coerce.string(), "max_supply": z.coerce.string(), "supply": z.coerce.string(), "supply_delta": z.coerce.number() }); export type SupplySchema = z.infer; -export const supportedChainsSchema = z.enum(["eos", "wax"]); +export const supportedChainsSchema = z.enum(["EOS", "WAX"]); export type SupportedChainsSchema = z.infer; -export const transferSchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.number(), "from": z.coerce.string(), "to": z.coerce.string(), "quantity": z.coerce.string(), "memo": z.coerce.string() }); +export const transferSchema = z.object({ "trx_id": z.coerce.string(), "action_index": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "precision": z.coerce.number(), "amount": z.coerce.number(), "value": z.coerce.number(), "block_num": z.coerce.number(), "timestamp": z.coerce.string(), "from": z.coerce.string(), "to": z.coerce.string(), "quantity": z.coerce.string(), "memo": z.coerce.string() }); export type TransferSchema = z.infer; @@ -78,12 +78,12 @@ export type MonitoringHealthQueryResponseSchema = z.infer; /** * @description Metrics as text. */ -export const monitoringMetricsQueryResponseSchema = z.coerce.string(); +export const monitoringMetricsQueryResponseSchema = z.object({}); export type MonitoringMetricsQueryResponseSchema = z.infer; /** diff --git a/src/typespec/README.md b/src/typespec/README.md index ea01018..fa0e934 100644 --- a/src/typespec/README.md +++ b/src/typespec/README.md @@ -14,6 +14,6 @@ The data models used for both outputs can be found in [`models.tsp`](./models.ts ## Compiling definitions -Use the `bun run types:watch` to auto-compile the definitions on file changes. Generated outputs can be found in the [`tsp-output`](/tsp-output/) folder. +Use the `bun run types:watch` to auto-compile the definitions on file changes. Generated outputs can be found in the [`static`](/static/) folder. Typescript compiler options can be found in [`tspconfig.yaml`](/tspconfig.yaml). \ No newline at end of file diff --git a/src/typespec/openapi3.tsp b/src/typespec/openapi3.tsp index 58764e5..9133b53 100644 --- a/src/typespec/openapi3.tsp +++ b/src/typespec/openapi3.tsp @@ -12,7 +12,7 @@ using TypeSpec.OpenAPI; name: "MIT", url: "https://github.com/pinax-network/antelope-token-api/blob/4f4bf36341b794c0ccf5b7a14fdf810be06462d2/LICENSE" }, - version: "4.0.0" + version: "5.0.0" }) // From @typespec/openapi namespace AntelopeTokenAPI; @@ -38,10 +38,12 @@ model APIError { message: string; } +alias TimestampType = string; + // Models will be present in the OpenAPI components -model Transfer is Models.Transfer; -model BalanceChange is Models.BalanceChange; -model Supply is Models.Supply; +model Transfer is Models.Transfer; +model BalanceChange is Models.BalanceChange; +model Supply is Models.Supply; model Holder { account: BalanceChange.account; balance: BalanceChange.value; @@ -71,8 +73,8 @@ model UsageResponse { } enum SupportedChains { - EOS: "eos", - WAX: "wax" + EOS, + WAX } // Alias will *not* be present in the OpenAPI components. @@ -84,7 +86,7 @@ alias PaginationQueryParams = { }; // Helper aliases for accessing underlying properties -alias BlockInfo = Models.BlockInfo; +alias BlockInfo = Models.BlockInfo; alias TokenIdentifier = Models.Scope; @tag("Usage") @@ -240,5 +242,5 @@ interface Monitoring { @summary("Prometheus metrics") @route("/metrics") @get - metrics(): string; + metrics(): Record; } diff --git a/src/usage.ts b/src/usage.ts index e60f022..c5ca36d 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -37,8 +37,7 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use if (endpoint !== "/chains") { const q = query_params as ValidUserParams; - // TODO: Document required database setup - database = `${q.chain}_tokens_v1`; + database = `${q.chain.toLowerCase()}_tokens_v1`; } if (endpoint == "/{chain}/balance" || endpoint == "/{chain}/supply") { @@ -104,7 +103,7 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use for (const chain of supportedChainsSchema._def.values) query += `SELECT '${chain}' as chain, MAX(block_num) as block_num` - + ` FROM ${chain}_tokens_v1.cursors GROUP BY id` + + ` FROM ${chain.toLowerCase()}_tokens_v1.cursors GROUP BY id` + ` UNION ALL `; query = query.substring(0, query.lastIndexOf(' UNION')); // Remove last item ` UNION` } else if (endpoint == "/{chain}/transfers/{trx_id}") { diff --git a/static/@openapi-to-graphql/graphql/schema.graphql b/static/@openapi-to-graphql/graphql/schema.graphql new file mode 100644 index 0000000..d544624 --- /dev/null +++ b/static/@openapi-to-graphql/graphql/schema.graphql @@ -0,0 +1,209 @@ +type Query { + """ + Balances of an account. + + Equivalent to GET /{chain}/balance + """ + balance(account: String!, block_num: Int, chain: Chain!, contract: String, limit: Int, page: Int, symcode: String): Balance + + """ + List of available Antelope chains and corresponding latest block for which data is available. + + Equivalent to GET /chains + """ + chains(limit: Int, page: Int): Chains + + """ + Checks database connection. + + Equivalent to GET /health + """ + health: String + + """ + List of holders of a token. + + Equivalent to GET /{chain}/holders + """ + holders(chain: Chain!, contract: String!, limit: Int, page: Int, symcode: String!): Holders + + """ + Prometheus metrics. + + Equivalent to GET /metrics + """ + metrics: JSON + + """ + Reflection endpoint to return OpenAPI JSON spec. Also used by Swagger to generate the frontpage. + + Equivalent to GET /openapi + """ + openapi: JSON + + """ + Total supply for a token. + + Equivalent to GET /{chain}/supply + """ + supply(block_num: Int, chain: Chain!, contract: String!, issuer: String, limit: Int, page: Int, symcode: String!): Supply + + """ + List of available tokens. + + Equivalent to GET /{chain}/tokens + """ + tokens(chain: Chain!, limit: Int, page: Int): Tokens + + """ + Specific transfer related to a token. + + Equivalent to GET /{chain}/transfers/{trx_id} + """ + transfer(chain: Chain!, limit: Int, page: Int, trx_id: String!): Transfer2 + + """ + All transfers related to a token. + + Equivalent to GET /{chain}/transfers + """ + transfers(block_range: [Int], chain: Chain!, contract: String, from: String, limit: Int, page: Int, symcode: String, to: String): Transfers + + """ + API version and Git short commit hash. + + Equivalent to GET /version + """ + version: Version +} + +type Balance { + data: [BalanceChange]! + meta: ResponseMetadata! +} + +type BalanceChange { + account: String! + action_index: Int! + amount: BigInt! + balance: String! + balance_delta: BigInt! + block_num: Int! + contract: String! + precision: Int! + symcode: String! + timestamp: String! + trx_id: String! + value: Float! +} + +""" +The `BigInt` scalar type represents non-fractional signed whole numeric values. +""" +scalar BigInt + +type ResponseMetadata { + next_page: BigInt! + previous_page: BigInt! + statistics: Statistics! + total_pages: BigInt! + total_results: BigInt! +} + +type Statistics { + bytes_read: BigInt! + elapsed: Float! + rows_read: BigInt! +} + +enum Chain { + EOS + WAX +} + +type Chains { + data: [DataListItem]! + meta: ResponseMetadata! +} + +type DataListItem { + block_num: Int! + chain: SupportedChains! +} + +enum SupportedChains { + EOS + WAX +} + +type Holders { + data: [Holder]! + meta: ResponseMetadata! +} + +type Holder { + account: String! + balance: Float! +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Supply { + data: [Supply2]! + meta: ResponseMetadata! +} + +type Supply2 { + action_index: Int! + amount: BigInt! + block_num: Int! + contract: String! + issuer: String! + max_supply: String! + precision: Int! + supply: String! + supply_delta: BigInt! + symcode: String! + timestamp: String! + trx_id: String! + value: Float! +} + +type Tokens { + data: [Supply2]! + meta: ResponseMetadata! +} + +type Transfer2 { + data: [Transfer]! + meta: ResponseMetadata! +} + +type Transfer { + action_index: Int! + amount: BigInt! + block_num: Int! + contract: String! + from: String! + memo: String! + precision: Int! + quantity: String! + symcode: String! + timestamp: String! + to: String! + trx_id: String! + value: Float! +} + +type Transfers { + data: [Transfer]! + meta: ResponseMetadata! +} + +type Version { + commit: String! + version: String! +} \ No newline at end of file diff --git a/static/@typespec/openapi3/openapi.json b/static/@typespec/openapi3/openapi.json new file mode 100644 index 0000000..93d72ab --- /dev/null +++ b/static/@typespec/openapi3/openapi.json @@ -0,0 +1,1188 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Antelope Token API", + "summary": "Tokens information from the Antelope blockchains, powered by Substreams", + "license": { + "name": "MIT", + "url": "https://github.com/pinax-network/antelope-token-api/blob/4f4bf36341b794c0ccf5b7a14fdf810be06462d2/LICENSE" + }, + "version": "5.0.0" + }, + "tags": [ + { + "name": "Usage" + }, + { + "name": "Docs" + }, + { + "name": "Monitoring" + } + ], + "paths": { + "/chains": { + "get": { + "tags": [ + "Usage" + ], + "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": "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 + } + } + ], + "responses": { + "200": { + "description": "Array of block information.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "chain": { + "$ref": "#/components/schemas/SupportedChains" + }, + "block_num": { + "type": "integer", + "format": "uint64" + } + }, + "required": [ + "chain", + "block_num" + ] + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/health": { + "get": { + "tags": [ + "Monitoring" + ], + "operationId": "Monitoring_health", + "summary": "Health check", + "description": "Checks database connection.", + "parameters": [], + "responses": { + "200": { + "description": "OK or APIError.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "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": "Metrics as text.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": {} + } + } + } + } + } + } + }, + "/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", + "additionalProperties": {} + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/{chain}/balance": { + "get": { + "tags": [ + "Usage" + ], + "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": 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", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 10 + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "uint64", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "Array of balances.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BalanceChange" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/{chain}/holders": { + "get": { + "tags": [ + "Usage" + ], + "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 + } + } + ], + "responses": { + "200": { + "description": "Array of accounts.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Holder" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/{chain}/supply": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_supply", + "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", + "required": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + { + "name": "issuer", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "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 + } + } + ], + "responses": { + "200": { + "description": "Array of supplies.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Supply" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/{chain}/tokens": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_tokens", + "summary": "Tokens", + "description": "List of available tokens.", + "parameters": [ + { + "name": "chain", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportedChains" + } + }, + { + "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 + } + } + ], + "responses": { + "200": { + "description": "Array of supplies.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Supply" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/{chain}/transfers": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_transfers", + "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", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64" + } + }, + "style": "form", + "explode": false + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "contract", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "symcode", + "in": "query", + "required": false, + "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 + } + } + ], + "responses": { + "200": { + "description": "Array of transfers.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transfer" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + }, + "/{chain}/transfers/{trx_id}": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_transfer", + "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", + "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 + } + } + ], + "responses": { + "200": { + "description": "Array of transfers.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transfer" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "APIError": { + "type": "object", + "required": [ + "status", + "code", + "message" + ], + "properties": { + "status": { + "type": "number", + "enum": [ + 500, + 504, + 400, + 401, + 403, + 404, + 405 + ] + }, + "code": { + "type": "string", + "enum": [ + "bad_database_response", + "bad_header", + "missing_required_header", + "bad_query_input", + "database_timeout", + "forbidden", + "internal_server_error", + "method_not_allowed", + "route_not_found", + "unauthorized" + ] + }, + "message": { + "type": "string" + } + } + }, + "BalanceChange": { + "type": "object", + "required": [ + "trx_id", + "action_index", + "contract", + "symcode", + "precision", + "amount", + "value", + "block_num", + "timestamp", + "account", + "balance", + "balance_delta" + ], + "properties": { + "trx_id": { + "type": "string" + }, + "action_index": { + "type": "integer", + "format": "uint32" + }, + "contract": { + "type": "string" + }, + "symcode": { + "type": "string" + }, + "precision": { + "type": "integer", + "format": "uint32" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "value": { + "type": "number", + "format": "double" + }, + "block_num": { + "type": "integer", + "format": "uint64" + }, + "timestamp": { + "type": "string" + }, + "account": { + "type": "string" + }, + "balance": { + "type": "string" + }, + "balance_delta": { + "type": "integer", + "format": "int64" + } + } + }, + "Holder": { + "type": "object", + "required": [ + "account", + "balance" + ], + "properties": { + "account": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + } + } + }, + "Pagination": { + "type": "object", + "required": [ + "next_page", + "previous_page", + "total_pages", + "total_results" + ], + "properties": { + "next_page": { + "type": "integer", + "format": "int64" + }, + "previous_page": { + "type": "integer", + "format": "int64" + }, + "total_pages": { + "type": "integer", + "format": "int64" + }, + "total_results": { + "type": "integer", + "format": "int64" + } + } + }, + "QueryStatistics": { + "type": "object", + "required": [ + "elapsed", + "rows_read", + "bytes_read" + ], + "properties": { + "elapsed": { + "type": "number" + }, + "rows_read": { + "type": "integer", + "format": "int64" + }, + "bytes_read": { + "type": "integer", + "format": "int64" + } + } + }, + "ResponseMetadata": { + "type": "object", + "required": [ + "statistics", + "next_page", + "previous_page", + "total_pages", + "total_results" + ], + "properties": { + "statistics": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/QueryStatistics" + } + ], + "nullable": true + }, + "next_page": { + "type": "integer", + "format": "int64" + }, + "previous_page": { + "type": "integer", + "format": "int64" + }, + "total_pages": { + "type": "integer", + "format": "int64" + }, + "total_results": { + "type": "integer", + "format": "int64" + } + } + }, + "Supply": { + "type": "object", + "required": [ + "trx_id", + "action_index", + "contract", + "symcode", + "precision", + "amount", + "value", + "block_num", + "timestamp", + "issuer", + "max_supply", + "supply", + "supply_delta" + ], + "properties": { + "trx_id": { + "type": "string" + }, + "action_index": { + "type": "integer", + "format": "uint32" + }, + "contract": { + "type": "string" + }, + "symcode": { + "type": "string" + }, + "precision": { + "type": "integer", + "format": "uint32" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "value": { + "type": "number", + "format": "double" + }, + "block_num": { + "type": "integer", + "format": "uint64" + }, + "timestamp": { + "type": "string" + }, + "issuer": { + "type": "string" + }, + "max_supply": { + "type": "string" + }, + "supply": { + "type": "string" + }, + "supply_delta": { + "type": "integer", + "format": "int64" + } + } + }, + "SupportedChains": { + "type": "string", + "enum": [ + "EOS", + "WAX" + ] + }, + "Transfer": { + "type": "object", + "required": [ + "trx_id", + "action_index", + "contract", + "symcode", + "precision", + "amount", + "value", + "block_num", + "timestamp", + "from", + "to", + "quantity", + "memo" + ], + "properties": { + "trx_id": { + "type": "string" + }, + "action_index": { + "type": "integer", + "format": "uint32" + }, + "contract": { + "type": "string" + }, + "symcode": { + "type": "string" + }, + "precision": { + "type": "integer", + "format": "uint32" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "value": { + "type": "number", + "format": "double" + }, + "block_num": { + "type": "integer", + "format": "uint64" + }, + "timestamp": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "quantity": { + "type": "string" + }, + "memo": { + "type": "string" + } + } + }, + "Version": { + "type": "object", + "required": [ + "version", + "commit" + ], + "properties": { + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$" + }, + "commit": { + "type": "string", + "pattern": "^[0-9a-f]{7}$" + } + } + } + } + } +} diff --git a/static/@typespec/protobuf/antelope/eosio/token/v1.proto b/static/@typespec/protobuf/antelope/eosio/token/v1.proto new file mode 100644 index 0000000..76f028a --- /dev/null +++ b/static/@typespec/protobuf/antelope/eosio/token/v1.proto @@ -0,0 +1,38 @@ +// Generated by Microsoft TypeSpec + +syntax = "proto3"; + +package antelope.eosio.token.v1; + +import "google/protobuf/timestamp.proto"; + +message Transfer { + string trx_id = 1; + uint32 action_index = 2; + string contract = 3; + string symcode = 4; + uint32 precision = 9; + int64 amount = 10; + double value = 11; + uint64 block_num = 12; + google.protobuf.Timestamp timestamp = 13; + string from = 5; + string to = 6; + string quantity = 7; + string memo = 8; +} + +message BalanceChange { + string trx_id = 1; + uint32 action_index = 2; + string contract = 3; + string symcode = 4; + uint32 precision = 8; + int64 amount = 9; + double value = 10; + uint64 block_num = 11; + google.protobuf.Timestamp timestamp = 12; + string account = 5; + string balance = 6; + int64 balance_delta = 7; +} diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..5a651bc --- /dev/null +++ b/static/README.md @@ -0,0 +1,12 @@ +> [!CAUTION] +> +> Static files generated at build time. **DO NOT EDIT MANUALLY**. +> Use `bun run types` to run the static file generation. + +### `@openapi-to-graphql` + +GraphQL schema generated with [`openapi-to-graphql-cli`](https://www.npmjs.com/package/openapi-to-graphql-cli) from the [`openapi.json`](@typespec/openapi3/openapi.json) generated by Typespec. + +### `@typespec` + +Protobuf definitions and OpenAPI schemas generated with Typespec. \ No newline at end of file diff --git a/tsp-output/@typespec/openapi3/openapi.json b/tsp-output/@typespec/openapi3/openapi.json index 794f3ee..93d72ab 100644 --- a/tsp-output/@typespec/openapi3/openapi.json +++ b/tsp-output/@typespec/openapi3/openapi.json @@ -7,7 +7,7 @@ "name": "MIT", "url": "https://github.com/pinax-network/antelope-token-api/blob/4f4bf36341b794c0ccf5b7a14fdf810be06462d2/LICENSE" }, - "version": "4.0.0" + "version": "5.0.0" }, "tags": [ { @@ -151,7 +151,8 @@ "content": { "application/json": { "schema": { - "type": "string" + "type": "object", + "additionalProperties": {} } } } @@ -914,8 +915,7 @@ "format": "uint64" }, "timestamp": { - "type": "integer", - "format": "int32" + "type": "string" }, "account": { "type": "string" @@ -1078,8 +1078,7 @@ "format": "uint64" }, "timestamp": { - "type": "integer", - "format": "int32" + "type": "string" }, "issuer": { "type": "string" @@ -1099,8 +1098,8 @@ "SupportedChains": { "type": "string", "enum": [ - "eos", - "wax" + "EOS", + "WAX" ] }, "Transfer": { @@ -1151,8 +1150,7 @@ "format": "uint64" }, "timestamp": { - "type": "integer", - "format": "int32" + "type": "string" }, "from": { "type": "string"