From fcf17b6dd634e295b32c5849d90e3c00a8f27bd1 Mon Sep 17 00:00:00 2001 From: Bobo Date: Fri, 25 Aug 2023 11:21:32 +0200 Subject: [PATCH 1/4] Basic Squid service --- src/container.ts | 2 + src/containertypes.ts | 1 + src/controllers/DappsStakingController.ts | 3 ++ src/services/GiantSquidService.ts | 54 +++++++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/services/GiantSquidService.ts diff --git a/src/container.ts b/src/container.ts index 4344402..4b9b03a 100644 --- a/src/container.ts +++ b/src/container.ts @@ -29,6 +29,7 @@ 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'; const container = new Container(); @@ -103,6 +104,7 @@ container .to(DappsStakingStatsService) .inRequestScope(); container.bind(ContainerTypes.DappRadarService).to(DappRadarService).inRequestScope(); +container.bind(ContainerTypes.GiantSquidService).to(GiantSquidService).inRequestScope(); // 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..602cbcd 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,6 +301,7 @@ 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( req.params.network as NetworkType, diff --git a/src/services/GiantSquidService.ts b/src/services/GiantSquidService.ts new file mode 100644 index 0000000..b6a2bc6 --- /dev/null +++ b/src/services/GiantSquidService.ts @@ -0,0 +1,54 @@ +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'; + +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); + + 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 + palletName + argsStr + callerPublicKey + extrinsicHash + success + timestamp + id + } + }`; + + const result = await axios.post(this.getApiUrl(network), { + operationName: 'MyQuery', + query, + }); + console.log(result.data.calls); + + return null; + } + + private getApiUrl(network: NetworkType): string { + return `https://squid.subsquid.io/gs-explorer-${network}/graphql`; + } +} \ No newline at end of file From 0814800950cc4c58e603c8382599204d46b2ccac Mon Sep 17 00:00:00 2001 From: Bobo Date: Fri, 25 Aug 2023 16:59:22 +0200 Subject: [PATCH 2/4] Call parsers implementation --- public/swagger.json | 2 +- src/container.ts | 20 ++++++ src/containertypes.ts | 1 + src/controllers/DappsStakingController.ts | 4 +- src/models/DappStaking.ts | 8 +++ src/services/DappsStakingStatsService.ts | 8 +-- src/services/GiantSquid/Call.ts | 5 ++ src/services/GiantSquid/CallNameMapping.ts | 11 ++++ src/services/GiantSquid/CallParser.ts | 71 ++++++++++++++++++++++ src/services/GiantSquid/ResponseData.ts | 13 ++++ src/services/GiantSquid/index.ts | 4 ++ src/services/GiantSquidService.ts | 69 +++++++++++++-------- 12 files changed, 181 insertions(+), 35 deletions(-) create mode 100644 src/models/DappStaking.ts create mode 100644 src/services/GiantSquid/Call.ts create mode 100644 src/services/GiantSquid/CallNameMapping.ts create mode 100644 src/services/GiantSquid/CallParser.ts create mode 100644 src/services/GiantSquid/ResponseData.ts create mode 100644 src/services/GiantSquid/index.ts diff --git a/public/swagger.json b/public/swagger.json index 74906ec..79e7cde 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -5,7 +5,7 @@ "title": "Astar token statistics API", "description": "Provides Astar networks statistic information." }, - "host": "api.astar.network", + "host": "localhost:3000", "basePath": "/", "schemes": [ "https", diff --git a/src/container.ts b/src/container.ts index 4b9b03a..0307006 100644 --- a/src/container.ts +++ b/src/container.ts @@ -30,6 +30,16 @@ import { MonthlyActiveWalletsController } from './controllers/MonthlyActiveWalle 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(); @@ -106,6 +116,16 @@ container 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); container.bind(ContainerTypes.Controller).to(DappsStakingController); diff --git a/src/containertypes.ts b/src/containertypes.ts index b4948f5..1d3e8ae 100644 --- a/src/containertypes.ts +++ b/src/containertypes.ts @@ -14,4 +14,5 @@ export const ContainerTypes = { DappsStakingStatsService: 'DappsStakingStatsService', DappRadarService: 'DappRadarService', GiantSquidService: 'GiantSquidService', + A: 'A', }; diff --git a/src/controllers/DappsStakingController.ts b/src/controllers/DappsStakingController.ts index 602cbcd..e27f32c 100644 --- a/src/controllers/DappsStakingController.ts +++ b/src/controllers/DappsStakingController.ts @@ -301,9 +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); + // 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 index b6a2bc6..55e41b6 100644 --- a/src/services/GiantSquidService.ts +++ b/src/services/GiantSquidService.ts @@ -4,23 +4,31 @@ 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; + getUserCalls(network: NetworkType, address: string, period: PeriodType): Promise; } - -// Handles calls to SubSquid giant squid indexer. +// 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); + 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 privateKey = `0x${Buffer.from(decodeAddress(address)).toString('hex')}`; + const range = this.getDateRange(period); - const query = `query MyQuery { + const query = `query MyQuery { calls(where: { palletName_eq: "DappsStaking", callerPublicKey_eq: "${privateKey}", @@ -29,26 +37,37 @@ export class GiantSquidService extends ServiceBase implements IGiantSquidService callName_not_contains: "claim" }, orderBy: block_id_DESC) { callName - palletName argsStr - callerPublicKey extrinsicHash success timestamp - id } }`; - const result = await axios.post(this.getApiUrl(network), { - operationName: 'MyQuery', - query, - }); - console.log(result.data.calls); - - return null; - } - - private getApiUrl(network: NetworkType): string { - return `https://squid.subsquid.io/gs-explorer-${network}/graphql`; - } -} \ No newline at end of file + 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; + } +} From 4acbbc569980f8570050cafd5457ba53ccd1bf5b Mon Sep 17 00:00:00 2001 From: Bobo Date: Fri, 25 Aug 2023 17:18:16 +0200 Subject: [PATCH 3/4] swagger.json fix --- public/swagger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/swagger.json b/public/swagger.json index 79e7cde..74906ec 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -5,7 +5,7 @@ "title": "Astar token statistics API", "description": "Provides Astar networks statistic information." }, - "host": "localhost:3000", + "host": "api.astar.network", "basePath": "/", "schemes": [ "https", From e996b5eb0db12e72c5a3b22bd167fa6410a47d87 Mon Sep 17 00:00:00 2001 From: Bobo Date: Tue, 29 Aug 2023 09:05:36 +0200 Subject: [PATCH 4/4] PR comments fixes --- src/containertypes.ts | 1 - tests/services/StatsService.test.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/containertypes.ts b/src/containertypes.ts index 1d3e8ae..b4948f5 100644 --- a/src/containertypes.ts +++ b/src/containertypes.ts @@ -14,5 +14,4 @@ export const ContainerTypes = { DappsStakingStatsService: 'DappsStakingStatsService', DappRadarService: 'DappRadarService', GiantSquidService: 'GiantSquidService', - A: 'A', }; 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); + }); +});