minimize-pragma.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const graphlib = require('graphlib');
  4. const semver = require('semver');
  5. const pLimit = require('p-limit').default;
  6. const { hideBin } = require('yargs/helpers');
  7. const yargs = require('yargs/yargs');
  8. const getContractsMetadata = require('./get-contracts-metadata');
  9. const { versions: allSolcVersions, compile } = require('./solc-versions');
  10. const {
  11. argv: { pattern, skipPatterns, minVersionForContracts, minVersionForInterfaces, concurrency, _: artifacts },
  12. } = yargs(hideBin(process.argv))
  13. .env('')
  14. .options({
  15. pattern: { alias: 'p', type: 'string', default: 'contracts/**/*.sol' },
  16. skipPatterns: { alias: 's', type: 'string', default: 'contracts/mocks/**/*.sol' },
  17. minVersionForContracts: { type: 'string', default: '0.8.20' },
  18. minVersionForInterfaces: { type: 'string', default: '0.0.0' },
  19. concurrency: { alias: 'c', type: 'number', default: 8 },
  20. });
  21. // limit concurrency
  22. const limit = pLimit(concurrency);
  23. /********************************************************************************************************************
  24. * HELPERS *
  25. ********************************************************************************************************************/
  26. /**
  27. * Updates the pragma in the given file to the newPragma version.
  28. * @param {*} file Absolute path to the file to update.
  29. * @param {*} pragma New pragma version to set. (ex: '>=0.8.4')
  30. */
  31. const updatePragma = (file, pragma) =>
  32. fs.writeFileSync(
  33. file,
  34. fs.readFileSync(file, 'utf8').replace(/pragma solidity [><=^]*[0-9]+.[0-9]+.[0-9]+;/, `pragma solidity ${pragma};`),
  35. 'utf8',
  36. );
  37. /**
  38. * Get the applicable pragmas for a given file by compiling it with all solc versions.
  39. * @param {*} file Absolute path to the file to compile.
  40. * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5'])
  41. * @returns {Promise<string[]>} List of applicable pragmas.
  42. */
  43. const getApplicablePragmas = (file, candidates = allSolcVersions) =>
  44. Promise.all(
  45. candidates.map(version =>
  46. limit(() =>
  47. compile(file, version).then(
  48. () => version,
  49. () => null,
  50. ),
  51. ),
  52. ),
  53. ).then(versions => versions.filter(Boolean));
  54. /**
  55. * Get the minimum applicable pragmas for a given file.
  56. * @param {*} file Absolute path to the file to compile.
  57. * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5'])
  58. * @returns {Promise<string>} Smallest applicable pragma out of the list.
  59. */
  60. const getMinimalApplicablePragma = (file, candidates = allSolcVersions) =>
  61. getApplicablePragmas(file, candidates).then(valid => {
  62. if (valid.length == 0) {
  63. throw new Error(`No valid pragma found for ${file}`);
  64. } else {
  65. return valid.sort(semver.compare).at(0);
  66. }
  67. });
  68. /**
  69. * Get the minimum applicable pragmas for a given file, and update the file to use it.
  70. * @param {*} file Absolute path to the file to compile.
  71. * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5'])
  72. * @param {*} prefix Prefix to use when building the pragma (ex: '^')
  73. * @returns {Promise<string>} Version that was used and set in the file
  74. */
  75. const setMinimalApplicablePragma = (file, candidates = allSolcVersions, prefix = '>=') =>
  76. getMinimalApplicablePragma(file, candidates)
  77. .then(version => `${prefix}${version}`)
  78. .then(pragma => {
  79. updatePragma(file, pragma);
  80. return pragma;
  81. });
  82. /********************************************************************************************************************
  83. * MAIN *
  84. ********************************************************************************************************************/
  85. // Build metadata from artifact files (hardhat compilation)
  86. const metadata = getContractsMetadata(pattern, skipPatterns, artifacts);
  87. // Build dependency graph
  88. const graph = new graphlib.Graph({ directed: true });
  89. Object.keys(metadata).forEach(file => {
  90. graph.setNode(file);
  91. metadata[file].sources.forEach(dep => graph.setEdge(dep, file));
  92. });
  93. // Weaken all pragma to allow exploration
  94. Object.keys(metadata).forEach(file => updatePragma(file, '>=0.0.0'));
  95. // Do a topological traversal of the dependency graph, minimizing pragma for each file we encounter
  96. (async () => {
  97. const queue = graph.sources();
  98. const pragmas = {};
  99. while (queue.length) {
  100. const file = queue.shift();
  101. if (!Object.hasOwn(pragmas, file)) {
  102. if (Object.hasOwn(metadata, file)) {
  103. const minVersion = metadata[file].interface ? minVersionForInterfaces : minVersionForContracts;
  104. const parentsPragmas = graph
  105. .predecessors(file)
  106. .map(file => pragmas[file])
  107. .filter(Boolean);
  108. const candidates = allSolcVersions.filter(
  109. v => semver.gte(v, minVersion) && parentsPragmas.every(p => semver.satisfies(v, p)),
  110. );
  111. const pragmaPrefix = metadata[file].interface ? '>=' : '^';
  112. process.stdout.write(
  113. `[${Object.keys(pragmas).length + 1}/${Object.keys(metadata).length}] Searching minimal version for ${file} ... `,
  114. );
  115. const pragma = await setMinimalApplicablePragma(file, candidates, pragmaPrefix);
  116. console.log(pragma);
  117. pragmas[file] = pragma;
  118. }
  119. queue.push(...graph.successors(file));
  120. }
  121. }
  122. })();