diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 141dd73..5f3c760 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -31,7 +31,7 @@ template: | ## Changes $CHANGES - **Full Changelog**: https://github.com/pinax-network/substreams-clock-api/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION + **Full Changelog**: https://github.com/pinax-network/antelope-token-api/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION autolabeler: - label: 'documentation' @@ -46,6 +46,8 @@ autolabeler: branch: - '/feature\/.+/' - label: 'ops' + branch: + - '/ops\/.+/' files: - '.github/*.yml' - '.github/workflows/*.yml' diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index feca44f..ba7b392 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -1,7 +1,10 @@ name: GitHub Container Registry on: - release: - types: [ published ] + push: + tags: + - "v*" + branches: + - "*" env: REGISTRY: ghcr.io @@ -19,7 +22,7 @@ jobs: uses: actions/checkout@v3 - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -27,14 +30,16 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | + type=sha,prefix= + type=raw,enable=${{ !startsWith(github.ref, 'refs/tags/') }},value=develop type=semver,pattern={{raw}} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 12b1f6c..8373dcb 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -3,7 +3,7 @@ name: Release Drafter on: push: branches: - - main + - develop pull_request: types: [opened, reopened, synchronize] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7bed123 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,170 @@ +# Contributing to Antelope Token API + +Welcome to the Antelope Token API repository ! You'll find here guidelines on how the repository is set up and how to possibly contribute to it. + + + +## Table of Contents + +- [Asking Questions](#asking-questions) +- [Contributing](#contributing) + - [Reporting Bugs](#reporting-bugs) + - [Suggesting Enhancements](#suggesting-enhancements) + - [Submitting PRs](#submitting-prs) +- [Style guides](#style-guides) + - [Code](#code) + - [Commit Messages](#commit-messages) + +## Asking Questions + +> [!NOTE] +> Make sure you have read the [documentation](README.md) first ! + +Before you ask a question, it is best to search for existing [Issues](https://github.com/pinax-network/antelope-token-api/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. + + +If you then still feel the need to ask a question and need clarification, we recommend the following: + +- Open an [Issue](https://github.com/pinax-network/antelope-token-api/issues/new). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions depending on what seems relevant. + +## Contributing + + + +### Reporting Bugs + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help fix any potential bug as fast as possible. + +- Make sure that you are using the [latest version](https://github.com/pinax-network/antelope-token-api/releases). If you're using the binary, you can check with `antelope-token-api --version`. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (make sure that you have read the [documentation](README.md). If you are looking for support, you might want to check [this section](#asking-questions)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/pinax-network/antelope-token-api/issues?q=label%3Abug). +- Also make sure to search the internet (including Stack Overflow) to see if users outside the GitHub community have discussed the issue. +- Collect information about the bug: + - Stack trace if possible + - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) + - Version of the [Bun](https://bun.sh/) binary, `bun --version` + - Possibly your environment variables and the output + - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + +#### How Do I Submit a Good Bug Report? + + + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [Issue](https://github.com/pinax-network/antelope-token-api/issues/new). +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for Antelope Token API, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + +#### Before Submitting an Enhancement + +- Make sure that you are using the [latest version](https://github.com/pinax-network/antelope-token-api/releases). If you're using the binary, you can check with `antelope-token-api --version`. +- Read the [documentation](README.md) carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Perform a [search](https://github.com/pinax-network/antelope-token-api/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. Keep in mind that features should be useful to the majority of users and not just a small subset. + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://github.com/pinax-network/antelope-token-api/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- **Explain why this enhancement would be useful** to most Antelope Token API users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + + + +### Submitting PRs + +You can follow the instructions from the `Quick Start` section of the [`README.md`](README.md/#quick-start) for setting up the environment. + +The repository contains one `main` branch. Any changes to `main` must go through a pull request of a branch with a specific naming pattern (see below). + +Any push to `main` branch will be tagged with the commit hash and the latest commit will additionally be tagged with `develop` to enable pulling latest development image (this is done automatically). You can retrieve the latest stable version of the API by checking out the latest tagged version commit (following [*semver*](https://semver.org/)). + +PRs should be submitted from separate branches of the `main` branch. Ideally, your PR should fall into one the following categories: +- **Feature**: `feature/xxx` +- **Bug fix**: `fix/xxx`, try to make separate PRs for different bug fixes unless the change solves multiple bugs at once. +- **Documentation**: `docs/xxx`, adding comments to files should be counted as documentation and changes made into a separate branch. +- **Operations**: `ops/xxx` +- **Others**: any other branching scheme or no branch will be counted as a miscellaneous change, avoid if possible. + +The reasoning behind these categories is to make it easier to track changes as well as drafting future releases (see [`release-drafter.yml`](.github/release-drafter.yml) action for more details). + +> [!WARNING] +> Make sure to tag any issues associated with the PR if one (or more) exists in your commit message. + +## Style guides + +### Code + +If you're using a standard IDE like [VSCode](https://code.visualstudio.com/), [Sublime Text](https://www.sublimetext.com/), etc. there shouldn't be any formatting issues. The code is formatted accorded to what the [LSP Typescript](https://github.com/typescript-language-server/typescript-language-server) standard client is using. Details about the settings used can be found [here](https://github.com/sublimelsp/LSP-typescript/blob/00aef378fd99283ae8451fe8f3f2483fa62b7d8e/LSP-typescript.sublime-settings#L61). + +### Commit Messages + +Here's a helpful commit message template adapted from [cbeams' article](https://cbea.ms/git-commit/): *How to Write a Git Commit Message*. + +``` +Summarize changes in around 50 characters or less +50 characters limit ############################## + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of the commit and the rest of the text as the body. The +blank line separating the summary from the body is critical (unless +you omit the body entirely); various tools like `log`, `shortlog` +and `rebase` can get confused if you run the two together. +72 characters limit #################################################### + +Explain the problem that this commit is solving. Focus on why you +are making this change as opposed to how (the code explains that). +Are there side effects or other unintuitive consequences of this +change? Here's the place to explain them. + +Further paragraphs come after blank lines. + + - Bullet points are okay, too + - Typically a hyphen or asterisk is used for the bullet, preceded + by a single space, with blank lines in between, but conventions + vary here + +Put references to relevant issues at the bottom, like this: + +Resolves: #123 +See also: #456, #789 +``` + +To use it, simply save it as a `.gitmessage` file and use the following comment to make `git` use it: +```console +git config commit.template ~/.gitmessage # Make sure to have the right path to your message file +``` +or to configure it globally +```console +git config --global commit.template ~/.gitmessage # Make sure to have the right path to your message file +``` + + + +## Attribution + +This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! \ No newline at end of file diff --git a/README.md b/README.md index f09ff7a..728a8d7 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ | Method | Path | Description | | :---: | --- | --- | -| GET
`text/html` | `/` | Swagger API playground | +| GET
`text/html` | `/` | [Swagger](https://swagger.io/) API playground | | GET
`application/json` | `/supply` | Antelope Tokens total supply | | GET
`application/json` | `/balance` | Antelope Tokens balance changes | | GET
`application/json` | `/transfers` | Antelope Tokens transfers | | GET
`text/plain` | `/health` | Performs health checks and checks if the database is accessible | -| GET
`text/plain` | `/metrics` | Prometheus metrics | -| GET
`application/json` | `/openapi` | OpenAPI specification | +| GET
`text/plain` | `/metrics` | [Prometheus](https://prometheus.io/) metrics | +| GET
`application/json` | `/openapi` | [OpenAPI](https://www.openapis.org/) specification | ## Requirements - [ClickHouse](clickhouse.com/) -- [Substreams Sink ClickHouse](https://github.com/pinax-network/substreams-sink-clickhouse/) +- (Optional) 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/streamingfast/substreams-sink-sql). ## Quick start @@ -82,17 +82,27 @@ VERBOSE=true ## Docker environment -Pull from GitHub Container registry +- Pull from GitHub Container registry + +**For latest release** ```bash docker pull ghcr.io/pinax-network/antelope-token-api:latest ``` +**For head of `develop` branch** +```bash +docker pull ghcr.io/pinax-network/antelope-token-api:develop +``` -Build from source +- Build from source ```bash docker build -t antelope-token-api . ``` -Run with `.env` file +- Run with `.env` file ```bash docker run -it --rm --env-file .env ghcr.io/pinax-network/antelope-token-api ``` + +## Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md). \ No newline at end of file diff --git a/package.json b/package.json index 9ab741f..4a04ba1 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": "2.0.0", + "version": "2.1.0", "homepage": "https://github.com/pinax-network/antelope-token-api", "license": "MIT", "type": "module", diff --git a/src/clickhouse/createClient.ts b/src/clickhouse/createClient.ts index e566dfe..79ae600 100644 --- a/src/clickhouse/createClient.ts +++ b/src/clickhouse/createClient.ts @@ -2,6 +2,7 @@ import { createClient } from "@clickhouse/client-web"; import { ping } from "./ping.js"; import { APP_NAME, config } from "../config.js"; +// TODO: Make client connect to all DB instances const client = createClient({ ...config, clickhouse_settings: { diff --git a/src/clickhouse/makeQuery.ts b/src/clickhouse/makeQuery.ts index 1de3e1f..af35945 100644 --- a/src/clickhouse/makeQuery.ts +++ b/src/clickhouse/makeQuery.ts @@ -10,6 +10,7 @@ export interface Query { meta: Meta[], data: T[], rows: number, + rows_before_limit_at_least: number, statistics: { elapsed: number, rows_read: number, @@ -21,7 +22,7 @@ export async function makeQuery(query: string) { try { const response = await client.query({ query }) const data: Query = await response.json(); - + prometheus.query.inc(); prometheus.bytes_read.inc(data.statistics.bytes_read); prometheus.rows_read.inc(data.statistics.rows_read); @@ -30,9 +31,18 @@ export async function makeQuery(query: string) { return data; } catch (e: any) { - console.error(e.message) + logger.error(e.message) - return { data: [] } + return { + meta: [], + data: [], + rows: 0, + rows_before_limit_at_least: 0, + statistics: { + elapsed: 0, + rows_read: 0, + bytes_read: 0, + } + }; } - } \ No newline at end of file diff --git a/src/fetch/GET.ts b/src/fetch/GET.ts index 5ff8ae7..e6340c3 100644 --- a/src/fetch/GET.ts +++ b/src/fetch/GET.ts @@ -13,11 +13,16 @@ export default async function (req: Request) { const { pathname } = new URL(req.url); prometheus.request.inc({ pathname }); + // Landing page if (pathname === "/") return new Response(Bun.file(swaggerHtml)); if (pathname === "/favicon.png") return new Response(Bun.file(swaggerFavicon)); + + // Utils if (pathname === "/health") return health(req); if (pathname === "/metrics") return new Response(await registry.metrics(), { headers: { "Content-Type": registry.contentType } }); if (pathname === "/openapi") return new Response(openapi, { headers: { "Content-Type": "application/json" } }); + + // Token endpoints if (pathname === "/supply") return supply(req); if (pathname === "/balance") return balance(req); if (pathname === "/transfers") return transfers(req); diff --git a/src/fetch/balance.ts b/src/fetch/balance.ts index 6f2f1d9..d470c97 100644 --- a/src/fetch/balance.ts +++ b/src/fetch/balance.ts @@ -2,7 +2,8 @@ import { makeQuery } from "../clickhouse/makeQuery.js"; import { logger } from "../logger.js"; import { getBalanceChanges } from "../queries.js"; import * as prometheus from "../prometheus.js"; -import { toJSON } from "./utils.js"; +import { addMetadata, toJSON } from "./utils.js"; +import { parseLimit, parsePage } from "../utils.js"; function verifyParams(searchParams: URLSearchParams) { const account = searchParams.get("account"); @@ -20,7 +21,14 @@ export default async function (req: Request) { const query = getBalanceChanges(searchParams); const response = await makeQuery(query) - return toJSON(response.data); + return toJSON( + addMetadata( + response.data, + response.rows_before_limit_at_least, + parseLimit(searchParams.get("limit")), + parsePage(searchParams.get("page")) + ) + ); } catch (e: any) { logger.error(e); prometheus.request_error.inc({ pathname: "/balance", status: 400 }); diff --git a/src/fetch/health.ts b/src/fetch/health.ts index 97c7c30..24719a5 100644 --- a/src/fetch/health.ts +++ b/src/fetch/health.ts @@ -2,6 +2,7 @@ import client from "../clickhouse/createClient.js"; import { logger } from "../logger.js"; import * as prometheus from "../prometheus.js"; +// TODO: Add log entry export default async function (_req: Request) { try { const response = await client.ping(); diff --git a/src/fetch/openapi.ts b/src/fetch/openapi.ts index d46d6bf..abf9521 100644 --- a/src/fetch/openapi.ts +++ b/src/fetch/openapi.ts @@ -49,11 +49,10 @@ const parameterLimit: ParameterObject = { schema: { type: "number", maximum: config.maxLimit, minimum: 1 }, } -// TODO: Determine offset from `limit` and replace this with page numbers const parameterOffset: ParameterObject = { - name: "offset", + name: "page", in: "query", - description: "Index offset for results pagination.", + description: "Page index for results pagination.", required: false, schema: { type: "number", minimum: 1 }, } diff --git a/src/fetch/supply.ts b/src/fetch/supply.ts index 082ea4f..280ac50 100644 --- a/src/fetch/supply.ts +++ b/src/fetch/supply.ts @@ -2,7 +2,8 @@ import { makeQuery } from "../clickhouse/makeQuery.js"; import { logger } from "../logger.js"; import { getTotalSupply } from "../queries.js"; import * as prometheus from "../prometheus.js"; -import { toJSON } from "./utils.js"; +import { addMetadata, toJSON } from "./utils.js"; +import { parseLimit, parsePage } from "../utils.js"; function verifyParams(searchParams: URLSearchParams) { const contract = searchParams.get("contract"); @@ -20,7 +21,14 @@ export default async function (req: Request) { const query = getTotalSupply(searchParams); const response = await makeQuery(query) - return toJSON(response.data); + return toJSON( + addMetadata( + response.data, + response.rows_before_limit_at_least, + parseLimit(searchParams.get("limit")), + parsePage(searchParams.get("page")) + ) + ); } catch (e: any) { logger.error(e); prometheus.request_error.inc({ pathname: "/supply", status: 400 }); diff --git a/src/fetch/transfers.ts b/src/fetch/transfers.ts index 77fb92b..5ec0b8d 100644 --- a/src/fetch/transfers.ts +++ b/src/fetch/transfers.ts @@ -2,7 +2,8 @@ import { makeQuery } from "../clickhouse/makeQuery.js"; import { logger } from "../logger.js"; import { getTransfers } from "../queries.js"; import * as prometheus from "../prometheus.js"; -import { toJSON } from "./utils.js"; +import { addMetadata, toJSON } from "./utils.js"; +import { parseLimit, parsePage } from "../utils.js"; export default async function (req: Request) { try { @@ -12,7 +13,14 @@ export default async function (req: Request) { const query = getTransfers(searchParams); const response = await makeQuery(query) - return toJSON(response.data); + return toJSON( + addMetadata( + response.data, + response.rows_before_limit_at_least, + parseLimit(searchParams.get("limit")), + parsePage(searchParams.get("page")) + ) + ); } catch (e: any) { logger.error(e); prometheus.request_error.inc({ pathname: "/transfers", status: 400 }); diff --git a/src/fetch/utils.spec.ts b/src/fetch/utils.spec.ts new file mode 100644 index 0000000..06144e9 --- /dev/null +++ b/src/fetch/utils.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test"; +import { addMetadata } from "./utils.js"; + +test("addMetadata pagination", () => { + const limit = 5; + const mock_query_reponse = { + data: Array(limit), + rows: limit, + rows_before_limit_at_least: 5*limit, // Simulate query with more total results than the query limit making pagination relevant + }; + + const first_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 1); + expect(first_page.meta.next_page).toBe(2); + expect(first_page.meta.previous_page).toBe(1); // Previous page should be set to 1 on first page + expect(first_page.meta.total_pages).toBe(5); + expect(first_page.meta.total_results).toBe(5*limit); + + const odd_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 3); + expect(odd_page.meta.next_page).toBe(4); + expect(odd_page.meta.previous_page).toBe(2); + expect(odd_page.meta.total_pages).toBe(5); + expect(odd_page.meta.total_results).toBe(5*limit); + + const even_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 4); + expect(even_page.meta.next_page).toBe(5); + expect(even_page.meta.previous_page).toBe(3); + expect(even_page.meta.total_pages).toBe(5); + expect(even_page.meta.total_results).toBe(5*limit); + + const last_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 5); + expect(last_page.meta.next_page).toBe(last_page.meta.total_pages); // Next page should be capped to total_pages on last page + expect(last_page.meta.previous_page).toBe(4); + expect(last_page.meta.total_pages).toBe(5); + expect(last_page.meta.total_results).toBe(5*limit); + + // TODO: Expect error message on beyond last page + // const beyond_last_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 6); +}); \ No newline at end of file diff --git a/src/fetch/utils.ts b/src/fetch/utils.ts index be854ec..2601853 100644 --- a/src/fetch/utils.ts +++ b/src/fetch/utils.ts @@ -1,3 +1,16 @@ export function toJSON(data: any, status: number = 200) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } }); +} + +export function addMetadata(data: any[], total_before_limit: number, limit: number, page: number) { + // TODO: Catch page number greater than total_pages and return error + return { + data, + meta: { + "next_page": (page * limit >= total_before_limit) ? page : page + 1, + "previous_page": (page <= 1) ? page : page - 1, + "total_pages": Math.ceil(total_before_limit / limit), + "total_results": total_before_limit + } + } } \ No newline at end of file diff --git a/src/prometheus.ts b/src/prometheus.ts index 32fb386..a9c0b98 100644 --- a/src/prometheus.ts +++ b/src/prometheus.ts @@ -1,5 +1,6 @@ // From https://github.com/pinax-network/substreams-sink-websockets/blob/main/src/prometheus.ts import client, { Counter, CounterConfiguration, Gauge, GaugeConfiguration } from 'prom-client'; +import { logger } from "./logger.js"; export const registry = new client.Registry(); @@ -9,7 +10,7 @@ export function registerCounter(name: string, help = "help", labelNames: string[ registry.registerMetric(new Counter({ name, help, labelNames, ...config })); return registry.getSingleMetric(name) as Counter; } catch (e) { - console.error({ name, e }); + logger.error({ name, e }); throw new Error(`${e}`); } } @@ -19,7 +20,7 @@ export function registerGauge(name: string, help = "help", labelNames: string[] registry.registerMetric(new Gauge({ name, help, labelNames, ...config })); return registry.getSingleMetric(name) as Gauge; } catch (e) { - console.error({ name, e }); + logger.error({ name, e }); throw new Error(`${e}`); } } diff --git a/src/queries.spec.ts b/src/queries.spec.ts index 8887815..42e9210 100644 --- a/src/queries.spec.ts +++ b/src/queries.spec.ts @@ -6,6 +6,7 @@ import { getTransfers, addAmountFilter, } from "./queries.js"; +import { config } from "./config.js"; const contract = "eosio.token"; const account = "push.sx"; @@ -64,7 +65,7 @@ test("getTotalSupply", () => { ) ); expect(query).toContain(formatSQL(`ORDER BY block_number DESC`)); - expect(query).toContain(formatSQL(`LIMIT 1`)); + expect(query).toContain(formatSQL(`LIMIT ${config.maxLimit}`)); }); test("getTotalSupply with options", () => { @@ -98,7 +99,7 @@ test("getBalanceChange", () => { ) ); expect(query).toContain(formatSQL(`ORDER BY timestamp DESC`)); - expect(query).toContain(formatSQL(`LIMIT 1`)); + expect(query).toContain(formatSQL(`LIMIT ${config.maxLimit}`)); }); test("getBalanceChanges with options", () => { @@ -133,5 +134,5 @@ test("getTransfers", () => { ) ); expect(query).toContain(formatSQL(`ORDER BY timestamp DESC`)); - expect(query).toContain(formatSQL(`LIMIT 100`)); + expect(query).toContain(formatSQL(`LIMIT ${config.maxLimit}`)); }); \ No newline at end of file diff --git a/src/queries.ts b/src/queries.ts index 991453f..5ff3b65 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,9 +1,29 @@ -import { DEFAULT_SORT_BY } from "./config.js"; -import { parseLimit, parseTimestamp } from "./utils.js"; +import { DEFAULT_SORT_BY, config } from "./config.js"; +import { parseLimit, parsePage, parseTimestamp } from "./utils.js"; // For reference on Clickhouse Database tables: // https://raw.githubusercontent.com/pinax-network/substreams-antelope-tokens/main/schema.sql +// Query for count of unique token holders grouped by token (contract, symcode) pairs +/* +SELECT + Count(*), + contract, + symcode +FROM + ( + SELECT + DISTINCT account, + contract, + symcode + FROM + eos_tokens_v1.account_balances FINAL + ) +GROUP BY + (contract, symcode) +order BY + (contract, symcode) ASC +*/ export function addTimestampBlockFilter(searchParams: URLSearchParams, where: any[]) { const operators = [ ["greater_or_equals", ">="], @@ -62,11 +82,11 @@ export function getTotalSupply(searchParams: URLSearchParams, example?: boolean) query += ` ORDER BY block_number ${sort_by ?? DEFAULT_SORT_BY} `; } - const limit = parseLimit(searchParams.get("limit")); - query += ` LIMIT ${limit} `; - - const offset = searchParams.get("offset"); - if (offset) query += ` OFFSET ${offset} `; + const limit = parseLimit(searchParams.get("limit"), config.maxLimit); + if (limit) query += ` LIMIT ${limit}`; + + const page = parsePage(searchParams.get("page")); + if (page) query += ` OFFSET ${limit * (page - 1)} `; return query; } @@ -95,11 +115,11 @@ export function getBalanceChanges(searchParams: URLSearchParams, example?: boole //if (contract && !account) query += `GROUP BY (contract, account) ORDER BY timestamp DESC`; } - const limit = parseLimit(searchParams.get("limit")); - query += ` LIMIT ${limit} `; + const limit = parseLimit(searchParams.get("limit"), config.maxLimit); + if (limit) query += ` LIMIT ${limit}`; - const offset = searchParams.get("offset"); - if (offset) query += ` OFFSET ${offset} `; + const page = parsePage(searchParams.get("page")); + if (page) query += ` OFFSET ${limit * (page - 1)} `; return query; } @@ -134,11 +154,11 @@ export function getTransfers(searchParams: URLSearchParams, example?: boolean) { query += ` ORDER BY timestamp DESC`; } - const limit = parseLimit(searchParams.get("limit"), 100); - query += ` LIMIT ${limit} `; + const limit = parseLimit(searchParams.get("limit"), config.maxLimit); + if (limit) query += ` LIMIT ${limit}`; - const offset = searchParams.get("offset"); - if (offset) query += ` OFFSET ${offset} `; + const page = parsePage(searchParams.get("page")); + if (page) query += ` OFFSET ${limit * (page - 1)} `; return query; } diff --git a/src/utils.spec.ts b/src/utils.spec.ts index ded6da8..9d2f6e4 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -1,13 +1,27 @@ import { expect, test } from "bun:test"; -import { parseBlockId, parseTimestamp } from "./utils.js"; +import { parseBlockId, parseLimit, parsePage, parseTimestamp } from "./utils.js"; +import { config } from "./config.js"; test("parseBlockId", () => { expect(parseBlockId("0x123") as string).toBe("123"); }); +test("parseLimit", () => { + expect(parseLimit("1")).toBe(1); + expect(parseLimit("0")).toBe(0); + expect(parseLimit(10)).toBe(10); + expect(parseLimit(config.maxLimit + 1)).toBe(config.maxLimit); +}); + +test("parsePage", () => { + expect(parsePage("1")).toBe(1); + expect(parsePage("0")).toBe(1); + expect(parsePage(10)).toBe(10); +}); + test("parseTimestamp", () => { expect(parseTimestamp("1697587100")).toBe(1697587100); expect(parseTimestamp("1697587100000")).toBe(1697587100); expect(parseTimestamp("awdawd")).toBeNaN(); expect(parseTimestamp(null)).toBeUndefined(); -}); +}); \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index e399ef9..667df3d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,20 +1,35 @@ import { config } from "./config.js"; +export function parseBlockId(block_id?: string | null) { + return block_id ? block_id.replace("0x", "") : undefined; +} + export function parseLimit(limit?: string | null | number, defaultLimit?: number) { - let value = 1 // default 1 + let value = 0; // default 0 (no limit) if (defaultLimit) value = defaultLimit; if (limit) { if (typeof limit === "string") value = parseInt(limit); if (typeof limit === "number") value = limit; } - // limit must be between 1 and maxLimit + // limit must be between 0 (no limit) and maxLimit + if (value < 0) value = 0; if (value > config.maxLimit) value = config.maxLimit; return value; } -export function parseBlockId(block_id?: string | null) { - return block_id ? block_id.replace("0x", "") : undefined; +export function parsePage(page?: string | null | number) { + let value = 1; + + if (page) { + if (typeof page === "string") value = parseInt(page); + if (typeof page === "number") value = page; + } + + if (value <= 0) + value = 1; + + return value; } export function parseTimestamp(timestamp?: string | null | number) {