diff --git a/public/swagger.json b/public/swagger.json index 892ae83..61139df 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -81,6 +81,32 @@ } } }, + "/api/v1/{network}/token/extendedstats": { + "get": { + "tags": [ + "Token" + ], + "description": "Retrieves token statistics for a given network.", + "parameters": [ + { + "name": "network", + "in": "path", + "required": true, + "type": "string", + "description": "The network name. Supported networks: astar, shiden, shibuya, rocstar", + "enum": [ + "astar", + "shiden" + ] + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/{network}/token/circulation": { "get": { "tags": [ diff --git a/src/client/BaseApi.ts b/src/client/BaseApi.ts index fb08e80..dfd2853 100644 --- a/src/client/BaseApi.ts +++ b/src/client/BaseApi.ts @@ -302,7 +302,7 @@ export class BaseApi implements IAstarApi { return await localApi.isReadyOrError.then( (api: ApiPromise) => { - // Connection suceed + // Connection succeed this._api = api; return api; }, diff --git a/src/controllers/TokenStatsController.ts b/src/controllers/TokenStatsController.ts index f108af0..6f96d53 100644 --- a/src/controllers/TokenStatsController.ts +++ b/src/controllers/TokenStatsController.ts @@ -92,6 +92,27 @@ export class TokenStatsController extends ControllerBase implements IControllerB } }); + /** + * @description Token extended statistics route v1. + */ + app.route('/api/v1/:network/token/extendedstats').get(async (req: Request, res: Response) => { + /* + #swagger.description = 'Retrieves token statistics for a given network.' + #swagger.tags = ['Token'] + #swagger.parameters['network'] = { + in: 'path', + description: 'The network name. Supported networks: astar, shiden, shibuya, rocstar', + required: true, + enum: ['astar', 'shiden'] + } + */ + try { + res.json(await this._statsService.getTokenStatsExtended(req.params.network as NetworkType)); + } catch (err) { + this.handleError(res, err as Error); + } + }); + /** * @description Test route */ diff --git a/src/services/StatsService.ts b/src/services/StatsService.ts index befa1cd..219d2da 100644 --- a/src/services/StatsService.ts +++ b/src/services/StatsService.ts @@ -1,4 +1,3 @@ -import { PalletBalancesAccountData } from '@polkadot/types/lookup'; import { formatBalance, BN } from '@polkadot/util'; import { injectable, inject } from 'inversify'; import { IApiFactory } from '../client/ApiFactory'; @@ -10,6 +9,7 @@ import { AccountData } from '../models/AccountData'; import { Guard } from '../guard'; import { DappStakingV3IndexerBase } from './DappStakingV3IndexerBase'; import axios from 'axios'; +import { IPriceProvider } from './IPriceProvider'; export type TotalSupply = { block: number; @@ -17,8 +17,21 @@ export type TotalSupply = { balance: bigint; }; +export type ExtendedTokenStats = { + symbol: string; + currencyCode: string; + marketCap: number; + circulatingSupply: number; + maxSupply: number; + provider: string; + lastUpdatedTimestamp: number; + accTradePrice24h: number | null; + price: number; +}; + export interface IStatsService { getTokenStats(network: NetworkType): Promise; + getTokenStatsExtended(network: NetworkType): Promise; getTotalSupply(network: NetworkType): Promise; getTotalIssuanceHistory(network: NetworkType): Promise; } @@ -28,7 +41,10 @@ export interface IStatsService { * Token statistics calculation service. */ export class StatsService extends DappStakingV3IndexerBase implements IStatsService { - constructor(@inject(ContainerTypes.ApiFactory) private _apiFactory: IApiFactory) { + constructor( + @inject(ContainerTypes.ApiFactory) private _apiFactory: IApiFactory, + @inject(ContainerTypes.PriceProviderWithFailover) private _priceProvider: IPriceProvider, + ) { super(); } @@ -62,6 +78,54 @@ export class StatsService extends DappStakingV3IndexerBase implements IStatsServ } } + /** + * Calculates token circulation supply by substracting sum of all token holder accounts + * not in circulation from total token supply. + * @param network NetworkType (astar or shiden) to calculate token supply for. + * @returns Token statistics including total supply and circulating supply. + */ + public async getTokenStatsExtended(network: NetworkType): Promise { + if (network !== 'astar' && network !== 'shiden') { + throw new Error(`This method is not supported for the network ${network}`); + } + + try { + const currency = 'usd'; + + const api = this._apiFactory.getApiInstance(network); + const apiClient = await api.getApiPromise(); + + const chainTokens = apiClient.registry.chainTokens; + const tokenSymbol = chainTokens[0]; + + const [chainDecimals, totalSupply, balancesToExclude, price] = await Promise.all([ + api.getChainDecimals(), + api.getTotalSupply(), + api.getBalances(addressesToExclude), + this._priceProvider.getPrice(tokenSymbol.toLowerCase(), currency), + ]); + + const totalBalancesToExclude = this.getTotalBalanceToExclude(balancesToExclude); + const circulatingSupplyWei = totalSupply.sub(totalBalancesToExclude); + const circulatingSupply = this.formatBalance(circulatingSupplyWei, chainDecimals); + + return { + symbol: tokenSymbol, + currencyCode: currency.toUpperCase(), + price, + marketCap: circulatingSupply * price, + accTradePrice24h: null, + circulatingSupply, + maxSupply: this.formatBalance(totalSupply, chainDecimals), + provider: 'Stake Technologies Pte Ltd', + lastUpdatedTimestamp: Date.now(), + }; + } catch (e) { + console.error(e); + throw new Error('Unable to fetch token statistics from a node.'); + } + } + public async getTotalSupply(network: NetworkType): Promise { Guard.ThrowIfUndefined(network, 'network'); this.GuardNetwork(network);