diff --git a/.github/workflows/governance-checks.yaml b/.github/workflows/governance-checks.yaml index 97eec883035..81112901885 100644 --- a/.github/workflows/governance-checks.yaml +++ b/.github/workflows/governance-checks.yaml @@ -52,6 +52,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} + RPC_URL: ${{ secrets.RPC_URL }} DAO_NAME: ${{ matrix.DAO_NAME }} GOVERNOR_ADDRESS: ${{ matrix.GOVERNOR_ADDRESS }} + RUNNING_LOCALLY: 0 diff --git a/.gitignore b/.gitignore index 64326427b71..551d0deb9ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules .idea - +reports/ +.env +.env.local diff --git a/README.md b/README.md index ef634397e81..25580f2ff39 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,29 @@ including automated scripts that apply checks to live proposals to allow for better informed voting. -# Reports +## Reports -Find the reports [here](https://github.com/Uniswap/governance-seatbelt/tree/reports) \ No newline at end of file +Find the reports [here](https://github.com/Uniswap/governance-seatbelt/tree/reports) when run in CI, or in the `reports` folder if running locally + +## Development + +Create a file called `.env` with the following environment variables: + +```sh +# URL to your node, e.g. Infura or Alchemy endpoint +RPC_URL=yourNodeUrl + +# Etherscan API key: https://etherscan.io/myapikey +ETHERSCAN_API_KEY=yourEtherscanApiKey + +# Name of the DAO to check +DAO_NAME=Compound + +# Address of that DAO's governor contract +GOVERNOR_ADDRESS=0xc0Da02939E1441F497fd74F78cE7Decb17B66529 + +# Set to 1 if running locally, or 0 for CI +RUNNING_LOCALLY=1 +``` + +Other environment variables needed for CI can be found in `.github/workflows/governance-checks.yaml` diff --git a/index.ts b/index.ts index 668b010c042..b2010f5f080 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,13 @@ +require('dotenv').config() +import fs from 'fs'; import { DAO_NAME, GITHUB_REPO_NAME, GITHUB_REPO_OWNER, GOVERNOR_ADDRESS, REPORTS_BRANCH, + RUNNING_LOCALLY } from "./utils/constants"; -import { github } from "./utils/clients/github"; import { governorBravo } from "./utils/contracts/governor-bravo"; import { provider } from "./utils/clients/ethers"; import { AllCheckResults, Proposal } from "./types"; @@ -32,6 +34,7 @@ async function main() { .map((proposal) => proposal.args as unknown as Proposal); for (const proposal of activeProposals) { + console.log(`Checking proposal ${proposal.id}...`); const checkResults: AllCheckResults = Object.fromEntries( await Promise.all( Object.keys(ALL_CHECKS).map(async (checkId) => [ @@ -56,37 +59,46 @@ async function main() { : null, ]); - let sha: string | undefined; - try { - const { data } = await github.rest.repos.getContent({ + const report = toProposalReport( + { start: startBlock, end: endBlock, current: latestBlock }, + proposal, + checkResults + ); + + if (RUNNING_LOCALLY) { + // Running locally, dump to file + const dir = `./reports/${DAO_NAME}/${GOVERNOR_ADDRESS}/`; // TODO more robust way to keep this in sync with `path` + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(`${dir}/${proposal.id}.md`, report); + + } else { + // Running in CI, save to file on REPORTS_BRANCH + const { github } = await import("./utils/clients/github"); // lazy load to avoid errors about missing env vars when not in CI + let sha: string | undefined; + try { + const { data } = await github.rest.repos.getContent({ + owner: GITHUB_REPO_OWNER, + repo: GITHUB_REPO_NAME, + ref: REPORTS_BRANCH, + path, + }); + if ("sha" in data) { + sha = data.sha; + } + } catch (error) { + console.warn("Failed to get sha for file at path", path, error); + } + + await github.rest.repos.createOrUpdateFileContents({ owner: GITHUB_REPO_OWNER, repo: GITHUB_REPO_NAME, - ref: REPORTS_BRANCH, + branch: REPORTS_BRANCH, + message: `Update ${path} as of ${formattedDateTime}`, + content: Buffer.from(report, "utf-8").toString("base64"), path, + sha, }); - if ("sha" in data) { - sha = data.sha; - } - } catch (error) { - console.warn("Failed to get sha for file at path", path, error); } - - await github.rest.repos.createOrUpdateFileContents({ - owner: GITHUB_REPO_OWNER, - repo: GITHUB_REPO_NAME, - branch: REPORTS_BRANCH, - message: `Update ${path} as of ${formattedDateTime}`, - content: Buffer.from( - toProposalReport( - { start: startBlock, end: endBlock, current: latestBlock }, - proposal, - checkResults - ), - "utf-8" - ).toString("base64"), - path, - sha, - }); } catch (error) { console.error("Failed to update file contents", error); } diff --git a/package.json b/package.json index 57f9cc96dcd..83f4db246f8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@ethercast/etherscan-client": "^1.0.1", "@octokit/action": "^3.15.2", "@types/node": "^16.7.13", + "dotenv": "^10.0.0", "ethers": "^5.4.6", "ts-node": "^10.2.1", "typescript": "^4.4.2" diff --git a/utils/clients/ethers.ts b/utils/clients/ethers.ts index f3d6421b63b..1f96c53f285 100644 --- a/utils/clients/ethers.ts +++ b/utils/clients/ethers.ts @@ -1,4 +1,4 @@ import { providers } from "ethers"; -import { INFURA_API_KEY } from "../constants"; +import { RPC_URL } from "../constants"; -export const provider = new providers.InfuraProvider(1, INFURA_API_KEY); +export const provider = new providers.JsonRpcProvider(RPC_URL); diff --git a/utils/constants.ts b/utils/constants.ts index c649d165014..f27a9f86d28 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -2,7 +2,8 @@ export const REPOSITORY: string = process.env.GITHUB_REPOSITORY!; export const [GITHUB_REPO_OWNER, GITHUB_REPO_NAME] = typeof REPOSITORY === "string" ? REPOSITORY.split("/") : []; export const GOVERNOR_ADDRESS: string = process.env.GOVERNOR_ADDRESS!; -export const INFURA_API_KEY: string = process.env.INFURA_API_KEY!; +export const RPC_URL: string = process.env.RPC_URL!; export const ETHERSCAN_API_KEY: string = process.env.ETHERSCAN_API_KEY!; export const DAO_NAME: string = process.env.DAO_NAME!; export const REPORTS_BRANCH = "reports"; +export const RUNNING_LOCALLY = Boolean(process.env.RUNNING_LOCALLY) // use 1 or 0 because Boolean('false') = true \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 99db0daebc8..2312e2b749d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -578,6 +578,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + elliptic@6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"