Skip to content

Commit

Permalink
Fetch dapp staking user transactions history from SubsSquid Giant Squ…
Browse files Browse the repository at this point in the history
…id (#102)

* Basic Squid service

* Call parsers implementation

* swagger.json fix

* PR comments fixes
  • Loading branch information
bobo-k2 authored Aug 29, 2023
1 parent 04775b2 commit 66c1550
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 8 deletions.
22 changes: 22 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -103,6 +114,17 @@ container
.to(DappsStakingStatsService)
.inRequestScope();
container.bind<IDappRadarService>(ContainerTypes.DappRadarService).to(DappRadarService).inRequestScope();
container.bind<IGiantSquidService>(ContainerTypes.GiantSquidService).to(GiantSquidService).inRequestScope();

// Giant squid parsers
container.bind<ICallParser>(CallNameMapping.bond_and_stake).to(BondAndStakeParser).inSingletonScope();
container.bind<ICallParser>(CallNameMapping.unbond_and_unstake).to(UnbondAndUnstakeParser).inSingletonScope();
container.bind<ICallParser>(CallNameMapping.nomination_transfer).to(NominationTransferParser).inSingletonScope();
container.bind<ICallParser>(CallNameMapping.withdraw_unbonded).to(WithdrawParser).inSingletonScope();
container
.bind<ICallParser>(CallNameMapping.withdraw_from_unregistered)
.to(WithdrawFromUnbondedParser)
.inSingletonScope();

// controllers registration
container.bind<IControllerBase>(ContainerTypes.Controller).to(TokenStatsController);
Expand Down
1 change: 1 addition & 0 deletions src/containertypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const ContainerTypes = {
MonthlyActiveWalletsService: 'MonthlyActiveWalletsService',
DappsStakingStatsService: 'DappsStakingStatsService',
DappRadarService: 'DappRadarService',
GiantSquidService: 'GiantSquidService',
};
5 changes: 4 additions & 1 deletion src/controllers/DappsStakingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
}
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/models/DappStaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface UserEvent {
timestamp: number;
contractAddress?: string;
transaction: string;
amount?: string;
transactionHash: string;
transactionSuccess: boolean;
}
8 changes: 1 addition & 7 deletions src/services/DappsStakingStatsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -24,13 +25,6 @@ interface ContractStatsResponse {
};
}

interface UserEvent {
timestamp: number;
contractAddress: number;
transaction: string;
amount: string;
}

interface UserEventsResponse {
data: {
userTransactions: {
Expand Down
5 changes: 5 additions & 0 deletions src/services/GiantSquid/Call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class Call {
name: string;
contractAddress?: string;
amount?: bigint;
}
11 changes: 11 additions & 0 deletions src/services/GiantSquid/CallNameMapping.ts
Original file line number Diff line number Diff line change
@@ -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',
};
71 changes: 71 additions & 0 deletions src/services/GiantSquid/CallParser.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions src/services/GiantSquid/ResponseData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface DappStakingCallResponse {
data: {
calls: DappStakingCallData[];
};
}

export interface DappStakingCallData {
callName: string;
argsStr: string[];
extrinsicHash: string;
success: boolean;
timestamp: string;
}
4 changes: 4 additions & 0 deletions src/services/GiantSquid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './CallParser';
export * from './Call';
export * from './ResponseData';
export * from './CallNameMapping';
73 changes: 73 additions & 0 deletions src/services/GiantSquidService.ts
Original file line number Diff line number Diff line change
@@ -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<UserEvent[]>;
}

// 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<UserEvent[]> {
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<DappStakingCallResponse>(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<ICallParser>(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;
}
}
17 changes: 17 additions & 0 deletions tests/services/StatsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

0 comments on commit 66c1550

Please sign in to comment.