From cddab66097d01fcae83064ce8264fe2c6381d372 Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Tue, 20 Aug 2024 20:50:48 +0000 Subject: [PATCH] Add GraphQL support (#63) - `/graphql` endpoint presenting graphical interface for queries - GraphQL endpoint supports the same endpoint as the REST API --- README.md | 6 +- bun.lockb | Bin 137418 -> 191251 bytes index.ts | 93 +- kubb.config.ts | 2 +- package.json | 8 +- src/types/README.md | 7 +- src/types/api.ts | 13 +- src/types/zod.gen.ts | 12 +- src/typespec/README.md | 2 +- src/typespec/openapi3.tsp | 18 +- src/usage.ts | 5 +- .../graphql/schema.graphql | 209 +++ static/@typespec/openapi3/openapi.json | 1188 +++++++++++++++++ .../protobuf/antelope/eosio/token/v1.proto | 38 + static/README.md | 12 + tsp-output/@typespec/openapi3/openapi.json | 18 +- 16 files changed, 1575 insertions(+), 56 deletions(-) create mode 100644 static/@openapi-to-graphql/graphql/schema.graphql create mode 100644 static/@typespec/openapi3/openapi.json create mode 100644 static/@typespec/protobuf/antelope/eosio/token/v1.proto create mode 100644 static/README.md 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 0c19d78800dfcc5820aa5651e3259349a4751e0d..005f82c3c1738b8fd2cb25c985194842c9cb3fb6 100755 GIT binary patch delta 54009 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 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"