diff --git a/.gitignore b/.gitignore index ff75592..83a12c8 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ typings/ dist build lib +out # Gatsby files .cache/ diff --git a/public/swagger.json b/public/swagger.json index 6c1545f..c9cf7ac 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -407,7 +407,35 @@ "tags": [ "Dapps Staking" ], - "description": "Retrieves list of dapps registered for dapps staking", + "description": "Retrieves list of dapps (full model) registered for dapps staking", + "parameters": [ + { + "name": "network", + "in": "path", + "required": true, + "type": "string", + "description": "The network name. Supported networks: astar, shiden, shibuya, rocstar, development", + "enum": [ + "astar", + "shiden", + "shibuya", + "rocstar" + ] + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/{network}/dapps-staking/dappssimple": { + "get": { + "tags": [ + "Dapps Staking" + ], + "description": "Retrieves list of dapps (basic info) registered for dapps staking", "parameters": [ { "name": "network", @@ -589,6 +617,110 @@ } } }, + "/api/v3/{network}/dapps-staking/stats/dapp/{contractAddress}": { + "get": { + "tags": [ + "Dapps Staking" + ], + "description": "Retrieves raw stats of dapps staking events with types for a given smart contract address.", + "parameters": [ + { + "name": "network", + "in": "path", + "required": true, + "type": "string", + "description": "The network name. Supported networks: astar", + "enum": [ + "astar" + ] + }, + { + "name": "contractAddress", + "in": "path", + "required": true, + "type": "string", + "description": "Smart Contract address to get stats for" + }, + { + "name": "startDate", + "in": "query", + "description": "Start date for filtering the staking events (inclusive). Format: YYYY-MM-DD", + "required": false, + "type": "string", + "format": "date" + }, + { + "name": "endDate", + "in": "query", + "description": "End date for filtering the staking events (inclusive). Format: YYYY-MM-DD", + "required": false, + "type": "string", + "format": "date" + }, + { + "name": "limit", + "in": "query", + "description": "Number of records to retrieve per page. Defaults to 100 if not provided.", + "required": false, + "type": "integer", + "format": "int32", + "default": 10 + }, + { + "name": "offset", + "in": "query", + "description": "Number of records to skip for pagination. Defaults to 0 if not provided.", + "required": false, + "type": "integer", + "format": "int32", + "default": 0 + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v3/{network}/dapps-staking/stats/aggregated/{period}": { + "get": { + "tags": [ + "Dapps Staking" + ], + "description": "Retrieves aggregated stats of dapps staking events for a given period.", + "parameters": [ + { + "name": "network", + "in": "path", + "required": true, + "type": "string", + "description": "The network name. Supported networks: astar", + "enum": [ + "astar" + ] + }, + { + "name": "period", + "in": "path", + "required": true, + "type": "string", + "description": "The period type. Supported values: 7 days 30 days, 90 days, 1 year", + "enum": [ + "7 days", + "30 days", + "90 days", + "1 year" + ] + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v1/{network}/node/tx-perblock/total": { "get": { "tags": [ diff --git a/src/container.ts b/src/container.ts index 4478df8..0928800 100644 --- a/src/container.ts +++ b/src/container.ts @@ -25,6 +25,7 @@ import { DiaDataPriceProvider } from './services/DiaDataPriceProvider'; import { CoinGeckoPriceProvider } from './services/CoinGeckoPriceProvider'; import { PriceProviderWithFailover } from './services/PriceProviderWithFailover'; import { DappsStakingService2 } from './services/DappsStakingService2'; +import { DappsStakingEvents, IDappsStakingEvents } from './services/DappsStakingEvents'; import { IMonthlyActiveWalletsService, MonthlyActiveWalletsService } from './services/MonthlyActiveWalletsService'; import { MonthlyActiveWalletsController } from './controllers/MonthlyActiveWalletsController'; import { DappsStakingStatsService, IDappsStakingStatsService } from './services/DappsStakingStatsService'; @@ -71,6 +72,8 @@ container.bind(ContainerTypes.ApiFactory).to(ApiFactory).inSingleto // services registration container.bind(ContainerTypes.StatsService).to(StatsService).inSingletonScope(); +container.bind(ContainerTypes.DappsStakingEvents).to(DappsStakingEvents).inSingletonScope(); + container .bind(ContainerTypes.DappsStakingService) .to(DappsStakingService2) diff --git a/src/containertypes.ts b/src/containertypes.ts index 0bc945b..d699d1c 100644 --- a/src/containertypes.ts +++ b/src/containertypes.ts @@ -3,6 +3,7 @@ export const ContainerTypes = { StatsService: 'StatsService', Api: 'Api', ApiFactory: 'ApiFactory', + DappsStakingEvents: 'DappsStakingEvents', DappsStakingService: 'DappsStakingService', StatsIndexerService: 'StatsIndexerService', FirebaseService: 'FirebaseService', diff --git a/src/controllers/DappsStakingController.ts b/src/controllers/DappsStakingController.ts index dff175a..707b4d6 100644 --- a/src/controllers/DappsStakingController.ts +++ b/src/controllers/DappsStakingController.ts @@ -14,6 +14,7 @@ import { IStatsIndexerService } from '../services/StatsIndexerService'; import { ControllerBase } from './ControllerBase'; import { IControllerBase } from './IControllerBase'; import { IGiantSquidService } from '../services/GiantSquidService'; +import { IDappsStakingEvents } from '../services/DappsStakingEvents'; @injectable() export class DappsStakingController extends ControllerBase implements IControllerBase { @@ -23,6 +24,7 @@ export class DappsStakingController extends ControllerBase implements IControlle @inject(ContainerTypes.DappsStakingStatsService) private _statsService: IDappsStakingStatsService, @inject(ContainerTypes.DappRadarService) private _dappRadarService: IDappRadarService, @inject(ContainerTypes.GiantSquidService) private _giantSquidService: IGiantSquidService, + @inject(ContainerTypes.DappsStakingEvents) private _dappsStakingEvents: IDappsStakingEvents, ) { super(); } @@ -334,6 +336,106 @@ export class DappsStakingController extends ControllerBase implements IControlle }, ); + app.route('/api/v3/:network/dapps-staking/stats/dapp/:contractAddress').get( + async (req: Request, res: Response) => { + /* + #swagger.description = 'Retrieves raw stats of dapps staking events with types for a given smart contract address.' + #swagger.tags = ['Dapps Staking'] + #swagger.parameters['network'] = { + in: 'path', + description: 'The network name. Supported networks: astar', + required: true, + enum: ['astar'] + } + #swagger.parameters['contractAddress'] = { + in: 'path', + description: 'Smart Contract address to get stats for', + required: true + } + #swagger.parameters['startDate'] = { + in: 'query', + description: 'Start date for filtering the staking events (inclusive). Format: YYYY-MM-DD', + required: true, + type: 'string', + format: 'date' + } + #swagger.parameters['endDate'] = { + in: 'query', + description: 'End date for filtering the staking events (inclusive). Format: YYYY-MM-DD', + required: true, + type: 'string', + format: 'date' + } + #swagger.parameters['limit'] = { + in: 'query', + description: 'Number of records to retrieve per page. Defaults to 100 if not provided.', + required: false, + type: 'integer', + format: 'int32', + default: 100 + } + #swagger.parameters['offset'] = { + in: 'query', + description: 'Number of records to skip for pagination. Defaults to 0 if not provided.', + required: false, + type: 'integer', + format: 'int32', + default: 0 + } + */ + const startDate = req.query.startDate; + const endDate = req.query.endDate; + const limit = parseInt(req.query.limit as string, 10) || 100; // Default to 100 if not provided + const offset = parseInt(req.query.offset as string, 10) || 0; // Default to 0 if not provided + + try { + res.json( + await this._dappsStakingEvents.getStakingEvents( + req.params.network as NetworkType, + req.params.contractAddress, + startDate as string, + endDate as string, + limit as number, + offset as number, + ), + ); + } catch (err) { + this.handleError(res, err as Error); + } + }, + ); + + app.route('/api/v3/:network/dapps-staking/stats/aggregated/:period').get( + async (req: Request, res: Response) => { + /* + #swagger.description = 'Retrieves aggregated stats of dapps staking events for a given period.' + #swagger.tags = ['Dapps Staking'] + #swagger.parameters['network'] = { + in: 'path', + description: 'The network name. Supported networks: astar', + required: true, + enum: ['astar'] + } + #swagger.parameters['period'] = { + in: 'path', + description: 'The period type. Supported values: 7 days 30 days, 90 days, 1 year', + required: true, + enum: ['7 days', '30 days', '90 days', '1 year'] + } + */ + try { + res.json( + await this._dappsStakingEvents.getAggregatedData( + req.params.network as NetworkType, + req.params.period as PeriodType, + ), + ); + } catch (err) { + this.handleError(res, err as Error); + } + }, + ); + app.route('/api/v1/:network/dapps-staking/stats/transactions').get(async (req: Request, res: Response) => { /* #swagger.ignore = true diff --git a/src/services/DappStaking/ResponseData.ts b/src/services/DappStaking/ResponseData.ts new file mode 100644 index 0000000..0366e34 --- /dev/null +++ b/src/services/DappStaking/ResponseData.ts @@ -0,0 +1,36 @@ +export interface DappStakingEventResponse { + data: { + stakingEvents: DappStakingEventData[]; + }; +} + +export interface DappStakingAggregatedResponse { + data: { + groupedStakingEvents: DappStakingAggregatedData[]; + }; +} + +export enum UserTransactionType { + BondAndStake = 'BondAndStake', + UnbondAndUnstake = 'UnbondAndUnstake', + Withdraw = 'Withdraw', + WithdrawFromUnregistered = 'WithdrawFromUnregistered', + NominationTransfer = 'NominationTransfer', +} + +export interface DappStakingEventData { + id: string; + userAddress: string; + transaction: UserTransactionType; + contractAddress: string; + amount: bigint; + timestamp: bigint; + blockNumber: bigint; +} + +export interface DappStakingAggregatedData { + id: string; + transaction: UserTransactionType; + amount: bigint; + timestamp: bigint; +} diff --git a/src/services/DappStaking/index.ts b/src/services/DappStaking/index.ts new file mode 100644 index 0000000..9bf91f4 --- /dev/null +++ b/src/services/DappStaking/index.ts @@ -0,0 +1 @@ +export * from './ResponseData'; diff --git a/src/services/DappsStakingEvents.ts b/src/services/DappsStakingEvents.ts new file mode 100644 index 0000000..ae4aada --- /dev/null +++ b/src/services/DappsStakingEvents.ts @@ -0,0 +1,106 @@ +import { injectable } from 'inversify'; +import axios from 'axios'; +import { NetworkType } from '../networks'; +import { Guard } from '../guard'; +import { PeriodType, ServiceBase } from './ServiceBase'; +import { + DappStakingEventData, + DappStakingEventResponse, + DappStakingAggregatedData, + DappStakingAggregatedResponse, +} from './DappStaking/ResponseData'; + +export interface IDappsStakingEvents { + getStakingEvents( + network: NetworkType, + address: string, + startDate: string, + endDate: string, + limit?: number, + offset?: number, + ): Promise; + getAggregatedData(network: NetworkType, period: PeriodType): Promise; +} + +@injectable() +export class DappsStakingEvents extends ServiceBase implements IDappsStakingEvents { + public async getStakingEvents( + network: NetworkType, + contractAddress: string, + startDate: string, + endDate: string, + limit?: number, + offset?: number, + ): Promise { + Guard.ThrowIfUndefined('network', network); + Guard.ThrowIfUndefined('contractAddress', contractAddress); + Guard.ThrowIfUndefined('startDate', startDate); + Guard.ThrowIfUndefined('endDate', endDate); + + if (network !== 'astar') { + return []; + } + + const start = new Date(startDate); + const end = new Date(endDate); + + const query = `query MyQuery { + stakingEvents(where: { + contractAddress_eq: "${contractAddress}", + timestamp_gte: "${start.getTime()}", + timestamp_lte: "${end.getTime()}" + }, + orderBy: blockNumber_DESC, + limit: ${limit}, + offset: ${offset}) { + amount + blockNumber + contractAddress + id + timestamp + transaction + userAddress + } + }`; + + const result = await axios.post(this.getApiUrl(network), { + operationName: 'MyQuery', + query, + }); + + return result.data.data.stakingEvents; + } + + public async getAggregatedData(network: NetworkType, period: PeriodType): Promise { + Guard.ThrowIfUndefined('network', network); + + if (network !== 'astar') { + return []; + } + + const range = this.getDateRange(period); + + const query = `query MyQuery { + groupedStakingEvents(where: { + timestamp_gte: "${range.start.getTime()}", + timestamp_lte: "${range.end.getTime()}" + }, orderBy: timestamp_DESC) { + amount + id + timestamp + transaction + } + }`; + + const result = await axios.post(this.getApiUrl(network), { + operationName: 'MyQuery', + query, + }); + + return result.data.data.groupedStakingEvents; + } + + private getApiUrl(network: NetworkType): string { + return `https://squid.subsquid.io/dapps-staking-indexer/graphql`; + } +}