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
3 changes: 2 additions & 1 deletion pages/governance/ipfs-preview.governance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ export default function IpfsPreview() {
const [ipfs, setIpfs] = useState<IpfsType>();

async function fetchIpfs() {
const proposalMetadata = await getProposalMetadata(ipfsHash, governanceConfig.ipfsGateway);
const newIpfs = {
id: -1,
originalHash: ipfsHash,
...(await getProposalMetadata(ipfsHash, governanceConfig.ipfsGateway)),
...proposalMetadata,
};
setIpfs(newIpfs);
}
Expand Down
34 changes: 7 additions & 27 deletions pages/governance/proposal/index.governance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export default function DynamicProposal() {
const [ipfs, setIpfs] = useState<IpfsType>();
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,
Expand All @@ -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 <ProposalPage ipfs={ipfs} proposal={proposal} metadataError={fetchMetadataError} />;
Expand Down
6 changes: 5 additions & 1 deletion src/modules/governance/ProposalsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 56 additions & 11 deletions src/modules/governance/utils/getProposalMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ProposalMetadata>;
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<string, ProposalMetadata>;

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<ProposalMetadata> {
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<ProposalMetadata> {
// 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);
Expand Down
4 changes: 2 additions & 2 deletions src/ui-config/governanceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};