diff --git a/src/container.ts b/src/container.ts index 4344402..0307006 100644 --- a/src/container.ts +++ b/src/container.ts @@ -29,6 +29,17 @@ import { IMonthlyActiveWalletsService, MonthlyActiveWalletsService } from './ser import { MonthlyActiveWalletsController } from './controllers/MonthlyActiveWalletsController'; import { DappsStakingStatsService, IDappsStakingStatsService } from './services/DappsStakingStatsService'; import { IDappRadarService, DappRadarService } from './services/DappRadarService'; +import { GiantSquidService, IGiantSquidService } from './services/GiantSquidService'; +import { + BondAndStakeParser, + CallNameMapping, + CallParser, + ICallParser, + NominationTransferParser, + UnbondAndUnstakeParser, + WithdrawFromUnbondedParser, + WithdrawParser, +} from './services/GiantSquid'; const container = new Container(); @@ -103,6 +114,17 @@ container .to(DappsStakingStatsService) .inRequestScope(); container.bind(ContainerTypes.DappRadarService).to(DappRadarService).inRequestScope(); +container.bind(ContainerTypes.GiantSquidService).to(GiantSquidService).inRequestScope(); + +// Giant squid parsers +container.bind(CallNameMapping.bond_and_stake).to(BondAndStakeParser).inSingletonScope(); +container.bind(CallNameMapping.unbond_and_unstake).to(UnbondAndUnstakeParser).inSingletonScope(); +container.bind(CallNameMapping.nomination_transfer).to(NominationTransferParser).inSingletonScope(); +container.bind(CallNameMapping.withdraw_unbonded).to(WithdrawParser).inSingletonScope(); +container + .bind(CallNameMapping.withdraw_from_unregistered) + .to(WithdrawFromUnbondedParser) + .inSingletonScope(); // controllers registration container.bind(ContainerTypes.Controller).to(TokenStatsController); diff --git a/src/containertypes.ts b/src/containertypes.ts index 590d446..b4948f5 100644 --- a/src/containertypes.ts +++ b/src/containertypes.ts @@ -13,4 +13,5 @@ export const ContainerTypes = { MonthlyActiveWalletsService: 'MonthlyActiveWalletsService', DappsStakingStatsService: 'DappsStakingStatsService', DappRadarService: 'DappRadarService', + GiantSquidService: 'GiantSquidService', }; diff --git a/src/controllers/DappsStakingController.ts b/src/controllers/DappsStakingController.ts index 22b96d3..e27f32c 100644 --- a/src/controllers/DappsStakingController.ts +++ b/src/controllers/DappsStakingController.ts @@ -13,6 +13,7 @@ import { PeriodType, PeriodTypeEra } from '../services/ServiceBase'; import { IStatsIndexerService } from '../services/StatsIndexerService'; import { ControllerBase } from './ControllerBase'; import { IControllerBase } from './IControllerBase'; +import { IGiantSquidService } from '../services/GiantSquidService'; @injectable() export class DappsStakingController extends ControllerBase implements IControllerBase { @@ -21,6 +22,7 @@ export class DappsStakingController extends ControllerBase implements IControlle @inject(ContainerTypes.FirebaseService) private _firebaseService: IFirebaseService, @inject(ContainerTypes.DappsStakingStatsService) private _statsService: IDappsStakingStatsService, @inject(ContainerTypes.DappRadarService) private _dappRadarService: IDappRadarService, + @inject(ContainerTypes.GiantSquidService) private _giantSquidService: IGiantSquidService, ) { super(); } @@ -299,8 +301,9 @@ export class DappsStakingController extends ControllerBase implements IControlle enum: ['7 days', '30 days', '90 days', '1 year'] } */ + // this._giantSquidService.getUserCalls(req.params.network as NetworkType, req.params.userAddress, req.params.period as PeriodType); res.json( - await this._statsService.getUserEvents( + await this._giantSquidService.getUserCalls( req.params.network as NetworkType, req.params.userAddress, req.params.period as PeriodType, diff --git a/src/models/DappStaking.ts b/src/models/DappStaking.ts new file mode 100644 index 0000000..22d6ac0 --- /dev/null +++ b/src/models/DappStaking.ts @@ -0,0 +1,8 @@ +export interface UserEvent { + timestamp: number; + contractAddress?: string; + transaction: string; + amount?: string; + transactionHash: string; + transactionSuccess: boolean; +} diff --git a/src/services/DappsStakingStatsService.ts b/src/services/DappsStakingStatsService.ts index 7e2807b..d8daf94 100644 --- a/src/services/DappsStakingStatsService.ts +++ b/src/services/DappsStakingStatsService.ts @@ -4,6 +4,7 @@ import { IApiFactory } from '../client/ApiFactory'; import { ContainerTypes } from '../containertypes'; import { NetworkType } from '../networks'; import { PeriodType, PeriodTypeEra, ServiceBase } from './ServiceBase'; +import { UserEvent } from '../models/DappStaking'; const API_URLS = { astar: 'https://api.subquery.network/sq/bobo-k2/astar-dapp-staking-v2', @@ -24,13 +25,6 @@ interface ContractStatsResponse { }; } -interface UserEvent { - timestamp: number; - contractAddress: number; - transaction: string; - amount: string; -} - interface UserEventsResponse { data: { userTransactions: { diff --git a/src/services/GiantSquid/Call.ts b/src/services/GiantSquid/Call.ts new file mode 100644 index 0000000..3ed118f --- /dev/null +++ b/src/services/GiantSquid/Call.ts @@ -0,0 +1,5 @@ +export class Call { + name: string; + contractAddress?: string; + amount?: bigint; +} diff --git a/src/services/GiantSquid/CallNameMapping.ts b/src/services/GiantSquid/CallNameMapping.ts new file mode 100644 index 0000000..2d64239 --- /dev/null +++ b/src/services/GiantSquid/CallNameMapping.ts @@ -0,0 +1,11 @@ +export interface Map { + [key: string]: string; +} + +export const CallNameMapping: Map = { + bond_and_stake: 'BondAndStake', + unbond_and_unstake: 'UnbondAndUnstake', + nomination_transfer: 'NominationTransfer', + withdraw_unbonded: 'Withdraw', + withdraw_from_unregistered: 'WithdrawFromUnregistered', +}; diff --git a/src/services/GiantSquid/CallParser.ts b/src/services/GiantSquid/CallParser.ts new file mode 100644 index 0000000..74f55de --- /dev/null +++ b/src/services/GiantSquid/CallParser.ts @@ -0,0 +1,71 @@ +import { injectable } from 'inversify'; +import { UserEvent } from '../../models/DappStaking'; +import { DappStakingCallData } from './ResponseData'; +import { CallNameMapping } from './CallNameMapping'; + +export interface ICallParser { + parse(call: DappStakingCallData): UserEvent; +} + +@injectable() +export class CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + return { + timestamp: new Date(call.timestamp).getTime(), + transaction: CallNameMapping[call.callName], + transactionHash: call.extrinsicHash, + transactionSuccess: call.success, + }; + } +} + +@injectable() +export class BondAndStakeParser extends CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + const result = super.parse(call); + result.contractAddress = call.argsStr[1]; + result.amount = call.argsStr[2]; + + return result; + } +} + +@injectable() +export class UnbondAndUnstakeParser extends CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + const result = super.parse(call); + result.contractAddress = call.argsStr[1]; + result.amount = call.argsStr[2]; + + return result; + } +} + +@injectable() +export class NominationTransferParser extends CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + const result = super.parse(call); + result.contractAddress = call.argsStr[3]; + result.amount = call.argsStr[4]; + + return result; + } +} + +@injectable() +export class WithdrawParser extends CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + const result = super.parse(call); + + return result; + } +} + +@injectable() +export class WithdrawFromUnbondedParser extends CallParser implements ICallParser { + public parse(call: DappStakingCallData): UserEvent { + const result = super.parse(call); + + return result; + } +} diff --git a/src/services/GiantSquid/ResponseData.ts b/src/services/GiantSquid/ResponseData.ts new file mode 100644 index 0000000..f9025c1 --- /dev/null +++ b/src/services/GiantSquid/ResponseData.ts @@ -0,0 +1,13 @@ +export interface DappStakingCallResponse { + data: { + calls: DappStakingCallData[]; + }; +} + +export interface DappStakingCallData { + callName: string; + argsStr: string[]; + extrinsicHash: string; + success: boolean; + timestamp: string; +} diff --git a/src/services/GiantSquid/index.ts b/src/services/GiantSquid/index.ts new file mode 100644 index 0000000..5727cf3 --- /dev/null +++ b/src/services/GiantSquid/index.ts @@ -0,0 +1,4 @@ +export * from './CallParser'; +export * from './Call'; +export * from './ResponseData'; +export * from './CallNameMapping'; diff --git a/src/services/GiantSquidService.ts b/src/services/GiantSquidService.ts new file mode 100644 index 0000000..55e41b6 --- /dev/null +++ b/src/services/GiantSquidService.ts @@ -0,0 +1,73 @@ +import { injectable } from 'inversify'; +import axios from 'axios'; +import { NetworkType } from '../networks'; +import { Guard } from '../guard'; +import { decodeAddress } from '@polkadot/util-crypto'; +import { PeriodType, ServiceBase } from './ServiceBase'; +import { UserEvent } from '../models/DappStaking'; +import { DappStakingCallData, DappStakingCallResponse } from './GiantSquid/ResponseData'; +import container from '../container'; +import { ICallParser } from './GiantSquid/CallParser'; +import { CallNameMapping } from './GiantSquid/CallNameMapping'; + +export interface IGiantSquidService { + getUserCalls(network: NetworkType, address: string, period: PeriodType): Promise; +} + +// Handles calls to SubSquid giant squid indexer. +@injectable() +export class GiantSquidService extends ServiceBase implements IGiantSquidService { + public async getUserCalls(network: NetworkType, address: string, period: PeriodType): Promise { + Guard.ThrowIfUndefined('network', network); + Guard.ThrowIfUndefined('address', address); + + if (network !== 'shiden' && network !== 'astar' && network !== 'shibuya') { + return []; + } + + const privateKey = `0x${Buffer.from(decodeAddress(address)).toString('hex')}`; + const range = this.getDateRange(period); + + const query = `query MyQuery { + calls(where: { + palletName_eq: "DappsStaking", + callerPublicKey_eq: "${privateKey}", + timestamp_gte: "${range.start.toISOString()}", + timestamp_lte: "${range.end.toISOString()}", + callName_not_contains: "claim" + }, orderBy: block_id_DESC) { + callName + argsStr + extrinsicHash + success + timestamp + } + }`; + + const result = await axios.post(this.getApiUrl(network), { + operationName: 'MyQuery', + query, + }); + + return this.parseUserCalls(result.data.data.calls); + } + + private getApiUrl(network: NetworkType): string { + return `https://squid.subsquid.io/gs-explorer-${network}/graphql`; + } + + private parseUserCalls(calls: DappStakingCallData[]): UserEvent[] { + const result: UserEvent[] = []; + + for (const call of calls) { + if (CallNameMapping[call.callName]) { + const parser = container.get(CallNameMapping[call.callName]); + result.push(parser.parse(call)); + } else { + // Call is not supported. Do nothing. Currently only calls defined in CallNameMapping are supported. + } + } + + return result; + } +} diff --git a/tests/services/StatsService.test.ts b/tests/services/StatsService.test.ts index a0df467..e5264f4 100644 --- a/tests/services/StatsService.test.ts +++ b/tests/services/StatsService.test.ts @@ -29,3 +29,20 @@ describe('getTokenStats', () => { expect(result.totalSupply).toBe(100); }); }); + +describe('getTotalSupply', () => { + let apiFactory: IApiFactory; + + beforeEach(() => { + apiFactory = new ApiFactory(); + apiFactory.getApiInstance = jest.fn().mockReturnValue(new AstarApiMock()); + }); + + it('returns valid total supply', async () => { + const service = new StatsService(apiFactory); + + const result = await service.getTotalSupply('astar'); + + expect(result).toBe(100); + }); +});