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 ###