diff --git a/.env.example b/.env.example index c747410..8948dc9 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,7 @@ HOST=http://127.0.0.1:8123 DATABASE=default USERNAME=default PASSWORD= -TABLE= MAX_LIMIT=500 # Logging -VERBOSE=true +VERBOSE=true \ No newline at end of file diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml index 50ecb1e..7981b3b 100644 --- a/.github/workflows/bun-test.yml +++ b/.github/workflows/bun-test.yml @@ -29,4 +29,3 @@ jobs: HOST: ${{ vars.HOST }} USERNAME: ${{ secrets.USERNAME }} PASSWORD: ${{ secrets.PASSWORD }} - TABLE: ${{ secrets.TABLE }} diff --git a/README.md b/README.md index 85ff669..1fca386 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,22 @@ -## REST API +## Swagger API ### Usage | Method | Path | Query parameters
(* = **Required**) | Description | | :---: | --- | --- | --- | | GET
`text/html` | `/` | - | [Swagger](https://swagger.io/) API playground | -| GET
`application/json` | `/chains` | `limit`
`page` | Information about the chains and latest head block in the database | -| GET
`application/json` | `/{chain}/balance` | `block_num`
`contract`
`symcode`
**`account*`**
`limit`
`page` | Balances of an account. | -| GET
`application/json` | `/{chain}/holders` | **`contract*`**
**`symcode*`**
`limit`
`page` | List of holders of a token | -| GET
`application/json` | `/{chain}/supply` | `block_num`
`issuer`
**`contract*`**
**`symcode*`**
`limit`
`page` | Total supply for a token | -| GET
`application/json` | `/{chain}/tokens` | `limit`
`page` | List of available tokens | -| GET
`application/json` | `/{chain}/transfers` | `block_range`
`from`
`to`
`contract`
`symcode`
`limit`
`page` | All transfers related to a token | -| GET
`application/json` | `/{chain}/transfers/{trx_id}` | `limit`
`page` | Specific transfer related to a token | +| GET
`application/json` | `/balance` | **`account*`**
`contract`
`symcode`
`limit`
`page` | Balances of an account | +| GET
`application/json` | `/balance/historical` | **`account*`**
`block_num`
`contract`
`symcode`
`limit`
`page` | Historical token balances | +| GET
`application/json` | `/head` | `limit`
`page` | Head block information | +| GET
`application/json` | `/holders` | **`contract*`**
**`symcode*`**
`limit`
`page` | List of holders of a token | +| GET
`application/json` | `/supply` | `block_num`
`issuer`
**`contract*`**
**`symcode*`**
`limit`
`page` | Total supply for a token | +| GET
`application/json` | `/tokens` | `limit`
`page` | List of available tokens | +| GET
`application/json` | `/transfers` | `block_range`
**`contract*`**
**`symcode*`**
`limit`
`page` | All transfers related to a token | +| GET
`application/json` | `/transfers/account` | **`account*`**
`block_range`
`from`
`to`
`contract`
`symcode`
`limit`
`page` | All transfers related to an account | +| GET
`application/json` | `/transfers/id` | **`trx_id*`**
`limit`
`page` | Specific transfer related to a token | ### Docs @@ -40,12 +42,20 @@ Go to `/graphql` for a GraphIQL interface. +### `X-Api-Key` + +Use the `Variables` tab at the bottom to add your API key: +```json +{ + "X-Api-Key": "changeme" +} +``` + ### Additional notes - For the `block_range` parameter in `transfers`, you can pass a single integer value (low bound) or an array of two values (inclusive range). -- If you input the same account in the `from` and `to` field for transfers, you'll get all inbound and outbound transfers for that account. -- The more parameters you add (i.e. the more precise your query is), the faster it should be for the back-end to fetch it. -- Don't forget to request for the `meta` fields in the response to get access to pagination and statistics ! +- Use the `from` and `to` field for transfers of an account to further filter the results (i.e. incoming or outgoing transactions from/to another account). +- Don't forget to request the `meta` fields in the response to get access to pagination and statistics ! ## Requirements @@ -158,7 +168,6 @@ HOST=http://127.0.0.1:8123 DATABASE=default USERNAME=default PASSWORD= -TABLE= MAX_LIMIT=500 # Logging diff --git a/index.ts b/index.ts index 91979d8..347d3d6 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ import { Hono, type Context } from "hono"; -import { type RootResolver, graphqlServer } from '@hono/graphql-server'; +import { type RootResolver, graphqlServer, getGraphQLParams } from '@hono/graphql-server'; import { buildSchema } from 'graphql'; -import { z } from 'zod'; +import { SafeParseSuccess, z } from 'zod'; import client from './src/clickhouse/client.js'; import openapi from "./static/@typespec/openapi3/openapi.json"; @@ -72,7 +72,7 @@ async function AntelopeTokenAPI() { app.get( "/metrics", - async (ctx: Context) => new Response(await prometheus.registry.metrics()) + async () => new Response(await prometheus.registry.metrics()) ); // -------------------------- @@ -95,7 +95,7 @@ async function AntelopeTokenAPI() { ctx, endpoint, { - ...path_params.data as ValidPathParams, + ...path_params.data as SafeParseSuccess>, ...query_params.data } as ValidUserParams ); @@ -112,12 +112,22 @@ async function AntelopeTokenAPI() { // --- GraphQL endpoint --- // ------------------------ + // TODO: Make GraphQL endpoint use the same $SERVER parameter as Swagger if set ? const schema = buildSchema(await Bun.file("./static/@openapi-to-graphql/graphql/schema.graphql").text()); + const filterFields: Array = ['metrics']; + + // @ts-ignore Ignore private field warning for filtering out certain operations from the schema + filterFields.forEach(f => delete schema._queryType._fields[f]); + const rootResolver: RootResolver = async (ctx?: Context) => { if (ctx) { + // GraphQL resolver uses the same SQL queries backend as the REST API (`makeUsageQuery`) const createGraphQLUsageResolver = (endpoint: UsageEndpoints) => - async (args: ValidUserParams) => await (await makeUsageQuery(ctx, endpoint, { ...args })).json(); + async (args: ValidUserParams) => { + return await (await makeUsageQuery(ctx, endpoint, { ...args })).json(); + }; + return Object.keys(usageOperationsToEndpointsMap).reduce( // SQL queries endpoints (resolver, op) => Object.assign( @@ -140,6 +150,10 @@ async function AntelopeTokenAPI() { } }; + // TODO: Find way to log GraphQL queries (need to workaround middleware consuming Request) + // See: https://github.com/honojs/middleware/issues/81 + //app.use('/graphql', async (ctx: Context) => logger.trace(await ctx.req.json())) + app.use( '/graphql', graphqlServer({ diff --git a/package.json b/package.json index 6e6da75..d456019 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": "5.0.0", + "version": "6.0.0", "homepage": "https://github.com/pinax-network/antelope-token-api", "license": "MIT", "authors": [ @@ -38,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 --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": "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 --no-viewer -H 'X-Api-Key:changeme' && 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" diff --git a/src/types/api.ts b/src/types/api.ts index 42b36ea..73a9a3f 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -13,22 +13,23 @@ export type UsageResponse = EndpointReturnTypes["da export type UsageParameters = EndpointParameters; export type ValidPathParams = EndpointParameters["path"]; -export type ValidUserParams = EndpointParameters extends { path: undefined; } ? +export type ValidUserParams = NonNullable extends { path: undefined; } ? // Combine path and query parameters only if path exists to prevent "never" on intersection z.infer["query"]> : - z.infer["query"] & ValidPathParams>; + z.infer["query"] & ValidPathParams>>; 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`) +// Map stripped operations name (e.g. `Usage_transfers` stripped to `transfers`) to endpoint paths (e.g. `/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] + // Split once on first underscore to create keys (e.g. `Usage_transfersAccount` => `transfersAccount`) + [k.split('_')[1] as string]: Object.entries(paths).find(([_, 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 985f833..d2e05a7 100644 --- a/src/types/zod.gen.ts +++ b/src/types/zod.gen.ts @@ -5,6 +5,10 @@ export const apiErrorSchema = z.object({ "status": z.union([z.literal(500), z.li export type ApiErrorSchema = z.infer; +export const balanceSchema = z.object({ "last_updated_block": z.coerce.number(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "balance": z.coerce.number() }); +export type BalanceSchema = 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.string(), "account": z.coerce.string(), "balance": z.coerce.string(), "balance_delta": z.coerce.number() }); export type BalanceChangeSchema = z.infer; @@ -13,6 +17,10 @@ export const holderSchema = z.object({ "account": z.coerce.string(), "balance": export type HolderSchema = z.infer; +export const modelsScopeSchema = z.object({ "contract": z.coerce.string(), "symcode": z.coerce.string() }); +export type ModelsScopeSchema = z.infer; + + export const paginationSchema = z.object({ "next_page": z.coerce.number(), "previous_page": z.coerce.number(), "total_pages": z.coerce.number(), "total_results": z.coerce.number() }); export type PaginationSchema = z.infer; @@ -29,10 +37,6 @@ export const supplySchema = z.object({ "trx_id": z.coerce.string(), "action_inde export type SupplySchema = z.infer; -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.string(), "from": z.coerce.string(), "to": z.coerce.string(), "quantity": z.coerce.string(), "memo": z.coerce.string() }); export type TransferSchema = z.infer; @@ -41,26 +45,64 @@ export const versionSchema = z.object({ "version": z.coerce.string().regex(new R export type VersionSchema = z.infer; -export const usageChainsQueryParamsSchema = z.object({ "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); -export type UsageChainsQueryParamsSchema = z.infer; +export const usageBalanceQueryParamsSchema = z.object({ "account": z.coerce.string(), "contract": z.coerce.string().optional(), "symcode": z.coerce.string().optional(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export type UsageBalanceQueryParamsSchema = z.infer; +/** + * @description Array of balances. + */ +export const usageBalance200Schema = z.object({ "data": z.array(z.lazy(() => balanceSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageBalance200Schema = z.infer; +/** + * @description An unexpected error response. + */ +export const usageBalanceErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageBalanceErrorSchema = z.infer; +/** + * @description Array of balances. + */ +export const usageBalanceQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => balanceSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageBalanceQueryResponseSchema = z.infer; + + +export const usageBalanceHistoricalQueryParamsSchema = z.object({ "account": z.coerce.string(), "block_num": z.coerce.number(), "contract": z.coerce.string().optional(), "symcode": z.coerce.string().optional(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export type UsageBalanceHistoricalQueryParamsSchema = z.infer; /** - * @description Array of block information. + * @description Array of balances. */ -export const usageChains200Schema = z.object({ "data": z.array(z.object({ "chain": z.lazy(() => supportedChainsSchema), "block_num": z.coerce.number() })), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageChains200Schema = z.infer; +export const usageBalanceHistorical200Schema = z.object({ "data": z.array(z.lazy(() => balanceChangeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageBalanceHistorical200Schema = z.infer; /** * @description An unexpected error response. */ -export const usageChainsErrorSchema = z.lazy(() => apiErrorSchema); -export type UsageChainsErrorSchema = z.infer; +export const usageBalanceHistoricalErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageBalanceHistoricalErrorSchema = z.infer; /** - * @description Array of block information. + * @description Array of balances. */ -export const usageChainsQueryResponseSchema = z.object({ "data": z.array(z.object({ "chain": z.lazy(() => supportedChainsSchema), "block_num": z.coerce.number() })), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageChainsQueryResponseSchema = z.infer; +export const usageBalanceHistoricalQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => balanceChangeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageBalanceHistoricalQueryResponseSchema = z.infer; + + +export const usageHeadQueryParamsSchema = z.object({ "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); +export type UsageHeadQueryParamsSchema = z.infer; +/** + * @description Head block information. + */ +export const usageHead200Schema = z.object({ "data": z.array(z.object({ "block_num": z.coerce.number(), "block_id": z.coerce.string() })), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageHead200Schema = z.infer; +/** + * @description An unexpected error response. + */ +export const usageHeadErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageHeadErrorSchema = z.infer; +/** + * @description Head block information. + */ +export const usageHeadQueryResponseSchema = z.object({ "data": z.array(z.object({ "block_num": z.coerce.number(), "block_id": z.coerce.string() })), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageHeadQueryResponseSchema = z.infer; /** - * @description OK or APIError. + * @description OK or ApiError. */ export const monitoringHealth200Schema = z.coerce.string(); export type MonitoringHealth200Schema = z.infer; @@ -70,11 +112,30 @@ export type MonitoringHealth200Schema = z.infer apiErrorSchema); export type MonitoringHealthErrorSchema = z.infer; /** - * @description OK or APIError. + * @description OK or ApiError. */ export const monitoringHealthQueryResponseSchema = z.coerce.string(); export type MonitoringHealthQueryResponseSchema = z.infer; + +export const usageHoldersQueryParamsSchema = z.object({ "contract": z.coerce.string(), "symcode": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export type UsageHoldersQueryParamsSchema = z.infer; +/** + * @description Array of accounts. + */ +export const usageHolders200Schema = z.object({ "data": z.array(z.lazy(() => holderSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageHolders200Schema = z.infer; +/** + * @description An unexpected error response. + */ +export const usageHoldersErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageHoldersErrorSchema = z.infer; +/** + * @description Array of accounts. + */ +export const usageHoldersQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => holderSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageHoldersQueryResponseSchema = z.infer; + /** * @description Metrics as text. */ @@ -107,71 +168,8 @@ export type DocsOpenapiErrorSchema = z.infer; export const docsOpenapiQueryResponseSchema = z.object({}); export type DocsOpenapiQueryResponseSchema = z.infer; - /** - * @description The API version and commit hash. - */ -export const docsVersion200Schema = z.lazy(() => versionSchema); -export type DocsVersion200Schema = z.infer; -/** - * @description An unexpected error response. - */ -export const docsVersionErrorSchema = z.lazy(() => apiErrorSchema); -export type DocsVersionErrorSchema = z.infer; -/** - * @description The API version and commit hash. - */ -export const docsVersionQueryResponseSchema = z.lazy(() => versionSchema); -export type DocsVersionQueryResponseSchema = z.infer; - - -export const usageBalancePathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema) }); -export type UsageBalancePathParamsSchema = z.infer; - - export const usageBalanceQueryParamsSchema = z.object({ "block_num": z.coerce.number().optional(), "contract": z.coerce.string().optional(), "symcode": z.coerce.string().optional(), "account": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); -export type UsageBalanceQueryParamsSchema = z.infer; -/** - * @description Array of balances. - */ -export const usageBalance200Schema = z.object({ "data": z.array(z.lazy(() => balanceChangeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageBalance200Schema = z.infer; -/** - * @description An unexpected error response. - */ -export const usageBalanceErrorSchema = z.lazy(() => apiErrorSchema); -export type UsageBalanceErrorSchema = z.infer; -/** - * @description Array of balances. - */ -export const usageBalanceQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => balanceChangeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageBalanceQueryResponseSchema = z.infer; - - -export const usageHoldersPathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema) }); -export type UsageHoldersPathParamsSchema = z.infer; - - export const usageHoldersQueryParamsSchema = z.object({ "contract": z.coerce.string(), "symcode": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); -export type UsageHoldersQueryParamsSchema = z.infer; -/** - * @description Array of accounts. - */ -export const usageHolders200Schema = z.object({ "data": z.array(z.lazy(() => holderSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageHolders200Schema = z.infer; -/** - * @description An unexpected error response. - */ -export const usageHoldersErrorSchema = z.lazy(() => apiErrorSchema); -export type UsageHoldersErrorSchema = z.infer; -/** - * @description Array of accounts. - */ -export const usageHoldersQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => holderSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageHoldersQueryResponseSchema = z.infer; - - -export const usageSupplyPathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema) }); -export type UsageSupplyPathParamsSchema = z.infer; - export const usageSupplyQueryParamsSchema = z.object({ "block_num": z.coerce.number().optional(), "issuer": z.coerce.string().optional(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export const usageSupplyQueryParamsSchema = z.object({ "block_num": z.coerce.number().optional(), "issuer": z.coerce.string().optional(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); export type UsageSupplyQueryParamsSchema = z.infer; /** * @description Array of supplies. @@ -190,15 +188,12 @@ export const usageSupplyQueryResponseSchema = z.object({ "data": z.array(z.lazy( export type UsageSupplyQueryResponseSchema = z.infer; -export const usageTokensPathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema) }); -export type UsageTokensPathParamsSchema = z.infer; - - export const usageTokensQueryParamsSchema = z.object({ "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); +export const usageTokensQueryParamsSchema = z.object({ "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); export type UsageTokensQueryParamsSchema = z.infer; /** - * @description Array of supplies. + * @description Array of token identifier. */ -export const usageTokens200Schema = z.object({ "data": z.array(z.lazy(() => supplySchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export const usageTokens200Schema = z.object({ "data": z.array(z.lazy(() => modelsScopeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); export type UsageTokens200Schema = z.infer; /** * @description An unexpected error response. @@ -206,16 +201,13 @@ export type UsageTokens200Schema = z.infer; export const usageTokensErrorSchema = z.lazy(() => apiErrorSchema); export type UsageTokensErrorSchema = z.infer; /** - * @description Array of supplies. + * @description Array of token identifier. */ -export const usageTokensQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => supplySchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export const usageTokensQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => modelsScopeSchema)), "meta": z.lazy(() => responseMetadataSchema) }); export type UsageTokensQueryResponseSchema = z.infer; -export const usageTransfersPathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema) }); -export type UsageTransfersPathParamsSchema = z.infer; - - export const usageTransfersQueryParamsSchema = z.object({ "block_range": z.array(z.coerce.number()).optional(), "from": z.coerce.string().optional(), "to": z.coerce.string().optional(), "contract": z.coerce.string().optional(), "symcode": z.coerce.string().optional(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); +export const usageTransfersQueryParamsSchema = z.object({ "block_range": z.array(z.coerce.number()).optional(), "contract": z.coerce.string(), "symcode": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); export type UsageTransfersQueryParamsSchema = z.infer; /** * @description Array of transfers. @@ -234,64 +226,96 @@ export const usageTransfersQueryResponseSchema = z.object({ "data": z.array(z.la export type UsageTransfersQueryResponseSchema = z.infer; -export const usageTransferPathParamsSchema = z.object({ "chain": z.lazy(() => supportedChainsSchema), "trx_id": z.coerce.string() }); -export type UsageTransferPathParamsSchema = z.infer; +export const usageTransfersAccountQueryParamsSchema = z.object({ "account": z.coerce.string(), "block_range": z.array(z.coerce.number()).optional(), "from": z.coerce.string().optional(), "to": z.coerce.string().optional(), "contract": z.coerce.string().optional(), "symcode": z.coerce.string().optional(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export type UsageTransfersAccountQueryParamsSchema = z.infer; +/** + * @description Array of transfers. + */ +export const usageTransfersAccount200Schema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageTransfersAccount200Schema = z.infer; +/** + * @description An unexpected error response. + */ +export const usageTransfersAccountErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageTransfersAccountErrorSchema = z.infer; +/** + * @description Array of transfers. + */ +export const usageTransfersAccountQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageTransfersAccountQueryResponseSchema = z.infer; + - export const usageTransferQueryParamsSchema = z.object({ "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }).optional(); -export type UsageTransferQueryParamsSchema = z.infer; +export const usageTransferIdQueryParamsSchema = z.object({ "trx_id": z.coerce.string(), "limit": z.coerce.number().optional(), "page": z.coerce.number().optional() }); +export type UsageTransferIdQueryParamsSchema = z.infer; /** * @description Array of transfers. */ -export const usageTransfer200Schema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageTransfer200Schema = z.infer; +export const usageTransferId200Schema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageTransferId200Schema = z.infer; /** * @description An unexpected error response. */ -export const usageTransferErrorSchema = z.lazy(() => apiErrorSchema); -export type UsageTransferErrorSchema = z.infer; +export const usageTransferIdErrorSchema = z.lazy(() => apiErrorSchema); +export type UsageTransferIdErrorSchema = z.infer; /** * @description Array of transfers. */ -export const usageTransferQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); -export type UsageTransferQueryResponseSchema = z.infer; +export const usageTransferIdQueryResponseSchema = z.object({ "data": z.array(z.lazy(() => transferSchema)), "meta": z.lazy(() => responseMetadataSchema) }); +export type UsageTransferIdQueryResponseSchema = z.infer; - export const operations = { "Usage_chains": { + /** + * @description The Api version and commit hash. + */ +export const docsVersion200Schema = z.lazy(() => versionSchema); +export type DocsVersion200Schema = z.infer; +/** + * @description An unexpected error response. + */ +export const docsVersionErrorSchema = z.lazy(() => apiErrorSchema); +export type DocsVersionErrorSchema = z.infer; +/** + * @description The Api version and commit hash. + */ +export const docsVersionQueryResponseSchema = z.lazy(() => versionSchema); +export type DocsVersionQueryResponseSchema = z.infer; + + export const operations = { "Usage_balance": { request: undefined, parameters: { path: undefined, - query: usageChainsQueryParamsSchema, + query: usageBalanceQueryParamsSchema, header: undefined }, responses: { - 200: usageChainsQueryResponseSchema, - default: usageChainsQueryResponseSchema + 200: usageBalanceQueryResponseSchema, + default: usageBalanceQueryResponseSchema }, errors: {} - }, "Monitoring_health": { + }, "Usage_balanceHistorical": { request: undefined, parameters: { path: undefined, - query: undefined, + query: usageBalanceHistoricalQueryParamsSchema, header: undefined }, responses: { - 200: monitoringHealthQueryResponseSchema, - default: monitoringHealthQueryResponseSchema + 200: usageBalanceHistoricalQueryResponseSchema, + default: usageBalanceHistoricalQueryResponseSchema }, errors: {} - }, "Monitoring_metrics": { + }, "Usage_head": { request: undefined, parameters: { path: undefined, - query: undefined, + query: usageHeadQueryParamsSchema, header: undefined }, responses: { - 200: monitoringMetricsQueryResponseSchema, - default: monitoringMetricsQueryResponseSchema + 200: usageHeadQueryResponseSchema, + default: usageHeadQueryResponseSchema }, errors: {} - }, "Docs_openapi": { + }, "Monitoring_health": { request: undefined, parameters: { path: undefined, @@ -299,50 +323,50 @@ export type UsageTransferQueryResponseSchema = z.infer; +alias TokenIdentifier = Models.Scope; // Models will be present in the OpenAPI components model Transfer is Models.Transfer; @@ -48,6 +54,11 @@ model Holder { account: BalanceChange.account; balance: BalanceChange.value; } +model Balance { + last_updated_block: BalanceChange.block_num, + ...TokenIdentifier, + balance: BalanceChange.value +} model QueryStatistics { elapsed: float; @@ -72,51 +83,59 @@ model UsageResponse { meta: ResponseMetadata; } -enum SupportedChains { - EOS, - 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; +alias ApiResponse = T | ApiError; alias PaginationQueryParams = { @query limit?: uint64 = 10; @query page?: uint64 = 1; }; -// Helper aliases for accessing underlying properties -alias BlockInfo = Models.BlockInfo; -alias TokenIdentifier = Models.Scope; - @tag("Usage") interface Usage { /** - Balances of an account. + Token balances of an account. @returns Array of balances. */ - @summary("Token balance") - @route("/{chain}/balance") + @summary("Token balances") + @route("/balance") @get + @useAuth(ApiKeyAuth) balance( - @path chain: SupportedChains, - @query block_num?: BlockInfo.block_num, + @query account: BalanceChange.account, @query contract?: TokenIdentifier.contract, @query symcode?: TokenIdentifier.symcode, + ...PaginationQueryParams, + ): ApiResponse>; + + /** + Historical token balances of an account. + @returns Array of balances. + */ + @summary("Historical token balances") + @route("/balance/historical") + @get + @useAuth(ApiKeyAuth) + balanceHistorical( @query account: BalanceChange.account, + @query block_num: BlockInfo.block_num, + @query contract?: TokenIdentifier.contract, + @query symcode?: TokenIdentifier.symcode, ...PaginationQueryParams, - ): APIResponse>; + ): ApiResponse>; /** - List of available Antelope chains and corresponding latest block for which data is available. - @returns Array of block information. + Current head block for which data is available (can be lower than head block of the chain). + @returns Head block information. */ - @summary("Chains and latest block available") - @route("/chains") + @summary("Head block information") + @route("/head") @get - chains(...PaginationQueryParams): APIResponse>; /** @@ -124,75 +143,88 @@ interface Usage { @returns Array of accounts. */ @summary("Token holders") - @route("/{chain}/holders") + @route("/holders") @get + @useAuth(ApiKeyAuth) holders( - @path chain: SupportedChains, @query contract: TokenIdentifier.contract, @query symcode: TokenIdentifier.symcode, ...PaginationQueryParams, - ): APIResponse>; + ): ApiResponse>; /** Total supply for a token. @returns Array of supplies. */ @summary("Token supply") - @route("/{chain}/supply") + @route("/supply") @get + @useAuth(ApiKeyAuth) supply( - @path chain: SupportedChains, @query block_num?: BlockInfo.block_num, @query issuer?: Supply.issuer, @query contract: TokenIdentifier.contract, @query symcode: TokenIdentifier.symcode, ...PaginationQueryParams, - ): APIResponse>; + ): ApiResponse>; /** List of available tokens. - @returns Array of supplies. + @returns Array of token identifier. */ @summary("Tokens") - @route("/{chain}/tokens") + @route("/tokens") @get + @useAuth(ApiKeyAuth) tokens( - @path chain: SupportedChains, ...PaginationQueryParams - ): APIResponse>; + ): ApiResponse>; /** All transfers related to a token. @returns Array of transfers. */ @summary("Token transfers") - @route("/{chain}/transfers") + @route("/transfers") @get + @useAuth(ApiKeyAuth) transfers( - @path chain: SupportedChains, - @query({ - format: "csv", - }) - block_range?: BlockInfo.block_num[], + @query({ format: "csv"}) block_range?: BlockInfo.block_num[], + @query contract: TokenIdentifier.contract, + @query symcode: TokenIdentifier.symcode, + ...PaginationQueryParams, + ): ApiResponse>; + + /** + All transfers related to an account. + @returns Array of transfers. + */ + @summary("Token transfers from and to an account") + @route("/transfers/account") + @get + @useAuth(ApiKeyAuth) + transfersAccount( + @query account: BalanceChange.account, + @query({ format: "csv"}) block_range?: BlockInfo.block_num[], @query from?: Transfer.from, @query to?: Transfer.to, @query contract?: TokenIdentifier.contract, @query symcode?: TokenIdentifier.symcode, ...PaginationQueryParams, - ): APIResponse>; + ): ApiResponse>; /** Specific transfer related to a token. @returns Array of transfers. */ @summary("Token transfer") - @route("/{chain}/transfers/{trx_id}") + @route("/transfers/id") @get - transfer( - @path chain: SupportedChains, - @path trx_id: Models.TraceInformation.trx_id, + @useAuth(ApiKeyAuth) + transferId( + @query trx_id: Models.TraceInformation.trx_id, ...PaginationQueryParams, - ): APIResponse>; + ): ApiResponse>; } model Version { @@ -212,28 +244,28 @@ interface Docs { @summary("OpenAPI JSON spec") @route("/openapi") @get - openapi(): APIResponse>; + openapi(): ApiResponse>; /** - API version and Git short commit hash. - @returns The API version and commit hash. + Api version and Git short commit hash. + @returns The Api version and commit hash. */ - @summary("API version") + @summary("Api version") @route("/version") @get - version(): APIResponse; + version(): ApiResponse; } @tag("Monitoring") interface Monitoring { /** Checks database connection. - @returns OK or APIError. + @returns OK or ApiError. */ @summary("Health check") @route("/health") @get - health(): APIResponse; + health(): ApiResponse; /** Prometheus metrics. @@ -242,5 +274,5 @@ interface Monitoring { @summary("Prometheus metrics") @route("/metrics") @get - metrics(): APIResponse; + metrics(): ApiResponse; } diff --git a/src/usage.ts b/src/usage.ts index 32c7aa5..a6a8526 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -3,8 +3,14 @@ import { APIErrorResponse } from "./utils.js"; import type { Context } from "hono"; import type { AdditionalQueryParams, UsageEndpoints, UsageResponse, ValidUserParams } from "./types/api.js"; -import { config } from "./config.js"; -import { supportedChainsSchema } from "./types/zod.gen.js"; + +/** + * This function creates and send the SQL queries to the ClickHouse database based on the endpoint requested. + * + * Both the REST API and GraphQL endpoint use those. + * `endpoint` is a valid "Usage" endpoint (e.g. not a `/version`, `/metrics`, etc. endpoint, an actual data endpoint). + * `user_params` is an key-value object created from the path and query parameters present in the request. + **/ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, user_params: ValidUserParams) { type UsageElementReturnType = UsageResponse[number]; @@ -19,7 +25,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" && k !== "chain")) { + for (const k of Object.keys(query_params).filter(k => k !== "limit" && k !== "block_range")) { const clickhouse_type = typeof query_params[k as keyof typeof query_params] === "number" ? "int" : "String"; if (k === 'symcode') // Special case to allow case-insensitive symcode input filters += ` (${k} = upper({${k}: ${clickhouse_type}})) AND`; @@ -33,37 +39,11 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use let query = ""; let additional_query_params: AdditionalQueryParams = {}; - let database = config.database; - - if (endpoint !== "/chains") { - const q = query_params as ValidUserParams; - database = `${q.chain.toLowerCase()}_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; - query += - `SELECT *` - + ` FROM ${endpoint == "/{chain}/balance" ? - `${database}.${q.block_num ? 'historical_' : ''}account_balances` - : `${database}.${q.block_num ? 'historical_' : ''}token_supplies`}` - + ` ${filters}`; - } else if (endpoint == "/{chain}/transfers") { - query += `SELECT * FROM `; + // Parse block range for endpoints that uses it. Check for single value or two comma-separated values. + if (endpoint == "/transfers" || endpoint == "/transfers/account") { const q = query_params as ValidUserParams; - // Find all incoming and outgoing transfers from single account - if (q.from && q.to && q.from === q.to) - filters = filters.replace( - "(from = {from: String}) AND (to = {to: String})", - "((from = {from: String}) OR (to = {to: String}))", - ); - if (q.block_range) { - query += `${database}.transfers_block_num`; - if (q.block_range[0] && q.block_range[1]) { filters += `${filters.length ? "AND" : "WHERE"}` + @@ -77,30 +57,51 @@ export async function makeUsageQuery(ctx: Context, endpoint: UsageEndpoints, use ` (block_num >= {min_block: int})`; additional_query_params.min_block = q.block_range[0]; } - } else if (q.from) { - query += `${database}.transfers_from`; - } else if (q.to) { - query += `${database}.transfers_to`; - } else if (q.contract || q.symcode) { - query += `${database}.transfers_contract`; - } else { - query += `${database}.transfers_block_num`; } + } + + if (endpoint == "/balance") { + query += + `SELECT block_num AS last_updated_block, contract, symcode, value as balance FROM token_holders FINAL` + + ` ${filters} ORDER BY value DESC` + } else if (endpoint == "/balance/historical") { + query += + `SELECT * FROM historical_account_balances` + + ` ${filters} ORDER BY value DESC` + } else if (endpoint == "/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; + query += + `SELECT * FROM ${q.block_num ? 'historical_' : ''}token_supplies` + +` ${filters} ORDER BY block_num DESC`; + } else if (endpoint == "/transfers") { + query += `SELECT * FROM `; + + const q = query_params as ValidUserParams; + if (q.contract && q.symcode) + query += `transfers_contract`; + else + query += `transfers_block_num`; query += ` ${filters} ORDER BY block_num DESC`; - } else if (endpoint == "/{chain}/holders") { - query += `SELECT account, value AS balance FROM ${database}.token_holders FINAL ${filters} ORDER BY value DESC`; - } else if (endpoint == "/chains") { - for (const chain of supportedChainsSchema._def.values) - query += - `SELECT '${chain}' as chain, MAX(block_num) as block_num` - + ` 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}") { - query += `SELECT * FROM ${database}.transfer_events ${filters} ORDER BY action_index`; - } else if (endpoint == "/{chain}/tokens") { - query += `SELECT * FROM ${database}.token_supplies FINAL ${filters} ORDER BY block_num DESC`; + } else if (endpoint == "/transfers/account") { + // Remove `account` from filters, only using it in the subquery + filters.replace('(account = {account: String})', ''); + query += + `SELECT * FROM` + + ` (SELECT DISTINCT * FROM transfers_from WHERE ((from = {account: String}) OR (to = {account: String})))` + + ` ${filters} ORDER BY block_num DESC`; + } else if (endpoint == "/transfers/id") { + query += `SELECT * FROM transfer_events ${filters} ORDER BY action_index`; + } else if (endpoint == "/holders") { + query += `SELECT account, value AS balance FROM token_holders FINAL ${filters} ORDER BY value DESC`; + } else if (endpoint == "/head") { + query += `SELECT block_num, block_id FROM cursors FINAL`; + } else if (endpoint == "/tokens") { + // NB: Using `account_balances` seems to return the most results + // Have to try with fully synced chain to compare with `create_events` and others + query += `SELECT contract, symcode FROM account_balances GROUP BY (contract, symcode) ${filters}`; } query += " LIMIT {limit: int}"; diff --git a/static/@openapi-to-graphql/graphql/schema.graphql b/static/@openapi-to-graphql/graphql/schema.graphql index 3add36c..916e645 100644 --- a/static/@openapi-to-graphql/graphql/schema.graphql +++ b/static/@openapi-to-graphql/graphql/schema.graphql @@ -1,17 +1,24 @@ type Query { """ - Balances of an account. + Token balances of an account. - Equivalent to GET /{chain}/balance + Equivalent to GET /balance """ - balance(account: String!, block_num: Int, chain: Chain!, contract: String, limit: Int, page: Int, symcode: String): Balance + balance(account: String!, contract: String, limit: Int, page: Int, symcode: String): Balance """ - List of available Antelope chains and corresponding latest block for which data is available. + Historical token balances of an account. - Equivalent to GET /chains + Equivalent to GET /balance/historical """ - chains(limit: Int, page: Int): Chains + balanceHistorical(account: String!, block_num: Int!, contract: String, limit: Int, page: Int, symcode: String): BalanceHistorical + + """ + Current head block for which data is available (can be lower than head block of the chain). + + Equivalent to GET /head + """ + head(limit: Int, page: Int): Head """ Checks database connection. @@ -23,9 +30,9 @@ type Query { """ List of holders of a token. - Equivalent to GET /{chain}/holders + Equivalent to GET /holders """ - holders(chain: Chain!, contract: String!, limit: Int, page: Int, symcode: String!): Holders + holders(contract: String!, limit: Int, page: Int, symcode: String!): Holders """ Prometheus metrics. @@ -44,33 +51,40 @@ type Query { """ Total supply for a token. - Equivalent to GET /{chain}/supply + Equivalent to GET /supply """ - supply(block_num: Int, chain: Chain!, contract: String!, issuer: String, limit: Int, page: Int, symcode: String!): Supply + supply(block_num: Int, contract: String!, issuer: String, limit: Int, page: Int, symcode: String!): Supply """ List of available tokens. - Equivalent to GET /{chain}/tokens + Equivalent to GET /tokens """ - tokens(chain: Chain!, limit: Int, page: Int): Tokens + tokens(limit: Int, page: Int): Tokens """ - Specific transfer related to a token. + All transfers related to a token. - Equivalent to GET /{chain}/transfers/{trx_id} + Equivalent to GET /transfers """ - transfer(chain: Chain!, limit: Int, page: Int, trx_id: String!): Transfer2 + transfers(block_range: [Int], contract: String!, limit: Int, page: Int, symcode: String!): Transfers """ - All transfers related to a token. + All transfers related to an account. - Equivalent to GET /{chain}/transfers + Equivalent to GET /transfers/account """ - transfers(block_range: [Int], chain: Chain!, contract: String, from: String, limit: Int, page: Int, symcode: String, to: String): Transfers + transfersAccount(account: String!, block_range: [Int], contract: String, from: String, limit: Int, page: Int, symcode: String, to: String): TransfersAccount """ - API version and Git short commit hash. + Specific transfer related to a token. + + Equivalent to GET /transfers/id + """ + transfersId(limit: Int, page: Int, trx_id: String!): TransfersId + + """ + Api version and Git short commit hash. Equivalent to GET /version """ @@ -78,30 +92,17 @@ type Query { } type Balance { - data: [BalanceChange]! + data: [Balance2]! meta: ResponseMetadata! } -type BalanceChange { - account: String! - action_index: Int! - amount: BigInt! - balance: String! - balance_delta: BigInt! - block_num: Int! +type Balance2 { + balance: Float! contract: String! - precision: Int! + last_updated_block: 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! @@ -110,30 +111,45 @@ type ResponseMetadata { total_results: BigInt! } +""" +The `BigInt` scalar type represents non-fractional signed whole numeric values. +""" +scalar BigInt + type Statistics { bytes_read: BigInt! elapsed: Float! rows_read: BigInt! } -enum Chain { - EOS - WAX -} - -type Chains { - data: [DataListItem]! +type BalanceHistorical { + data: [BalanceChange]! meta: ResponseMetadata! } -type DataListItem { +type BalanceChange { + account: String! + action_index: Int! + amount: BigInt! + balance: String! + balance_delta: BigInt! block_num: Int! - chain: SupportedChains! + contract: String! + precision: Int! + symcode: String! + timestamp: String! + trx_id: String! + value: Float! +} + +type Head { + data: [Data3ListItem]! + meta: ResponseMetadata! } -enum SupportedChains { - EOS - WAX +type Data3ListItem { + block_id: String! + block_num: Int! } type Holders { @@ -173,11 +189,16 @@ type Supply2 { } type Tokens { - data: [Supply2]! + data: [ModelsScope]! meta: ResponseMetadata! } -type Transfer2 { +type ModelsScope { + contract: String! + symcode: String! +} + +type Transfers { data: [Transfer]! meta: ResponseMetadata! } @@ -198,7 +219,12 @@ type Transfer { value: Float! } -type Transfers { +type TransfersAccount { + data: [Transfer]! + meta: ResponseMetadata! +} + +type TransfersId { data: [Transfer]! meta: ResponseMetadata! } diff --git a/static/@typespec/openapi3/openapi.json b/static/@typespec/openapi3/openapi.json index a931736..a8d7f12 100644 --- a/static/@typespec/openapi3/openapi.json +++ b/static/@typespec/openapi3/openapi.json @@ -1,13 +1,13 @@ { "openapi": "3.0.0", "info": { - "title": "Antelope Token API", + "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" + "version": "6.0.0" }, "tags": [ { @@ -21,15 +21,146 @@ } ], "paths": { - "/chains": { + "/balance": { "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.", + "operationId": "Usage_balance", + "summary": "Token balances", + "description": "Token balances of an account.", + "parameters": [ + { + "name": "account", + "in": "query", + "required": true, + "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 balances.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Balance" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/balance/historical": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_balanceHistorical", + "summary": "Historical token balances", + "description": "Historical token balances of an account.", "parameters": [ + { + "name": "account", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "block_num", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + { + "name": "contract", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "symcode", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "limit", "in": "query", @@ -53,7 +184,81 @@ ], "responses": { "200": { - "description": "Array of block information.", + "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" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/head": { + "get": { + "tags": [ + "Usage" + ], + "operationId": "Usage_head", + "summary": "Head block information", + "description": "Current head block for which data is available (can be lower than head block of the chain).", + "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": "Head block information.", "content": { "application/json": { "schema": { @@ -68,17 +273,17 @@ "items": { "type": "object", "properties": { - "chain": { - "$ref": "#/components/schemas/SupportedChains" - }, "block_num": { "type": "integer", "format": "uint64" + }, + "block_id": { + "type": "string" } }, "required": [ - "chain", - "block_num" + "block_num", + "block_id" ] } }, @@ -95,7 +300,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } @@ -114,7 +319,7 @@ "parameters": [], "responses": { "200": { - "description": "OK or APIError.", + "description": "OK or ApiError.", "content": { "application/json": { "schema": { @@ -128,7 +333,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } @@ -136,22 +341,74 @@ } } }, - "/metrics": { + "/holders": { "get": { "tags": [ - "Monitoring" + "Usage" + ], + "operationId": "Usage_holders", + "summary": "Token holders", + "description": "List of holders of a token.", + "parameters": [ + { + "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 + } + } ], - "operationId": "Monitoring_metrics", - "summary": "Prometheus metrics", - "description": "Prometheus metrics.", - "parameters": [], "responses": { "200": { - "description": "Metrics as text.", + "description": "Array of accounts.", "content": { "application/json": { "schema": { - "type": "string" + "type": "object", + "required": [ + "data", + "meta" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Holder" + } + }, + "meta": { + "$ref": "#/components/schemas/ResponseMetadata" + } + } } } } @@ -161,31 +418,35 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/openapi": { + "/metrics": { "get": { "tags": [ - "Docs" + "Monitoring" ], - "operationId": "Docs_openapi", - "summary": "OpenAPI JSON spec", - "description": "Reflection endpoint to return OpenAPI JSON spec. Also used by Swagger to generate the frontpage.", + "operationId": "Monitoring_metrics", + "summary": "Prometheus metrics", + "description": "Prometheus metrics.", "parameters": [], "responses": { "200": { - "description": "The OpenAPI JSON spec", + "description": "Metrics as text.", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": {} + "type": "string" } } } @@ -195,7 +456,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } @@ -203,22 +464,23 @@ } } }, - "/version": { + "/openapi": { "get": { "tags": [ "Docs" ], - "operationId": "Docs_version", - "summary": "API version", - "description": "API version and Git short commit hash.", + "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 API version and commit hash.", + "description": "The OpenAPI JSON spec", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Version" + "type": "object", + "additionalProperties": {} } } } @@ -228,7 +490,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } @@ -236,23 +498,15 @@ } } }, - "/{chain}/balance": { + "/supply": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_balance", - "summary": "Token balance", - "description": "Balances of an account.", + "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", @@ -263,7 +517,7 @@ } }, { - "name": "contract", + "name": "issuer", "in": "query", "required": false, "schema": { @@ -271,15 +525,15 @@ } }, { - "name": "symcode", + "name": "contract", "in": "query", - "required": false, + "required": true, "schema": { "type": "string" } }, { - "name": "account", + "name": "symcode", "in": "query", "required": true, "schema": { @@ -309,7 +563,7 @@ ], "responses": { "200": { - "description": "Array of balances.", + "description": "Array of supplies.", "content": { "application/json": { "schema": { @@ -322,7 +576,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/BalanceChange" + "$ref": "#/components/schemas/Supply" } }, "meta": { @@ -338,47 +592,28 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/{chain}/holders": { + "/tokens": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_holders", - "summary": "Token holders", - "description": "List of holders of a token.", + "operationId": "Usage_tokens", + "summary": "Tokens", + "description": "List of available tokens.", "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", @@ -402,7 +637,7 @@ ], "responses": { "200": { - "description": "Array of accounts.", + "description": "Array of token identifier.", "content": { "application/json": { "schema": { @@ -415,7 +650,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Holder" + "$ref": "#/components/schemas/Models.Scope" } }, "meta": { @@ -431,47 +666,41 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/{chain}/supply": { + "/transfers": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_supply", - "summary": "Token supply", - "description": "Total supply for a token.", + "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_num", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "uint64" - } - }, - { - "name": "issuer", + "name": "block_range", "in": "query", "required": false, "schema": { - "type": "string" - } + "type": "array", + "items": { + "type": "integer", + "format": "uint64" + } + }, + "style": "form", + "explode": false }, { "name": "contract", @@ -512,7 +741,7 @@ ], "responses": { "200": { - "description": "Array of supplies.", + "description": "Array of transfers.", "content": { "application/json": { "schema": { @@ -525,7 +754,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Supply" + "$ref": "#/components/schemas/Transfer" } }, "meta": { @@ -541,106 +770,34 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$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 - } - }, + }, + "security": [ { - "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" - } - } - } + "ApiKeyAuth": [] } - } + ] } }, - "/{chain}/transfers": { + "/transfers/account": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_transfers", - "summary": "Token transfers", - "description": "All transfers related to a token.", + "operationId": "Usage_transfersAccount", + "summary": "Token transfers from and to an account", + "description": "All transfers related to an account.", "parameters": [ { - "name": "chain", - "in": "path", + "name": "account", + "in": "query", "required": true, "schema": { - "$ref": "#/components/schemas/SupportedChains" + "type": "string" } }, { @@ -741,34 +898,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/{chain}/transfers/{trx_id}": { + "/transfers/id": { "get": { "tags": [ "Usage" ], - "operationId": "Usage_transfer", + "operationId": "Usage_transferId", "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", + "in": "query", "required": true, "schema": { "type": "string" @@ -826,7 +980,45 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/APIError" + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/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" } } } @@ -837,7 +1029,7 @@ }, "components": { "schemas": { - "APIError": { + "ApiError": { "type": "object", "required": [ "status", @@ -877,6 +1069,31 @@ } } }, + "Balance": { + "type": "object", + "required": [ + "last_updated_block", + "contract", + "symcode", + "balance" + ], + "properties": { + "last_updated_block": { + "type": "integer", + "format": "uint64" + }, + "contract": { + "type": "string" + }, + "symcode": { + "type": "string" + }, + "balance": { + "type": "number", + "format": "double" + } + } + }, "BalanceChange": { "type": "object", "required": [ @@ -954,6 +1171,21 @@ } } }, + "Models.Scope": { + "type": "object", + "required": [ + "contract", + "symcode" + ], + "properties": { + "contract": { + "type": "string" + }, + "symcode": { + "type": "string" + } + } + }, "Pagination": { "type": "object", "required": [ @@ -1104,13 +1336,6 @@ } } }, - "SupportedChains": { - "type": "string", - "enum": [ - "EOS", - "WAX" - ] - }, "Transfer": { "type": "object", "required": [ @@ -1192,6 +1417,13 @@ } } } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } } } } diff --git a/swagger/index.html b/swagger/index.html index b17f0bf..9267022 100644 --- a/swagger/index.html +++ b/swagger/index.html @@ -16,10 +16,11 @@