Skip to content

Commit

Permalink
feat(polygon): Add delegated MATIC (Zapper-fi#938)
Browse files Browse the repository at this point in the history
  • Loading branch information
immasandwich committed Jul 23, 2022
1 parent 016c187 commit 615691e
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 0 deletions.
Binary file added src/apps/polygon/assets/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/apps/polygon/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable, Inject } from '@nestjs/common';

import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface';
import { ContractFactory } from '~contract/contracts';
import { Network } from '~types/network.interface';
// eslint-disable-next-line
type ContractOpts = { address: string; network: Network };

@Injectable()
export class PolygonContractFactory extends ContractFactory {
constructor(@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit) {
super((network: Network) => appToolkit.getNetworkProvider(network));
}
}
85 changes: 85 additions & 0 deletions src/apps/polygon/ethereum/polygon.balance-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Inject } from '@nestjs/common';
import { gql } from 'graphql-request';
import { compact, sumBy } from 'lodash';

import { drillBalance } from '~app-toolkit';
import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { Register } from '~app-toolkit/decorators';
import { presentBalanceFetcherResponse } from '~app-toolkit/helpers/presentation/balance-fetcher-response.present';
import { BalanceFetcher } from '~balance/balance-fetcher.interface';
import { ContractPositionBalance } from '~position/position-balance.interface';
import { Network } from '~types/network.interface';

import { POLYGON_DEFINITION } from '../polygon.definition';

import { PolygonStakingContractPositionDataProps } from './polygon.staking.contract-position-fetcher';

type Eth2DepositsResponse = {
delegators: {
validatorId: string;
delegatedAmount: string;
}[];
};

const GQL_ENDPOINT = `https://api.thegraph.com/subgraphs/name/maticnetwork/mainnet-root-subgraphs`;

const DELEGATED_MATIC_QUERY = gql`
query getDelegatedMatic($address: String!, $first: Int, $lastId: String) {
delegators(where: { address: $address, id_gt: $lastId }, first: $first) {
validatorId
delegatedAmount
}
}
`;

const network = Network.ETHEREUM_MAINNET;

