generate-snapshots.ts 4.75 KB
Newer Older
1 2 3
import fs from 'fs'
import path from 'path'

4 5 6
const root = path.join(__dirname, '..', '..')
const outdir = process.argv[2] || path.join(root, 'snapshots')
const forgeArtifactsDir = path.join(root, 'forge-artifacts')
7

8
const getAllContractsSources = (): Array<string> => {
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
  const paths = []
  const readFilesRecursively = (dir: string) => {
    const files = fs.readdirSync(dir)

    for (const file of files) {
      const filePath = path.join(dir, file)
      const fileStat = fs.statSync(filePath)

      if (fileStat.isDirectory()) {
        readFilesRecursively(filePath)
      } else {
        paths.push(filePath)
      }
    }
  }
24
  readFilesRecursively(path.join(root, 'src'))
25 26

  return paths
27
    .filter((x) => x.endsWith('.sol'))
28
    .map((p: string) => path.basename(p))
29 30 31
    .sort()
}

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
type ForgeArtifact = {
  abi: object
  ast: {
    nodeType: string
    nodes: any[]
  }
  storageLayout: {
    storage: [{ type: string; label: string; offset: number; slot: number }]
    types: { [key: string]: { label: string; numberOfBytes: number } }
  }
  bytecode: {
    object: string
  }
}

47
type AbiSpecStorageLayoutEntry = {
inphi's avatar
inphi committed
48
  label: string
49 50
  slot: number
  offset: number
51
  bytes: number
52
  type: string
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
}
const sortKeys = (obj: any) => {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  return Object.keys(obj)
    .sort()
    .reduce(
      (acc, key) => {
        acc[key] = sortKeys(obj[key])
        return acc
      },
      Array.isArray(obj) ? [] : {}
    )
}

69 70 71
// ContractName.0.9.8.json -> ContractName.sol
// ContractName.json -> ContractName.sol
const parseArtifactName = (artifactVersionFile: string): string => {
inphi's avatar
inphi committed
72
  const match = artifactVersionFile.match(/(.*?)\.([0-9]+\.[0-9]+\.[0-9]+)?/)
73
  if (!match) {
inphi's avatar
inphi committed
74
    throw new Error(`Invalid artifact file name: ${artifactVersionFile}`)
75
  }
inphi's avatar
inphi committed
76
  return match[1]
77 78
}

79
const main = async () => {
80
  console.log(`writing abi and storage layout snapshots to ${outdir}`)
inphi's avatar
inphi committed
81 82 83

  const storageLayoutDir = path.join(outdir, 'storageLayout')
  const abiDir = path.join(outdir, 'abi')
84 85
  fs.rmSync(storageLayoutDir, { recursive: true })
  fs.rmSync(abiDir, { recursive: true })
inphi's avatar
inphi committed
86 87
  fs.mkdirSync(storageLayoutDir, { recursive: true })
  fs.mkdirSync(abiDir, { recursive: true })
88

89 90 91 92 93 94 95 96 97 98 99 100
  const contractSources = getAllContractsSources()
  const knownAbis = {}

  for (const contractFile of contractSources) {
    const contractArtifacts = path.join(forgeArtifactsDir, contractFile)
    for (const name of fs.readdirSync(contractArtifacts)) {
      const data = fs.readFileSync(path.join(contractArtifacts, name))
      const artifact: ForgeArtifact = JSON.parse(data.toString())

      const contractName = parseArtifactName(name)

      // HACK: This is a hack to ignore libraries and abstract contracts. Not robust against changes to solc's internal ast repr
101 102 103 104 105
      if (artifact.ast === undefined) {
        throw new Error(
          "ast isn't present in forge-artifacts. Did you run forge build with `--ast`?"
        )
      }
106 107 108 109 110 111 112 113 114 115 116 117 118
      const isContract = artifact.ast.nodes.some((node: any) => {
        return (
          node.nodeType === 'ContractDefinition' &&
          node.name === contractName &&
          node.contractKind === 'contract' &&
          (node.abstract === undefined || // solc < 0.6 doesn't have explicit abstract contracts
            node.abstract === false)
        )
      })
      if (!isContract) {
        console.log(`ignoring library/interface ${contractName}`)
        continue
      }
119

120 121 122 123 124 125 126 127 128 129 130 131 132 133
      const storageLayout: AbiSpecStorageLayoutEntry[] = []
      for (const storageEntry of artifact.storageLayout.storage) {
        // convert ast-based type to solidity type
        const typ = artifact.storageLayout.types[storageEntry.type]
        if (typ === undefined) {
          throw new Error(
            `undefined type for ${contractName}:${storageEntry.label}`
          )
        }
        storageLayout.push({
          label: storageEntry.label,
          bytes: typ.numberOfBytes,
          offset: storageEntry.offset,
          slot: storageEntry.slot,
134
          type: typ.label,
135 136
        })
      }
137

138 139 140 141 142 143 144
      if (knownAbis[contractName] === undefined) {
        knownAbis[contractName] = artifact.abi
      } else if (
        JSON.stringify(knownAbis[contractName]) !== JSON.stringify(artifact.abi)
      ) {
        throw Error(
          `detected multiple artifact versions with different ABIs for ${contractFile}`
inphi's avatar
inphi committed
145
        )
146 147
      } else {
        console.log(`detected multiple artifacts for ${contractName}`)
148
      }
149

150 151 152 153 154 155 156 157 158 159
      // Sort snapshots for easier manual inspection
      fs.writeFileSync(
        `${abiDir}/${contractName}.json`,
        JSON.stringify(sortKeys(artifact.abi), null, 2)
      )
      fs.writeFileSync(
        `${storageLayoutDir}/${contractName}.json`,
        JSON.stringify(sortKeys(storageLayout), null, 2)
      )
    }
160 161 162 163
  }
}

main()