diff --git a/package.json b/package.json index 9632276c51..deaf9e3899 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@aave/contract-helpers": "1.21.2-2022bac22871a85776f017f1d6214d9ecfcc5abe.0", + "@aave/contract-helpers": "1.21.2-a36249a4280cf8f987643baeec8c685814f5fb2b.0", "@aave/math-utils": "1.21.2-2022bac22871a85776f017f1d6214d9ecfcc5abe.0", "@bgd-labs/aave-address-book": "^2.13.1", "@emotion/cache": "11.10.3", diff --git a/src/architecture/FixedPointDecimal.ts b/src/architecture/FixedPointDecimal.ts new file mode 100644 index 0000000000..bef226dd04 --- /dev/null +++ b/src/architecture/FixedPointDecimal.ts @@ -0,0 +1,30 @@ +import { BigNumber, BigNumberish } from 'ethers'; +import { formatUnits } from 'ethers/lib/utils'; + +export class FixedPointDecimal { + private readonly _value: BigNumber; + + constructor(_value: BigNumberish, private readonly _decimals: number) { + this._value = BigNumber.from(_value); + } + + get value() { + return this._value; + } + + get decimals() { + return this._decimals; + } + + add(value: FixedPointDecimal) { + return new FixedPointDecimal(this._value.add(value._value), this._decimals); + } + + eq(value: FixedPointDecimal) { + return this._value.eq(value._value); + } + + format() { + return formatUnits(this._value, this._decimals); + } +} diff --git a/src/components/transactions/GovDelegation/GovDelegationModalContent.tsx b/src/components/transactions/GovDelegation/GovDelegationModalContent.tsx index d4995ac48a..6eba6962a3 100644 --- a/src/components/transactions/GovDelegation/GovDelegationModalContent.tsx +++ b/src/components/transactions/GovDelegation/GovDelegationModalContent.tsx @@ -1,13 +1,13 @@ import { canBeEnsAddress } from '@aave/contract-helpers'; import { t, Trans } from '@lingui/macro'; import { FormControl, TextField, Typography } from '@mui/material'; -import { utils } from 'ethers'; +import { constants, utils } from 'ethers'; import { parseUnits } from 'ethers/lib/utils'; import { useEffect, useState } from 'react'; import { TextWithTooltip } from 'src/components/TextWithTooltip'; import { GovernancePowerTypeApp } from 'src/helpers/types'; +import { useGovernanceDelegatees } from 'src/hooks/governance/useDelegateeData'; import { useGovernanceTokens } from 'src/hooks/governance/useGovernanceTokens'; -import { usePowers } from 'src/hooks/governance/usePowers'; import { ModalType, useModalContext } from 'src/hooks/useModal'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { useRootStore } from 'src/store/root'; @@ -46,11 +46,10 @@ export const GovDelegationModalContent: React.FC const { gasLimit, mainTxState: txState, txError } = useModalContext(); const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig); const currentChainId = useRootStore((store) => store.currentChainId); - const currentMarketData = useRootStore((store) => store.currentMarketData); const { data: { aave, stkAave, aAave }, } = useGovernanceTokens(); - const { data: powers, refetch } = usePowers(currentMarketData); + const { data: powers, refetch } = useGovernanceDelegatees(); // error states // selector states @@ -63,12 +62,17 @@ export const GovDelegationModalContent: React.FC const onlyOnePowerToRevoke = isRevokeModal && !!powers && - ((powers.aaveVotingDelegatee === '' && powers.stkAaveVotingDelegatee === '') || - (powers.aavePropositionDelegatee === '' && powers.stkAavePropositionDelegatee === '')); + ((powers.aaveVotingDelegatee === constants.AddressZero && + powers.stkAaveVotingDelegatee === constants.AddressZero) || + (powers.aavePropositionDelegatee === constants.AddressZero && + powers.stkAavePropositionDelegatee === constants.AddressZero)); useEffect(() => { if (onlyOnePowerToRevoke) { - if (powers.aaveVotingDelegatee === '' && powers.stkAaveVotingDelegatee === '') + if ( + powers.aaveVotingDelegatee === constants.AddressZero && + powers.stkAaveVotingDelegatee === constants.AddressZero + ) setDelegationType(GovernancePowerTypeApp.PROPOSITION); else setDelegationType(GovernancePowerTypeApp.VOTING); } @@ -154,9 +158,10 @@ export const GovDelegationModalContent: React.FC )} {(isRevokeModal && !!powers && - ((powers.aaveVotingDelegatee === '' && powers.stkAaveVotingDelegatee === '') || - (powers.aavePropositionDelegatee === '' && - powers.stkAavePropositionDelegatee === ''))) || ( + ((powers.aaveVotingDelegatee === constants.AddressZero && + powers.stkAaveVotingDelegatee === constants.AddressZero) || + (powers.aavePropositionDelegatee === constants.AddressZero && + powers.stkAavePropositionDelegatee === constants.AddressZero))) || ( <> {isRevokeModal ? 'Power to revoke' : 'Power to delegate'} diff --git a/src/components/transactions/GovVote/GovVoteActions.tsx b/src/components/transactions/GovVote/GovVoteActions.tsx index dc5f9bae55..35041d47ff 100644 --- a/src/components/transactions/GovVote/GovVoteActions.tsx +++ b/src/components/transactions/GovVote/GovVoteActions.tsx @@ -156,8 +156,7 @@ export const GovVoteActions = ({ const user = useRootStore((store) => store.account); const estimateGasLimit = useRootStore((store) => store.estimateGasLimit); const { sendTx, signTxData } = useWeb3Context(); - const currentMarketData = useRootStore((store) => store.currentMarketData); - const tokenPowers = useGovernanceTokensAndPowers(currentMarketData); + const tokenPowers = useGovernanceTokensAndPowers(); const [signature, setSignature] = useState(undefined); const proposalId = proposal.proposal.proposalId; const blockHash = proposal.proposalData.proposalData.snapshotBlockHash; diff --git a/src/hooks/governance/useDelegateeData.ts b/src/hooks/governance/useDelegateeData.ts new file mode 100644 index 0000000000..053d8f1060 --- /dev/null +++ b/src/hooks/governance/useDelegateeData.ts @@ -0,0 +1,50 @@ +import { useQueries } from '@tanstack/react-query'; +import { useRootStore } from 'src/store/root'; +import { governanceV3Config } from 'src/ui-config/governanceConfig'; +import { queryKeysFactory } from 'src/ui-config/queries'; +import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider'; + +export const useTokenDelegatees = (tokens: string[]) => { + const { delegationTokenService } = useSharedDependencies(); + const user = useRootStore((store) => store.account); + return useQueries({ + queries: tokens.map((token) => ({ + queryFn: () => + delegationTokenService.getTokenDelegatees(user, token, governanceV3Config.coreChainId), + queryKey: queryKeysFactory.tokenDelegatees(user, token, governanceV3Config.coreChainId), + enabled: !!user, + })), + }); +}; + +export const useGovernanceDelegatees = () => { + const queries = useTokenDelegatees(Object.values(governanceV3Config.votingAssets)); + const isLoading = queries.some((elem) => elem.isLoading); + const error = queries.find((elem) => elem.error)?.error; + const refetch = () => queries.forEach((elem) => elem.refetch()); + const allData = queries.reduce((acum, elem) => { + if (elem.data) { + return acum.concat([elem.data]); + } + return acum; + }, [] as { votingDelegatee: string; propositionDelegatee: string }[]); + if (allData.length !== 3) { + return { + data: undefined, + isLoading, + error, + refetch, + }; + } + return { + data: { + aaveVotingDelegatee: allData[0].votingDelegatee, + aavePropositionDelegatee: allData[0].propositionDelegatee, + aAaveVotingDelegatee: allData[1].votingDelegatee, + aAavePropositionDelegatee: allData[1].propositionDelegatee, + stkAaveVotingDelegatee: allData[2].votingDelegatee, + stkAavePropositionDelegatee: allData[2].propositionDelegatee, + }, + refetch, + }; +}; diff --git a/src/hooks/governance/useGovernanceTokensAndPowers.ts b/src/hooks/governance/useGovernanceTokensAndPowers.ts index a4a8e2bb50..6e7c271cac 100644 --- a/src/hooks/governance/useGovernanceTokensAndPowers.ts +++ b/src/hooks/governance/useGovernanceTokensAndPowers.ts @@ -1,7 +1,6 @@ import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { Powers } from 'src/services/GovernanceService'; import { GovernanceTokensBalance } from 'src/services/WalletBalanceService'; -import { MarketDataType } from 'src/ui-config/marketsConfig'; import { useGovernanceTokens } from './useGovernanceTokens'; import { usePowers } from './usePowers'; @@ -15,10 +14,8 @@ interface GovernanceTokensAndPowers extends Powers, GovernanceTokensBalance { ) => Promise>; } -export const useGovernanceTokensAndPowers = ( - marketData: MarketDataType -): GovernanceTokensAndPowers | undefined => { - const { data: powers, refetch: refetchPowers } = usePowers(marketData); +export const useGovernanceTokensAndPowers = (): GovernanceTokensAndPowers | undefined => { + const { data: powers, refetch: refetchPowers } = usePowers(); const { data: governanceTokens } = useGovernanceTokens(); if (!powers || !governanceTokens) { diff --git a/src/hooks/governance/usePowers.ts b/src/hooks/governance/usePowers.ts index de6cebea08..387e0a9223 100644 --- a/src/hooks/governance/usePowers.ts +++ b/src/hooks/governance/usePowers.ts @@ -1,16 +1,15 @@ import { useQuery } from '@tanstack/react-query'; import { useRootStore } from 'src/store/root'; import { governanceV3Config } from 'src/ui-config/governanceConfig'; -import { MarketDataType } from 'src/ui-config/marketsConfig'; import { POLLING_INTERVAL, queryKeysFactory } from 'src/ui-config/queries'; import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider'; -export const usePowers = (marketData: MarketDataType) => { +export const usePowers = () => { const { governanceService } = useSharedDependencies(); const user = useRootStore((store) => store.account); return useQuery({ queryFn: () => governanceService.getPowers(governanceV3Config.coreChainId, user), - queryKey: queryKeysFactory.powers(user, marketData), + queryKey: queryKeysFactory.powers(user, governanceV3Config.coreChainId), enabled: !!user, refetchInterval: POLLING_INTERVAL, }); diff --git a/src/hooks/governance/useTokensPower.ts b/src/hooks/governance/useTokensPower.ts new file mode 100644 index 0000000000..db1d0a9932 --- /dev/null +++ b/src/hooks/governance/useTokensPower.ts @@ -0,0 +1,53 @@ +import { useQueries } from '@tanstack/react-query'; +import { FixedPointDecimal } from 'src/architecture/FixedPointDecimal'; +import { TokenDelegationPower } from 'src/services/DelegationTokenService'; +import { useRootStore } from 'src/store/root'; +import { governanceV3Config } from 'src/ui-config/governanceConfig'; +import { queryKeysFactory } from 'src/ui-config/queries'; +import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider'; + +export const useTokensPowers = (tokens: string[]) => { + const { delegationTokenService } = useSharedDependencies(); + const user = useRootStore((store) => store.account); + return useQueries({ + queries: tokens.map((token) => ({ + queryFn: () => + delegationTokenService.getTokenPowers(user, token, governanceV3Config.coreChainId), + queryKey: queryKeysFactory.tokenPowers(user, token, governanceV3Config.coreChainId), + enabled: !!user, + })), + }); +}; + +export const useTotalTokensPowers = (tokens: string[]) => { + const queries = useTokensPowers(tokens); + const isLoading = queries.some((elem) => elem.isLoading); + const error = queries.find((elem) => elem.error)?.error; + const allData = queries.reduce((acum, elem) => { + if (elem.data) { + return acum.concat([elem.data]); + } + return acum; + }, [] as TokenDelegationPower[]); + if (allData.length !== tokens.length) { + return { + data: undefined, + isLoading, + error, + }; + } + return { + data: allData.reduce( + (acum, elem) => { + return { + propositionPower: acum.propositionPower.add(elem.propositionPower), + votingPower: acum.votingPower.add(elem.votingPower), + }; + }, + { + propositionPower: new FixedPointDecimal(0, 18), + votingPower: new FixedPointDecimal(0, 18), + } + ), + }; +}; diff --git a/src/modules/governance/DelegatedInfoPanel.tsx b/src/modules/governance/DelegatedInfoPanel.tsx index db89dd4ab2..675dcf11c9 100644 --- a/src/modules/governance/DelegatedInfoPanel.tsx +++ b/src/modules/governance/DelegatedInfoPanel.tsx @@ -1,21 +1,21 @@ import { Trans } from '@lingui/macro'; import { Button, Divider, Paper, Typography } from '@mui/material'; import { Box } from '@mui/system'; +import { constants } from 'ethers'; import { AvatarSize } from 'src/components/Avatar'; import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; import { Link } from 'src/components/primitives/Link'; import { Row } from 'src/components/primitives/Row'; import { TokenIcon } from 'src/components/primitives/TokenIcon'; import { ExternalUserDisplay } from 'src/components/UserDisplay'; +import { useGovernanceDelegatees } from 'src/hooks/governance/useDelegateeData'; import { useGovernanceTokens } from 'src/hooks/governance/useGovernanceTokens'; -import { usePowers } from 'src/hooks/governance/usePowers'; import { useModalContext } from 'src/hooks/useModal'; import { ZERO_ADDRESS } from 'src/modules/governance/utils/formatProposal'; import { useRootStore } from 'src/store/root'; import { GENERAL } from 'src/utils/mixPanelEvents'; type DelegatedPowerProps = { - user: string; aavePower: string; stkAavePower: string; aaveDelegatee: string; @@ -26,7 +26,6 @@ type DelegatedPowerProps = { }; const DelegatedPower: React.FC = ({ - user, aavePower, stkAavePower, aaveDelegatee, @@ -35,9 +34,9 @@ const DelegatedPower: React.FC = ({ aAavePower, title, }) => { - const isAaveSelfDelegated = !aaveDelegatee || user === aaveDelegatee; - const isStkAaveSelfDelegated = !stkAaveDelegatee || user === stkAaveDelegatee; - const isAAaveSelfDelegated = !aAaveDelegatee || user === aAaveDelegatee; + const isAaveSelfDelegated = !aaveDelegatee || constants.AddressZero === aaveDelegatee; + const isStkAaveSelfDelegated = !stkAaveDelegatee || constants.AddressZero === stkAaveDelegatee; + const isAAaveSelfDelegated = !aAaveDelegatee || constants.AddressZero === aAaveDelegatee; if (isAaveSelfDelegated && isStkAaveSelfDelegated && isAAaveSelfDelegated) return null; @@ -133,11 +132,10 @@ const DelegatedPower: React.FC = ({ export const DelegatedInfoPanel = () => { const address = useRootStore((store) => store.account); - const currentMarketData = useRootStore((store) => store.currentMarketData); const { data: { aave, stkAave, aAave }, } = useGovernanceTokens(); - const { data: powers } = usePowers(currentMarketData); + const { data: powers } = useGovernanceDelegatees(); const { openGovDelegation, openRevokeGovDelegation } = useModalContext(); const trackEvent = useRootStore((store) => store.trackEvent); @@ -147,20 +145,20 @@ export const DelegatedInfoPanel = () => { Number(aave) <= 0 && Number(stkAave) <= 0 && Number(aAave) <= 0 && - powers.aavePropositionDelegatee === '' && - powers.aaveVotingDelegatee === '' && - powers.stkAavePropositionDelegatee === '' && - powers.stkAaveVotingDelegatee === '' && - powers.aAavePropositionDelegatee === '' && - powers.aAaveVotingDelegatee === ''; + powers.aavePropositionDelegatee === constants.AddressZero && + powers.aaveVotingDelegatee === constants.AddressZero && + powers.stkAavePropositionDelegatee === constants.AddressZero && + powers.stkAaveVotingDelegatee === constants.AddressZero && + powers.aAavePropositionDelegatee === constants.AddressZero && + powers.aAaveVotingDelegatee === constants.AddressZero; const showRevokeButton = - powers.aavePropositionDelegatee !== '' || - powers.aaveVotingDelegatee !== '' || - powers.stkAavePropositionDelegatee !== '' || - powers.stkAaveVotingDelegatee !== '' || - powers.aAaveVotingDelegatee !== '' || - powers.aAavePropositionDelegatee !== ''; + powers.aavePropositionDelegatee !== constants.AddressZero || + powers.aaveVotingDelegatee !== constants.AddressZero || + powers.stkAavePropositionDelegatee !== constants.AddressZero || + powers.stkAaveVotingDelegatee !== constants.AddressZero || + powers.aAaveVotingDelegatee !== constants.AddressZero || + powers.aAavePropositionDelegatee !== constants.AddressZero; return ( @@ -198,7 +196,6 @@ export const DelegatedInfoPanel = () => { aAaveDelegatee={powers.aAaveVotingDelegatee} aaveDelegatee={powers.aaveVotingDelegatee} stkAaveDelegatee={powers.stkAaveVotingDelegatee} - user={address} title="Voting power" /> { stkAavePower={stkAave} aaveDelegatee={powers.aavePropositionDelegatee} stkAaveDelegatee={powers.stkAavePropositionDelegatee} - user={address} title="Proposition power" /> diff --git a/src/modules/governance/VotingPowerInfoPanel.tsx b/src/modules/governance/VotingPowerInfoPanel.tsx index 108e4806c0..f3d3324977 100644 --- a/src/modules/governance/VotingPowerInfoPanel.tsx +++ b/src/modules/governance/VotingPowerInfoPanel.tsx @@ -1,19 +1,17 @@ import { Trans } from '@lingui/macro'; -import { Box, Paper, Typography } from '@mui/material'; +import { Box, Paper, Skeleton, Typography } from '@mui/material'; import { AvatarSize } from 'src/components/Avatar'; import { CompactMode } from 'src/components/CompactableTypography'; import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; import { Link } from 'src/components/primitives/Link'; import { TextWithTooltip } from 'src/components/TextWithTooltip'; import { UserDisplay } from 'src/components/UserDisplay'; -import { usePowers } from 'src/hooks/governance/usePowers'; -import { useRootStore } from 'src/store/root'; +import { useTotalTokensPowers } from 'src/hooks/governance/useTokensPower'; +import { governanceV3Config } from 'src/ui-config/governanceConfig'; import { GENERAL } from 'src/utils/mixPanelEvents'; export function VotingPowerInfoPanel() { - const user = useRootStore((store) => store.account); - const currentMarketData = useRootStore((store) => store.currentMarketData); - const { data: powers } = usePowers(currentMarketData); + const { data: powers } = useTotalTokensPowers(Object.values(governanceV3Config.votingAssets)); return ( - {user && ( + {powers ? ( @@ -106,12 +104,14 @@ export function VotingPowerInfoPanel() { + ) : ( + )} ); diff --git a/src/services/DelegationTokenService.ts b/src/services/DelegationTokenService.ts new file mode 100644 index 0000000000..746cee1fa5 --- /dev/null +++ b/src/services/DelegationTokenService.ts @@ -0,0 +1,37 @@ +import { AaveTokenV3Service } from '@aave/contract-helpers'; +import { Provider } from '@ethersproject/providers'; +import { FixedPointDecimal } from 'src/architecture/FixedPointDecimal'; + +export interface TokenDelegationPower { + address: string; + votingPower: FixedPointDecimal; + propositionPower: FixedPointDecimal; +} + +export class DelegationTokenService { + constructor(private readonly getProvider: (chainId: number) => Provider) {} + + private getDelegationTokenService(tokenAddress: string, chainId: number) { + const provider = this.getProvider(chainId); + return new AaveTokenV3Service(tokenAddress, provider); + } + + async getTokenPowers( + user: string, + token: string, + chainId: number + ): Promise { + const service = this.getDelegationTokenService(token, chainId); + const result = await service.getPowers(user); + return { + address: token, + votingPower: new FixedPointDecimal(result.votingPower, 18), + propositionPower: new FixedPointDecimal(result.propositionPower, 18), + }; + } + + async getTokenDelegatees(user: string, token: string, chainId: number) { + const service = this.getDelegationTokenService(token, chainId); + return service.getDelegateeData(user); + } +} diff --git a/src/ui-config/SharedDependenciesProvider.tsx b/src/ui-config/SharedDependenciesProvider.tsx index ab6b9a418d..add63b3d72 100644 --- a/src/ui-config/SharedDependenciesProvider.tsx +++ b/src/ui-config/SharedDependenciesProvider.tsx @@ -1,5 +1,6 @@ import { createContext, useContext } from 'react'; import { ApprovedAmountService } from 'src/services/ApprovedAmountService'; +import { DelegationTokenService } from 'src/services/DelegationTokenService'; import { GovernanceService } from 'src/services/GovernanceService'; import { GovernanceV3Service } from 'src/services/GovernanceV3Service'; import { UiIncentivesService } from 'src/services/UIIncentivesService'; @@ -23,6 +24,7 @@ interface SharedDependenciesContext { approvedAmountService: ApprovedAmountService; uiIncentivesService: UiIncentivesService; uiPoolService: UiPoolService; + delegationTokenService: DelegationTokenService; } const SharedDependenciesContext = createContext(null); @@ -49,6 +51,7 @@ export const SharedDependenciesProvider: React.FC = ({ children }) => { const poolTokensBalanceService = new WalletBalanceService(getProvider); const uiStakeDataService = new UiStakeDataService(getStakeProvider); const approvedAmountService = new ApprovedAmountService(getProvider); + const delegationTokenService = new DelegationTokenService(getGovernanceProvider); const uiPoolService = new UiPoolService(getProvider); const uiIncentivesService = new UiIncentivesService(getProvider); @@ -65,6 +68,7 @@ export const SharedDependenciesProvider: React.FC = ({ children }) => { approvedAmountService, uiPoolService, uiIncentivesService, + delegationTokenService, }} > {children} diff --git a/src/ui-config/queries.ts b/src/ui-config/queries.ts index ecc3ad1737..ebd5288ccd 100644 --- a/src/ui-config/queries.ts +++ b/src/ui-config/queries.ts @@ -12,10 +12,10 @@ export const queryKeysFactory = { marketData.market, ], user: (user: string) => [user], - powers: (user: string, marketData: MarketDataType) => [ + powers: (user: string, chainId: number) => [ ...queryKeysFactory.governance, ...queryKeysFactory.user(user), - ...queryKeysFactory.market(marketData), + chainId, 'powers', ], voteOnProposal: (user: string, proposalId: number, marketData: MarketDataType) => [ @@ -92,6 +92,18 @@ export const queryKeysFactory = { spender, 'approvedAmount', ], + tokenPowers: (user: string, token: string, chainId: number) => [ + ...queryKeysFactory.user(user), + token, + chainId, + 'tokenPowers', + ], + tokenDelegatees: (user: string, token: string, chainId: number) => [ + ...queryKeysFactory.user(user), + token, + chainId, + 'tokenDelegatees', + ], }; export const POLLING_INTERVAL = 60000; diff --git a/yarn.lock b/yarn.lock index 4219b2fc8a..08cc82b8c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@aave/contract-helpers@1.21.2-2022bac22871a85776f017f1d6214d9ecfcc5abe.0": - version "1.21.2-2022bac22871a85776f017f1d6214d9ecfcc5abe.0" - resolved "https://registry.yarnpkg.com/@aave/contract-helpers/-/contract-helpers-1.21.2-2022bac22871a85776f017f1d6214d9ecfcc5abe.0.tgz#9d6deffe48fe4ddf701f90dc99f955044470075e" - integrity sha512-Pq8DwV5cWVBZff0O6Is6uATp2ac24QKZoBj1mep9teCYXb/v7vNs41rFw4IwN8rd5SyBKI/bCFVQ/8U/JysDRA== +"@aave/contract-helpers@1.21.2-a36249a4280cf8f987643baeec8c685814f5fb2b.0": + version "1.21.2-a36249a4280cf8f987643baeec8c685814f5fb2b.0" + resolved "https://registry.yarnpkg.com/@aave/contract-helpers/-/contract-helpers-1.21.2-a36249a4280cf8f987643baeec8c685814f5fb2b.0.tgz#111db931247b44cc5bb22d7e238e35e44e60ee16" + integrity sha512-z7nMMb3BW7NsyNWZ3VzkmqGh/RVam8f8+jqyBfwiNW9nyFJrZ51CbNOloxITdM8eJcyQwWReMm1W27xHngdfyQ== dependencies: isomorphic-unfetch "^3.1.0"