Skip to content

Commit

Permalink
Rework endpoints and support X-Api-Key header auth (#69)
Browse files Browse the repository at this point in the history
- Added `/transfers/account` endpoint for transfers related to/from a
given account.
- Removed path parameter from `/transfers/{trx_id}`, now uses query
parameter. Path changed to `/transfers/id`.
- Removed `from/to` query parameters from `/transfers` and requires
`contract/symcode` instead.
- Added `/balance/historical` for block ranges queries.
- Added `X-Api-Key` header authentication support for Swagger/GraphQL.
- Fixed some endpoint return format
- Only support a single chain, `/chains` changed to `/head`.
- Updated GraphQL resolvers to support authentication parameters. Can
also filter out some operations from the schema.
- Removed unused `TABLE` env variable.
  • Loading branch information
0237h committed Aug 28, 2024
1 parent 41ad594 commit 442443a
Show file tree
Hide file tree
Showing 14 changed files with 1,404 additions and 789 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion .github/workflows/bun-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@ jobs:
HOST: ${{ vars.HOST }}
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.PASSWORD }}
TABLE: ${{ secrets.TABLE }}
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
<a href="https://pinax.network/en/chain/eos"><img src="https://img.shields.io/badge/EOS-grey?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAxIiB2aWV3Qm94PSIwIDAgNjcgMTAxIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY2XzI5KSI+DQo8cGF0aCBkPSJNMzMuMzUwOCAwLjIyMTE5MUw5Ljk1MDg2IDMyLjQyMTJMMC4xNTA4NzkgODAuMDIxMkwzMy4zNTA4IDEwMC4yMjFMNjYuNTUwOCA4MC4wMjEyTDU2LjU1MDggMzIuMjIxMkwzMy4zNTA4IDAuMjIxMTkxWk01LjU1MDg3IDc3LjgyMTJMMTIuOTUwOSA0MS42MjEyTDI5Ljc1MDggOTIuNjIxMkw1LjU1MDg3IDc3LjgyMTJaTTE1LjM1MDkgMzMuNDIxMkwzMy4zNTA4IDguNjIxMTlMNTEuMzUwOCAzMy40MjEyTDMzLjM1MDggODcuODIxMkwxNS4zNTA5IDMzLjQyMTJaTTM2Ljc1MDggOTIuNjIxMkw1My41NTA4IDQxLjYyMTJMNjAuOTUwOCA3Ny44MjEyTDM2Ljc1MDggOTIuNjIxMloiIGZpbGw9IndoaXRlIi8+DQo8L2c+DQo8ZGVmcz4NCjxjbGlwUGF0aCBpZD0iY2xpcDBfNjZfMjkiPg0KPHJlY3Qgd2lkdGg9IjY2LjM5OTkiIGhlaWdodD0iMTAwIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4xNTA4NzkgMC4yMjExOTEpIi8+DQo8L2NsaXBQYXRoPg0KPC9kZWZzPg0KPC9zdmc+DQo=&logoSize=auto" height="30" /></a>
<a href="https://pinax.network/en/chain/wax"><img src="https://img.shields.io/badge/WAX-grey?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTAxIiBoZWlnaHQ9IjEwMSIgdmlld0JveD0iMCAwIDEwMSAxMDEiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BDQo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNjdfOSkiPg0KPHBhdGggZD0iTTUwLjEzOTQgMTAwLjIyMUM3Ny43NTM2IDEwMC4yMjEgMTAwLjEzOSA3Ny44MzU0IDEwMC4xMzkgNTAuMjIxMkMxMDAuMTM5IDIyLjYwNyA3Ny43NTM2IDAuMjIxMTkxIDUwLjEzOTQgMC4yMjExOTFDMjIuNTI1MiAwLjIyMTE5MSAwLjEzOTQwNCAyMi42MDcgMC4xMzk0MDQgNTAuMjIxMkMwLjEzOTQwNCA3Ny44MzU0IDIyLjUyNTIgMTAwLjIyMSA1MC4xMzk0IDEwMC4yMjFaIiBmaWxsPSIjRjg5MDIyIi8%2BDQo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTg3LjYzOTUgNTkuNDc3Mkg4MC4yNTJMNzUuMDExNCA1NC45ODk3TDY5Ljc4OTUgNTkuNDU4NEg2My41NDU4TDYwLjU2MTQgNTUuODMzNEg1MC4yNzM5TDUyLjg3NyA1Mi42MTc4SDU3LjkzNjRMNTQuMDgzMyA0Ny45MDUzTDQwLjYzMDIgNjQuMjgzNEgzNC4zODk1TDM4LjI4MzMgNTkuNTI3MkgzMS43ODAyTDI4LjI2NDUgNDkuNjcwOUwyNC43NzcgNTkuNDUyMkgxOC4xODAyTDEyLjYzOTUgNDQuMDk5MUgxNy43MDJMMjEuNDI3IDU0LjU1NTNMMjUuMTM5NSA0NC4xNDU5SDMxLjM4OTVMMzUuMDkyNyA1NC41MzM0TDM4Ljc5MjcgNDQuMTQyOEg0My44NzA4TDM4LjI4MzMgNTkuNTI3MkwzOS41MjcgNTguMDA4NEw1MC45Mzk1IDQ0LjExNzhINTcuMjIwOEw2Ni43MTc3IDU1LjcwNTNMNzEuMjg2NCA1MS43NzcyTDU2LjgyNyAzOS4yODM0SDY0LjI0ODlMODcuNjM5NSA1OS40NzcyWk04MC4zMTE0IDUwLjE4OTdMNzYuODI3IDQ3LjIwMjJMODAuMzA1MiA0NC4yMzk3TDg3LjMzNjQgNDQuMjQ1OUw4MC4zMTE0IDUwLjE4OTdaIiBmaWxsPSJ3aGl0ZSIvPg0KPC9nPg0KPGRlZnM%2BDQo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY3XzkiPg0KPHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjEzOTQwNCAwLjIyMTE5MSkiLz4NCjwvY2xpcFBhdGg%2BDQo8L2RlZnM%2BDQo8L3N2Zz4NCg%3D%3D&logoSize=auto" height="30" /></a>

