getProposalMetadata.ts
1 import { ProposalMetadata } from '@aave/contract-helpers'; 2 import { base58 } from 'ethers/lib/utils'; 3 import matter from 'gray-matter'; 4 import fetch from 'isomorphic-unfetch'; 5 import { fallbackIpfsGateway, ipfsGateway } from 'src/ui-config/governanceConfig'; 6 7 type MemorizeMetadata = Record<string, ProposalMetadata>; 8 const MEMORIZE: MemorizeMetadata = {}; 9 10 /** 11 * Composes a URI based off of a given IPFS CID hash and gateway 12 * @param {string} hash - The IPFS CID hash 13 * @param {string} gateway - The IPFS gateway host 14 * @returns string 15 */ 16 export function getLink(hash: string, gateway: string): string { 17 return `${gateway}/${hash}`; 18 } 19 20 /** 21 * Fetches the IPFS metadata JSON from our preferred public gateway, once. 22 * If the gateway fails, attempt to fetch recursively with a fallback gateway, once. 23 * If the fallback fails, throw an error. 24 * @param {string} hash - The IPFS CID hash to query 25 * @param {string} gateway - The IPFS gateway host 26 * @returns Promise 27 */ 28 export async function getProposalMetadata( 29 hash: string, 30 gateway: string 31 ): Promise<ProposalMetadata> { 32 try { 33 return await fetchFromIpfs(hash, gateway); 34 } catch (e) { 35 console.groupCollapsed('Fetching proposal metadata from IPFS...'); 36 console.info('failed with', gateway); 37 38 // Primary gateway failed, retry with fallback 39 if (gateway === ipfsGateway) { 40 console.info('retrying with', fallbackIpfsGateway); 41 console.error(e); 42 console.groupEnd(); 43 return getProposalMetadata(hash, fallbackIpfsGateway); 44 } 45 46 // Fallback gateway failed, exit 47 console.info('exiting'); 48 console.error(e); 49 console.groupEnd(); 50 throw e; 51 } 52 } 53 54 export async function parseRawIpfs(rawIpfsContent: string, hash: string) { 55 const ipfsHash = hash.startsWith('0x') 56 ? base58.encode(Buffer.from(`1220${hash.slice(2)}`, 'hex')) 57 : hash; 58 if (MEMORIZE[ipfsHash]) return MEMORIZE[ipfsHash]; 59 try { 60 const response: ProposalMetadata = await JSON.parse(rawIpfsContent); 61 const { content, data } = matter(response.description); 62 MEMORIZE[ipfsHash] = { 63 ...response, 64 ipfsHash, 65 description: content, 66 ...data, 67 }; 68 } catch (e) { 69 const { content, data } = matter(rawIpfsContent); 70 MEMORIZE[ipfsHash] = { 71 ...(data as ProposalMetadata), 72 ipfsHash, 73 description: content, 74 }; 75 } 76 return MEMORIZE[ipfsHash]; 77 } 78 79 /** 80 * Fetches data from a provided IPFS gateway with a simple caching mechanism. 81 * Cache keys are the hashes, values are ProposalMetadata objects. 82 * The cache does not implement any invalidation mechanisms nor sets expiries. 83 * @param {string} hash - The IPFS CID hash to query 84 * @param {string} gateway - The IPFS gateway host 85 * @returns Promise 86 */ 87 async function fetchFromIpfs(hash: string, gateway: string): Promise<ProposalMetadata> { 88 // Read from cache 89 const ipfsHash = hash.startsWith('0x') 90 ? base58.encode(Buffer.from(`1220${hash.slice(2)}`, 'hex')) 91 : hash; 92 if (MEMORIZE[ipfsHash]) return MEMORIZE[ipfsHash]; 93 94 // Fetch 95 const ipfsResponse: Response = await fetch(getLink(ipfsHash, gateway)); 96 if (!ipfsResponse.ok) throw Error('Fetch not working'); 97 const clone = await ipfsResponse.clone(); 98 99 // Store in cache 100 try { 101 const response: ProposalMetadata = await ipfsResponse.json(); 102 const { content, data } = matter(response.description); 103 MEMORIZE[ipfsHash] = { 104 ...response, 105 ipfsHash, 106 description: content, 107 ...data, 108 }; 109 } catch (e) { 110 const text = await clone.text(); 111 const { content, data } = matter(text); 112 MEMORIZE[ipfsHash] = { 113 ...(data as ProposalMetadata), 114 ipfsHash, 115 description: content, 116 }; 117 } 118 return MEMORIZE[ipfsHash]; 119 }