@Register.BalanceFetcher(POLYGON_DEFINITION.id, network)
export class EthereumPolygonBalanceFetcher implements BalanceFetcher {
constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {}

async getDelegatedBalances(address: string) {
const data = await this.appToolkit.helpers.theGraphHelper.gqlFetchAll<Eth2DepositsResponse>({
endpoint: GQL_ENDPOINT,
query: DELEGATED_MATIC_QUERY,
dataToSearch: 'delegators',
variables: {
address: address.toLowerCase(),
},
});

const positions = await this.appToolkit.getAppContractPositions<PolygonStakingContractPositionDataProps>({
appId: POLYGON_DEFINITION.id,
groupIds: [POLYGON_DEFINITION.groups.staking.id],
network,
});

const balances = data.delegators.map(delegator => {
const position = positions.find(v => v.dataProps.validatorId === Number(delegator.validatorId));
if (!position) return null;

const tokens = [drillBalance(position.tokens[0], delegator.delegatedAmount)];
const balanceUSD = sumBy(tokens, v => v.balanceUSD);
const balance: ContractPositionBalance<PolygonStakingContractPositionDataProps> = {
...position,
tokens,
balanceUSD,
};

return balance;
});

return compact(balances);
}

async getBalances(address: string) {
const [delegatedBalances] = await Promise.all([this.getDelegatedBalances(address)]);

return presentBalanceFetcherResponse([
{
label: 'Delegated',
assets: [...delegatedBalances],
},
]);
}
}
108 changes: 108 additions & 0 deletions src/apps/polygon/ethereum/polygon.staking.contract-position-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Inject } from '@nestjs/common';
import axios from 'axios';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { Register } from '~app-toolkit/decorators';
import { getImagesFromToken, getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { CacheOnInterval } from '~cache/cache-on-interval.decorator';
import { ContractType } from '~position/contract.interface';
import { PositionFetcher } from '~position/position-fetcher.interface';
import { ContractPosition } from '~position/position.interface';
import { supplied } from '~position/position.utils';
import { Network } from '~types/network.interface';

import { POLYGON_DEFINITION } from '../polygon.definition';

const appId = POLYGON_DEFINITION.id;
const groupId = POLYGON_DEFINITION.groups.staking.id;
const network = Network.ETHEREUM_MAINNET;

type ValidatorsResponse = {
result: {
id: number;
name: string;
description: string;
logoUrl: string | null;
owner: string;
signer: string;
commissionPercent: number;
signerPublicKey: string;
selfStake: number;
delegatedStake: number;
isInAuction: boolean;
auctionAmount: number;
claimedReward: number;
activationEpoch: 1;
totalStaked: number;
deactivationEpoch: number;
jailEndEpoch: number;
status: string;
contractAddress: string;
uptimePercent: number;
delegationEnabled: boolean;
missedLatestCheckpointcount: number;
}[];
};

export type PolygonStakingContractPositionDataProps = {
validatorId: number;
validatorName: string;
validatorShareAddress: string;
};

@Register.ContractPositionFetcher({ appId, groupId, network })
export class EthereumPolygonStakingContractPositionFetcher implements PositionFetcher<ContractPosition> {
constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {}

@CacheOnInterval({
key: `studio:${appId}:${groupId}:${network}:validators`,
timeout: 15 * 60 * 1000,
})
async getValidators() {
const url = `https://sentinel.matic.network/api/v2/validators?limit=1000`;
const { data } = await axios.get<ValidatorsResponse>(url);
return data;
}

async getPositions() {
const validators = await this.getValidators();
const baseTokens = await this.appToolkit.getBaseTokenPrices(network);

const positions = await Promise.all(
validators.result.map(async validator => {
const maticPosStakingAddress = '0x5e3ef299fddf15eaa0432e6e66473ace8c13d908';
const maticAddress = '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0';
const maticToken = baseTokens.find(v => v.address === maticAddress)!;
const tokens = [supplied(maticToken)];

const label = `Delegated ${getLabelFromToken(maticToken)}: ${validator.name} (ID: ${validator.id})`;
const images = getImagesFromToken(maticToken);

const position: ContractPosition<PolygonStakingContractPositionDataProps> = {
type: ContractType.POSITION,
address: maticPosStakingAddress,
appId,
groupId,
network,
tokens,

dataProps: {
validatorId: validator.id,
validatorName: validator.name,
validatorShareAddress: validator.contractAddress,
},

displayProps: {
label,
images,
},
};

position.key = this.appToolkit.getPositionKey(position, ['validatorId']);
return position;
}),
);

return positions;
}
}
3 changes: 3 additions & 0 deletions src/apps/polygon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { POLYGON_DEFINITION, PolygonAppDefinition } from './polygon.definition';
export { PolygonAppModule } from './polygon.module';
export { PolygonContractFactory } from './contracts';
37 changes: 37 additions & 0 deletions src/apps/polygon/polygon.definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Register } from '~app-toolkit/decorators';
import { appDefinition, AppDefinition } from '~app/app.definition';
import { AppAction, AppTag, GroupType } from '~app/app.interface';
import { Network } from '~types/network.interface';

export const POLYGON_DEFINITION = appDefinition({
id: 'polygon',
name: 'Polygon',
description:
'Polygon believes in Web3 for all. Polygon is a decentralised Ethereum scaling platform that enables developers to build scalable user-friendly dApps with low transaction fees without ever sacrificing on security.',
url: 'https://polygon.technology/',
groups: {
staking: {
id: 'staking',
type: GroupType.POSITION,
label: 'Staking',
},
},
tags: [AppTag.STAKING],
keywords: [],
links: {},

supportedNetworks: {
[Network.ETHEREUM_MAINNET]: [AppAction.VIEW],
},

primaryColor: '#fff',
});

@Register.AppDefinition(POLYGON_DEFINITION.id)
export class PolygonAppDefinition extends AppDefinition {
constructor() {
super(POLYGON_DEFINITION);
}
}

export default POLYGON_DEFINITION;
18 changes: 18 additions & 0 deletions src/apps/polygon/polygon.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Register } from '~app-toolkit/decorators';
import { AbstractApp } from '~app/app.dynamic-module';

import { PolygonContractFactory } from './contracts';
import { EthereumPolygonBalanceFetcher } from './ethereum/polygon.balance-fetcher';
import { EthereumPolygonStakingContractPositionFetcher } from './ethereum/polygon.staking.contract-position-fetcher';
import { PolygonAppDefinition, POLYGON_DEFINITION } from './polygon.definition';

@Register.AppModule({
appId: POLYGON_DEFINITION.id,
providers: [
PolygonAppDefinition,
PolygonContractFactory,
EthereumPolygonStakingContractPositionFetcher,
EthereumPolygonBalanceFetcher,
],
})
export class PolygonAppModule extends AbstractApp() {}

0 comments on commit 615691e

Please sign in to comment.