diff --git a/.github/workflows/cleanup-actions.yml b/.github/workflows/cleanup-actions.yml new file mode 100644 index 00000000..ad82df31 --- /dev/null +++ b/.github/workflows/cleanup-actions.yml @@ -0,0 +1,45 @@ +name: "Cleanup - Actions" +on: + workflow_dispatch: + inputs: + days: + description: 'Retain days' + required: true + type: string + default: 30 + minimum_runs: + description: 'Minimum runs to keep for each workflow' + required: true + type: string + default: 0 + delete_workflow_pattern: + description: 'Name/filename of workflow. Default is all.' + required: false + type: string + delete_workflow_by_state_pattern: + description: 'Remove workflow by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually' + required: true + default: All + type: choice + options: + - All + - active + - deleted + - disabled_inactivity + - disabled_manually + +jobs: + + delete-runs: + name: "Delete old workflow runs" + runs-on: ubuntu-latest + + steps: + - uses: Mattraks/delete-workflow-runs@v2 + with: + token: ${{ github.token }} + repository: ${{ github.repository }} + retain_days: ${{ github.event.inputs.days }} + keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} + delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }} + delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }} diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 00000000..09697c19 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -0,0 +1,21 @@ +name: "Cleanup - Cache" +on: + workflow_dispatch: + inputs: + dry-run: + description: "Dry run only?" + required: true + type: boolean + default: false + +jobs: + + delete-caches: + name: "Delete Actions caches" + runs-on: ubuntu-latest + + steps: + - name: "Wipe Github Actions cache" + uses: easimon/wipe-cache@v1 + with: + dry-run: ${{ github.event.inputs.dry-run }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 05fc6c29..fecaa0dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -54,7 +54,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -70,3 +70,17 @@ jobs: WEBHOOK_URL env: WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} + + cache-purge: + name: "Purge branch Actions cache" + needs: staging-deploy + runs-on: ubuntu-latest + + steps: + + - name: "Delete Branch Cache Action" + uses: snnaplab/delete-branch-cache-action@v1.0.0 + with: + # Specify explicitly because the ref at the time of merging will be a branch name such as 'main', 'develop' + ref: refs/pull/${{ github.event.number }}/merge + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 53e959bf..06d0e72b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,18 +8,18 @@ defaults: jobs: - # md-link-check: - # name: "Broken Markdown links" - # runs-on: ubuntu-latest + md-link-check: + name: "Broken Markdown links" + runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v3 - # - name: Run Markdown link check - # uses: gaurav-nelson/github-action-markdown-link-check@v1 - # with: - # config-file: '.github/linters/mlc_config.json' - # use-quiet-mode: 'yes' + - name: Run Markdown link check + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + config-file: '.github/linters/mlc_config.json' + use-quiet-mode: 'yes' super-lint: name: "Super Linter" @@ -45,8 +45,6 @@ jobs: VALIDATE_JAVASCRIPT_ES: true VALIDATE_JSONC: true VALIDATE_MARKDOWN: true - VALIDATE_OPENAPI: true VALIDATE_TSX: true VALIDATE_TYPESCRIPT_ES: true - VALIDATE_XML: true VALIDATE_YAML: true diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e77066f7..84abd594 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,4 +30,4 @@ jobs: # to override config-conventional rules, specify a relative path to your rules module, actions/checkout is required for this setting! commitlintRulesPath: "./.github/linters/.commitlint.rules.js" # default: undefined # if the PR contains a single commit, fail if the commit message and the PR title do not match - commitTitleMatch: "false" # default: 'true' + commitTitleMatch: false # default: 'true' diff --git a/.gitignore b/.gitignore index e5de2ef8..01a3e171 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ### APP-SPECIFIC EXCLUSIONS ### wrangler.dev.toml watchlist.json -watchlist-old.json +watchlist-*.json ### GENERAL EXCLUSIONS ### diff --git a/README.md b/README.md index 4558cdaf..7b9c32c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## ℹ️ Overview -Cosmos SDK offers [APIs for built-in modules using gRPC, REST, and Tendermint RPC](https://docs.cosmos.network/master/core/grpc_rest.html). This project aims to provide simple REST APIs for data that default Cosmos SDK APIs can't provide. +Cosmos SDK offers [APIs for built-in modules using gRPC, REST, and Tendermint RPC](https://docs.cosmos.network/main/core/grpc_rest.html). This project aims to provide simple REST APIs for data that default Cosmos SDK APIs can't provide. This collection of custom APIs can be deployed as a [Cloudflare Worker](https://workers.cloudflare.com/) or compatible serverless platforms. @@ -32,7 +32,7 @@ While this figure is available from Cosmos SDK's built-in [`/cosmos/bank/v1beta1 #### Endpoint -`data-api.cheqd.io/supply/circulating` +[`data-api.cheqd.io/supply/circulating`](https://data-api.cheqd.io/supply/circulating) #### Response @@ -44,12 +44,7 @@ Cryptocurrency tracking websites such as [CoinMarketCap](https://coinmarketcap.c This figure is _not_ available from any Cosmos SDK API, because the [criteria for determining circulating vs "non-circulating" accounts is defined by CoinMarketCap](https://support.coinmarketcap.com/hc/en-us/articles/360043396252-Supply-Circulating-Total-Max-). -This API calculates the circulating supply by **subtracting** the account balances of a defined list of wallet addresses ("circulating supply watchlist"). Different types of accounts defined in the watchlist are handled as follows: - -1. **Base accounts and Continuous Vesting accounts**: These will always have an entry in BigDipper block explorer, since these accounts have transactions that trigger indexing. -2. **Delayed Vesting accounts**: These accounts present a complex scenario since BigDipper does _not_ index all delayed vesting accounts by default. - 1. **If there have been ANY transactions involving the delayed vesting account**: Delayed vesting accounts can still stake their original vesting allowance, or the account holder may have transferred additional funds into the account. In this scenario, the account _will_ be indexed by BigDipper and the account balance can be fetched via the GraphQL API. - 2. **If there have been NO transactions involving the delayed vesting account**: Delayed vesting accounts with no other transactions beyond the original creation are _not_ indexed by BigDipper. Balances for these accounts are fetched using the standard Cosmos SDK `/cosmos/bank/v1beta1/balances/
` REST API endpoint. +This API calculates the circulating supply by **subtracting** the account balances of a defined list of wallet addresses ("circulating supply watchlist") from the total supply. ### 🥩 Total staked supply @@ -65,34 +60,6 @@ Overall tokens staked, in CHEQ. Provides the overall amount staked pulled from the block explorer. -### ➕ Overall number of delegators - -#### Endpoint - -[`data-api.cheqd.io/staking/delegators/total`](https://data-api.cheqd.io/staking/delegators/total) - -#### Response - -Total number of delegators across every validator on the network. - -#### Rationale - -The only way to derive this figure from the Cosmos SDK APIs is by iterating over every validator and counting the number of delegators. - -### 🗳 Delegator count by validator - -#### Endpoint - -[`data-api.cheqd.io/staking/delegators/`](https://data-api.cheqd.io/staking/delegators/cheqdvaloper1lg0vwuu888hu4arnt9egtqrm2662kcrtf2unrs) - -#### Response - -Number of delegators who delegate to a specific validator. - -#### Rationale - -There is no simple Cosmos SDK API to fetch the number of delegators for a given validator. - ### 🔐 Vesting Account Balance #### Endpoint @@ -202,19 +169,34 @@ While our deployment uses Cloudflare Wrangler, the application itself could be m Wrangler CLI uses [`wrangler.toml` for configuring](https://developers.cloudflare.com/workers/wrangler/configuration/) the application. If you're using this for your own purposes, you will need to replace values for `account_id`, [Cloudflare KV](https://developers.cloudflare.com/workers/learning/how-kv-works/) bindings, `route`, etc. for the application to work correctly along with your own [Cloudflare API tokens](https://developers.cloudflare.com/api/tokens/create). -For the circulating supply API endpoint, Cloudflare Workers will expect to find a Cloudflare KV namespace called `CIRCULATING_SUPPLY_WATCHLIST` with a list of addresses in the `key`. The application _only_ uses the key, so value can be anything. +#### Environment variables + +The application expects these environment variables to be set on Cloudflare: + +1. `TOKEN_EXPONENT`: Denominator for token (default `9` for CHEQ token). +2. `REST_API`: REST API for a Cosmos/cheqd node to target for queries. +3. `REST_API_PAGINATION_LIMIT`: Number of results to fetch in a single query, for queries that require iterating multiple times. (E.g., many account balance queries require this, to be able to get all delegations etc.) +4. `GRAPHQL_API`: GraphQL API for a BigDipper explorer instance for some queries. E.g., the GraphQL API for [cheqd's block explorer](https://explorer.cheqd.io/) is `https://explorer-gql.cheqd.io/v1/graphql`. +5. `CIRCULATING_SUPPLY_GROUPS`: Number of sub-groups the circulating supply watchlist is split into (see sample JSON file below). This is to ensure that any lookups from APIs can be spaced out. +6. `MARKET_MONITORING_API`: Upstream API for running queries from CoinGecko API (see the [market-monitoring repository](https://github.com/cheqd/market-monitoring)). +7. `WEBHOOK_URL`: Zapier webhook URL to send market monitoring data to. Since this is a secret, it's not set in plaintext in `wrangler.toml` but passed via GitHub Actions secrets. + +#### Cloudflare KV bindings + +Cached data for computationally-expensive queries are stored in [Cloudflare KV](https://developers.cloudflare.com/workers/learning/how-kv-works/). -Delayed vesting accounts that have never been involved in a transaction (as described above) should be prefixed with a `delayed:` prefix in the JSON file. Cloudflare allows [filtering KV pair `key`s by prefixes](https://developers.cloudflare.com/workers/runtime-apis/kv/#more-detail) when using a list operation. +1. `CIRCULATING_SUPPLY_WATCHLIST`: This KV is pre-populated with a list of addresses to monitor for circulating supply. Initially, the *value* portion of this can be set to anything, since it will get replaced when [periodic cron triggers](https://developers.cloudflare.com/workers/platform/cron-triggers) run to set the account balance breakdown for this account. In case you have a lot of accounts to monitor, we recommend prefixing the *key* with a `group_N` prefix which will stagger the API lookup across multiple cron executions. +2. `ACTIVE_VALIDATORS`: List of active validators, fetch from block explorer GraphQL API. When a cron trigger is executed, the total delegator count and update time is stored in this KV. ```jsonc // Sample watchlist JSON file structure [ { - "key": "cheqd1...xxx", - "value": "26-May-2022" // This can be any value + "key": "group_1:cheqd1...xxx", // Group 1 prefix + "value": "26-May-2022" // This can be any value, and will be updated with account balance breakdown periodically }, { - "key": "delayed:cheqd1...xxx", // This is a delayed account that won't be indexed by BigDipper + "key": "group_2:cheqd1...xxx", // Group 2 prefix "value": "26-May-2022" } ] diff --git a/package-lock.json b/package-lock.json index c29a674f..fa6c6ac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@cloudflare/workers-types": "^3.18.0", "@types/node": "^17.0.45", "typescript": "^4.8.4", - "wrangler": "^2.1.13" + "wrangler": "^2.1.15" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -1481,9 +1481,9 @@ } }, "node_modules/wrangler": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-2.1.13.tgz", - "integrity": "sha512-FWarJ9pBaXOU/wj3BoLo1Azi4VvadD0PfDIYfvY9hoKVyPMSr4dpPNUGgtMhsVuDp7K9mdixnmGEJxR7pbs3kQ==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-2.1.15.tgz", + "integrity": "sha512-5iqtFNo+zbu1FTnQQU/1Y+WWxIEuPIy71fe0uvqqFl0pSlkAtZJ+ufw8UYVxf2Mprw4ia4mSDdhV+hHpZO1sLQ==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "^0.2.0", @@ -1535,9 +1535,9 @@ } }, "node_modules/xxhash-wasm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.1.tgz", - "integrity": "sha512-Lc9CTvDrH2vRoiaUzz25q7lRaviMhz90pkx6YxR9EPYtF99yOJnv2cB+CQ0hp/TLoqrUsk8z/W2EN31T568Azw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", + "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==", "dev": true }, "node_modules/yallist": { @@ -2553,9 +2553,9 @@ } }, "wrangler": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-2.1.13.tgz", - "integrity": "sha512-FWarJ9pBaXOU/wj3BoLo1Azi4VvadD0PfDIYfvY9hoKVyPMSr4dpPNUGgtMhsVuDp7K9mdixnmGEJxR7pbs3kQ==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-2.1.15.tgz", + "integrity": "sha512-5iqtFNo+zbu1FTnQQU/1Y+WWxIEuPIy71fe0uvqqFl0pSlkAtZJ+ufw8UYVxf2Mprw4ia4mSDdhV+hHpZO1sLQ==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "^0.2.0", @@ -2584,9 +2584,9 @@ "requires": {} }, "xxhash-wasm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.1.tgz", - "integrity": "sha512-Lc9CTvDrH2vRoiaUzz25q7lRaviMhz90pkx6YxR9EPYtF99yOJnv2cB+CQ0hp/TLoqrUsk8z/W2EN31T568Azw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", + "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==", "dev": true }, "yallist": { diff --git a/package.json b/package.json index 28e5f6e9..d363610c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@cloudflare/workers-types": "^3.18.0", "@types/node": "^17.0.45", "typescript": "^4.8.4", - "wrangler": "^2.1.13" + "wrangler": "^2.1.15" }, "private": true } diff --git a/src/api/bigDipperApi.ts b/src/api/bigDipperApi.ts index b9008e9f..83b41126 100644 --- a/src/api/bigDipperApi.ts +++ b/src/api/bigDipperApi.ts @@ -1,90 +1,31 @@ import { GraphQLClient } from '../helpers/graphql'; import { - ActiveValidatorsResponse, - Coin, + TotalSupplyResponse, TotalStakedCoinsResponse, - ValidatorDelegationsCountResponse, -} from '../types/node'; -import { Account } from '../types/bigDipper'; -import { NodeApi } from './nodeApi'; + ActiveValidatorsResponse, +} from '../types/bigDipper'; export class BigDipperApi { constructor(public readonly graphql_client: GraphQLClient) {} - async get_total_supply(): Promise { - let query = `query Supply { - supply(order_by: {height:desc} limit: 1) { - coins - height - } - }`; + async getTotalSupply(): Promise { + let query = `query TotalSupply { + supply { + coins + } + }`; let resp = await this.graphql_client.query<{ - data: { supply: { coins: Coin[] }[] }; + data: TotalSupplyResponse; }>(query); - return resp.data.supply[0].coins; + return Number( + resp.data.supply[0].coins.find((coin) => coin.denom === 'ncheq') + ?.amount || '0' + ); } - get_delegator_count_for_validator = async ( - address: string - ): Promise => { - let query = `query ValidatorDelegations($address: String!, $pagination: Boolean! = true) { - delegations: action_validator_delegations(address: $address, count_total: $pagination) { - pagination - } - } - `; - - const params = { - address: address, - }; - - const resp = await this.graphql_client.query<{ - data: ValidatorDelegationsCountResponse; - }>(query, params); - - return resp.data.delegations.pagination.total; - }; - - get_total_delegator_count = async (): Promise => { - const queryActiveValidators = `query ActiveValidators { - validator_info(distinct_on: operator_address, where: {validator: {validator_statuses: {jailed: {_eq: false}}}}) { - operator_address - } - }`; - - const data = []; - const uniques = new Set(); - - const activeValidator = await this.graphql_client.query<{ - data: ActiveValidatorsResponse; - }>(queryActiveValidators); - - for (let i = 0; i < activeValidator.data.validator_info.length; i++) { - const operator_address = - activeValidator.data.validator_info[i].operator_address; - const resp = await new NodeApi( - REST_API - ).staking_get_delegators_per_validator(operator_address); - data.push({ - validator: operator_address, - delegators: resp.delegation_responses, - }); - } - - for (let i = 0; i < data.length; i++) { - const delegators = data[i].delegators; - for (let j = 0; j < delegators.length; j++) { - uniques.add( - `${delegators[j].delegation.delegator_address}${delegators[j].delegation.validator_address}` - ); - } - } - return uniques.size; - }; - - get_total_staked_coins = async (): Promise => { + getTotalStakedCoins = async (): Promise => { let query = `query StakingInfo{ staking_pool { bonded_tokens diff --git a/src/api/marketMonitorApi.ts b/src/api/marketMonitorApi.ts index 28ebcc49..82300a55 100644 --- a/src/api/marketMonitorApi.ts +++ b/src/api/marketMonitorApi.ts @@ -2,7 +2,7 @@ import { MarketMonitorData } from '../types/marketMonitor'; export class MarketMonitorApi { constructor(public readonly base_market_monitor_api_url: string) {} - async get_market_monitor_data(): Promise { + async getMarketMonitoringData(): Promise { const requestOptions = { method: 'GET', }; diff --git a/src/api/nodeApi.ts b/src/api/nodeApi.ts index 01cac7f7..4c05d623 100644 --- a/src/api/nodeApi.ts +++ b/src/api/nodeApi.ts @@ -3,22 +3,13 @@ import { Coin, DelegationsResponse, UnbondingResponse, - ValidatorDetailResponse, + RewardsResponse } from '../types/node'; export class NodeApi { constructor(public readonly base_rest_api_url: string) {} - async bank_get_total_supply_ncheq(): Promise { - let resp = await fetch( - `${this.base_rest_api_url}/cosmos/bank/v1beta1/supply/ncheq` - ); - let respJson = (await resp.json()) as { amount: { amount: number } }; - - return respJson.amount.amount; - } - - async auth_get_account(address: string): Promise { + async getAccountInfo(address: string): Promise { let resp = await fetch( `${this.base_rest_api_url}/cosmos/auth/v1beta1/accounts/${address}` ); @@ -27,7 +18,7 @@ export class NodeApi { return respJson.account; } - async bank_get_account_balances(address: string): Promise { + async getAvailableBalance(address: string): Promise { let resp = await fetch( `${this.base_rest_api_url}/cosmos/bank/v1beta1/balances/${address}` ); @@ -36,61 +27,53 @@ export class NodeApi { return respJson.balances; } - async distribution_get_total_rewards(address: string): Promise { + async distributionGetRewards(address: string): Promise { let resp = await fetch( `${this.base_rest_api_url}/cosmos/distribution/v1beta1/delegators/${address}/rewards` ); - let respJson = (await resp.json()) as { - rewards: Record[]; - total: Coin[]; - }; + let respJson = (await resp.json()) as RewardsResponse; return Number(respJson?.total?.[0]?.amount ?? '0'); } - async staking_get_delegators_per_validator( - address: string - ): Promise { - let resp = await fetch( - `${this.base_rest_api_url}/cosmos/staking/v1beta1/validators/${address}/delegations?pagination.limit=10000` - ); - - return await resp.json(); - } - - async staking_get_all_delegations_for_delegator( + async getAllDelegations( address: string, - next_key?: string + offset: number, + should_count_total: boolean, + limit?: number ) { + // order of query params: count_total -> offset -> limit + const pagination_count_total = should_count_total + ? 'pagination.count_total=true' + : 'pagination.count_total=false'; + const pagination_limit = `pagination.limit=${ + limit ? limit : REST_API_PAGINATION_LIMIT + }`; + const pagination_offset = `pagination.offset=${offset}`; + // NOTE: be cautious of newlines or spaces. Might make the request URL malformed const resp = await fetch( - `${this.base_rest_api_url}/cosmos/staking/v1beta1/delegations/${address}${ - next_key ? `?pagination.key=${next_key}` : '' - }` + `${this.base_rest_api_url}/cosmos/staking/v1beta1/delegations/${address}?${pagination_count_total}&${pagination_limit}&${pagination_offset}` ); return (await resp.json()) as DelegationsResponse; } - async staking_get_all_unboding_delegations_for_delegator( + async getAllUnbondingDelegations( address: string, - next_key?: string + offset: number, + should_count_total: boolean ) { + // order of query params: count_total -> offset -> limit + const pagination_count_total = should_count_total + ? 'pagination.count_total=true' + : 'pagination.count_total=false'; + const pagination_limit = `pagination.limit=${REST_API_PAGINATION_LIMIT}`; + const pagination_offset = `pagination.offset=${offset}`; + // NOTE: be cautious of new lines or spaces. Might make the request URL malformed const resp = await fetch( - `${ - this.base_rest_api_url - }/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations${ - next_key ? `?pagination.key=${next_key}` : '' - }` + `${this.base_rest_api_url}/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations?${pagination_count_total}&${pagination_limit}&${pagination_offset}` ); return (await resp.json()) as UnbondingResponse; } - - async get_latest_block_height(): Promise { - const resp = await fetch(`${this.base_rest_api_url}/blocks/latest`); - let respJson = (await resp.json()) as { - block: { header: { height: number } }; - }; - return Number(respJson.block.header.height); - } } diff --git a/src/bindings.d.ts b/src/bindings.d.ts index 5de1c213..931a0958 100644 --- a/src/bindings.d.ts +++ b/src/bindings.d.ts @@ -1,6 +1,7 @@ declare global { const TOKEN_EXPONENT: number; const REST_API: string; + const REST_API_PAGINATION_LIMIT: number; const GRAPHQL_API: string; const CIRCULATING_SUPPLY_WATCHLIST: KVNamespace; const CIRCULATING_SUPPLY_GROUPS: number; diff --git a/src/handlers/allArbitrageOpportunities.ts b/src/handlers/allArbitrageOpportunities.ts index 76badc5b..6d1177f0 100644 --- a/src/handlers/allArbitrageOpportunities.ts +++ b/src/handlers/allArbitrageOpportunities.ts @@ -4,7 +4,7 @@ export async function fetchPrices() { let market_monitor_api = new MarketMonitorApi( `${MARKET_MONITORING_API}` ); - return await market_monitor_api.get_market_monitor_data(); + return await market_monitor_api.getMarketMonitoringData(); } export async function handler(request: Request): Promise { const payload = await fetchPrices(); diff --git a/src/handlers/arbitrageOpportunities.ts b/src/handlers/arbitrageOpportunities.ts index 2f89204b..bc6cdede 100644 --- a/src/handlers/arbitrageOpportunities.ts +++ b/src/handlers/arbitrageOpportunities.ts @@ -5,7 +5,7 @@ async function fetchPrices() { let market_monitor_api = new MarketMonitorApi( `${MARKET_MONITORING_API}` ); - return await market_monitor_api.get_market_monitor_data(); + return await market_monitor_api.getMarketMonitoringData(); } export async function filterArbitrageOpportunities(): Promise< diff --git a/src/handlers/circulatingSupply.ts b/src/handlers/circulatingSupply.ts index 1ab52428..8ee599e4 100644 --- a/src/handlers/circulatingSupply.ts +++ b/src/handlers/circulatingSupply.ts @@ -1,46 +1,13 @@ import { Request } from 'itty-router'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; -import { NodeApi } from '../api/nodeApi'; -import { AccountBalanceInfos } from '../types/node'; - -async function get_total_supply(): Promise { - let node_api = new NodeApi(REST_API); - let total_supply_ncheq = await node_api.bank_get_total_supply_ncheq(); - const total_supply = Number(ncheq_to_cheq_fixed(total_supply_ncheq)); - - return total_supply; -} - -async function get_circulating_supply(): Promise { - const total_supply = await get_total_supply(); +import { getCirculatingSupply } from '../helpers/circulating'; +export async function handler(request: Request): Promise { try { - const cached = await CIRCULATING_SUPPLY_WATCHLIST.list(); - console.log(`Total cached entries: ${cached.keys.length}`); - - let shareholders_total_balance = Number(0); - for (const key of cached.keys) { - let data: AccountBalanceInfos | null = - await CIRCULATING_SUPPLY_WATCHLIST.get(key.name, { - type: 'json', - }); - - if (data !== null && data.totalBalance !== null) { - shareholders_total_balance += Number(data.totalBalance); - } - } - - console.log('Total supply', total_supply); - console.log(`Watchlist total balance: ${shareholders_total_balance}`); - - return total_supply - shareholders_total_balance; - } catch (e: any) { - throw new Error(e.toString); + let circulating_supply = await getCirculatingSupply(); + return new Response(circulating_supply.toString()); + } + catch (err: any) { + console.log(err); + throw new Error(err.message); } -} - -export async function handler(request: Request): Promise { - let circulating_supply = await get_circulating_supply(); - - return new Response(circulating_supply.toString()); } diff --git a/src/handlers/delegatorCount.ts b/src/handlers/delegatorCount.ts deleted file mode 100644 index 5f18c7f3..00000000 --- a/src/handlers/delegatorCount.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request } from "itty-router"; -import { BigDipperApi } from "../api/bigDipperApi"; -import { GraphQLClient } from "../helpers/graphql"; - -export async function handler(request: Request): Promise { - const address = request.params?.['validator_address']; - - if (!address) { - throw new Error("No address specified or wrong address format."); - } - - let gql_client = new GraphQLClient(GRAPHQL_API); - let bd_api = new BigDipperApi(gql_client); - - let delegators = await bd_api.get_delegator_count_for_validator(address); - return new Response(delegators.toString()); -} diff --git a/src/handlers/liquidBalance.ts b/src/handlers/liquidBalance.ts index a944ff8e..eb34969b 100644 --- a/src/handlers/liquidBalance.ts +++ b/src/handlers/liquidBalance.ts @@ -1,35 +1,35 @@ import { Request } from "itty-router"; -import { is_delayed_vesting_account_type, is_vesting_account_type, validate_cheqd_address } from "../helpers/validate"; +import { isDelayedVestingAccount, isVestingAccount, isValidAddress } from "../helpers/validate"; import { NodeApi } from "../api/nodeApi"; -import { calculate_vested_coins } from "../helpers/vesting"; -import { ncheq_to_cheq_fixed } from "../helpers/currency"; +import { calculateVesting } from "../helpers/vesting"; +import { convertToMainTokenDenom } from "../helpers/currency"; export async function handler(request: Request): Promise { const address = request.params?.['address']; - if (!address || !validate_cheqd_address(address)) { + if (!address || !isValidAddress(address)) { throw new Error("No address specified or wrong address format."); } let api = new NodeApi(REST_API); - const account = await api.auth_get_account(address) + const account = await api.getAccountInfo(address) - if (!is_vesting_account_type(account["@type"])) { + if (!isVestingAccount(account["@type"])) { throw new Error(`Only vesting accounts are supported. Accounts type '${account["@type"]}'.`) } - if(is_delayed_vesting_account_type(account?.["@type"])) { - let balance = account?.base_vesting_account?.base_account?.sequence !== '0' ? Number(await (await api.bank_get_account_balances(address)).find(b => b.denom === "ncheq")?.amount ?? '0') : 0; - let rewards = Number(await (await api.distribution_get_total_rewards(address)) ?? '0'); + if(isDelayedVestingAccount(account?.["@type"])) { + let balance = account?.base_vesting_account?.base_account?.sequence !== '0' ? Number(await (await api.getAvailableBalance(address)).find(b => b.denom === "ncheq")?.amount ?? '0') : 0; + let rewards = Number(await (await api.distributionGetRewards(address)) ?? '0'); let delegated = Number(account?.base_vesting_account?.delegated_free?.find(d => d.denom === "ncheq")?.amount ?? '0'); - return new Response(ncheq_to_cheq_fixed(balance + rewards + delegated)); + return new Response(convertToMainTokenDenom(balance + rewards + delegated)); } - let vested_coins = calculate_vested_coins(account); - let balance = Number(await (await api.bank_get_account_balances(address)).find(b => b.denom === "ncheq")?.amount ?? '0') - let rewards = Number(await (await api.distribution_get_total_rewards(address)) ?? '0'); + let vested_coins = Number(calculateVesting(account)?.vested); + let balance = Number(await (await api.getAvailableBalance(address)).find(b => b.denom === "ncheq")?.amount ?? '0') + let rewards = Number(await (await api.distributionGetRewards(address)) ?? '0'); let liquid_coins = vested_coins + balance + rewards; - return new Response(ncheq_to_cheq_fixed(liquid_coins)); + return new Response(convertToMainTokenDenom(liquid_coins)); } diff --git a/src/handlers/totalBalance.ts b/src/handlers/totalBalance.ts index b8c780ef..28fd0892 100644 --- a/src/handlers/totalBalance.ts +++ b/src/handlers/totalBalance.ts @@ -1,9 +1,9 @@ import { Request } from 'itty-router'; -import { get_account_balance_infos_from_node_api } from '../helpers/balance'; +import { fetchAccountBalances } from '../helpers/balance'; export async function handler(request: Request): Promise { const address = request.params?.['address']; - let account_balance_infos = await get_account_balance_infos_from_node_api( + let account_balance_infos = await fetchAccountBalances( address!! ); return new Response(account_balance_infos?.totalBalance.toString()); diff --git a/src/handlers/totalDelegators.ts b/src/handlers/totalDelegators.ts deleted file mode 100644 index e6dce728..00000000 --- a/src/handlers/totalDelegators.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Request } from 'itty-router'; -import { BigDipperApi } from '../api/bigDipperApi'; -import { GraphQLClient } from '../helpers/graphql'; - -export async function handler(request: Request): Promise { - let gql_client = new GraphQLClient(GRAPHQL_API); - let bd_api = new BigDipperApi(gql_client); - - const delegators = await bd_api.get_total_delegator_count(); - return new Response(JSON.stringify(delegators)); -} diff --git a/src/handlers/totalStakedCoins.ts b/src/handlers/totalStakedCoins.ts index c4ffd105..cb7abf89 100644 --- a/src/handlers/totalStakedCoins.ts +++ b/src/handlers/totalStakedCoins.ts @@ -1,13 +1,13 @@ import { Request } from 'itty-router'; import { BigDipperApi } from '../api/bigDipperApi'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; +import { convertToMainTokenDenom } from '../helpers/currency'; import { GraphQLClient } from '../helpers/graphql'; export async function handler(request: Request): Promise { let gql_client = new GraphQLClient(GRAPHQL_API); let bd_api = new BigDipperApi(gql_client); - let total_staked_coins = await bd_api.get_total_staked_coins(); + let total_staked_coins = await bd_api.getTotalStakedCoins(); - return new Response(ncheq_to_cheq_fixed(Number(total_staked_coins))); + return new Response(convertToMainTokenDenom(Number(total_staked_coins))); } diff --git a/src/handlers/totalSupply.ts b/src/handlers/totalSupply.ts index 8b15f287..55c950b5 100644 --- a/src/handlers/totalSupply.ts +++ b/src/handlers/totalSupply.ts @@ -1,10 +1,11 @@ import { Request } from 'itty-router'; -import { NodeApi } from '../api/nodeApi'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; +import { BigDipperApi } from '../api/bigDipperApi'; +import { convertToMainTokenDenom } from '../helpers/currency'; +import { GraphQLClient } from '../helpers/graphql'; export async function handler(request: Request): Promise { - let nodeApi = new NodeApi(REST_API); - let totalSupply = await nodeApi.bank_get_total_supply_ncheq(); - - return new Response(ncheq_to_cheq_fixed(totalSupply)); + let gql_client = new GraphQLClient(GRAPHQL_API); + let bd_api = new BigDipperApi(gql_client); + const total_supply = await bd_api.getTotalSupply(); + return new Response(convertToMainTokenDenom(total_supply)); } diff --git a/src/handlers/vestedBalance.ts b/src/handlers/vestedBalance.ts index 028eb97c..afb261fa 100644 --- a/src/handlers/vestedBalance.ts +++ b/src/handlers/vestedBalance.ts @@ -1,29 +1,29 @@ import { Request } from 'itty-router'; import { - is_vesting_account_type, - validate_cheqd_address, + isVestingAccount, + isValidAddress, } from '../helpers/validate'; import { NodeApi } from '../api/nodeApi'; -import { calculate_vested_coins, estimatedVesting } from '../helpers/vesting'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; +import { calculateVesting } from '../helpers/vesting'; +import { convertToMainTokenDenom } from '../helpers/currency'; export async function handler(request: Request): Promise { const address = request.params?.['address']; - if (!address || !validate_cheqd_address(address)) { + if (!address || !isValidAddress(address)) { throw new Error('No address specified or wrong address format.'); } let api = new NodeApi(REST_API); - const account = await api.auth_get_account(address); + const account = await api.getAccountInfo(address); - if (!is_vesting_account_type(account['@type'])) { + if (!isVestingAccount(account['@type'])) { throw new Error( `Only vesting accounts are supported. Accounts type '${account['@type']}'.` ); } - let vested_coins = estimatedVesting(account)?.vested; + let vested_coins = calculateVesting(account)?.vested; - return new Response(ncheq_to_cheq_fixed(vested_coins!!)); + return new Response(convertToMainTokenDenom(vested_coins!!)); } diff --git a/src/handlers/vestingBalance.ts b/src/handlers/vestingBalance.ts index 10d6ff3f..cc34551e 100644 --- a/src/handlers/vestingBalance.ts +++ b/src/handlers/vestingBalance.ts @@ -1,29 +1,29 @@ import { Request } from 'itty-router'; import { - is_vesting_account_type, - validate_cheqd_address, + isVestingAccount, + isValidAddress, } from '../helpers/validate'; import { NodeApi } from '../api/nodeApi'; -import { calculate_vesting_coins, estimatedVesting } from '../helpers/vesting'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; +import { calculateVesting } from '../helpers/vesting'; +import { convertToMainTokenDenom } from '../helpers/currency'; export async function handler(request: Request): Promise { const address = request.params?.['address']; - if (!address || !validate_cheqd_address(address)) { + if (!address || !isValidAddress(address)) { throw new Error('No address specified or wrong address format.'); } let api = new NodeApi(REST_API); - const account = await api.auth_get_account(address); + const account = await api.getAccountInfo(address); - if (!is_vesting_account_type(account['@type'])) { + if (!isVestingAccount(account['@type'])) { throw new Error( `Only vesting accounts are supported. Accounts type '${account['@type']}'.` ); } - let vestingCoins = estimatedVesting(account)?.vesting; + let vestingCoins = calculateVesting(account)?.vesting; - return new Response(ncheq_to_cheq_fixed(vestingCoins!!)); + return new Response(convertToMainTokenDenom(vestingCoins!!)); } diff --git a/src/handlers/webhookTriggers.ts b/src/handlers/webhookTriggers.ts index 4adc9633..d54893e2 100644 --- a/src/handlers/webhookTriggers.ts +++ b/src/handlers/webhookTriggers.ts @@ -1,39 +1,46 @@ -import { updateGroupBalances } from '../helpers/balanceGroup'; +import { updateCirculatingSupply } from '../helpers/circulating'; import { filterArbitrageOpportunities } from './arbitrageOpportunities'; -export async function webhookTriggers(event: Event) { +export async function webhookTriggers(event: ScheduledEvent) { console.log('Triggering webhook...'); await sendPriceDiscrepancies(); - await updateGroupBalances(getRandomGroup()); + + await updateCirculatingSupply( + getRandomGroup(Number(CIRCULATING_SUPPLY_GROUPS)) + ); } export async function sendPriceDiscrepancies() { - console.log('Sending price discrepancies...'); + try { + console.log('Sending price discrepancies...'); - const arbitrageOpportunities = await filterArbitrageOpportunities(); - const hasArbitrageOpportunities = arbitrageOpportunities.length > 0; - if (hasArbitrageOpportunities) { - console.log('Arbitrage opportunities...'); - try { - const init = { - body: JSON.stringify({ - arbitrage_opportunities: arbitrageOpportunities, - }), - method: 'POST', - headers: { - 'content-type': 'application/json;charset=UTF-8', - }, - }; + const arbitrageOpportunities = await filterArbitrageOpportunities(); + const hasArbitrageOpportunities = arbitrageOpportunities.length > 0; + if (hasArbitrageOpportunities) { + console.log('Arbitrage opportunities...'); + try { + const init = { + body: JSON.stringify({ + arbitrage_opportunities: arbitrageOpportunities, + }), + method: 'POST', + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }; - await fetch(WEBHOOK_URL, init); - } catch (err: any) { - console.log(err); + await fetch(WEBHOOK_URL, init); + } catch (err: any) { + console.log(err); + } } + } catch (e) { + console.log('Error at: ', 'sendPriceDiscrepancies'); } } -function getRandomGroup(): number { +function getRandomGroup(group: number): number { let min = 1; - let max = Math.floor(CIRCULATING_SUPPLY_GROUPS); + let max = Math.floor(group); return Math.floor(Math.random() * (max - min + 1)) + min; } diff --git a/src/helpers/balance.ts b/src/helpers/balance.ts index 844c4f36..656549c7 100644 --- a/src/helpers/balance.ts +++ b/src/helpers/balance.ts @@ -1,104 +1,133 @@ -import { BigDipperApi } from '../api/bigDipperApi'; import { NodeApi } from '../api/nodeApi'; -import { Account } from '../types/bigDipper'; -import { AccountBalanceInfos } from '../types/node'; -import { ncheq_to_cheq_fixed } from './currency'; -import { GraphQLClient } from './graphql'; -import { - calculate_total_delegations_balance_for_delegator_in_ncheq, - calculate_total_unboding_delegations_balance_for_delegator_in_ncheq, -} from './node'; - -function extract_account_infos(account: Account) { - let balance = Number( - account?.accountBalance?.coins.find((c) => c.denom === 'ncheq')?.amount || - '0' - ); - - let delegated = 0; - if ( - account?.delegationBalance?.coins && - account?.delegationBalance?.coins.length > 0 - ) { - delegated = Number(account?.delegationBalance?.coins[0]?.amount || '0'); - } +import { + AccountBalanceInfos, + DelegationsResponse, + UnbondingResponse +} from '../types/node'; +import { convertToMainTokenDenom } from './currency'; +import { } from '../types/node'; - let unbonding = 0; - if ( - account?.unbondingBalance?.coins && - account?.unbondingBalance?.coins.length > 0 - ) { - unbonding = Number(account?.unbondingBalance?.coins[0]?.amount || '0'); - } - - let rewards = 0; - if (account?.rewardBalance?.length > 0) { - for (let i = 0; i < account?.rewardBalance.length; i++) { - rewards += Number(account?.rewardBalance[i]?.coins[0]?.amount || '0'); - } - } - - return { - balance, - rewards, - delegated, - unbonding, - }; -} - -export async function get_account_balance_infos_from_node_api( +export async function fetchAccountBalances( address: string ): Promise { const node_api = new NodeApi(REST_API); - const available_balance = await node_api.bank_get_account_balances(address); + const available_balance = await node_api.getAvailableBalance(address); let available_balance_in_ncheq = 0; if (available_balance.length > 0) { available_balance_in_ncheq = Number(available_balance[0]?.amount); } - const reward_balance_in_ncheq = await node_api.distribution_get_total_rewards( + const reward_balance_in_ncheq = await node_api.distributionGetRewards( address ); const total_delegation_balance_in_ncheq = - await calculate_total_delegations_balance_for_delegator_in_ncheq( - await node_api.staking_get_all_delegations_for_delegator(address) + await calculateTotalDelegationBalance( + await node_api.getAllDelegations( + address, + 0, // first call + true + ), + Number(REST_API_PAGINATION_LIMIT) // second call ); const total_unbonding_balance_in_ncheq = - await calculate_total_unboding_delegations_balance_for_delegator_in_ncheq( - await node_api.staking_get_all_unboding_delegations_for_delegator(address) + await calculateTotalUnbondingBalance( + await node_api.getAllUnbondingDelegations( + address, + 0, // first call + true + ), + Number(REST_API_PAGINATION_LIMIT) // second call ); return { totalBalance: Number( - ncheq_to_cheq_fixed( + convertToMainTokenDenom( available_balance_in_ncheq + reward_balance_in_ncheq + total_delegation_balance_in_ncheq + total_unbonding_balance_in_ncheq ) ), - availableBalance: Number(ncheq_to_cheq_fixed(available_balance_in_ncheq)), - rewards: Number(ncheq_to_cheq_fixed(reward_balance_in_ncheq)), - delegated: Number(ncheq_to_cheq_fixed(total_delegation_balance_in_ncheq)), - unbonding: Number(ncheq_to_cheq_fixed(total_unbonding_balance_in_ncheq)), + availableBalance: Number(convertToMainTokenDenom(available_balance_in_ncheq)), + rewards: Number(convertToMainTokenDenom(reward_balance_in_ncheq)), + delegated: Number(convertToMainTokenDenom(total_delegation_balance_in_ncheq)), + unbonding: Number(convertToMainTokenDenom(total_unbonding_balance_in_ncheq)), timeUpdated: new Date().toUTCString(), }; } -export async function updateCachedBalance(addr: string, grpN: number) { - try { - const account_balance_infos = await get_account_balance_infos_from_node_api( - addr +export async function calculateTotalDelegationBalance( + delegationsResp: DelegationsResponse, + current_offset: number +): Promise { + let total_delegation_balance_in_ncheq = 0; + const total_count = Number(delegationsResp.pagination.total); + + for (let i = 0; i < delegationsResp.delegation_responses.length; i++) { + total_delegation_balance_in_ncheq += Number( + delegationsResp.delegation_responses[i].balance.amount ); + } + + if (current_offset < total_count) { + const node_api = new NodeApi(REST_API); + const delegator_address = + delegationsResp.delegation_responses[0].delegation.delegator_address; + + const resp = await node_api.getAllDelegations( + delegator_address, + current_offset, // our current offset will be updated by recursive call below + true // we count total again , since it's implemented recursively + ); + + total_delegation_balance_in_ncheq += + await calculateTotalDelegationBalance( + resp, + current_offset + Number(REST_API_PAGINATION_LIMIT) + ); + } - const data = JSON.stringify(account_balance_infos); + return total_delegation_balance_in_ncheq; +} + +export async function calculateTotalUnbondingBalance( + unbondingResp: UnbondingResponse, + current_offset: number +): Promise { + let total_unbonding_balance_in_ncheq = 0; + const total_count = Number(unbondingResp.pagination.total); + for (let i = 0; i < unbondingResp.unbonding_responses.length; i++) { + for ( + let j = 0; + j < unbondingResp.unbonding_responses[i].entries.length; + j++ + ) { + total_unbonding_balance_in_ncheq += Number( + unbondingResp.unbonding_responses[i].entries[j].balance + ); + } + } + + if (current_offset < total_count) { + const node_api = new NodeApi(REST_API); + const delegator_address = + unbondingResp.unbonding_responses[0].delegator_address; - await CIRCULATING_SUPPLY_WATCHLIST.put(`grp_${grpN}:${addr}`, data); + const resp = + await node_api.getAllUnbondingDelegations( + delegator_address, + current_offset, + true + ); - console.log(`account "${addr}" balance updated. (${data})`); - } catch (e: any) { - console.log(`error updateCachedBalance: ${e}`); + total_unbonding_balance_in_ncheq += + await calculateTotalUnbondingBalance( + resp, + current_offset + Number(REST_API_PAGINATION_LIMIT) + ); } + + return total_unbonding_balance_in_ncheq; } diff --git a/src/helpers/balanceGroup.ts b/src/helpers/balanceGroup.ts deleted file mode 100644 index ed71da2b..00000000 --- a/src/helpers/balanceGroup.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { updateCachedBalance } from './balance'; - -export function extract_group_number_and_address(key: string) { - const parts = key.split(':'); - let addr = parts[1]; - let grpN = Number(parts[0].split('_')[1]); - return { - address: addr, - groupNumber: grpN, - }; -} - -export async function updateGroupBalances(groupNumber: number) { - const cached = await CIRCULATING_SUPPLY_WATCHLIST.list({ - prefix: `grp_${groupNumber}:`, - }); - - console.log( - `found ${cached.keys.length} cached accounts for group ${groupNumber}` - ); - - for (const key of cached.keys) { - const parts = extract_group_number_and_address(key.name); - let addr = parts.address; - let grpN = parts.groupNumber; - - const found = await CIRCULATING_SUPPLY_WATCHLIST.get(`grp_${grpN}:${addr}`); - if (found) { - console.log(`found ${key.name} (addr=${addr}) grp=${grpN}`); - - const account = await updateCachedBalance(addr, grpN); - - if (account !== null) { - console.log( - `updating account (grp_${grpN}:${addr}) balance (${JSON.stringify( - account - )})` - ); - } - } - } -} diff --git a/src/helpers/circulating.ts b/src/helpers/circulating.ts new file mode 100644 index 00000000..b4b9e850 --- /dev/null +++ b/src/helpers/circulating.ts @@ -0,0 +1,89 @@ +import { fetchAccountBalances } from './balance'; +import { convertToMainTokenDenom } from '../helpers/currency'; +import { AccountBalanceInfos } from '../types/node'; +import { extractPrefixAndKey } from './kv'; +import { BigDipperApi } from '../api/bigDipperApi'; +import { GraphQLClient } from '../helpers/graphql'; + +export async function updateCirculatingSupply(groupNumber: number) { + try { + const cached = await CIRCULATING_SUPPLY_WATCHLIST.list({ + prefix: `group_${groupNumber}:`, + }); + + console.log( + `found ${cached.keys.length} cached accounts for group ${groupNumber}` + ); + + for (const key of cached.keys) { + const parts = extractPrefixAndKey(key.name); + let addr = parts.address; + let grpN = parts.groupNumber; + + const found = await CIRCULATING_SUPPLY_WATCHLIST.get( + `group_${grpN}:${addr}` + ); + if (found) { + console.log(`found ${key.name} (addr=${addr}) grp=${grpN}`); + + const account = await updateCachedBalance(addr, grpN); + + if (account !== null) { + console.log( + `updating account (group_${grpN}:${addr}) balance (${JSON.stringify( + account + )})` + ); + } + } + } + } catch (e) { + console.log('Error at: ', 'updateCirculatingSupply'); + } +} + +export async function updateCachedBalance(addr: string, grpN: number) { + try { + const account_balance_infos = await fetchAccountBalances( + addr + ); + + const data = JSON.stringify(account_balance_infos); + + await CIRCULATING_SUPPLY_WATCHLIST.put(`group_${grpN}:${addr}`, data); + + console.log(`account "${addr}" balance updated. (${data})`); + } catch (e: any) { + console.log(`error updateCachedBalance: ${e}`); + } +} + +export async function getCirculatingSupply(): Promise { + let gql_client = new GraphQLClient(GRAPHQL_API); + let bd_api = new BigDipperApi(gql_client); + let total_supply_ncheq = await bd_api.getTotalSupply(); + const total_supply = Number(convertToMainTokenDenom(total_supply_ncheq)); + + try { + const cached = await CIRCULATING_SUPPLY_WATCHLIST.list(); + console.log(`Total cached entries: ${cached.keys.length}`); + let shareholders_total_balance = Number(0); + for (const key of cached.keys) { + let data: AccountBalanceInfos | null = + await CIRCULATING_SUPPLY_WATCHLIST.get(key.name, { + type: 'json', + }); + + if (data !== null && data.totalBalance !== null) { + shareholders_total_balance += Number(data.totalBalance); + } + } + + console.log('Total supply', total_supply); + console.log(`Watchlist total balance: ${shareholders_total_balance}`); + + return total_supply - shareholders_total_balance; + } catch (e: any) { + throw new Error(e.toString); + } +} diff --git a/src/helpers/currency.ts b/src/helpers/currency.ts index 82d8dadc..560224e1 100644 --- a/src/helpers/currency.ts +++ b/src/helpers/currency.ts @@ -1,13 +1,9 @@ import { TOKEN_DECIMALS } from "./constants"; -export function ncheq_to_cheqd(ncheq: number): number { +export function convertToLowestDenom(ncheq: number): number { return ncheq / TOKEN_DECIMALS; } -export function cheqd_to_ncheq(cheqd: number): number { - return cheqd * TOKEN_DECIMALS; +export function convertToMainTokenDenom(ncheq: number): string { + return convertToLowestDenom(ncheq).toFixed(0); } - -export function ncheq_to_cheq_fixed(ncheq: number): string { - return ncheq_to_cheqd(ncheq).toFixed(0); -} \ No newline at end of file diff --git a/src/helpers/graphql.ts b/src/helpers/graphql.ts index e16a5d4c..bb9daab3 100644 --- a/src/helpers/graphql.ts +++ b/src/helpers/graphql.ts @@ -16,7 +16,7 @@ export class GraphQLClient { let json: { errors: any } = await resp.json() if (json.errors) { - throw new Error(`query failed: ${JSON.stringify(json.errors)}`) + throw new Error(`Query failed: ${JSON.stringify(json.errors)}`) } return json as T; diff --git a/src/helpers/kv.ts b/src/helpers/kv.ts new file mode 100644 index 00000000..722c4205 --- /dev/null +++ b/src/helpers/kv.ts @@ -0,0 +1,9 @@ +export function extractPrefixAndKey(key: string) { + const parts = key.split(':'); + let addr = parts[1]; + let grpN = Number(parts[0].split('_')[1]); + return { + address: addr, + groupNumber: grpN, + }; +} diff --git a/src/helpers/node.ts b/src/helpers/node.ts deleted file mode 100644 index 60a0c854..00000000 --- a/src/helpers/node.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NodeApi } from '../api/nodeApi'; -import { Account } from '../types/bigDipper'; -import { Coin, DelegationsResponse, UnbondingResponse } from '../types/node'; - -export function total_balance_ncheq(account: Account): number { - let balance = Number( - account?.accountBalance?.coins.find((c) => c.denom === 'ncheq')?.amount || - '0' - ); - - let delegations = 0; - if ( - account?.delegationBalance?.coins && - account?.delegationBalance?.coins.length > 0 - ) { - delegations = Number(account?.delegationBalance?.coins[0].amount); - } - - let unbonding = 0; - if ( - account?.unbondingBalance?.coins && - account?.unbondingBalance?.coins.length > 0 - ) { - unbonding = Number(account?.unbondingBalance?.coins[0]?.amount); - } - - let rewards = 0; - if (account?.rewardBalance?.length > 0) { - for (let i = 0; i < account?.rewardBalance.length; i++) { - rewards += Number(account?.rewardBalance[i]?.coins[0].amount); - } - } - - return balance + delegations + unbonding + rewards; -} - -export function delayed_balance_ncheq(balance: Coin[]): number { - return Number(balance.find((c) => c.denom === 'ncheq')?.amount || '0'); -} - -export async function calculate_total_delegations_balance_for_delegator_in_ncheq( - delegationsResp: DelegationsResponse -): Promise { - let total_delegation_balance_in_ncheq = 0; - const next_key = delegationsResp.pagination.next_key; - - for (let i = 0; i < delegationsResp.delegation_responses.length; i++) { - total_delegation_balance_in_ncheq += Number( - delegationsResp.delegation_responses[i].balance.amount - ); - } - - if (next_key !== null) { - const node_api = new NodeApi(REST_API); - const delegator_address = - delegationsResp.delegation_responses[0].delegation.delegator_address; - - const resp = await node_api.staking_get_all_delegations_for_delegator( - delegator_address, - next_key - ); - - total_delegation_balance_in_ncheq += - await calculate_total_delegations_balance_for_delegator_in_ncheq(resp); - } - - return total_delegation_balance_in_ncheq; -} - -export async function calculate_total_unboding_delegations_balance_for_delegator_in_ncheq( - unbondingResp: UnbondingResponse -): Promise { - let total_unbonding_balance_in_ncheq = 0; - const next_key = unbondingResp.pagination.next_key; - - for (let i = 0; i < unbondingResp.unbonding_responses.length; i++) { - for ( - let j = 0; - j < unbondingResp.unbonding_responses[i].entries.length; - j++ - ) { - total_unbonding_balance_in_ncheq += Number( - unbondingResp.unbonding_responses[i].entries[j].balance - ); - } - } - - if (next_key !== null) { - const node_api = new NodeApi(REST_API); - const delegator_address = - unbondingResp.unbonding_responses[0].delegator_address; - - const resp = - await node_api.staking_get_all_unboding_delegations_for_delegator( - delegator_address, - next_key - ); - - total_unbonding_balance_in_ncheq += - await calculate_total_unboding_delegations_balance_for_delegator_in_ncheq( - resp - ); - } - - return total_unbonding_balance_in_ncheq; -} diff --git a/src/helpers/validate.ts b/src/helpers/validate.ts index 58e8878a..aff5344a 100644 --- a/src/helpers/validate.ts +++ b/src/helpers/validate.ts @@ -1,27 +1,16 @@ // TODO: This doesn't take checksum into account -export function validate_cheqd_address(address: string): boolean { +export function isValidAddress(address: string): boolean { return /^(cheqd)1[a-z0-9]{38}$/.test(address) } -export function is_vesting_account_type(account_type: string): boolean { +export function isVestingAccount(account_type: string): boolean { return account_type === '/cosmos.vesting.v1beta1.ContinuousVestingAccount' || account_type === '/cosmos.vesting.v1beta1.DelayedVestingAccount'; } -export function is_continuous_vesting_account_type(account_type: string): boolean { +export function isContinuousVestingAccount(account_type: string): boolean { return account_type === '/cosmos.vesting.v1beta1.ContinuousVestingAccount'; } -export function is_delayed_vesting_account_type(account_type: string): boolean { +export function isDelayedVestingAccount(account_type: string): boolean { return account_type === '/cosmos.vesting.v1beta1.DelayedVestingAccount'; } - -export function marked_as_delayed_vesting_account(address: string): boolean { - return /^delayed:/.test(address); -} - -export function filter_marked_as_account_types(addresses: string[]): Record { - return { - delayed: addresses.filter(address => marked_as_delayed_vesting_account(address)).map(address => address.replace('delayed:', '')), - other: addresses.filter(address => !marked_as_delayed_vesting_account(address)).map(address => address) - }; -} \ No newline at end of file diff --git a/src/helpers/vesting.ts b/src/helpers/vesting.ts index 77dc28ed..a929ade5 100644 --- a/src/helpers/vesting.ts +++ b/src/helpers/vesting.ts @@ -1,50 +1,16 @@ import { Account } from '../types/node'; import { - is_continuous_vesting_account_type, - is_delayed_vesting_account_type, + isContinuousVestingAccount, + isDelayedVestingAccount, } from './validate'; -// TODO: This method computes the amount of coins vested. This is not the same as coins that user can spend. -// To calculate spendable tokens we need to take into account initial balance + sent and received tokens as well. -// Here is the explanation of how to do it properly: -// https://docs.cosmos.network/master/modules/auth/05_vesting.html#transferring-sending -export function calculate_vested_coins(account: Account): number { - if ( - account?.['@type'] === '/cosmos.vesting.v1beta1.DelayedVestingAccount' && - Date.now() < account?.base_vesting_account?.end_time * 1000 - ) - return 0; - - const start_time = new Date(account.start_time * 1000).getTime(); - const end_time = new Date( - account.base_vesting_account.end_time * 1000 - ).getTime(); - const now = new Date().getTime(); - - const time_elapsed = Math.abs(now - start_time) / 1000; - const time_vested = Math.abs(end_time - start_time) / 1000; - - const ratio = Number(time_elapsed / time_vested); - - return ( - ratio * Number(account.base_vesting_account.original_vesting[0].amount) - ); -} - -export function calculate_vesting_coins(account: Account): number { - return ( - Number(account.base_vesting_account.original_vesting[0].amount) - - calculate_vested_coins(account) - ); -} - // Taken from our wallet app -export function estimatedVesting(account: Account, t?: Date) { +export function calculateVesting(account: Account, t?: Date) { if (!t) { t = new Date(); } - if (is_continuous_vesting_account_type(account?.['@type'])) { + if (isContinuousVestingAccount(account?.['@type'])) { const startsAt = account.start_time; const endsAt = account.base_vesting_account.end_time; @@ -65,16 +31,16 @@ export function estimatedVesting(account: Account, t?: Date) { vesting, }; } - if (is_delayed_vesting_account_type(account?.['@type'])) { + if (isDelayedVestingAccount(account?.['@type'])) { const endsAt = account.base_vesting_account.end_time; - const orginalVesting = Number( + const originalVesting = Number( account.base_vesting_account.original_vesting[0]?.amount ); const doneRatio = t > new Date(endsAt) ? 1 : 0; - const vested = Math.ceil(Number(orginalVesting) * doneRatio); - const vesting = Math.ceil(Number(orginalVesting) * (1.0 - doneRatio)); + const vested = Math.ceil(Number(originalVesting) * doneRatio); + const vesting = Math.ceil(Number(originalVesting) * (1.0 - doneRatio)); return { vested, diff --git a/src/index.ts b/src/index.ts index a0af81f0..13d117ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,14 +5,12 @@ import { handler as circulatingSupplyHandler } from './handlers/circulatingSuppl import { handler as liquidBalanceHandler } from './handlers/liquidBalance'; import { handler as vestingBalanceHandler } from './handlers/vestingBalance'; import { handler as vestedBalanceHandler } from './handlers/vestedBalance'; -import { handler as delegatorCountHandler } from './handlers/delegatorCount'; -import { handler as totalDelegatorsHandler } from './handlers/totalDelegators'; import { handler as totalStakedCoinsHandler } from './handlers/totalStakedCoins'; import { handler as allArbitrageOpportunitiesHandler } from './handlers/allArbitrageOpportunities'; import { handler as arbitrageOpportunitiesHandler } from './handlers/arbitrageOpportunities'; import { webhookTriggers } from './handlers/webhookTriggers'; -addEventListener('scheduled', (event: any) => { +addEventListener('scheduled', (event: ScheduledEvent) => { event.waitUntil(webhookTriggers(event)); }); @@ -30,8 +28,6 @@ function registerRoutes(router: Router) { router.get('/balances/total/:address', totalBalanceHandler); router.get('/balances/vested/:address', vestedBalanceHandler); router.get('/balances/vesting/:address', vestingBalanceHandler); - router.get('/staking/delegators/total', totalDelegatorsHandler); - router.get('/staking/delegators/:validator_address', delegatorCountHandler); router.get('/supply/circulating', circulatingSupplyHandler); router.get('/supply/staked', totalStakedCoinsHandler); router.get('/supply/total', totalSupplyHandler); diff --git a/src/types/bigDipper.ts b/src/types/bigDipper.ts index 8c841d54..3e4a53bb 100644 --- a/src/types/bigDipper.ts +++ b/src/types/bigDipper.ts @@ -1,35 +1,28 @@ -import { Coin } from './node'; +export interface TotalSupplyResponse { + supply: [ + { + coins: [ + { + denom: string; + amount: string; + } + ]; + } + ]; +} -export class Account { - public accountBalance: { coins: Coin[] }; - public delegationBalance: { coins: Coin[] }; - public unbondingBalance: { coins: Coin[] }; - public rewardBalance: [{ coins: Coin[] }]; - public vesting_account: { - id: string; - type: string; - original_vesting: Coin[]; - start_time: number; - end_time: number; - }[]; +export interface TotalStakedCoinsResponse { + staking_pool: [ + { + bonded_tokens: string; + } + ]; +} - constructor( - account_balance: { coins: Coin[] }, - delegation_balance: { coins: Coin[] }, - unbonding_balance: { coins: Coin[] }, - reward_balance: [{ coins: Coin[] }], - vesting_account: { - id: string; - type: string; - original_vesting: Coin[]; - start_time: number; - end_time: number; - }[] - ) { - this.accountBalance = account_balance; - this.delegationBalance = delegation_balance; - this.unbondingBalance = unbonding_balance; - this.rewardBalance = reward_balance; - this.vesting_account = vesting_account; - } +export interface ActiveValidatorsResponse { + validator_info: [ + { + operator_address: string; + } + ]; } diff --git a/src/types/kv.ts b/src/types/kv.ts new file mode 100644 index 00000000..f7277895 --- /dev/null +++ b/src/types/kv.ts @@ -0,0 +1,4 @@ +export interface ActiveValidatorsKV { + totalDelegatorsCount?: string; + updatedAt?: string; +} diff --git a/src/types/node.ts b/src/types/node.ts index f1b0e440..d8f4287d 100644 --- a/src/types/node.ts +++ b/src/types/node.ts @@ -32,49 +32,24 @@ export class Coin { } } -export class Delegation { - public amount: Coin; - public delegatorAddress: string; - - constructor(amount: Coin, delegatorAddress: string) { - this.delegatorAddress = delegatorAddress; - this.amount = amount; - } -} - -export interface ValidatorDelegationsCountResponse { - delegations: { - pagination: { - total: number; - }; - }; -} - export interface ValidatorDetailResponse { delegation_responses: [ { delegation: { delegator_address: string; validator_address: string; + shares: string; + }; + balance: { + denom: string; + amount: string; }; } ]; -} - -export interface ActiveValidatorsResponse { - validator_info: [ - { - operator_address: string; - } - ]; -} - -export interface TotalStakedCoinsResponse { - staking_pool: [ - { - bonded_tokens: string; - } - ]; + pagination: { + next_key: string; + total: string; + }; } export interface AccountBalanceInfos { @@ -126,3 +101,8 @@ export interface UnbondingResponse { total: string; }; } + +export interface RewardsResponse { + rewards: Record[]; + total: Coin[]; +} diff --git a/wrangler.toml b/wrangler.toml index 7ad4a019..05d0469a 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -31,9 +31,10 @@ route = { pattern = "data-api.cheqd.io/*", zone_id = "afe3b66243382f27140e6feeaa # KV Namespaces accessible from the Worker # Details: https://developers.cloudflare.com/workers/learning/how-kv-works # @default `[]` -[[kv_namespaces]] -binding = "CIRCULATING_SUPPLY_WATCHLIST" -id = "a9bc7aaa54ee4394ae6b9abe43e05ad6" + +kv_namespaces = [ + { binding = "CIRCULATING_SUPPLY_WATCHLIST", id = "270dbd79fa434edf9174fac7d5bc1bdf" } +] # Map of environment variables to set when deploying the Worker # Not inherited. @default `{}` @@ -42,11 +43,13 @@ id = "a9bc7aaa54ee4394ae6b9abe43e05ad6" TOKEN_EXPONENT = "9" # Standard Cosmosd SDK REST API endpoint for a node on target network REST_API = "https://api.cheqd.net" +# REST API pagination limit +REST_API_PAGINATION_LIMIT = "50" # GraphQL API endpoint for target network. Must be sourced from a BigDipper instance. GRAPHQL_API = "https://explorer-gql.cheqd.io/v1/graphql" # Number of groups circulating supply watchlist is split into -CIRCULATING_SUPPLY_GROUPS = "4" -# Moniter market API base url +CIRCULATING_SUPPLY_GROUPS = "24" +# Market monitoring API endpoint MARKET_MONITORING_API = "https://market-monitoring.cheqd.net" # The necessary secrets are: @@ -54,7 +57,7 @@ MARKET_MONITORING_API = "https://market-monitoring.cheqd.net" # Run `echo | wrangler secret put ` for each of these [triggers] -crons = ["0 * * * *"] +crons = ["0/2 * * * *"] ############################################################### ### SECTION 3: Local Development ### @@ -87,8 +90,7 @@ route = { pattern = "data-api-staging.cheqd.io/*", zone_id = "afe3b66243382f2714 # Map of environment variables to set when deploying the Worker # Not inherited. @default `{}` -vars = { ENVIRONMENT = "staging", TOKEN_EXPONENT = "9", REST_API = "https://api.cheqd.net", GRAPHQL_API = "https://explorer-gql.cheqd.io/v1/graphql", CIRCULATING_SUPPLY_GROUPS = "4", MARKET_MONITORING_API = "https://market-monitoring-staging.cheqd.net"} - +vars = { ENVIRONMENT = "staging", TOKEN_EXPONENT = "9", REST_API = "https://api.cheqd.net", REST_API_PAGINATION_LIMIT = "50", GRAPHQL_API = "https://explorer-gql.cheqd.io/v1/graphql", CIRCULATING_SUPPLY_GROUPS = "24", MARKET_MONITORING_API = "https://market-monitoring-staging.cheqd.net" } # The necessary secrets are: # - WEBHOOK_URL @@ -99,12 +101,12 @@ vars = { ENVIRONMENT = "staging", TOKEN_EXPONENT = "9", REST_API = "https://api. # @default `[]` kv_namespaces = [ - { binding = "CIRCULATING_SUPPLY_WATCHLIST", id = "86891d184f7f40ee9b403a94a76fcdab" } + { binding = "CIRCULATING_SUPPLY_WATCHLIST", id = "83699afe22654413ae141bb70d37554d" } ] # Cron triggers for staging worker [env.staging.triggers] -crons = ["0 * * * *"] +crons = [ "0 * * * *"] ############################################################### ### OPTIONAL: Build Configuration ###