diff --git a/src/api/bigDipperApi.ts b/src/api/bigDipperApi.ts index 17bbf013..b9008e9f 100644 --- a/src/api/bigDipperApi.ts +++ b/src/api/bigDipperApi.ts @@ -11,54 +11,6 @@ import { NodeApi } from './nodeApi'; export class BigDipperApi { constructor(public readonly graphql_client: GraphQLClient) {} - async get_account(address: string): Promise { - let query = `query Account($address: String!, $where: vesting_account_bool_exp) { - accountBalance: action_account_balance(address: $address) { - coins - } - delegationBalance: action_delegation_total(address: $address) { - coins - } - unbondingBalance: action_unbonding_delegation_total(address: $address) { - coins - } - redelegationBalance: action_redelegation(address: $address) { - redelegations - } - rewardBalance: action_delegation_reward(address: $address) { - coins - } - vesting_account(where: $where) { - id - type - original_vesting - start_time - end_time - } - }`; - - let params = { - address: address, - where: { - address: { - _eq: address, - }, - }, - }; - - try { - let resp = await this.graphql_client.query<{ - data: any; - errors: any; - }>(query, params); - - return resp.data as Account; - } catch (e: any) { - console.error(new Map(e)); - return null; - } - } - async get_total_supply(): Promise { let query = `query Supply { supply(order_by: {height:desc} limit: 1) { diff --git a/src/api/nodeApi.ts b/src/api/nodeApi.ts index 1e53d2f3..01cac7f7 100644 --- a/src/api/nodeApi.ts +++ b/src/api/nodeApi.ts @@ -1,4 +1,10 @@ -import { Account, Coin, ValidatorDetailResponse } from '../types/node'; +import { + Account, + Coin, + DelegationsResponse, + UnbondingResponse, + ValidatorDetailResponse, +} from '../types/node'; export class NodeApi { constructor(public readonly base_rest_api_url: string) {} @@ -51,4 +57,40 @@ export class NodeApi { return await resp.json(); } + + async staking_get_all_delegations_for_delegator( + address: string, + next_key?: string + ) { + const resp = await fetch( + `${this.base_rest_api_url}/cosmos/staking/v1beta1/delegations/${address}${ + next_key ? `?pagination.key=${next_key}` : '' + }` + ); + + return (await resp.json()) as DelegationsResponse; + } + + async staking_get_all_unboding_delegations_for_delegator( + address: string, + next_key?: string + ) { + const resp = await fetch( + `${ + this.base_rest_api_url + }/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations${ + next_key ? `?pagination.key=${next_key}` : '' + }` + ); + + 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 32c54676..5de1c213 100644 --- a/src/bindings.d.ts +++ b/src/bindings.d.ts @@ -3,6 +3,7 @@ declare global { const REST_API: string; const GRAPHQL_API: string; const CIRCULATING_SUPPLY_WATCHLIST: KVNamespace; + const CIRCULATING_SUPPLY_GROUPS: number; const MARKET_MONITORING_API: string; const WEBHOOK_URL: string; } diff --git a/src/handlers/circulatingSupply.ts b/src/handlers/circulatingSupply.ts index 8830e8cd..1ab52428 100644 --- a/src/handlers/circulatingSupply.ts +++ b/src/handlers/circulatingSupply.ts @@ -1,57 +1,46 @@ -import { GraphQLClient } from "../helpers/graphql"; -import { BigDipperApi } from "../api/bigDipperApi"; -import { Request } from "itty-router"; -import { ncheq_to_cheq_fixed } from "../helpers/currency"; -import { total_balance_ncheq } from "../helpers/node"; -import { Account } from "../types/bigDipper"; +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 { - let gql_client = new GraphQLClient(GRAPHQL_API); - let bd_api = new BigDipperApi(gql_client); - - let total_supply = await bd_api.get_total_supply(); - let total_supply_ncheq = Number(total_supply.find(c => c.denom === "ncheq")?.amount || '0'); - - try { - const cached = await CIRCULATING_SUPPLY_WATCHLIST.list() - console.log(`found ${cached.keys.length} cached items`) - - let non_circulating_supply_ncheq = Number(0); - for (const r of cached.keys) { - console.log(`looking for account: ${r.name} in cache`) - let data: any = await CIRCULATING_SUPPLY_WATCHLIST.get(r.name, { type: "json" }); - - if (data !== null) { - if (data.totalBalance === undefined) { - const balance = total_balance_ncheq(JSON.parse(data) as Account) - data = JSON.stringify({ totalBalance: balance, updatedAt: Date.now() }) - console.log(`updating bad cache entry: ${JSON.stringify(data)} totalBalance=${data.totalBalance} data=${JSON.stringify(data)}`) - await CIRCULATING_SUPPLY_WATCHLIST.put(r.name, data) - } - - console.log(`found cache entry: ${JSON.stringify(data)} totalBalance=${data.totalBalance}`) - - if (data.totalBalance !== null) { - non_circulating_supply_ncheq += data.totalBalance; - } - } - } - - console.log(`Non-circulating supply: ${non_circulating_supply_ncheq}`); - // Get total supply - let total_supply_ncheq = Number(total_supply.find(c => c.denom === "ncheq")?.amount || '0'); - console.log(`Total supply: ${total_supply_ncheq}`); - - // Calculate circulating supply - return total_supply_ncheq - non_circulating_supply_ncheq; - } catch (e: any) { - console.error(new Map(e)) - return total_supply_ncheq + const total_supply = await get_total_supply(); + + 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); + } } export async function handler(request: Request): Promise { - let circulating_supply = await get_circulating_supply(); + let circulating_supply = await get_circulating_supply(); - return new Response(ncheq_to_cheq_fixed(circulating_supply)); + return new Response(circulating_supply.toString()); } diff --git a/src/handlers/totalBalance.ts b/src/handlers/totalBalance.ts index 4be2220d..b8c780ef 100644 --- a/src/handlers/totalBalance.ts +++ b/src/handlers/totalBalance.ts @@ -1,50 +1,10 @@ import { Request } from 'itty-router'; -import { - is_delayed_vesting_account_type, - validate_cheqd_address, -} from '../helpers/validate'; -import { ncheq_to_cheq_fixed } from '../helpers/currency'; -import { BigDipperApi } from '../api/bigDipperApi'; -import { GraphQLClient } from '../helpers/graphql'; -import { total_balance_ncheq } from '../helpers/node'; -import { NodeApi } from '../api/nodeApi'; +import { get_account_balance_infos_from_node_api } from '../helpers/balance'; export async function handler(request: Request): Promise { const address = request.params?.['address']; - - if (!address || !validate_cheqd_address(address)) { - throw new Error('No address specified or wrong address format.'); - } - - let node_api = new NodeApi(REST_API); - let auth_account = await node_api.auth_get_account(address); - - if (is_delayed_vesting_account_type(auth_account?.['@type'])) { - let balance = Number( - (await ( - await node_api.bank_get_account_balances(address) - ).find((b) => b.denom === 'ncheq')?.amount) ?? '0' - ); - let rewards = Number( - (await await node_api.distribution_get_total_rewards(address)) ?? '0' - ); - let delegated = Number( - auth_account?.base_vesting_account?.delegated_vesting?.find( - (d) => d.denom === 'ncheq' - )?.amount ?? '0' - ); - - return new Response(ncheq_to_cheq_fixed(balance + rewards + delegated)); - } - - let gql_client = new GraphQLClient(GRAPHQL_API); - let bd_api = new BigDipperApi(gql_client); - - let account = await bd_api.get_account(address); - - if (!account) { - throw new Error('Account not found'); - } - - return new Response(ncheq_to_cheq_fixed(total_balance_ncheq(account))); + let account_balance_infos = await get_account_balance_infos_from_node_api( + address!! + ); + return new Response(account_balance_infos?.totalBalance.toString()); } diff --git a/src/handlers/totalSupply.ts b/src/handlers/totalSupply.ts index 2d7a6749..8b15f287 100644 --- a/src/handlers/totalSupply.ts +++ b/src/handlers/totalSupply.ts @@ -1,10 +1,10 @@ -import { NodeApi } from "../api/nodeApi"; -import { Request } from "itty-router"; -import { ncheq_to_cheq_fixed } from "../helpers/currency"; +import { Request } from 'itty-router'; +import { NodeApi } from '../api/nodeApi'; +import { ncheq_to_cheq_fixed } from '../helpers/currency'; export async function handler(request: Request): Promise { - let nodeApi = new NodeApi(REST_API); - let totalSupply = await nodeApi.bank_get_total_supply_ncheq(); + let nodeApi = new NodeApi(REST_API); + let totalSupply = await nodeApi.bank_get_total_supply_ncheq(); - return new Response(ncheq_to_cheq_fixed(totalSupply)); + return new Response(ncheq_to_cheq_fixed(totalSupply)); } diff --git a/src/handlers/vestedBalance.ts b/src/handlers/vestedBalance.ts index 14d08331..028eb97c 100644 --- a/src/handlers/vestedBalance.ts +++ b/src/handlers/vestedBalance.ts @@ -1,24 +1,29 @@ -import { Request } from "itty-router"; -import { is_vesting_account_type, validate_cheqd_address } from "../helpers/validate"; -import { NodeApi } from "../api/nodeApi"; -import { calculate_vested_coins } from "../helpers/vesting"; -import { ncheq_to_cheq_fixed } from "../helpers/currency"; +import { Request } from 'itty-router'; +import { + is_vesting_account_type, + validate_cheqd_address, +} from '../helpers/validate'; +import { NodeApi } from '../api/nodeApi'; +import { calculate_vested_coins, estimatedVesting } from '../helpers/vesting'; +import { ncheq_to_cheq_fixed } from '../helpers/currency'; export async function handler(request: Request): Promise { - const address = request.params?.['address']; + const address = request.params?.['address']; - if (!address || !validate_cheqd_address(address)) { - throw new Error("No address specified or wrong address format."); - } + if (!address || !validate_cheqd_address(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); + let api = new NodeApi(REST_API); + const account = await api.auth_get_account(address); - if (!is_vesting_account_type(account["@type"])) { - throw new Error(`Only vesting accounts are supported. Accounts type '${account["@type"]}'.`) - } + if (!is_vesting_account_type(account['@type'])) { + throw new Error( + `Only vesting accounts are supported. Accounts type '${account['@type']}'.` + ); + } - let vested_coins = calculate_vested_coins(account); + let vested_coins = estimatedVesting(account)?.vested; - return new Response(ncheq_to_cheq_fixed(vested_coins)); + return new Response(ncheq_to_cheq_fixed(vested_coins!!)); } diff --git a/src/handlers/vestingBalance.ts b/src/handlers/vestingBalance.ts index 3e085d3f..10d6ff3f 100644 --- a/src/handlers/vestingBalance.ts +++ b/src/handlers/vestingBalance.ts @@ -1,24 +1,29 @@ -import { Request } from "itty-router"; -import { is_vesting_account_type, validate_cheqd_address } from "../helpers/validate"; -import { NodeApi } from "../api/nodeApi"; -import { calculate_vesting_coins } from "../helpers/vesting"; -import { ncheq_to_cheq_fixed } from "../helpers/currency"; +import { Request } from 'itty-router'; +import { + is_vesting_account_type, + validate_cheqd_address, +} from '../helpers/validate'; +import { NodeApi } from '../api/nodeApi'; +import { calculate_vesting_coins, estimatedVesting } from '../helpers/vesting'; +import { ncheq_to_cheq_fixed } from '../helpers/currency'; export async function handler(request: Request): Promise { - const address = request.params?.['address']; + const address = request.params?.['address']; - if (!address || !validate_cheqd_address(address)) { - throw new Error("No address specified or wrong address format."); - } + if (!address || !validate_cheqd_address(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) + let api = new NodeApi(REST_API); + const account = await api.auth_get_account(address); - if (!is_vesting_account_type(account["@type"])) { - throw new Error(`Only vesting accounts are supported. Accounts type '${account["@type"]}'.`) - } + if (!is_vesting_account_type(account['@type'])) { + throw new Error( + `Only vesting accounts are supported. Accounts type '${account['@type']}'.` + ); + } - let vestingCoins = calculate_vesting_coins(account); + let vestingCoins = estimatedVesting(account)?.vesting; - return new Response(ncheq_to_cheq_fixed(vestingCoins)); + return new Response(ncheq_to_cheq_fixed(vestingCoins!!)); } diff --git a/src/handlers/webhookTriggers.ts b/src/handlers/webhookTriggers.ts index f674601e..4adc9633 100644 --- a/src/handlers/webhookTriggers.ts +++ b/src/handlers/webhookTriggers.ts @@ -1,8 +1,10 @@ +import { updateGroupBalances } from '../helpers/balanceGroup'; import { filterArbitrageOpportunities } from './arbitrageOpportunities'; export async function webhookTriggers(event: Event) { console.log('Triggering webhook...'); await sendPriceDiscrepancies(); + await updateGroupBalances(getRandomGroup()); } export async function sendPriceDiscrepancies() { @@ -29,3 +31,9 @@ export async function sendPriceDiscrepancies() { } } } + +function getRandomGroup(): number { + let min = 1; + let max = Math.floor(CIRCULATING_SUPPLY_GROUPS); + return Math.floor(Math.random() * (max - min + 1)) + min; +} diff --git a/src/helpers/balance.ts b/src/helpers/balance.ts index 454459e9..844c4f36 100644 --- a/src/helpers/balance.ts +++ b/src/helpers/balance.ts @@ -1,37 +1,104 @@ -import { NodeApi } from "../api/nodeApi"; -import { GraphQLClient } from "./graphql"; -import { BigDipperApi } from "../api/bigDipperApi"; -import { total_balance_ncheq } from "./node"; -import { Account } from "../types/bigDipper"; - -export async function updateCachedBalance(node_api: NodeApi, addr: string, grpN: number): Promise { - const gql_client = new GraphQLClient(GRAPHQL_API); - const bd_api = new BigDipperApi(gql_client); - const account = await bd_api.get_account(addr); - - if (!account) { - throw new Error(`Account not found for address "${addr}"`) +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'); + } + + 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, + }; +} - try { - const cachedAccount = await CIRCULATING_SUPPLY_WATCHLIST.get(`grp_${grpN}:${addr}`, { type: "json" }) +export async function get_account_balance_infos_from_node_api( + address: string +): Promise { + const node_api = new NodeApi(REST_API); + const available_balance = await node_api.bank_get_account_balances(address); - // if (cachedAccount !== undefined) { - console.log(`account "${addr}" found in cache: ${JSON.stringify(cachedAccount)}`) + let available_balance_in_ncheq = 0; + if (available_balance.length > 0) { + available_balance_in_ncheq = Number(available_balance[0]?.amount); + } - const totalBalance = total_balance_ncheq(account); - const data = JSON.stringify({ totalBalance: totalBalance }); + const reward_balance_in_ncheq = await node_api.distribution_get_total_rewards( + 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 CIRCULATING_SUPPLY_WATCHLIST.put(`grp_${grpN}:${addr}`, data) + 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) + ); - console.log(`account "${addr}" balance updated. (${data})`) + return { + totalBalance: Number( + ncheq_to_cheq_fixed( + 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)), + timeUpdated: new Date().toUTCString(), + }; +} - return account; - // } +export async function updateCachedBalance(addr: string, grpN: number) { + try { + const account_balance_infos = await get_account_balance_infos_from_node_api( + addr + ); - return account; - } catch (e: any) { - console.error(new Map(e)) - return null; - } + const data = JSON.stringify(account_balance_infos); + + await CIRCULATING_SUPPLY_WATCHLIST.put(`grp_${grpN}:${addr}`, data); + + console.log(`account "${addr}" balance updated. (${data})`); + } catch (e: any) { + console.log(`error updateCachedBalance: ${e}`); + } } diff --git a/src/helpers/balanceGroup.ts b/src/helpers/balanceGroup.ts index ce346564..ed71da2b 100644 --- a/src/helpers/balanceGroup.ts +++ b/src/helpers/balanceGroup.ts @@ -1,36 +1,42 @@ -import { updateCachedBalance } from "./balance"; -import { NodeApi } from "../api/nodeApi"; -import { Account } from "../types/bigDipper"; - -export async function updateGroupBalances(group: number, event: Event) { - let node_api = new NodeApi(REST_API); - let balances: { account: Account } [] = []; - - const cached = await CIRCULATING_SUPPLY_WATCHLIST.list({ prefix: `grp_${group}:` }); - - console.log(`found ${cached.keys.length} cached accounts`) - - for (const key of cached.keys) { - const parts = key.name.split(':') - let addr = parts[1] - let grpN = Number(parts[0].split("_")[1]) - - if (key.name.includes("delayed:")) { - addr = parts[2] - } - - 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(node_api, addr, grpN) +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, + }; +} - if (account !== null) { - console.log(`updating account (grp_${grpN}:${addr}) balance (${JSON.stringify(account)})`) - balances.push({ account: account }) - } - } +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 + )})` + ); + } } - - return balances + } } diff --git a/src/helpers/node.ts b/src/helpers/node.ts index 5419edd5..60a0c854 100644 --- a/src/helpers/node.ts +++ b/src/helpers/node.ts @@ -1,5 +1,6 @@ +import { NodeApi } from '../api/nodeApi'; import { Account } from '../types/bigDipper'; -import { Coin } from '../types/node'; +import { Coin, DelegationsResponse, UnbondingResponse } from '../types/node'; export function total_balance_ncheq(account: Account): number { let balance = Number( @@ -36,3 +37,70 @@ export function total_balance_ncheq(account: Account): number { 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/vesting.ts b/src/helpers/vesting.ts index f9c382c0..77dc28ed 100644 --- a/src/helpers/vesting.ts +++ b/src/helpers/vesting.ts @@ -1,24 +1,84 @@ -import { Account } from "../types/node"; +import { Account } from '../types/node'; +import { + is_continuous_vesting_account_type, + is_delayed_vesting_account_type, +} 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 + 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 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 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); + const ratio = Number(time_elapsed / time_vested); - return ratio * Number(account.base_vesting_account.original_vesting[0].amount); + 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); + 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) { + if (!t) { + t = new Date(); + } + + if (is_continuous_vesting_account_type(account?.['@type'])) { + const startsAt = account.start_time; + const endsAt = account.base_vesting_account.end_time; + + const totalCoins = Number( + account.base_vesting_account.original_vesting[0]?.amount + ); + + const elapsed = t.getTime() - new Date(startsAt * 1000).getTime(); + const delta = + new Date(endsAt * 1000).getTime() - new Date(startsAt * 1000).getTime(); + + const doneRatio = Math.min(1.0, Math.max(0, elapsed / delta)); + const vested = Math.ceil(Number(totalCoins) * doneRatio); + const vesting = Math.ceil(Number(totalCoins) * (1.0 - doneRatio)); + + return { + vested, + vesting, + }; + } + if (is_delayed_vesting_account_type(account?.['@type'])) { + const endsAt = account.base_vesting_account.end_time; + + const orginalVesting = 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)); + + return { + vested, + vesting, + }; + } } diff --git a/src/types/node.ts b/src/types/node.ts index 16faa481..f1b0e440 100644 --- a/src/types/node.ts +++ b/src/types/node.ts @@ -76,3 +76,53 @@ export interface TotalStakedCoinsResponse { } ]; } + +export interface AccountBalanceInfos { + totalBalance: number; + availableBalance: number; + rewards: number; + delegated: number; + unbonding: number; + timeUpdated: string; +} + +export interface DelegationsResponse { + delegation_responses: [ + { + delegation: { + delegator_address: string; + validator_address: string; + shares: string; + }; + balance: { + denom: string; + amount: string; + }; + } + ]; + pagination: { + next_key: string; + total: string; + }; +} + +export interface UnbondingResponse { + unbonding_responses: [ + { + delegator_address: string; + validator_address: string; + entries: [ + { + creation_height: string; + completion_time: string; + initial_balance: string; + balance: string; + } + ]; + } + ]; + pagination: { + next_key: string; + total: string; + }; +} diff --git a/wrangler.toml b/wrangler.toml index 000e6811..33e7fba3 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -45,6 +45,8 @@ TOKEN_EXPONENT = "9" REST_API = "https://api.cheqd.net" # 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 MARKET_MONITORING_API = "https://market-monitoring.cheqd.net" @@ -86,7 +88,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", MARKET_MONITORING_API = "https://market-monitoring-staging.cheqd.net"} +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"} # The necessary secrets are: @@ -96,14 +98,14 @@ vars = { ENVIRONMENT = "staging", TOKEN_EXPONENT = "9", REST_API = "https://api. # KV Namespaces accessible from the Worker # Details: https://developers.cloudflare.com/workers/learning/how-kv-works # @default `[]` -[[env.staging.kv_namespaces]] -binding = "CIRCULATING_SUPPLY_WATCHLIST" -id = "afb34dd4b9374cc4a7ae7c3aaf6e5ce2" -preview_id = "1e1032cbf6854d88b12317da8a792928" + +kv_namespaces = [ + { binding = "CIRCULATING_SUPPLY_WATCHLIST", id = "86891d184f7f40ee9b403a94a76fcdab" } +] # Cron triggers for staging worker [env.staging.triggers] -crons = ["0 9 * * *"] +crons = ["0 * * * *"] ############################################################### ### OPTIONAL: Build Configuration ###