diff --git a/pages/governance/ipfs-preview.governance.tsx b/pages/governance/ipfs-preview.governance.tsx index ed99c14cbc..ec41670e42 100644 --- a/pages/governance/ipfs-preview.governance.tsx +++ b/pages/governance/ipfs-preview.governance.tsx @@ -15,10 +15,11 @@ export default function IpfsPreview() { const [ipfs, setIpfs] = useState(); async function fetchIpfs() { + const proposalMetadata = await getProposalMetadata(ipfsHash, governanceConfig.ipfsGateway); const newIpfs = { id: -1, originalHash: ipfsHash, - ...(await getProposalMetadata(ipfsHash, governanceConfig.ipfsGateway)), + ...proposalMetadata, }; setIpfs(newIpfs); } diff --git a/pages/governance/proposal/index.governance.tsx b/pages/governance/proposal/index.governance.tsx index 858095d56e..170b5aa14c 100644 --- a/pages/governance/proposal/index.governance.tsx +++ b/pages/governance/proposal/index.governance.tsx @@ -19,12 +19,12 @@ export default function DynamicProposal() { const [ipfs, setIpfs] = useState(); const [fetchMetadataError, setFetchMetadataError] = useState(false); - // TODO: Abstract out this recursive try/fallback approach so it may be used in other places where we fetch the proposal metadata and may have errors from the initial gateway - async function initialize(_ipfsGateway: string, _useFallback: boolean) { + async function initialize(_ipfsGateway: string) { + const { values, ...rest } = await governanceContract.getProposal({ proposalId: id }); + const proposal = await enhanceProposalWithTimes(rest); + setProposal(proposal); + try { - const { values, ...rest } = await governanceContract.getProposal({ proposalId: id }); - const proposal = await enhanceProposalWithTimes(rest); - setProposal(proposal); const ipfsMetadata = await getProposalMetadata(proposal.ipfsHash, _ipfsGateway); const newIpfs = { id, @@ -33,32 +33,12 @@ export default function DynamicProposal() { }; setIpfs(newIpfs); } catch (e) { - // Recursion - Try again once with our fallback API - // Base case: If we are retrying with our fallback and it fails, return - // Rescursive case: If we haven't retried with our fallback yet, try it once - if (_useFallback) { - console.groupCollapsed('Fetching proposal metadata from IPFS...'); - console.info('failed with', _ipfsGateway); - console.info('exiting'); - console.error(e); - console.groupEnd(); - // To prevent continually adding onto the callstack with failed requests, return and show an error message in the UI - setFetchMetadataError(true); - return; - } else { - const fallback = governanceConfig.fallbackIpfsGateway; - console.groupCollapsed('Fetching proposal metadata from IPFS...'); - console.info('failed with', _ipfsGateway); - console.info('retrying with', fallback); - console.error(e); - console.groupEnd(); - initialize(fallback, true); - } + setFetchMetadataError(true); } } useEffect(() => { - id && initialize(governanceConfig.ipfsGateway, false); + id && initialize(governanceConfig.ipfsGateway); }, [id]); return ; diff --git a/src/modules/governance/ProposalsList.tsx b/src/modules/governance/ProposalsList.tsx index a0f02c9a77..126090352f 100644 --- a/src/modules/governance/ProposalsList.tsx +++ b/src/modules/governance/ProposalsList.tsx @@ -39,11 +39,15 @@ export function ProposalsList({ proposals: initialProposals }: GovernancePagePro for (let i = proposals.length; i < count; i++) { const { values, ...rest } = await governanceContract.getProposal({ proposalId: i }); const proposal = await enhanceProposalWithTimes(rest); + const proposalMetadata = await getProposalMetadata( + proposal.ipfsHash, + governanceConfig.ipfsGateway + ); nextProposals.push({ ipfs: { id: i, originalHash: proposal.ipfsHash, - ...(await getProposalMetadata(proposal.ipfsHash, governanceConfig.ipfsGateway)), + ...proposalMetadata, }, proposal: proposal, prerendered: false, diff --git a/src/modules/governance/utils/getProposalMetadata.ts b/src/modules/governance/utils/getProposalMetadata.ts index 84e47280c7..92a5157197 100644 --- a/src/modules/governance/utils/getProposalMetadata.ts +++ b/src/modules/governance/utils/getProposalMetadata.ts @@ -2,31 +2,76 @@ import { ProposalMetadata } from '@aave/contract-helpers'; import { base58 } from 'ethers/lib/utils'; import matter from 'gray-matter'; import fetch from 'isomorphic-unfetch'; +import { governanceConfig } from 'src/ui-config/governanceConfig'; +type MemorizeMetadata = Record; +const MEMORIZE: MemorizeMetadata = {}; + +/** + * Composes a URI based off of a given IPFS CID hash and gateway + * @param {string} hash - The IPFS CID hash + * @param {string} gateway - The IPFS gateway host + * @returns string + */ export function getLink(hash: string, gateway: string): string { return `${gateway}/${hash}`; } -type MemorizeMetadata = Record; - -const MEMORIZE: MemorizeMetadata = {}; +/** + * Fetches the IPFS metadata JSON from our preferred public gateway, once. + * If the gateway fails, attempt to fetch recursively with a fallback gateway, once. + * If the fallback fails, throw an error. + * @param {string} hash - The IPFS CID hash to query + * @param {string} gateway - The IPFS gateway host + * @returns Promise + */ export async function getProposalMetadata( hash: string, gateway: string ): Promise { + try { + return await fetchFromIpfs(hash, gateway); + } catch (e) { + console.groupCollapsed('Fetching proposal metadata from IPFS...'); + console.info('failed with', gateway); + + // Primary gateway failed, retry with fallback + if (gateway === governanceConfig.ipfsGateway) { + console.info('retrying with', governanceConfig.fallbackIpfsGateway); + console.error(e); + console.groupEnd(); + return getProposalMetadata(hash, governanceConfig.fallbackIpfsGateway); + } + + // Fallback gateway failed, exit + console.info('exiting'); + console.error(e); + console.groupEnd(); + throw e; + } +} + +/** + * Fetches data from a provided IPFS gateway with a simple caching mechanism. + * Cache keys are the hashes, values are ProposalMetadata objects. + * The cache does not implement any invalidation mechanisms nor sets expiries. + * @param {string} hash - The IPFS CID hash to query + * @param {string} gateway - The IPFS gateway host + * @returns Promise + */ +async function fetchFromIpfs(hash: string, gateway: string): Promise { + // Read from cache const ipfsHash = hash.startsWith('0x') ? base58.encode(Buffer.from(`1220${hash.slice(2)}`, 'hex')) : hash; if (MEMORIZE[ipfsHash]) return MEMORIZE[ipfsHash]; - const ipfsResponse: Response = await fetch(getLink(ipfsHash, gateway), { - headers: { - 'Content-Type': 'application/json', - }, - }); - if (!ipfsResponse.ok) { - throw Error('Fetch not working'); - } + + // Fetch + const ipfsResponse: Response = await fetch(getLink(ipfsHash, gateway)); + if (!ipfsResponse.ok) throw Error('Fetch not working'); const clone = await ipfsResponse.clone(); + + // Store in cache try { const response: ProposalMetadata = await ipfsResponse.json(); const { content, data } = matter(response.description); diff --git a/src/ui-config/governanceConfig.ts b/src/ui-config/governanceConfig.ts index f97c1a3b66..28deb75d63 100644 --- a/src/ui-config/governanceConfig.ts +++ b/src/ui-config/governanceConfig.ts @@ -42,6 +42,6 @@ export const governanceConfig: GovernanceConfig = { AAVE_GOVERNANCE_V2_EXECUTOR_LONG: '0xEE56e2B3D491590B5b31738cC34d5232F378a8D5', AAVE_GOVERNANCE_V2_HELPER: '0x16ff7583ea21055bf5f929ec4b896d997ff35847', }, - ipfsGateway: 'https://gateway.pinata.cloud/ipfs', - fallbackIpfsGateway: 'https://cloudflare-ipfs.com/ipfs', + ipfsGateway: 'https://cloudflare-ipfs.com/ipfs', + fallbackIpfsGateway: 'https://ipfs.io/ipfs', };