## REST API
## Swagger API

### Usage

| Method | Path | Query parameters<br>(* = **Required**) | Description |
| :---: | --- | --- | --- |
| GET <br>`text/html` | `/` | - | [Swagger](https://swagger.io/) API playground |
| GET <br>`application/json` | `/chains` | `limit`<br>`page` | Information about the chains and latest head block in the database |
| GET <br>`application/json` | `/{chain}/balance` | `block_num`<br>`contract`<br>`symcode`<br>**`account*`**<br>`limit`<br>`page` | Balances of an account. |
| GET <br>`application/json` | `/{chain}/holders` | **`contract*`**<br>**`symcode*`**<br>`limit`<br>`page` | List of holders of a token |
| GET <br>`application/json` | `/{chain}/supply` | `block_num`<br>`issuer`<br>**`contract*`**<br>**`symcode*`**<br>`limit`<br>`page` | Total supply for a token |
| GET <br>`application/json` | `/{chain}/tokens` | `limit`<br>`page` | List of available tokens |
| GET <br>`application/json` | `/{chain}/transfers` | `block_range`<br>`from`<br>`to`<br>`contract`<br>`symcode`<br>`limit`<br>`page` | All transfers related to a token |
| GET <br>`application/json` | `/{chain}/transfers/{trx_id}` | `limit`<br>`page` | Specific transfer related to a token |
| GET <br>`application/json` | `/balance` | **`account*`**<br>`contract`<br>`symcode`<br>`limit`<br>`page` | Balances of an account |
| GET <br>`application/json` | `/balance/historical` | **`account*`**<br>`block_num`<br>`contract`<br>`symcode`<br>`limit`<br>`page` | Historical token balances |
| GET <br>`application/json` | `/head` | `limit`<br>`page` | Head block information |
| GET <br>`application/json` | `/holders` | **`contract*`**<br>**`symcode*`**<br>`limit`<br>`page` | List of holders of a token |
| GET <br>`application/json` | `/supply` | `block_num`<br>`issuer`<br>**`contract*`**<br>**`symcode*`**<br>`limit`<br>`page` | Total supply for a token |
| GET <br>`application/json` | `/tokens` | `limit`<br>`page` | List of available tokens |
| GET <br>`application/json` | `/transfers` | `block_range`<br>**`contract*`**<br>**`symcode*`**<br>`limit`<br>`page` | All transfers related to a token |
| GET <br>`application/json` | `/transfers/account` | **`account*`**<br>`block_range`<br>`from`<br>`to`<br>`contract`<br>`symcode`<br>`limit`<br>`page` | All transfers related to an account |
| GET <br>`application/json` | `/transfers/id` | **`trx_id*`**<br>`limit`<br>`page` | Specific transfer related to a token |

### Docs

Expand All @@ -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

Expand Down Expand Up @@ -158,7 +168,6 @@ HOST=http://127.0.0.1:8123
DATABASE=default
USERNAME=default
PASSWORD=
TABLE=
MAX_LIMIT=500
# Logging
Expand Down
24 changes: 19 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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())
);

// --------------------------
Expand All @@ -95,7 +95,7 @@ async function AntelopeTokenAPI() {
ctx,
endpoint,
{
...path_params.data as ValidPathParams<typeof endpoint>,
...path_params.data as SafeParseSuccess<ValidPathParams<typeof endpoint>>,
...query_params.data
} as ValidUserParams<typeof endpoint>
);
Expand All @@ -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<keyof typeof usageOperationsToEndpointsMap> = ['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<typeof endpoint>) => await (await makeUsageQuery(ctx, endpoint, { ...args })).json();
async (args: ValidUserParams<typeof endpoint>) => {
return await (await makeUsageQuery(ctx, endpoint, { ...args })).json();
};


return Object.keys(usageOperationsToEndpointsMap).reduce(
// SQL queries endpoints
(resolver, op) => Object.assign(
Expand All @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ export type UsageResponse<E extends UsageEndpoints> = EndpointReturnTypes<E>["da
export type UsageParameters<E extends UsageEndpoints> = EndpointParameters<E>;

export type ValidPathParams<E extends UsageEndpoints> = EndpointParameters<E>["path"];
export type ValidUserParams<E extends UsageEndpoints> = EndpointParameters<E> extends { path: undefined; } ?
export type ValidUserParams<E extends UsageEndpoints> = NonNullable<EndpointParameters<E> extends { path: undefined; } ?
// Combine path and query parameters only if path exists to prevent "never" on intersection
z.infer<EndpointParameters<E>["query"]>
:
z.infer<EndpointParameters<E>["query"] & ValidPathParams<E>>;
z.infer<EndpointParameters<E>["query"] & ValidPathParams<E>>>;
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<UsageEndpoints> & 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 };
Loading

0 comments on commit 442443a

Please sign in to comment.