/ src / modules / governance / utils / getProposalMetadata.ts
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  }