Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 4 deletions src/components/WalletConnection/ReadOnlyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ import { ModalType, useModalContext } from 'src/hooks/useModal';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { useRootStore } from 'src/store/root';
import { AUTH } from 'src/utils/events';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';
import { getENSProvider } from 'src/utils/marketsAndNetworksConfig';
import { normalize } from 'viem/ens';
import { useAccount, useDisconnect } from 'wagmi';

import { BasicModal } from '../primitives/BasicModal';
import { TxModalTitle } from '../transactions/FlowCommons/TxModalTitle';

const viemClient = getENSClient();

export const ReadOnlyModal = () => {
const { disconnectAsync } = useDisconnect();
const { isConnected } = useAccount();
Expand All @@ -33,6 +31,7 @@ export const ReadOnlyModal = () => {
const { type, close } = useModalContext();
const { breakpoints } = useTheme();
const sm = useMediaQuery(breakpoints.down('sm'));
const mainnetProvider = getENSProvider();
const trackEvent = useRootStore((store) => store.trackEvent);
Comment on lines 32 to 35
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mainnetProvider is created inside the component body, so a new StaticJsonRpcProvider instance is constructed on every render. This can create unnecessary connections and makes caching harder. Prefer a module-level singleton (like other ENS helpers) or useMemo to construct it once.

Copilot uses AI. Check for mistakes.

const handleReadAddress = async (inputMockWalletAddress: string): Promise<void> => {
Expand All @@ -44,7 +43,7 @@ export const ReadOnlyModal = () => {
if (inputMockWalletAddress.slice(-4) === '.eth') {
const normalizedENS = normalize(inputMockWalletAddress);
// Attempt to resolve ENS name and use resolved address if valid
const resolvedAddress = await viemClient.getEnsAddress({ name: normalizedENS });
const resolvedAddress = await mainnetProvider.resolveName(normalizedENS);
if (resolvedAddress && utils.isAddress(resolvedAddress)) {
saveAndClose(resolvedAddress);
} else {
Comment on lines +46 to 49
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveName is awaited without try/catch. If the RPC call fails, the handler will throw and the UI won't set validAddressError, leaving the modal in a broken state. Catch resolution errors and treat them as an invalid ENS (set the error state and avoid throwing).

Suggested change
const resolvedAddress = await mainnetProvider.resolveName(normalizedENS);
if (resolvedAddress && utils.isAddress(resolvedAddress)) {
saveAndClose(resolvedAddress);
} else {
try {
const resolvedAddress = await mainnetProvider.resolveName(normalizedENS);
if (resolvedAddress && utils.isAddress(resolvedAddress)) {
saveAndClose(resolvedAddress);
} else {
setValidAddressError(true);
}
} catch {
// Treat resolution errors as invalid ENS

Copilot uses AI. Check for mistakes.
Expand Down
6 changes: 2 additions & 4 deletions src/components/transactions/Bridge/BridgeDestinationInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
import { isAddress } from 'ethers/lib/utils';
import { useEffect, useState } from 'react';
import { useIsContractAddress } from 'src/hooks/useIsContractAddress';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';

const viemClient = getENSClient();
import { getENSProvider } from 'src/utils/marketsAndNetworksConfig';

export const BridgeDestinationInput = ({
connectedAccount,
Expand Down Expand Up @@ -51,7 +49,7 @@ export const BridgeDestinationInput = ({
useEffect(() => {
const checkENS = async () => {
setValidatingENS(true);
const resolvedAddress = await viemClient.getEnsAddress({ name: destinationAccount });
const resolvedAddress = await getENSProvider().resolveName(destinationAccount);
if (resolvedAddress) {
setDestinationAccount(resolvedAddress.toLowerCase());
}
Expand Down
36 changes: 22 additions & 14 deletions src/hooks/governance/useGovernanceProposals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChainId } from '@aave/contract-helpers';
import { normalizeBN } from '@aave/math-utils';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { constants } from 'ethers';
import { constants, Contract } from 'ethers';
import { gql } from 'graphql-request';
import {
adaptCacheProposalToDetail,
Expand All @@ -26,7 +26,7 @@ import {
import { useRootStore } from 'src/store/root';
import { governanceV3Config } from 'src/ui-config/governanceConfig';
import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';
import { getProvider } from 'src/utils/marketsAndNetworksConfig';
import { subgraphRequest } from 'src/utils/subgraphRequest';

import { getProposal } from './useProposal';
Expand All @@ -42,7 +42,7 @@ const USE_GOVERNANCE_CACHE = process.env.NEXT_PUBLIC_USE_GOVERNANCE_CACHE === 't
const PAGE_SIZE = 10;
const VOTES_PAGE_SIZE = 50;
const SEARCH_RESULTS_LIMIT = 10;
const viemClient = getENSClient();
export const ENS_REVERSE_REGISTRAR = '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C';

// ============================================
// Subgraph search query
Expand Down Expand Up @@ -78,6 +78,16 @@ const getProposalVotesQuery = gql`
}
`;

const ensAbi = [
{
inputs: [{ internalType: 'address[]', name: 'addresses', type: 'address[]' }],
name: 'getNames',
outputs: [{ internalType: 'string[]', name: 'r', type: 'string[]' }],
stateMutability: 'view',
type: 'function',
},
];

type SubgraphVote = {
proposalId: string;
support: boolean;
Expand Down Expand Up @@ -325,14 +335,13 @@ export const useGovernanceVotersSplit = (
queryFn: async () => {
const votes = await fetchSubgraphVotes(proposalId, votingChainId as ChainId);
try {
const ensNames = await Promise.all(
votes.map((v) =>
viemClient.getEnsName({ address: v.voter as `0x${string}` }).catch(() => null)
)
);
const provider = getProvider(governanceV3Config.coreChainId);
const contract = new Contract(ENS_REVERSE_REGISTRAR, ensAbi);
const connectedContract = contract.connect(provider);
const ensNames: string[] = await connectedContract.getNames(votes.map((v) => v.voter));
return votes.map((vote, i) => ({
...vote,
ensName: ensNames[i] ?? undefined,
ensName: ensNames[i] || undefined,
}));
} catch {
return votes;
Expand All @@ -355,11 +364,10 @@ export const useGovernanceVotersSplit = (

const { data: cacheEnsNames } = useQuery({
queryFn: async () => {
const names = await Promise.all(
cacheVoterAddresses.map((addr) =>
viemClient.getEnsName({ address: addr as `0x${string}` }).catch(() => null)
)
);
const provider = getProvider(governanceV3Config.coreChainId);
const contract = new Contract(ENS_REVERSE_REGISTRAR, ensAbi);
const connectedContract = contract.connect(provider);
const names: string[] = await connectedContract.getNames(cacheVoterAddresses);
const map: Record<string, string> = {};
cacheVoterAddresses.forEach((addr, i) => {
if (names[i]) map[addr.toLowerCase()] = names[i];
Expand Down
32 changes: 23 additions & 9 deletions src/hooks/governance/useProposalVotes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ChainId } from '@aave/contract-helpers';
import { normalizeBN } from '@aave/math-utils';
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { Contract } from 'ethers';
import { gql } from 'graphql-request';
import { governanceV3Config } from 'src/ui-config/governanceConfig';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';
import { getProvider } from 'src/utils/marketsAndNetworksConfig';
import { subgraphRequest } from 'src/utils/subgraphRequest';

import { ENS_REVERSE_REGISTRAR } from './useGovernanceProposals';

export type ProposalVote = {
proposalId: string;
support: boolean;
Expand All @@ -24,7 +27,20 @@ export interface ProposalVotes {
isFetching: boolean;
}

const viemClient = getENSClient();
const abi = [
{
inputs: [{ internalType: 'contract ENS', name: '_ens', type: 'address' }],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
inputs: [{ internalType: 'address[]', name: 'addresses', type: 'address[]' }],
name: 'getNames',
outputs: [{ internalType: 'string[]', name: 'r', type: 'string[]' }],
stateMutability: 'view',
type: 'function',
},
];

const getProposalVotes = gql`
query getProposalVotes($proposalId: Int!) {
Expand Down Expand Up @@ -55,13 +71,11 @@ const fetchProposalVotes = async (
}));
};

const fetchProposalVotesEnsNames = async (addresses: string[]): Promise<string[]> => {
const names = await Promise.all(
addresses.map((addr) =>
viemClient.getEnsName({ address: addr as `0x${string}` }).catch(() => null)
)
);
return names.map((name) => name ?? '');
const fetchProposalVotesEnsNames = async (addresses: string[]) => {
const provider = getProvider(governanceV3Config.coreChainId);
const contract = new Contract(ENS_REVERSE_REGISTRAR, abi);
const connectedContract = contract.connect(provider);
return connectedContract.getNames(addresses) as Promise<string[]>;
Comment on lines +74 to +78
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchProposalVotesEnsNames now performs a single getNames call without any error handling. If the RPC call fails/reverts (rate limits, payload too large, etc.), the entire useProposalVotesQuery will reject and votes won't render. Wrap the ENS fetch in try/catch and fall back to empty strings/undefined, and consider chunking/deduping large address lists to avoid oversized eth_call payloads.

Copilot uses AI. Check for mistakes.
};

export const useProposalVotesQuery = ({
Expand Down
21 changes: 14 additions & 7 deletions src/libs/hooks/use-get-ens.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { blo } from 'blo';
import { utils } from 'ethers';
import { useEffect, useState } from 'react';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';
import { getENSProvider } from 'src/utils/marketsAndNetworksConfig';

const viemClient = getENSClient();
const mainnetProvider = getENSProvider();

interface EnsResponse {
name?: string;
Expand All @@ -12,20 +13,26 @@ interface EnsResponse {
const useGetEns = (address: string): EnsResponse => {
const [ensName, setEnsName] = useState<string | undefined>(undefined);
const [ensAvatar, setEnsAvatar] = useState<string | undefined>(undefined);

const getName = async (address: string) => {
try {
const name = await viemClient.getEnsName({ address: address as `0x${string}` });
setEnsName(name ?? undefined);
const name = await mainnetProvider.lookupAddress(address);
setEnsName(name ? name : undefined);
} catch (error) {
console.error('ENS name lookup error', error);
}
};

const getAvatar = async (name: string) => {
try {
const avatar = await viemClient.getEnsAvatar({ name });
setEnsAvatar(avatar ?? blo(address as `0x${string}`));
const labelHash = utils.keccak256(utils.toUtf8Bytes(name?.replace('.eth', '')));
const result: { background_image: string } = await (
await fetch(
`https://metadata.ens.domains/mainnet/0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85/${labelHash}/`
)
).json();
setEnsAvatar(
result && result.background_image ? result.background_image : blo(address as `0x${string}`)
);
Comment on lines 25 to +35
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAvatar is building an ENS NFT metadata URL and reading background_image, which is not the ENS avatar text-record and will likely return unrelated data (or fail) for many names/subdomains/TLDs. Ethers providers support ENS avatars directly (e.g., provider.getAvatar(name)), which matches the previous behavior more closely. Also, name.replace('.eth', '') is not anchored and can transform unexpected inputs.

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error('ENS avatar lookup error', error);
}
Expand Down
14 changes: 1 addition & 13 deletions src/modules/governance/proposal/VotersListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ExternalLinkIcon } from '@heroicons/react/solid';
import { Avatar, Box, SvgIcon, Typography } from '@mui/material';
import { blo } from 'blo';
import React, { useEffect, useState } from 'react';
import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
import { Link } from 'src/components/primitives/Link';
import { VoteDisplay } from 'src/modules/governance/types';
Expand All @@ -22,13 +21,6 @@ type VotersListItemProps = {
export const VotersListItem = ({ compact, voter }: VotersListItemProps): JSX.Element | null => {
const { voter: address, ensName } = voter;
const blockieAvatar = blo(address !== '' ? (address as `0x${string}`) : '0x');
const [avatar, setAvatar] = useState(blockieAvatar);

useEffect(() => {
if (ensName) {
setAvatar(`https://metadata.ens.domains/mainnet/avatar/${ensName}`);
}
}, [ensName]);
const trackEvent = useRootStore((store) => store.trackEvent);

const displayName = (name?: string) => {
Expand Down Expand Up @@ -63,11 +55,7 @@ export const VotersListItem = ({ compact, voter }: VotersListItemProps): JSX.Ele
<Box sx={{ my: 6, '&:first-of-type': { mt: 0 }, '&:last-of-type': { mb: 0 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center' }}>
<Avatar
src={avatar}
sx={{ width: 24, height: 24, mr: 2 }}
slotProps={{ img: { onError: () => setAvatar(blockieAvatar) } }}
/>
<Avatar src={blockieAvatar} sx={{ width: 24, height: 24, mr: 2 }} />
<Link
href={`https://etherscan.io/address/${address}`}
onClick={() =>
Expand Down
8 changes: 4 additions & 4 deletions src/store/utils/domain-fetchers/ens.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { DomainType, WalletDomain } from 'src/store/walletDomains';
import { getENSClient } from 'src/utils/marketsAndNetworksConfig';
import { getENSProvider } from 'src/utils/marketsAndNetworksConfig';
import { tFetch } from 'src/utils/tFetch';

const viemClient = getENSClient();
const mainnetProvider = getENSProvider();

const getEnsName = async (address: string): Promise<string | null> => {
try {
const name = await viemClient.getEnsName({ address: address as `0x${string}` });
return name ?? null;
const name = await mainnetProvider.lookupAddress(address);
return name;
} catch (error) {
console.error('ENS name lookup error', error);
}
Expand Down
13 changes: 4 additions & 9 deletions src/utils/marketsAndNetworksConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ChainId, ChainIdToNetwork } from '@aave/contract-helpers';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { ProviderWithSend } from 'src/components/transactions/GovVote/temporary/VotingMachineService';
import { createPublicClient, http, PublicClient } from 'viem';
import { mainnet } from 'viem/chains';

import {
CustomMarket,
Expand Down Expand Up @@ -190,13 +188,10 @@ export const getProvider = (chainId: ChainId): ProviderWithSend => {
return providers[chainId];
};

export const getENSClient = (): PublicClient => {
const config = getNetworkConfig(ChainId.mainnet);
return createPublicClient({
chain: mainnet,
transport: http(config.publicJsonRPCUrl[0]),
batch: { multicall: true },
});
export const getENSProvider = () => {
const chainId = 1;
const config = getNetworkConfig(chainId);
return new StaticJsonRpcProvider(config.publicJsonRPCUrl[0], chainId);
};
Comment on lines +191 to 195
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getENSProvider hard-codes chainId = 1 and always uses publicJsonRPCUrl[0], bypassing the existing provider caching/rotation logic in getProvider. This reduces resilience (no RPC failover) and introduces a magic number. Consider using ChainId.mainnet and returning a cached mainnet provider (e.g., reusing getProvider(ChainId.mainnet) or a module-level singleton) so ENS lookups benefit from the same fallback behavior.

Copilot uses AI. Check for mistakes.

const ammDisableProposal = 'https://governance-v2.aave.com/governance/proposal/44';
Expand Down
Loading