Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subgraph for delegation history #915

Merged
merged 22 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions modules/address/components/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { formatAddress } from 'lib/utils';
import { useWeb3 } from 'modules/web3/hooks/useWeb3';
import { getENS } from 'modules/web3/helpers/ens';
import React, { useEffect, useState } from 'react';
import { getDefaultProvider } from 'modules/web3/helpers/getDefaultProvider';
import { SupportedNetworks } from 'modules/web3/constants/networks';

export const Address = React.memo(function Address({
address,
Expand All @@ -19,14 +21,15 @@ export const Address = React.memo(function Address({
address: string;
maxLength?: number;
}): React.ReactElement {
const { provider } = useWeb3();
const [addressFormated, setAddressFormatted] = useState(formatAddress(address || '').toLowerCase());

async function fetchENSName() {
if (!address || !provider) {
if (!address) {
return;
}

const provider = getDefaultProvider(SupportedNetworks.MAINNET);

const ens = await getENS({ address, provider });

ens ? setAddressFormatted(ens) : setAddressFormatted(formatAddress(address).toLowerCase());
Expand Down
7 changes: 5 additions & 2 deletions modules/app/components/DateWithHover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ export function DateWithHover({
timeago,
label
}: {
date: Date | string | number;
date: Date | string | number | null;
timeago?: boolean;
label?: string;
}): React.ReactElement {
if (!date) {
return <Text>N/A</Text>;
}
return (
<Tooltip label={label ? label : formatDateWithTime(date ?? '')}>
<Tooltip label={label ? label : formatDateWithTime(date)}>
<Text>{timeago ? `${formatTimeAgo(date ?? '')}` : `${formatDateWithTime(date ?? '')}`}</Text>
</Tooltip>
);
Expand Down
2 changes: 1 addition & 1 deletion modules/contracts/eth-sdk.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const config: EthSdkConfig = {
pollingOld: '0xF9be8F0945acDdeeDaA64DFCA5Fe9629D0CF8E5D',
pot: '0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7',
vat: '0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B',
voteDelegateFactory: '0xD897F108670903D1d6070fcf818f9db3615AF272',
voteDelegateFactory: '0x4E76FbE44fa5Dae076a7f4f676250e7941421fbA',
voteProxyFactory: '0x6FCD258af181B3221073A96dD90D1f7AE7eEc408',
voteProxyFactoryOld: '0xa63E145309cadaa6A903a19993868Ef7E85058BE',
vow: '0xA950524441892A31ebddF91d3cEEFa04Bf454466'
Expand Down
50 changes: 32 additions & 18 deletions modules/delegates/api/fetchDelegatedTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,57 +9,71 @@ SPDX-License-Identifier: AGPL-3.0-or-later
import { add } from 'date-fns';
import { utils } from 'ethers';
import logger from 'lib/logger';
import { Query } from 'modules/gql/generated/graphql';
import { gqlRequest } from 'modules/gql/gqlRequest';
import { allDelegates } from 'modules/gql/queries/allDelegates';
import { mkrDelegatedToV2 } from 'modules/gql/queries/mkrDelegatedTo';
import { delegatorHistory } from 'modules/gql/queries/subgraph/delegatorHistory';
import { SupportedNetworks } from 'modules/web3/constants/networks';
import { networkNameToChainId } from 'modules/web3/helpers/chain';
import { isAboutToExpireCheck, isExpiredCheck } from 'modules/migration/helpers/expirationChecks';
import { DelegationHistoryWithExpirationDate, MKRDelegatedToDAIResponse } from '../types';
import { DelegationHistoryWithExpirationDate, MKRDelegatedToResponse } from '../types';
import { getNewOwnerFromPrevious } from 'modules/migration/delegateAddressLinks';
import { Query, AllDelegatesRecord } from 'modules/gql/generated/graphql';

export async function fetchDelegatedTo(
address: string,
network: SupportedNetworks
): Promise<DelegationHistoryWithExpirationDate[]> {
try {
// Returns the records with the aggregated delegated data
const data = await gqlRequest({
chainId: networkNameToChainId(network),
query: mkrDelegatedToV2,
variables: { argAddress: address.toLowerCase() }
});
// We fetch the delegates information from the DB to extract the expiry date of each delegate
// TODO: This information could be aggregated in the "mkrDelegatedTo" query in gov-polling-db, and returned there, as an improvement.
const chainId = networkNameToChainId(network);
const delegatesData = await gqlRequest<Query>({ chainId, query: allDelegates });
const delegates = delegatesData.allDelegates.nodes;

const res: MKRDelegatedToDAIResponse[] = data.mkrDelegatedToV2.nodes;
// Returns the records with the aggregated delegated data
const data = await gqlRequest({
chainId: networkNameToChainId(network),
useSubgraph: true,
query: delegatorHistory,
variables: { address: address.toLowerCase() }
});
const res: MKRDelegatedToResponse[] = data.delegationHistories.map(x => {
return {
delegateContractAddress: x.delegate.id,
lockAmount: x.amount,
blockTimestamp: x.timestamp,
hash: x.txnHash,
blockNumber: x.blockNumber,
immediateCaller: address
};
});

const delegatedTo = res.reduce((acc, { delegateContractAddress, lockAmount, blockTimestamp, hash }) => {
const existing = acc.find(({ address }) => address === delegateContractAddress) as
| DelegationHistoryWithExpirationDate
| undefined;

// We sum the total of lockAmounts in different events to calculate the current delegated amount
if (existing) {
existing.lockAmount = utils.formatEther(
utils.parseEther(existing.lockAmount).add(utils.parseEther(lockAmount))
);
existing.lockAmount = utils.formatEther(utils.parseEther(existing.lockAmount).add(lockAmount));
existing.events.push({ lockAmount, blockTimestamp, hash });
} else {
const delegatingTo = delegates.find(
i => i?.voteDelegate?.toLowerCase() === delegateContractAddress.toLowerCase()
);
) as (AllDelegatesRecord & { version: string }) | undefined;

if (!delegatingTo) {
return acc;
}

const delegatingToWalletAddress = delegatingTo?.delegate?.toLowerCase();
// Get the expiration date of the delegate

const expirationDate = add(new Date(delegatingTo?.blockTimestamp), { years: 1 });
const expirationDate = add(new Date(delegatingTo?.blockTimestamp * 1000), { years: 1 });

const isAboutToExpire = isAboutToExpireCheck(expirationDate);
const isExpired = isExpiredCheck(expirationDate);
//only v1 delegate contracts expire
const isAboutToExpire = delegatingTo.version === '1' && isAboutToExpireCheck(expirationDate);
const isExpired = delegatingTo.version === '1' && isExpiredCheck(expirationDate);

// If it has a new owner address, check if it has renewed the contract
const newOwnerAddress = getNewOwnerFromPrevious(delegatingToWalletAddress as string, network);
Expand All @@ -73,7 +87,7 @@ export async function fetchDelegatedTo(
expirationDate,
isExpired,
isAboutToExpire: !isExpired && isAboutToExpire,
lockAmount: utils.formatEther(utils.parseEther(lockAmount)),
lockAmount: utils.formatEther(lockAmount),
isRenewed: !!newRenewedContract,
events: [{ lockAmount, blockTimestamp, hash }]
} as DelegationHistoryWithExpirationDate);
Expand Down
16 changes: 8 additions & 8 deletions modules/delegates/api/fetchDelegates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export async function fetchDelegates(
const aSupport = a.mkrDelegated ? a.mkrDelegated : 0;
return new BigNumberJS(aSupport).gt(new BigNumberJS(bSupport)) ? -1 : 1;
} else if (sortBy === 'date') {
return a.expirationDate > b.expirationDate ? -1 : 1;
return a.expirationDate && b.expirationDate ? (a.expirationDate > b.expirationDate ? -1 : 1) : 0;
} else if (sortBy === 'delegators') {
const delegationHistoryA = formatDelegationHistory(a.mkrLockedDelegate);
const delegationHistoryB = formatDelegationHistory(b.mkrLockedDelegate);
Expand Down Expand Up @@ -444,7 +444,7 @@ export async function fetchDelegatesPaginated({
orderDirection,
seed,
delegateType,
searchTerm,
searchTerm
}: DelegatesValidatedQueryParams): Promise<DelegatesPaginatedAPIResponse> {
const chainId = networkNameToChainId(network);

Expand Down Expand Up @@ -473,8 +473,7 @@ export async function fetchDelegatesPaginated({
}
]
};
(searchTerm) &&
delegatesQueryFilter.and.push({ voteDelegate: { in: filteredDelegateAddresses } });
searchTerm && delegatesQueryFilter.and.push({ voteDelegate: { in: filteredDelegateAddresses } });
}

const delegatesQueryVariables = {
Expand All @@ -492,8 +491,8 @@ export async function fetchDelegatesPaginated({
delegatesQueryVariables['seed'] = seed;
}

const [githubExecutives, delegatesExecSupport, delegatesQueryRes, delegationMetricsRes] =
await Promise.all([
const [githubExecutives, delegatesExecSupport, delegatesQueryRes, delegationMetricsRes] = await Promise.all(
[
getGithubExecutives(network),
fetchDelegatesExecSupport(network),
gqlRequest<any>({
Expand All @@ -505,7 +504,8 @@ export async function fetchDelegatesPaginated({
chainId,
query: delegationMetricsQuery
})
]);
]
);

const delegatesData = {
paginationInfo: {
Expand Down Expand Up @@ -562,7 +562,7 @@ export async function fetchDelegatesPaginated({
previous: allDelegatesEntry?.previous,
next: allDelegatesEntry?.next
};
}) as DelegatePaginated[],
}) as DelegatePaginated[]
};

return delegatesData;
Expand Down
6 changes: 4 additions & 2 deletions modules/delegates/types/delegate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export type DelegateContractInformation = {
mkrDelegated: string;
proposalsSupported: number;
mkrLockedDelegate: MKRLockedDelegateAPIResponse[];
version: string;
lastVoteDate: number | null;
};

export type Delegate = {
Expand All @@ -42,7 +44,7 @@ export type Delegate = {
lastVoteDate: number | null;
expired: boolean;
isAboutToExpire: boolean;
expirationDate: Date;
expirationDate: Date | null;
externalUrl?: string;
combinedParticipation?: string;
pollParticipation?: string;
Expand Down Expand Up @@ -116,7 +118,7 @@ export type MKRLockedDelegateAPIResponse = {
hash: string;
};

export type MKRDelegatedToDAIResponse = MKRLockedDelegateAPIResponse & {
export type MKRDelegatedToResponse = MKRLockedDelegateAPIResponse & {
hash: string;
immediateCaller: string;
};
Expand Down
9 changes: 9 additions & 0 deletions modules/gql/gql.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export const STAGING_MAINNET_SPOCK_URL = 'https://pollingdb2-mainnet-staging.mak
export const MAINNET_SPOCK_URL = 'https://pollingdb2-mainnet-prod.makerdao.com/api/v1';
export const TENDERLY_SPOCK_URL = 'https://pollingdb2-tenderly-staging.makerdao.com/api/v1';

/* Subgraph URLs */

// const usePrivateSubgraph = process.env.USE_PRIVATE_SUBGRAPH === 'true';
// const permission = usePrivateSubgraph ? 'private' : 'public';
export const TENDERLY_SUBGRAPH_URL =
'https://query-subgraph-staging.sky.money/private/subgraphs/name/jetstreamgg/subgraph-testnet';
export const MAINNET_SUBGRAPH_URL =
'https://query-subgraph-staging.sky.money/private/subgraphs/name/jetstreamgg/subgraph-mainnet';

export enum QueryFilterNames {
Active = 'active',
PollId = 'pollId',
Expand Down
16 changes: 13 additions & 3 deletions modules/gql/gqlRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,32 @@ import { CHAIN_INFO } from 'modules/web3/constants/networks';

type GqlRequestProps = {
chainId?: SupportedChainId;
useSubgraph?: boolean;
query: RequestDocument;
variables?: Variables | null;
};

// TODO we'll be able to remove the "any" if we update all the instances of gqlRequest to pass <Query>
export const gqlRequest = async <TQuery = any>({
chainId,
useSubgraph = false,
query,
variables
}: GqlRequestProps): Promise<TQuery> => {
try {
const id = chainId ?? SupportedChainId.MAINNET;
const url = CHAIN_INFO[id].spockUrl;
let url;
if (useSubgraph) {
url = CHAIN_INFO[id].subgraphUrl;
} else {
url = CHAIN_INFO[id].spockUrl;
}
if (!url) {
return Promise.reject(new ApiError(`Missing spock url in configuration for chainId: ${id}`));
return Promise.reject(
new ApiError(`Missing ${useSubgraph ? 'subgraph' : 'spock'} url in configuration for chainId: ${id}`)
);
}

const resp = await backoffRetry(
3,
() => request(url, query, variables),
Expand All @@ -42,7 +52,7 @@ export const gqlRequest = async <TQuery = any>({
return resp;
} catch (e) {
const status = e.response ? e.response.status : 500;
const errorMessage = status === 403 ? 'Rate limited on gov polling' : e.message;
const errorMessage = e.message;
const message = `Error on GraphQL query, Chain ID: ${chainId}, query: ${query}, message: ${errorMessage}`;
throw new ApiError(message, status, 'Error fetching gov polling data');
}
Expand Down
18 changes: 18 additions & 0 deletions modules/gql/queries/subgraph/allDelegations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: © 2023 Dai Foundation <www.daifoundation.org>
SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { gql } from 'graphql-request';

export const allDelegations = gql`
{
delegations {
delegator
delegate {
id
}
amount
}
}
`;
24 changes: 24 additions & 0 deletions modules/gql/queries/subgraph/delegateHistoryArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
SPDX-FileCopyrightText: © 2023 Dai Foundation <www.daifoundation.org>
SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { gql } from 'graphql-request';

export const delegateHistoryArray = gql`
query delegateHistoryArray($delegates: [String!]!) {
delegates(where: { id_in: $delegates }) {
delegationHistory {
amount
accumulatedAmount
delegator
blockNumber
timestamp
txnHash
delegate {
id
}
}
}
}
`;
22 changes: 22 additions & 0 deletions modules/gql/queries/subgraph/delegatorHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
SPDX-FileCopyrightText: © 2023 Dai Foundation <www.daifoundation.org>

SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { gql } from 'graphql-request';

export const delegatorHistory = gql`
query delegatorHistory($address: String!) {
delegationHistories(where: { delegator: $address }) {
amount
accumulatedAmount
delegate {
id
}
timestamp
txnHash
blockNumber
}
}
`;
12 changes: 9 additions & 3 deletions modules/migration/components/MigrationBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,25 @@ export function MigrationBanner(): React.ReactElement | null {
isDelegatedToExpiredContract,
isDelegateContractExpired,
isDelegateContractExpiring,
isShadowDelegate
isShadowDelegate,
isDelegateV1Contract,
isDelegatedToV1Contract
} = useMigrationStatus();
const showDelegationMigrationBanner =
(isDelegateContractExpired && !isShadowDelegate) ||
(isDelegateContractExpiring && !isShadowDelegate) ||
(isDelegateV1Contract && !isShadowDelegate) ||
isDelegatedToExpiringContract ||
isDelegatedToExpiredContract;
isDelegatedToExpiredContract ||
isDelegatedToV1Contract;

const { variant, href, copy } = getMigrationBannerContent({
isDelegatedToExpiredContract,
isDelegateContractExpired: isDelegateContractExpired && !isShadowDelegate,
isDelegatedToExpiringContract,
isDelegateContractExpiring: isDelegateContractExpiring && !isShadowDelegate
isDelegateContractExpiring: isDelegateContractExpiring && !isShadowDelegate,
isDelegateV1Contract,
isDelegatedToV1Contract
});

return showDelegationMigrationBanner ? (
Expand Down
Loading
Loading