generate-markdown.ts 7.5 KB
import dirtree from 'directory-tree'
import fs from 'fs'
import path from 'path'
import { predeploys } from '../src'

interface DeploymentInfo {
  folder: string
  name: string
  chainid: number
  rpc?: string
  l1Explorer?: string
  l2Explorer?: string
  notice?: string
}

const PUBLIC_DEPLOYMENTS: DeploymentInfo[] = [
  {
    folder: 'mainnet',
    name: 'Optimism (mainnet)',
    chainid: 10,
    rpc: 'https://mainnet.optimism.io',
    l1Explorer: 'https://etherscan.io',
    l2Explorer: 'https://optimistic.etherscan.io',
  },
  {
    folder: 'kovan',
    name: 'Optimism Kovan (public testnet)',
    chainid: 69,
    rpc: 'https://kovan.optimism.io',
    l1Explorer: 'https://kovan.etherscan.io',
    l2Explorer: 'https://kovan-optimistic.etherscan.io',
  },
  {
    folder: 'goerli',
    name: 'Optimism Goerli (internal devnet)',
    chainid: 420,
    notice: `Optimism Goerli is an internal Optimism development network. You're probably looking for [Optimism Kovan](../kovan#readme), the public Optimism testnet.`,
    l1Explorer: 'https://goerli.etherscan.io',
  },
]

// List of contracts that are part of a deployment but aren't meant to be used by the general
// public. E.g., implementation addresses for proxy contracts or helpers used during the
// deployment process. Although these addresses are public and users can technically try to use
// them, there's no point in doing so. As a result, we hide these addresses to avoid confusion.
const HIDDEN_CONTRACTS = [
  // Used for being able to verify the ChugSplashProxy contract.
  'L1StandardBridge_for_verification_only',
  // Implementation address for the Proxy__OVM_L1CrossDomainMessenger.
  'OVM_L1CrossDomainMessenger',
  // Utility for modifying many records in the AddressManager at the same time.
  'AddressDictator',
  // Utility for modifying a ChugSplashProxy during an upgrade.
  'ChugSplashDictator',
]

interface ContractInfo {
  name: string
  address: string
}

/**
 * Gets the full deployment folder path for a given deployment.
 *
 * @param name Deployment folder name.
 * @returns Full path to the deployment folder.
 */
const getDeploymentFolderPath = (name: string): string => {
  return path.resolve(__dirname, `../deployments/${name}`)
}

/**
 * Helper function for adding a line to a string. Avoids having to add the ugly \n to each new line
 * that you want to add a string.
 *
 * @param str String to add a line to.
 * @param line Line to add to the string.
 * @returns String with the added line and a newline at the end.
 */
const addline = (str: string, line: string): string => {
  return str + line + '\n'
}

/**
 * Generates a nicely formatted table presenting a list of contracts.
 *
 * @param contracts List of contracts to display.
 * @param explorer URL for etherscan for the network that the contracts are deployed to.
 * @returns Nicely formatted markdown-compatible table as a string.
 */
const buildContractsTable = (
  contracts: ContractInfo[],
  explorer?: string
): string => {
  // Being very verbose within this function to make it clear what's going on.
  // We use HTML instead of markdown so we can get a table that displays well on GitHub.
  // GitHub READMEs are 1012px wide. Adding a 506px image to each table header is a hack that
  // allows us to create a table where each column is 1/2 the full README width.
  let table = ``
  table = addline(table, '<table>')
  table = addline(table, '<tr>')
  table = addline(table, '<th>')
  table = addline(table, '<img width="506px" height="0px" />')
  table = addline(table, '<p><small>Contract</small></p>')
  table = addline(table, '</th>')
  table = addline(table, '<th>')
  table = addline(table, '<img width="506px" height="0px" />')
  table = addline(table, '<p><small>Address</small></p>')
  table = addline(table, '</th>')
  table = addline(table, '</tr>')

  for (const contract of contracts) {
    // Don't add records for contract addresses that aren't meant to be public-facing.
    if (HIDDEN_CONTRACTS.includes(contract.name)) {
      continue
    }

    table = addline(table, '<tr>')
    table = addline(table, '<td>')
    table = addline(table, contract.name)
    table = addline(table, '</td>')
    table = addline(table, '<td align="center">')
    if (explorer) {
      table = addline(
        table,
        `<a href="${explorer}/address/${contract.address}">`
      )
      table = addline(table, `<code>${contract.address}</code>`)
      table = addline(table, '</a>')
    } else {
      table = addline(table, `<code>${contract.address}</code>`)
    }
    table = addline(table, '</td>')
    table = addline(table, '</tr>')
  }

  table = addline(table, '</table>')
  return table
}

/**
 * Gets the list of L1 contracts for a given deployment.
 *
 * @param deployment Folder where the deployment is located.
 * @returns List of L1 contracts for thegiven deployment.
 */
const getL1Contracts = (deployment: string): ContractInfo[] => {
  const l1ContractsFolder = getDeploymentFolderPath(deployment)
  return dirtree(l1ContractsFolder)
    .children.filter((child) => {
      return child.extension === '.json'
    })
    .map((child) => {
      return {
        name: child.name.replace('.json', ''),
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        address: require(path.join(l1ContractsFolder, child.name)).address,
      }
    })
}

/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Gets the list of L2 contracts for a given deployment.
 *
 * @param deployment Folder where the deployment is located.
 * @returns List of L2 system contracts for the given deployment.
 */
const getL2Contracts = (deployment: string): ContractInfo[] => {
  // Deployment parameter is currently unused because all networks have the same predeploy
  // addresses. However, we've had situations in the past where we've had to deploy one-off
  // system contracts to a network. If we want to do that again in the future then we'll want some
  // kind of custom logic based on the network in question. Hence, the deployment parameter.
  return Object.entries(predeploys).map(([name, address]) => {
    return {
      name,
      address,
    }
  })
}
/* eslint-enable @typescript-eslint/no-unused-vars */

const main = async () => {
  for (const deployment of PUBLIC_DEPLOYMENTS) {
    let md = ``
    md = addline(md, `# ${deployment.name}`)
    if (deployment.notice) {
      md = addline(md, `## Notice`)
      md = addline(md, deployment.notice)
    }
    md = addline(md, `## Network Info`)
    md = addline(md, `- **Chain ID**: ${deployment.chainid}`)
    if (deployment.rpc) {
      md = addline(md, `- **Public RPC**: ${deployment.rpc}`)
    }
    if (deployment.l2Explorer) {
      md = addline(md, `- **Block Explorer**: ${deployment.l2Explorer}`)
    }
    md = addline(md, `## Layer 1 Contracts`)
    md = addline(
      md,
      buildContractsTable(
        getL1Contracts(deployment.folder),
        deployment.l1Explorer
      )
    )
    md = addline(md, `## Layer 2 Contracts`)
    md = addline(
      md,
      buildContractsTable(
        getL2Contracts(deployment.folder),
        deployment.l2Explorer
      )
    )

    // Write the README file for the deployment
    fs.writeFileSync(
      path.join(getDeploymentFolderPath(deployment.folder), 'README.md'),
      md
    )
  }

  let primary = ``
  primary = addline(primary, `# Optimism Deployments`)
  for (const deployment of PUBLIC_DEPLOYMENTS) {
    primary = addline(
      primary,
      `- [${deployment.name}](./${deployment.folder}#readme)`
    )
  }

  // Write the primary README file
  fs.writeFileSync(path.resolve(__dirname, '../deployments/README.md'), primary)
}

main()