├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── scripts │ └── rename.sh └── workflows │ ├── ci.yml │ └── create.yml ├── .gitignore ├── .gitpod.yml ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.6.1.cjs ├── .yarnrc.yml ├── README.md ├── config ├── api-keys.js └── networks.js ├── contracts └── Lock.sol ├── hardhat.config.js ├── package.json ├── scripts ├── deploy.js └── verify.js ├── tasks ├── accounts.js └── index.js ├── test └── Lock.test.js ├── utils ├── constants.js ├── contracts.js ├── files.js ├── format.js ├── misc.js ├── string.js └── verify.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.sol] 15 | indent_size = 4 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Defaults to ACCOUNT_TYPE="MNEMONIC", if you want to use private keys instead then set: 2 | # ACCOUNT_TYPE="PRIVATE_KEYS" 3 | 4 | # Your mnemonic if ACCOUNT_TYPE="MNEMONIC" (Default) 5 | MNEMONIC="here is where your twelve words mnemonic should be put my friend" 6 | 7 | # Your private keys if ACCOUNT_TYPE="PRIVATE_KEYS" 8 | PRIVATE_KEY_1="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 9 | # PRIVATE_KEY_2="" 10 | # PRIVATE_KEY_3="" 11 | # PRIVATE_KEY_4="" 12 | # PRIVATE_KEY_5="" 13 | 14 | # Your infura key 15 | INFURA_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 16 | 17 | # Block explorer API keys (Optional) 18 | ETHERSCAN_API_KEY="" 19 | BSCSCAN_API_KEY="" 20 | POLYGONSCAN_API_KEY="" 21 | OPTIMISM_API_KEY="" 22 | ARBISCAN_API_KEY="" 23 | 24 | # Gas Reporter (Optional) 25 | GAS_PRICE=20 26 | # Your coin market cap api key. 27 | # https://coinmarketcap.com/api/documentation/v1/#section/Introduction 28 | COIN_MARKET_CAP_API_KEY="" 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/* 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/docs 13 | 14 | # files 15 | *.env 16 | *.log 17 | .pnp.* 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es2020: true, 6 | jest: true, 7 | }, 8 | extends: ["eslint:recommended", "prettier"], 9 | ignorePatterns: ["node_modules/**/*"], 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | }, 14 | rules: { 15 | "func-names": "off", 16 | "import/no-dynamic-require": "off", 17 | indent: "off", 18 | "no-unused-vars": "warn", 19 | "no-extra-boolean-cast": "off", 20 | "no-constant-condition": "off", 21 | "no-async-promise-executor": "off", 22 | "no-console": "off", 23 | "prefer-destructuring": "off", 24 | "prefer-template": "off", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/scripts/rename.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca 4 | set -euo pipefail 5 | 6 | # Define the input vars 7 | GITHUB_REPOSITORY=${1?Error: Please pass username/repo, e.g. ahmedali8/hardhat-js-starterkit} 8 | GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. ahmedali8} 9 | GITHUB_REPOSITORY_DESCRIPTION=${3:-""} # If null then replace with empty string 10 | 11 | echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" 12 | echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" 13 | echo "GITHUB_REPOSITORY_DESCRIPTION: $GITHUB_REPOSITORY_DESCRIPTION" 14 | 15 | # jq is like sed for JSON data 16 | JQ_OUTPUT=`jq \ 17 | --arg NAME "@$GITHUB_REPOSITORY" \ 18 | --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ 19 | --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ 20 | --arg DESCRIPTION "$GITHUB_REPOSITORY_DESCRIPTION" \ 21 | '.name = $NAME | .description = $DESCRIPTION | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ 22 | package.json 23 | ` 24 | 25 | # Overwrite package.json 26 | echo "$JQ_OUTPUT" > package.json 27 | 28 | # Make sed command compatible in both Mac and Linux environments 29 | # Reference: https://stackoverflow.com/a/38595160/8696958 30 | sedi () { 31 | sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" 32 | } 33 | 34 | # Rename instances of "ahmedali8/hardhat-js-starterkit" to the new repo name in README.md for badges only 35 | sedi "/gitpod/ s|ahmedali8/hardhat-js-starterkit|"${GITHUB_REPOSITORY}"|;" "README.md" 36 | sedi "/gitpod-badge/ s|ahmedali8/hardhat-js-starterkit|"${GITHUB_REPOSITORY}"|;" "README.md" 37 | sedi "/gha/ s|ahmedali8/hardhat-js-starterkit|"${GITHUB_REPOSITORY}"|;" "README.md" 38 | sedi "/gha-badge/ s|ahmedali8/hardhat-js-starterkit|"${GITHUB_REPOSITORY}"|;" "README.md" 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | DOTENV_CONFIG_PATH: "./.env.example" 5 | 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | push: 10 | branches: 11 | - "main" 12 | 13 | jobs: 14 | ### Run Linters ### 15 | lint: 16 | name: "Run Linters" 17 | runs-on: "ubuntu-latest" 18 | steps: 19 | - name: "Check out the repo" 20 | uses: "actions/checkout@v3" 21 | 22 | - name: "Install Node.js" 23 | uses: "actions/setup-node@v3" 24 | with: 25 | cache: "yarn" 26 | node-version: "lts/*" 27 | 28 | - name: "Install the Node.js dependencies" 29 | run: "yarn install --immutable --inline-builds" 30 | 31 | - name: "Lint the contracts" 32 | run: "yarn lint:sol" 33 | 34 | - name: "Add lint summary" 35 | run: | 36 | echo "## Lint results" >> $GITHUB_STEP_SUMMARY 37 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 38 | 39 | ### Build Contracts ### 40 | build: 41 | name: "Build Contracts" 42 | runs-on: "ubuntu-latest" 43 | steps: 44 | - name: "Check out the repo" 45 | uses: "actions/checkout@v3" 46 | 47 | - name: "Install Node.js" 48 | uses: "actions/setup-node@v3" 49 | with: 50 | cache: "yarn" 51 | node-version: "lts/*" 52 | 53 | - name: "Install the Node.js dependencies" 54 | run: "yarn install --immutable --inline-builds" 55 | 56 | - name: "Compile the contracts" 57 | run: | 58 | yarn clean 59 | yarn compile 60 | 61 | - name: "Add build summary" 62 | run: | 63 | echo "## Build result" >> $GITHUB_STEP_SUMMARY 64 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 65 | 66 | ### Run Tests ### 67 | test: 68 | name: "Run Tests" 69 | needs: ["lint", "build"] 70 | runs-on: "ubuntu-latest" 71 | steps: 72 | - name: "Check out the repo" 73 | uses: "actions/checkout@v3" 74 | 75 | - name: "Install Node.js" 76 | uses: "actions/setup-node@v3" 77 | with: 78 | cache: "yarn" 79 | node-version: "lts/*" 80 | 81 | - name: "Install the Node.js dependencies" 82 | run: "yarn install --immutable --inline-builds" 83 | 84 | - name: "Run tests" 85 | run: "yarn test:gas" 86 | 87 | - name: "Add test summary" 88 | run: | 89 | echo "## Tests result" >> $GITHUB_STEP_SUMMARY 90 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 91 | 92 | ### Run Coverage ### 93 | coverage: 94 | name: "Run Coverage" 95 | needs: ["lint", "build", "test"] 96 | runs-on: "ubuntu-latest" 97 | steps: 98 | - name: "Check out the repo" 99 | uses: "actions/checkout@v3" 100 | 101 | - name: "Install Node.js" 102 | uses: "actions/setup-node@v3" 103 | with: 104 | cache: "yarn" 105 | node-version: "lts/*" 106 | 107 | - name: "Install the Node.js dependencies" 108 | run: "yarn install --immutable --inline-builds" 109 | 110 | - name: "Generate the coverage report" 111 | run: "yarn coverage" 112 | 113 | - name: "Add coverage summary" 114 | run: | 115 | echo "## Coverage results" >> $GITHUB_STEP_SUMMARY 116 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 117 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | name: "Create" 2 | 3 | env: 4 | DOTENV_CONFIG_PATH: "./.env.example" 5 | 6 | # The workflow will run only when `use this template` is used 7 | on: 8 | create: 9 | 10 | jobs: 11 | create: 12 | # We only run this action when the repository isn't the template repository. References: 13 | # - https://docs.github.com/en/actions/learn-github-actions/contexts 14 | # - https://docs.github.com/en/actions/learn-github-actions/expressions 15 | if: ${{ !github.event.repository.is_template }} 16 | 17 | runs-on: ubuntu-latest 18 | permissions: write-all 19 | 20 | steps: 21 | - name: "Check out the repo" 22 | uses: "actions/checkout@v3" 23 | 24 | - name: "Update package.json" 25 | env: 26 | GITHUB_REPOSITORY_DESCRIPTION: 27 | ${{ github.event.repository.description }} 28 | run: 29 | ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" 30 | "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" 31 | 32 | - name: "Remove `rename.sh` and `create.yml`" 33 | run: | 34 | rm -f "./.github/scripts/rename.sh" 35 | rm -f "./.github/workflows/create.yml" 36 | 37 | - name: "Add remove summary" 38 | run: | 39 | echo "## Remove result" >> $GITHUB_STEP_SUMMARY 40 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 41 | 42 | - name: "Add rename summary" 43 | run: | 44 | echo "## Commit results" >> $GITHUB_STEP_SUMMARY 45 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 46 | 47 | - name: "Install the Node.js dependencies" 48 | run: "yarn install --inline-builds" 49 | 50 | - name: "Update commit" 51 | uses: stefanzweifel/git-auto-commit-action@v4 52 | with: 53 | commit_message: "feat: initial commit" 54 | commit_options: "--amend" 55 | push_options: "--force" 56 | skip_fetch: true 57 | 58 | - name: "Add commit summary" 59 | run: | 60 | echo "## Commit results" >> $GITHUB_STEP_SUMMARY 61 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/releases 5 | !.yarn/plugins 6 | !.yarn/sdks 7 | !.yarn/versions 8 | **/artifacts 9 | **/build 10 | **/cache 11 | **/coverage 12 | **/.coverage_artifacts 13 | **/.coverage_cache 14 | **/.coverage_contracts 15 | **/dist 16 | **/node_modules 17 | **/abi_exporter 18 | **/flattened 19 | **/docs 20 | 21 | # files 22 | *.env 23 | *.log 24 | .pnp.* 25 | coverage.json 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | tenderly.yaml 30 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: "gitpod/workspace-node:latest" 2 | 3 | tasks: 4 | - init: "yarn install" 5 | 6 | vscode: 7 | extensions: 8 | - "esbenp.prettier-vscode" 9 | - "juanblanco.solidity" 10 | - "ritwickdey.LiveServer" 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/types 13 | **/docs 14 | 15 | # files 16 | *.env 17 | *.log 18 | .pnp.* 19 | coverage.json 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "endOfLine": "auto", 4 | "printWidth": 80, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "es5", 8 | "proseWrap": "always", 9 | 10 | "overrides": [ 11 | { 12 | "files": "*.sol", 13 | "options": { 14 | "compiler": "0.8.17", 15 | "tabWidth": 4, 16 | "useTabs": false, 17 | "bracketSpacing": false, 18 | "explicitTypes": "always" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs"); 2 | 3 | module.exports = { 4 | istanbulReporter: ["html", "lcov"], 5 | onIstanbulComplete: async function (_config) { 6 | // We need to do this because solcover generates bespoke artifacts. 7 | shell.rm("-rf", "./artifacts"); 8 | }, 9 | skipFiles: ["mocks", "test"], 10 | }; 11 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.8.4"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ], 18 | "reason-string": ["warn", { "maxLength": 64 }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .yarn/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "dotenv.dotenv-vscode", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "juanblanco.solidity" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.formatOnSave": true, 4 | "solidity.linter": "solhint", 5 | "solidity.defaultCompiler": "remote", 6 | "solidity.compileUsingRemoteVersion": "v0.8.19", 7 | "solidity.enabledAsYouTypeCompilationErrorCheck": true, 8 | "solidity.validationDelay": 1500, 9 | "solidity.formatter": "prettier", 10 | "[solidity]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hardhat Starterkit (JavaScript) [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat] 2 | 3 | [gitpod]: https://gitpod.io/#https://github.com/ahmedali8/hardhat-js-starterkit 4 | [gitpod-badge]: 5 | https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod 6 | [gha]: https://github.com/ahmedali8/hardhat-js-starterkit/actions 7 | [gha-badge]: 8 | https://github.com/ahmedali8/hardhat-js-starterkit/actions/workflows/ci.yml/badge.svg 9 | [hardhat]: https://hardhat.org/ 10 | [hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg 11 | 12 | A Hardhat-based template for developing Solidity smart contracts, with sensible 13 | defaults. 14 | 15 | #### Inspiration - [Hardhat Template](https://github.com/paulrberg/hardhat-template) and [Hardhat TS Template](https://github.com/ahmedali8/hardhat-ts-template) 16 | 17 | - [Hardhat](https://github.com/nomiclabs/hardhat): compile, run and test smart 18 | contracts 19 | - [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library 20 | and wallet implementation 21 | - [Solhint Community](https://github.com/solhint-community/solhint-community): 22 | code linter 23 | - [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage 24 | - [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): 25 | code formatter 26 | 27 | ## Getting Started 28 | 29 | Click the 30 | [`Use this template`](https://github.com/ahmedali8/hardhat-js-starterkit/generate) 31 | button at the top of the page to create a new repository with this repo as the 32 | initial state. 33 | 34 | ## Features 35 | 36 | This template builds upon the frameworks and libraries mentioned above, so for 37 | details about their specific features, please consult their respective 38 | documentations. 39 | 40 | For example, for Hardhat, you can refer to the 41 | [Hardhat Tutorial](https://hardhat.org/tutorial) and the 42 | [Hardhat Docs](https://hardhat.org/docs). You might be in particular interested 43 | in reading the 44 | [Testing Contracts](https://hardhat.org/tutorial/testing-contracts) section. 45 | 46 | ### Sensible Defaults 47 | 48 | This template comes with sensible default configurations in the following files: 49 | 50 | ```text 51 | ├── .editorconfig 52 | ├── .eslintignore 53 | ├── .eslintrc.js 54 | ├── .gitignore 55 | ├── .prettierignore 56 | ├── .prettierrc 57 | ├── .solcover.js 58 | ├── .solhintignore 59 | ├── .solhint.json 60 | ├── .yarnrc.yml 61 | └── hardhat.config.js 62 | ``` 63 | 64 | ### GitHub Actions 65 | 66 | This template comes with GitHub Actions pre-configured. Your contracts will be 67 | linted and tested on every push and pull request made to the `main` branch. 68 | 69 | Note though that by default it injects `.env.example` env variables into github 70 | action's `$GITHUB_ENV`. 71 | 72 | You can edit the CI script in 73 | [.github/workflows/ci.yml](./.github/workflows/ci.yml). 74 | 75 | ## Usage 76 | 77 | ### Pre Requisites 78 | 79 | Before running any command, you need to create a `.env` file and set all 80 | necessary environment variables. Follow the example in `.env.example`. You can 81 | either use mnemonic or individual private keys by setting 82 | 83 | ```sh 84 | $ ACCOUNT_TYPE="MNEMONIC" (Default) 85 | or 86 | $ ACCOUNT_TYPE="PRIVATE_KEYS" 87 | ``` 88 | 89 | If you don't already have a mnemonic, use this 90 | [mnemonic-website](https://iancoleman.io/bip39/) to generate one Or if you don't 91 | already have a private key, use this 92 | [privatekey-website](https://vanity-eth.tk/) to generate one. 93 | 94 | Then, proceed with installing dependencies: 95 | 96 | ```sh 97 | $ yarn install 98 | ``` 99 | 100 | ### Run a Hardhat chain 101 | 102 | To run a local network with all your contracts in it, run the following: 103 | 104 | ``` 105 | $ yarn chain 106 | ``` 107 | 108 | ### Compile 109 | 110 | Compile the smart contracts with Hardhat: 111 | 112 | ```sh 113 | $ yarn compile 114 | ``` 115 | 116 | ### Test 117 | 118 | Run the tests with Hardhat: 119 | 120 | ```sh 121 | $ yarn test 122 | 123 | or 124 | 125 | $ yarn test:gas # shows gas report and contract size 126 | 127 | or 128 | 129 | $ yarn test:trace # shows logs + calls 130 | 131 | or 132 | 133 | $ yarn test:fulltrace # shows logs + calls + sloads + sstores 134 | ``` 135 | 136 | Optional: 137 | 138 | - See the actual fiat currency rates by setting your coingecko api key from 139 | [here](https://coinmarketcap.com/api/pricing/) in `.env` file or command. 140 | 141 | - Set custom gas price (gwei) in `.env` file or command or let it automatically 142 | fetched by ethgasstationapi. 143 | 144 | ```sh 145 | $ GAS_PRICE=20 146 | $ COIN_MARKET_CAP_API_KEY="your_api_key" 147 | ``` 148 | 149 | ### Lint Solidity 150 | 151 | Lint the Solidity code: 152 | 153 | ```sh 154 | $ yarn lint:sol 155 | ``` 156 | 157 | ### Forking mainnet 158 | 159 | Starts a local hardhat chain with the state of the last `mainnet` block 160 | 161 | ``` 162 | $ yarn fork 163 | ``` 164 | 165 | ### Coverage 166 | 167 | Generate the code coverage report: 168 | 169 | ```sh 170 | $ yarn coverage 171 | ``` 172 | 173 | ### Clean 174 | 175 | Delete the smart contract artifacts, the coverage reports and the Hardhat cache: 176 | 177 | ```sh 178 | $ yarn clean 179 | ``` 180 | 181 | ### Deploy 182 | 183 | Deploy the contracts to Hardhat Network: 184 | 185 | ```sh 186 | $ yarn deploy 187 | ``` 188 | 189 | Deploy the contracts to a specific network, such as the Goerli testnet: 190 | 191 | ```sh 192 | $ yarn deploy:network goerli 193 | ``` 194 | 195 | ### Generate Natspec Doc 196 | 197 | Generate natspec documentation for your contracts by runing 198 | 199 | ``` 200 | $ yarn dodoc 201 | ``` 202 | 203 | For more information on Natspec 204 | [click here](https://docs.soliditylang.org/en/v0.8.12/natspec-format.html#natspec) 205 | and for dodoc repo 206 | [click here](https://github.com/primitivefinance/primitive-dodoc) 207 | 208 | ### View Contracts Size 209 | 210 | ``` 211 | $ yarn size 212 | ``` 213 | 214 | or turn on for every compile 215 | 216 | ``` 217 | $ CONTRACT_SIZER=true 218 | ``` 219 | 220 | ## Verify Contract 221 | 222 | ### Manual Verify 223 | 224 | ```sh 225 | $ npx hardhat verify --network DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1" "Constructor argument 2" 226 | ``` 227 | 228 | For complex arguments you can refer 229 | [here](https://hardhat.org/plugins/nomiclabs-hardhat-etherscan.html) 230 | 231 | ```sh 232 | $ npx hardhat verify --contract contracts/CONTRACT_NAME.sol:CONTRACT_NAME --network --constructor-args arguments.js DEPLOYED_CONTRACT_ADDRESS 233 | ``` 234 | 235 | ### Verify Contract Programmatically 236 | 237 | Verify the contract using `verifyContract` function in 238 | [verify.js](./utils/verify.js) 239 | 240 | Set block explorer api key in `.env` file or using command, refer to 241 | `.env.example` for more insight. 242 | 243 | Example deploy script with `verifyContract` function is 244 | [here](./scripts/deploy.js) 245 | 246 | ## Syntax Highlighting 247 | 248 | If you use VSCode, you can enjoy syntax highlighting for your Solidity code via 249 | the [vscode-solidity](https://github.com/juanfranblanco/vscode-solidity) 250 | extension. 251 | 252 | ## Using GitPod 253 | 254 | [GitPod](https://www.gitpod.io/) is an open-source developer platform for remote 255 | development. 256 | 257 | To view the coverage report generated by `yarn coverage`, just click `Go Live` 258 | from the status bar to turn the server on/off. 259 | 260 | ## Contributing 261 | 262 | Contributions are always welcome! Open a PR or an issue! 263 | 264 | ## Thank You! 265 | 266 | ## Resources 267 | 268 | - [Hardhat Documentation](https://hardhat.org/getting-started/) 269 | -------------------------------------------------------------------------------- /config/api-keys.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { config: dotenvConfig } = require("dotenv"); 3 | 4 | const dotenvConfigPath = process.env.DOTENV_CONFIG_PATH || "./.env"; 5 | dotenvConfig({ path: resolve(process.cwd(), dotenvConfigPath) }); 6 | 7 | const API_KEYS = { 8 | // ETHEREUM 9 | mainnet: process.env.ETHERSCAN_API_KEY || "", 10 | goerli: process.env.ETHERSCAN_API_KEY || "", 11 | sepolia: process.env.ETHERSCAN_API_KEY || "", 12 | 13 | // BINANCE SMART CHAIN 14 | bsc: process.env.BSCSCAN_API_KEY || "", 15 | bscTestnet: process.env.BSCSCAN_API_KEY || "", 16 | 17 | // MATIC/POLYGON 18 | polygon: process.env.POLYGONSCAN_API_KEY || "", 19 | polygonMumbai: process.env.POLYGONSCAN_API_KEY || "", 20 | 21 | // OPTIMISM 22 | optimisticEthereum: process.env.OPTIMISM_API_KEY || "", 23 | optimisticGoerli: process.env.OPTIMISM_API_KEY || "", 24 | 25 | // ARBITRUM 26 | arbitrumOne: process.env.ARBISCAN_API_KEY || "", 27 | arbitrumTestnet: process.env.ARBISCAN_API_KEY || "", 28 | }; 29 | 30 | module.exports = { 31 | API_KEYS, 32 | }; 33 | -------------------------------------------------------------------------------- /config/networks.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { config: dotenvConfig } = require("dotenv"); 3 | 4 | const dotenvConfigPath = process.env.DOTENV_CONFIG_PATH || "./.env"; 5 | dotenvConfig({ path: resolve(process.cwd(), dotenvConfigPath) }); 6 | 7 | const INFURA_KEY = process.env.INFURA_API_KEY; 8 | if (typeof INFURA_KEY === "undefined") { 9 | throw new Error(`INFURA_API_KEY must be a defined environment variable`); 10 | } 11 | 12 | const infuraUrl = (network) => `https://${network}.infura.io/v3/${INFURA_KEY}`; 13 | 14 | /** 15 | * All supported network names 16 | * To use a network in your command use the value of each key 17 | * 18 | * e.g. 19 | * 20 | * $ yarn deploy:network mainnet 21 | * 22 | * $ npx hardhat run scripts/deploy.ts --network polygon-mainnet 23 | */ 24 | const networks = { 25 | // ETHEREUM 26 | mainnet: { 27 | chainId: 1, 28 | url: infuraUrl("mainnet"), 29 | }, 30 | goerli: { 31 | chainId: 5, 32 | url: infuraUrl("goerli"), 33 | }, 34 | sepolia: { 35 | chainId: 11_155_111, 36 | url: infuraUrl("sepolia"), 37 | }, 38 | 39 | // BINANCE SMART CHAIN 40 | bsc: { 41 | chainId: 56, 42 | url: "https://bsc-dataseed1.defibit.io/", 43 | }, 44 | "bsc-testnet": { 45 | chainId: 97, 46 | url: "https://data-seed-prebsc-2-s1.binance.org:8545/", 47 | }, 48 | 49 | // MATIC/POLYGON 50 | "polygon-mainnet": { 51 | chainId: 137, 52 | url: infuraUrl("polygon-mainnet"), 53 | }, 54 | "polygon-mumbai": { 55 | chainId: 80001, 56 | url: infuraUrl("polygon-mumbai"), 57 | }, 58 | 59 | // OPTIMISM 60 | "optimism-mainnet": { 61 | chainId: 10, 62 | url: infuraUrl("optimism-mainnet"), 63 | }, 64 | "optimism-goerli": { 65 | chainId: 420, 66 | url: infuraUrl("optimism-goerli"), 67 | }, 68 | 69 | // ARBITRUM 70 | "arbitrum-mainnet": { 71 | chainId: 42_161, 72 | url: infuraUrl("arbitrum-mainnet"), 73 | }, 74 | "arbitrum-goerli": { 75 | chainId: 421_611, 76 | url: infuraUrl("arbitrum-goerli"), 77 | }, 78 | }; 79 | 80 | module.exports = { 81 | networks, 82 | }; 83 | -------------------------------------------------------------------------------- /contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | // Uncomment this line to use console.log 5 | // import "hardhat/console.sol"; 6 | 7 | contract Lock { 8 | uint256 public unlockTime; 9 | address payable public owner; 10 | 11 | event Withdrawal(uint256 amount, uint256 when); 12 | 13 | constructor(uint256 _unlockTime) payable { 14 | require( 15 | block.timestamp < _unlockTime, 16 | "Unlock time should be in the future" 17 | ); 18 | 19 | unlockTime = _unlockTime; 20 | owner = payable(msg.sender); 21 | } 22 | 23 | function withdraw() public { 24 | // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal 25 | // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); 26 | 27 | require(block.timestamp >= unlockTime, "You can't withdraw yet"); 28 | require(msg.sender == owner, "You aren't the owner"); 29 | 30 | emit Withdrawal(address(this).balance, block.timestamp); 31 | 32 | owner.transfer(address(this).balance); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("@primitivefi/hardhat-dodoc"); 3 | const { config: dotenvConfig } = require("dotenv"); 4 | require("hardhat-contract-sizer"); 5 | const { resolve } = require("path"); 6 | 7 | const { API_KEYS } = require("./config/api-keys"); 8 | const { networks } = require("./config/networks"); 9 | require("./tasks"); 10 | 11 | const dotenvConfigPath = process.env.DOTENV_CONFIG_PATH || "./.env"; 12 | dotenvConfig({ path: resolve(process.cwd(), dotenvConfigPath) }); 13 | 14 | const ACCOUNT_TYPE = process.env.ACCOUNT_TYPE; 15 | const mnemonic = process.env.MNEMONIC; 16 | if (ACCOUNT_TYPE === "MNEMONIC" && !mnemonic) { 17 | throw new Error("Please set your MNEMONIC in a .env file"); 18 | } 19 | if ( 20 | ACCOUNT_TYPE === "PRIVATE_KEYS" && 21 | typeof process.env.PRIVATE_KEY_1 === "undefined" 22 | ) { 23 | throw new Error("Please set at least one PRIVATE_KEY_1 in a .env file"); 24 | } 25 | 26 | const getAccounts = () => { 27 | if (ACCOUNT_TYPE === "PRIVATE_KEYS") { 28 | // can add as many private keys as you want 29 | return [ 30 | `0x${process.env.PRIVATE_KEY_1}`, 31 | // `0x${process.env.PRIVATE_KEY_2}`, 32 | // `0x${process.env.PRIVATE_KEY_3}`, 33 | // `0x${process.env.PRIVATE_KEY_4}`, 34 | // `0x${process.env.PRIVATE_KEY_5}`, 35 | ]; 36 | } else { 37 | return { 38 | mnemonic, 39 | count: 10, 40 | path: "m/44'/60'/0'/0", 41 | }; 42 | } 43 | }; 44 | 45 | function getChainConfig(network) { 46 | return { 47 | accounts: getAccounts(), 48 | chainId: networks[network].chainId, 49 | url: networks[network].url, 50 | }; 51 | } 52 | 53 | module.exports = { 54 | contractSizer: { 55 | alphaSort: true, 56 | runOnCompile: process.env.CONTRACT_SIZER ? true : false, 57 | disambiguatePaths: false, 58 | }, 59 | defaultNetwork: "hardhat", 60 | dodoc: { 61 | runOnCompile: false, 62 | debugMode: false, 63 | keepFileStructure: true, 64 | freshOutput: true, 65 | outputDir: "./docs", 66 | include: ["contracts"], 67 | }, 68 | etherscan: { 69 | apiKey: API_KEYS, 70 | }, 71 | gasReporter: { 72 | enabled: process.env.REPORT_GAS ? true : false, 73 | currency: "USD", 74 | // if commented out then it fetches from ethGasStationAPI 75 | // gasPrice: process.env.GAS_PRICE, 76 | coinmarketcap: process.env.COIN_MARKET_CAP_API_KEY || null, 77 | excludeContracts: [], 78 | src: "./contracts", 79 | }, 80 | networks: { 81 | // LOCAL 82 | hardhat: { chainId: 31337 }, 83 | "truffle-dashboard": { 84 | url: "http://localhost:24012/rpc", 85 | }, 86 | ganache: { chainId: 1337, url: "http://127.0.0.1:7545" }, 87 | 88 | // ETHEREUM 89 | mainnet: getChainConfig("mainnet"), 90 | goerli: getChainConfig("goerli"), 91 | sepolia: getChainConfig("sepolia"), 92 | 93 | // BINANCE SMART CHAIN 94 | bsc: getChainConfig("bsc"), 95 | "bsc-testnet": getChainConfig("bsc-testnet"), 96 | 97 | // MATIC/POLYGON 98 | "polygon-mainnet": getChainConfig("polygon-mainnet"), 99 | "polygon-mumbai": getChainConfig("polygon-mumbai"), 100 | 101 | // OPTIMISM 102 | "optimism-mainnet": getChainConfig("optimism-mainnet"), 103 | "optimism-goerli": getChainConfig("optimism-goerli"), 104 | 105 | // ARBITRUM 106 | "arbitrum-mainnet": getChainConfig("arbitrum-mainnet"), 107 | "arbitrum-goerli": getChainConfig("arbitrum-goerli"), 108 | }, 109 | paths: { 110 | artifacts: "./artifacts", 111 | cache: "./cache", 112 | sources: "./contracts", 113 | tests: "./test", 114 | }, 115 | solidity: { 116 | compilers: [ 117 | { 118 | version: "0.8.19", 119 | settings: { 120 | metadata: { 121 | // Not including the metadata hash 122 | // https://github.com/paulrberg/solidity-template/issues/31 123 | bytecodeHash: "none", 124 | }, 125 | // Disable the optimizer when debugging 126 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 127 | optimizer: { 128 | enabled: true, 129 | runs: 200, 130 | }, 131 | evmVersion: "paris", 132 | }, 133 | }, 134 | ], 135 | }, 136 | }; 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ahmedali8/hardhat-js-starterkit", 3 | "description": "Starter kit for writing Solidity smart contracts in hardhat environment", 4 | "version": "0.1.0", 5 | "author": { 6 | "name": "Ahmed Ali Bhatti", 7 | "url": "https://github.com/ahmedali8" 8 | }, 9 | "scripts": { 10 | "chain": "hardhat node --network hardhat", 11 | "clean": "shx rm -rf ./artifacts ./cache ./coverage ./coverage.json", 12 | "compile": "hardhat clean && hardhat compile", 13 | "coverage": "hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"./test/**/*.js\"", 14 | "deploy": "hardhat run scripts/deploy.js --network hardhat", 15 | "deploy:network": "hardhat run scripts/deploy.js --network", 16 | "dodoc": "hardhat dodoc", 17 | "fork": "hardhat node --network hardhat --fork https://mainnet.infura.io/v3/460f40a260564ac4a4f4b3fffb032dad", 18 | "lint": "yarn run lint:sol && yarn run lint:js && yarn run prettier:check", 19 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 20 | "lint:js": "eslint --config ./.eslintrc.js --ignore-path ./.eslintignore --ext .js .", 21 | "postinstall": "DOTENV_CONFIG_PATH=./.env.example yarn hardhat compile", 22 | "prettier:write": "prettier --config .prettierrc --write \"**/*.{js,json,md,yml,sol}\"", 23 | "prettier:check": "prettier --config .prettierrc \"**/*.{js,json,md,yml,sol}\"", 24 | "size": "hardhat size-contracts", 25 | "test": "hardhat test", 26 | "test:gas": "REPORT_GAS=true CONTRACT_SIZER=true hardhat test", 27 | "test:trace": "hardhat test --trace", 28 | "test:fulltrace": "hardhat test --fulltrace", 29 | "verify": "hardhat run scripts/verify.js --network" 30 | }, 31 | "devDependencies": { 32 | "@ethersproject/bignumber": "^5.7.0", 33 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.1", 34 | "@nomicfoundation/hardhat-ethers": "^3.0.3", 35 | "@nomicfoundation/hardhat-network-helpers": "^1.0.8", 36 | "@nomicfoundation/hardhat-toolbox": "^3.0.0", 37 | "@nomicfoundation/hardhat-verify": "^1.0.3", 38 | "@primitivefi/hardhat-dodoc": "^0.2.3", 39 | "@typechain/ethers-v6": "^0.4.0", 40 | "@typechain/hardhat": "^8.0.0", 41 | "@types/chai": "^4.3.5", 42 | "@types/mocha": "^10.0.1", 43 | "@types/node": "^18.16.19", 44 | "chai": "4.3.7", 45 | "chalk": "^4.1.2", 46 | "child_process": "^1.0.2", 47 | "dotenv": "^16.3.1", 48 | "eslint": "^8.44.0", 49 | "eslint-config-prettier": "^8.8.0", 50 | "ethers": "6.6.2", 51 | "evm-bn": "^1.1.2", 52 | "fs-extra": "^11.1.1", 53 | "hardhat": "2.16.1", 54 | "hardhat-contract-sizer": "2.10.0", 55 | "hardhat-gas-reporter": "1.0.9", 56 | "lodash": "^4.17.21", 57 | "mocha": "^10.2.0", 58 | "prettier": "^2.8.8", 59 | "prettier-plugin-solidity": "^1.1.3", 60 | "shx": "0.3.4", 61 | "solhint-community": "^3.5.2", 62 | "solhint-plugin-prettier": "^0.0.5", 63 | "solidity-coverage": "0.8.4", 64 | "ts-node": "^10.9.1", 65 | "typechain": "^8.2.0", 66 | "typescript": "^5.1.6" 67 | }, 68 | "license": "UNLICENSED", 69 | "files": [ 70 | "/contracts" 71 | ], 72 | "keywords": [ 73 | "blockchain", 74 | "ethereum", 75 | "hardhat", 76 | "smart-contracts", 77 | "solidity" 78 | ], 79 | "private": true, 80 | "packageManager": "yarn@3.6.1" 81 | } 82 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ethers } = require("hardhat"); 4 | const { preDeployConsole, postDeployConsole } = require("../utils/contracts"); 5 | const { toWei } = require("../utils/format"); 6 | const { verifyContract } = require("../utils/verify"); 7 | 8 | async function main() { 9 | const { chainId } = await ethers.provider.getNetwork(); 10 | const [owner] = await ethers.getSigners(); 11 | 12 | const currentTimestampInSeconds = Math.round(Date.now() / 1000); 13 | const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; 14 | const unlockTime = currentTimestampInSeconds + ONE_YEAR_IN_SECS; 15 | 16 | const lockedAmount = toWei("1"); 17 | 18 | const CONTRACT_NAME = "Lock"; 19 | 20 | await preDeployConsole({ 21 | signerAddress: owner.address, 22 | contractName: CONTRACT_NAME, 23 | }); 24 | const LockFactory = await ethers.getContractFactory(CONTRACT_NAME); 25 | let lock = await LockFactory.connect(owner).deploy(unlockTime, { 26 | value: lockedAmount, 27 | }); 28 | lock = await postDeployConsole({ 29 | contractName: CONTRACT_NAME, 30 | contract: lock, 31 | }); 32 | 33 | const lockAddress = await lock.getAddress(); 34 | 35 | console.log( 36 | `Lock with 1 ETH and unlock timestamp ${unlockTime} deployed to ${lockAddress}` 37 | ); 38 | 39 | // You don't want to verify on localhost 40 | // uncomment below code to programmatically verify contract 41 | try { 42 | if (chainId != 31337 && chainId != 1337) { 43 | const contractPath = `contracts/${CONTRACT_NAME}.sol:${CONTRACT_NAME}`; 44 | await verifyContract({ 45 | contractPath: contractPath, 46 | contractAddress: lockAddress, 47 | args: [unlockTime], 48 | }); 49 | } 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | } 54 | 55 | main() 56 | .then(() => process.exit(0)) 57 | .catch((error) => { 58 | console.log(error); 59 | process.exit(1); 60 | }); 61 | -------------------------------------------------------------------------------- /scripts/verify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ethers } = require("hardhat"); 4 | const { toWei } = require("../utils/format"); 5 | const { verifyContract } = require("../utils/verify"); 6 | 7 | async function main() { 8 | const { chainId } = await ethers.provider.getNetwork(); 9 | 10 | const contractName = "Token"; 11 | const contractPath = `contracts/${contractName}.sol:${contractName}`; 12 | const contractAddress = ""; 13 | const args = [ 14 | "TokenName", 15 | "TCT", 16 | toWei("6000000"), 17 | "0xde43f899587aaa2Ea6aD243F3d68a5027F2C6a94", 18 | ]; 19 | 20 | // You don't want to verify on localhost 21 | if (chainId != 31337 && chainId != 1337) { 22 | await verifyContract({ 23 | contractPath, 24 | contractAddress, 25 | args, 26 | }); 27 | } 28 | } 29 | 30 | main() 31 | .then(() => process.exit(0)) 32 | .catch((error) => { 33 | console.log(error); 34 | process.exit(1); 35 | }); 36 | -------------------------------------------------------------------------------- /tasks/accounts.js: -------------------------------------------------------------------------------- 1 | const { task } = require("hardhat/config"); 2 | const { fromWei } = require("../utils/format"); 3 | 4 | task("accounts", "Prints the list of accounts", async (_, { ethers }) => { 5 | const accounts = await ethers.getSigners(); 6 | 7 | const accountsArray = []; 8 | 9 | for (const account of accounts) { 10 | const address = account.address; 11 | const balanceInETH = fromWei(await account.getBalance()); 12 | 13 | accountsArray.push({ 14 | address, 15 | balanceInETH, 16 | }); 17 | } 18 | 19 | console.table(accountsArray); 20 | }); 21 | -------------------------------------------------------------------------------- /tasks/index.js: -------------------------------------------------------------------------------- 1 | require("./accounts"); 2 | -------------------------------------------------------------------------------- /test/Lock.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | time, 3 | loadFixture, 4 | } = require("@nomicfoundation/hardhat-network-helpers"); 5 | const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); 6 | const { expect } = require("chai"); 7 | const { ethers } = require("hardhat"); 8 | 9 | describe("Lock", function () { 10 | // We define a fixture to reuse the same setup in every test. 11 | // We use loadFixture to run this setup once, snapshot that state, 12 | // and reset Hardhat Network to that snapshot in every test. 13 | async function deployOneYearLockFixture() { 14 | const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; 15 | const ONE_GWEI = 1_000_000_000; 16 | 17 | const lockedAmount = ONE_GWEI; 18 | const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; 19 | 20 | // Contracts are deployed using the first signer/account by default 21 | const [owner, otherAccount] = await ethers.getSigners(); 22 | 23 | const Lock = await ethers.getContractFactory("Lock"); 24 | const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); 25 | 26 | return { lock, unlockTime, lockedAmount, owner, otherAccount }; 27 | } 28 | 29 | describe("Deployment", function () { 30 | it("Should set the right unlockTime", async function () { 31 | const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); 32 | 33 | expect(await lock.unlockTime()).to.equal(unlockTime); 34 | }); 35 | 36 | it("Should set the right owner", async function () { 37 | const { lock, owner } = await loadFixture(deployOneYearLockFixture); 38 | 39 | expect(await lock.owner()).to.equal(owner.address); 40 | }); 41 | 42 | it("Should receive and store the funds to lock", async function () { 43 | const { lock, lockedAmount } = await loadFixture( 44 | deployOneYearLockFixture 45 | ); 46 | 47 | const address = await lock.getAddress(); 48 | const balance = await ethers.provider.getBalance(address); 49 | 50 | expect(balance).to.equal(lockedAmount); 51 | }); 52 | 53 | it("Should fail if the unlockTime is not in the future", async function () { 54 | // We don't use the fixture here because we want a different deployment 55 | const latestTime = await time.latest(); 56 | const Lock = await ethers.getContractFactory("Lock"); 57 | await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( 58 | "Unlock time should be in the future" 59 | ); 60 | }); 61 | }); 62 | 63 | describe("Withdrawals", function () { 64 | describe("Validations", function () { 65 | it("Should revert with the right error if called too soon", async function () { 66 | const { lock } = await loadFixture(deployOneYearLockFixture); 67 | 68 | await expect(lock.withdraw()).to.be.revertedWith( 69 | "You can't withdraw yet" 70 | ); 71 | }); 72 | 73 | it("Should revert with the right error if called from another account", async function () { 74 | const { lock, unlockTime, otherAccount } = await loadFixture( 75 | deployOneYearLockFixture 76 | ); 77 | 78 | // We can increase the time in Hardhat Network 79 | await time.increaseTo(unlockTime); 80 | 81 | // We use lock.connect() to send a transaction from another account 82 | await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( 83 | "You aren't the owner" 84 | ); 85 | }); 86 | 87 | it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { 88 | const { lock, unlockTime } = await loadFixture( 89 | deployOneYearLockFixture 90 | ); 91 | 92 | // Transactions are sent using the first signer by default 93 | await time.increaseTo(unlockTime); 94 | 95 | await expect(lock.withdraw()).not.to.be.reverted; 96 | }); 97 | }); 98 | 99 | describe("Events", function () { 100 | it("Should emit an event on withdrawals", async function () { 101 | const { lock, unlockTime, lockedAmount } = await loadFixture( 102 | deployOneYearLockFixture 103 | ); 104 | 105 | await time.increaseTo(unlockTime); 106 | 107 | await expect(lock.withdraw()) 108 | .to.emit(lock, "Withdrawal") 109 | .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg 110 | }); 111 | }); 112 | 113 | describe("Transfers", function () { 114 | it("Should transfer the funds to the owner", async function () { 115 | const { lock, unlockTime, lockedAmount, owner } = await loadFixture( 116 | deployOneYearLockFixture 117 | ); 118 | 119 | await time.increaseTo(unlockTime); 120 | 121 | await expect(lock.withdraw()).to.changeEtherBalances( 122 | [owner, lock], 123 | [lockedAmount, -lockedAmount] 124 | ); 125 | }); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 4 | 5 | const ZERO_BYTES32 = 6 | "0x0000000000000000000000000000000000000000000000000000000000000000"; 7 | 8 | const MAX_UINT256 = BigInt( 9 | "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" 10 | ); 11 | 12 | module.exports = { 13 | ZERO_ADDRESS, 14 | ZERO_BYTES32, 15 | MAX_UINT256, 16 | }; 17 | -------------------------------------------------------------------------------- /utils/contracts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const chalk = require("chalk"); 4 | const { ethers } = require("hardhat"); 5 | const { fromWei } = require("./format"); 6 | const { getExtraGasInfo } = require("./misc"); 7 | 8 | /** 9 | * Logs information about the signer, network, and balance before deploying a contract. 10 | * 11 | * @param {*} obj 12 | * @param {*} obj.signerAddress string - The address of the signer. 13 | * @param {*} obj.contractName string - The name of the contract to be deployed. 14 | */ 15 | async function preDeployConsole({ signerAddress, contractName }) { 16 | const { chainId, name } = await ethers.provider.getNetwork(); 17 | const ethBalance = await ethers.provider.getBalance(signerAddress); 18 | 19 | console.log( 20 | ` 🛰 Deploying: ${chalk.cyan( 21 | contractName 22 | )} to Network: ${name} & ChainId: ${chainId}` 23 | ); 24 | console.log( 25 | ` 🎭 Deployer: ${chalk.cyan(signerAddress)}, Balance: ${chalk.grey( 26 | fromWei(ethBalance ?? 0) 27 | )} ETH` 28 | ); 29 | } 30 | 31 | /** 32 | * Logs information about the deployment of a contract. 33 | * 34 | * @param {*} obj 35 | * @param {*} obj.contractName string - The name of the deployed contract. 36 | * @param {*} obj.contract BaseContract - The contract instance. 37 | * @returns contract instance 38 | */ 39 | async function postDeployConsole({ contractName, contract }) { 40 | // Wait for the contract to be deployed 41 | await contract.waitForDeployment(); 42 | 43 | // Get extra gas information of the deployment transaction 44 | let extraGasInfo = ""; 45 | const deploymentTransaction = contract.deploymentTransaction(); 46 | if (deploymentTransaction) { 47 | extraGasInfo = (await getExtraGasInfo(deploymentTransaction)) ?? ""; 48 | } 49 | const contractAddress = await contract.getAddress(); 50 | 51 | // Log the deployment information 52 | console.log( 53 | " 📄", 54 | chalk.cyan(contractName), 55 | "deployed to:", 56 | chalk.magenta(contractAddress) 57 | ); 58 | console.log(" ⛽", chalk.grey(extraGasInfo)); 59 | 60 | return contract; 61 | } 62 | 63 | module.exports = { 64 | preDeployConsole, 65 | postDeployConsole, 66 | }; 67 | -------------------------------------------------------------------------------- /utils/files.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const path = require("path"); 3 | const fs = require("fs-extra"); 4 | 5 | const existsAsync = fs.pathExists; 6 | const makeDirectoryAsync = fs.mkdir; 7 | const readFileAsync = fs.readFile; 8 | const writeFileAsync = fs.writeFile; 9 | 10 | const ensureDirectoryExists = async (directory) => { 11 | try { 12 | await makeDirectoryAsync(directory, { recursive: true }); 13 | } catch (err) { 14 | console.log(err); 15 | } 16 | }; 17 | 18 | const writeFile = async (filePath, data) => { 19 | await ensureDirectoryExists(path.dirname(filePath)); 20 | await writeFileAsync(filePath, data); 21 | }; 22 | 23 | const writeJSONFile = async (filePath, data) => { 24 | await writeFile(filePath, JSON.stringify(data, null, 2)); 25 | }; 26 | 27 | const parseFile = async (filePath) => { 28 | if (await existsAsync(filePath)) { 29 | const contents = await readFileAsync(filePath); 30 | return JSON.parse(contents.toString()); 31 | } 32 | 33 | return null; 34 | }; 35 | 36 | const deleteFolderRecursive = (directoryPath) => { 37 | if (fs.existsSync(directoryPath)) { 38 | fs.readdirSync(directoryPath).forEach((file) => { 39 | const curPath = path.join(directoryPath, file); 40 | if (fs.lstatSync(curPath).isDirectory()) { 41 | // recurse 42 | deleteFolderRecursive(curPath); 43 | } else { 44 | // delete file 45 | fs.unlinkSync(curPath); 46 | } 47 | }); 48 | fs.rmdirSync(directoryPath); 49 | } 50 | }; 51 | 52 | module.exports = { 53 | ensureDirectoryExists, 54 | writeFile, 55 | writeJSONFile, 56 | parseFile, 57 | deleteFolderRecursive, 58 | }; 59 | -------------------------------------------------------------------------------- /utils/format.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { formatUnits } = require("ethers"); 4 | const { BigNumber } = require("@ethersproject/bignumber"); 5 | const { fromBn, toBn } = require("evm-bn"); 6 | 7 | /** 8 | * Converts a number to a string representation in the International Currency System (e.g., Billions, Millions, Thousands). 9 | * 10 | * @param labelValue - The number to be converted. 11 | * @returns The string value in the International Currency System, or undefined if the input is falsy. 12 | */ 13 | function convertToInternationalCurrencySystem(labelValue) { 14 | if (!labelValue) { 15 | return undefined; 16 | } 17 | 18 | const absValue = Math.abs(labelValue); 19 | 20 | if (absValue >= 1e9) { 21 | // Billion 22 | return (absValue / 1e9).toFixed(2).replace(/\.?0+$/, "") + " B"; 23 | } else if (absValue >= 1e6) { 24 | // Million 25 | return (absValue / 1e6).toFixed(2).replace(/\.?0+$/, "") + " M"; 26 | } else if (absValue >= 1e3) { 27 | // Thousand 28 | return (absValue / 1e3).toFixed(2).replace(/\.?0+$/, "") + " K"; 29 | } else { 30 | return absValue.toString(); 31 | } 32 | } 33 | 34 | /** 35 | * Returns the string representation of a number, removing unnecessary trailing zeros. 36 | * 37 | * @param value - The number to be converted. 38 | * @returns The string value with trailing zeros removed. 39 | */ 40 | function removeTrailingZeros(value) { 41 | return value.toString().replace(/\.?0+$/, ""); 42 | } 43 | 44 | /** 45 | * Returns the string representation of a number with a specified precision. 46 | * 47 | * @param value - The number to be converted. 48 | * @param precision - The number of decimal places to include in the string representation. 49 | * @returns The string value with the specified precision. 50 | */ 51 | function numWithPrecision(value, precision = 4) { 52 | return value.toFixed(precision); 53 | } 54 | 55 | /** 56 | * Converts a gas price value to gwei. 57 | * 58 | * formatUnits(value: BigNumberish, unit?: string | Numeric | undefined): string 59 | * BigNumberish -> string | Numeric 60 | * Numeric -> number | bigint 61 | * 62 | * @param gasPrice - The gas price value to be converted. 63 | * @returns The string value of the gas price in gwei. 64 | */ 65 | function toGwei(gasPrice) { 66 | return formatUnits(gasPrice, "gwei"); 67 | } 68 | 69 | /** 70 | * Converts a value to wei. 71 | * 72 | * parseUnits(value: string, unit?: string | Numeric): bigint 73 | * BigNumberish -> string | Numeric 74 | * Numeric -> number | bigint 75 | * 76 | * @param value - The value to be converted. 77 | * @param decimals - The number of decimal places in the value. 78 | * @returns The bigint value in wei. 79 | */ 80 | function toWei(value, decimals = 18) { 81 | return toBn(value, decimals).toBigInt(); 82 | } 83 | 84 | /** 85 | * Converts a value from wei to a string representation. 86 | * 87 | * formatUnits(value: BigNumberish, unit?: string | Numeric | undefined): string 88 | * BigNumberish -> string | Numeric 89 | * Numeric -> number | bigint 90 | * 91 | * @param value - The value to be converted from wei. 92 | * @param decimals - The number of decimal places in the value. 93 | * @returns The string representation of the value. 94 | */ 95 | function fromWei(value, decimals = 18) { 96 | return fromBn(BigNumber.from(value), decimals); 97 | } 98 | 99 | /** 100 | * Converts a value from wei to a floating-point number. 101 | * 102 | * BigNumberish -> string | Numeric 103 | * Numeric -> number | bigint 104 | * 105 | * @param value - The value to be converted from wei. 106 | * @param decimals - The number of decimal places in the value. 107 | * @returns The floating-point number representation of the value. 108 | */ 109 | function fromWeiToNum(value, decimals = 18) { 110 | const fromWeiString = fromWei(value, decimals); 111 | return parseFloat(fromWeiString); 112 | } 113 | 114 | /** 115 | * Converts a value from wei to a fixed-point number with a specified precision. 116 | * 117 | * BigNumberish -> string | Numeric 118 | * Numeric -> number | bigint 119 | * 120 | * @param value - The value to be converted from wei. 121 | * @param decimals - The number of decimal places in the value. 122 | * @param precision - The number of decimal places to include in the fixed-point number. 123 | * @returns The fixed-point number representation of the value. 124 | */ 125 | function fromWeiTodNumWithPrecision(value, decimals = 18, precision = 4) { 126 | const fromWeiNum = fromWeiToNum(value, decimals); 127 | const fromWeiNumToFixed = numWithPrecision(fromWeiNum, precision); 128 | 129 | return parseFloat(fromWeiNumToFixed); 130 | } 131 | 132 | /** 133 | * Calculates the percentage of a BigNumber value. 134 | * 135 | * @param bn - The BigNumber value. 136 | * @param percent - The percentage value. 137 | * @returns The calculated percentage as a BigNumber. 138 | */ 139 | function calculatePercentageInBn(bn, percent) { 140 | return bn.mul(percent).div(100); 141 | } 142 | 143 | /** 144 | * Calculates the percentage of a bigint value. 145 | * 146 | * @param bn - The bigint value. 147 | * @param percent - The percentage value. 148 | * @returns The calculated percentage as a bigint. 149 | */ 150 | function calculatePercentageInBi(bn, percent) { 151 | return (bn * BigInt(percent)) / 100n; 152 | } 153 | 154 | /** 155 | * Generates a random integer between a minimum and maximum value (inclusive). 156 | * 157 | * @param min - The minimum value. 158 | * @param max - The maximum value. 159 | * @returns The random integer. 160 | */ 161 | const randomInteger = (min, max) => { 162 | return Math.floor(Math.random() * (max - min + 1)) + min; 163 | }; 164 | 165 | /** 166 | * Generates a random number between a minimum and maximum value. 167 | * 168 | * @param min - The minimum value. 169 | * @param max - The maximum value. 170 | * @returns The random number. 171 | */ 172 | const randomNumber = (min, max) => { 173 | return Math.random() * (max - min) + min; 174 | }; 175 | 176 | module.exports = { 177 | convertToInternationalCurrencySystem, 178 | removeTrailingZeros, 179 | numWithPrecision, 180 | toGwei, 181 | toWei, 182 | fromWei, 183 | fromWeiToNum, 184 | fromWeiTodNumWithPrecision, 185 | calculatePercentageInBn, 186 | calculatePercentageInBi, 187 | randomInteger, 188 | randomNumber, 189 | }; 190 | -------------------------------------------------------------------------------- /utils/misc.js: -------------------------------------------------------------------------------- 1 | const { 2 | computeAddress, 3 | getAddress, 4 | solidityPackedKeccak256, 5 | } = require("ethers"); 6 | 7 | const { fromWei, toGwei } = require("./format"); 8 | 9 | /** 10 | * Asynchronously sleeps for the specified number of milliseconds. 11 | * 12 | * @param ms - The number of milliseconds to sleep. 13 | * @returns A promise that resolves after the specified time. 14 | */ 15 | async function sleep(ms) { 16 | await new Promise((resolve) => setTimeout(resolve, ms)); 17 | } 18 | 19 | /** 20 | * Asynchronously logs a message and waits for the specified number of milliseconds. 21 | * 22 | * @param ms - The number of milliseconds to wait. 23 | */ 24 | async function delayLog(ms) { 25 | console.log(`Waiting for ${ms / 1000}s...`); 26 | await sleep(ms); 27 | } 28 | 29 | /** 30 | * Checks if the provided address is valid and returns the checksummed address if valid. 31 | * Otherwise, returns false. 32 | * 33 | * @param value - The address to be checked. 34 | * @returns The checksummed address if valid, or false. 35 | */ 36 | function isAddress(value) { 37 | try { 38 | return getAddress(value); 39 | } catch { 40 | return false; 41 | } 42 | } 43 | 44 | /** 45 | * Creates a random checksummed address using the provided salt. 46 | * 47 | * @param salt - The salt to generate the address. 48 | * @returns The checksummed address. 49 | */ 50 | function createRandomChecksumAddress(salt) { 51 | const signerAddress = computeAddress( 52 | solidityPackedKeccak256(["string"], [salt]) 53 | ); 54 | const checkSummedSignerAddress = getAddress(signerAddress); 55 | return checkSummedSignerAddress; 56 | } 57 | 58 | /** 59 | * Retrieves necessary gas information of a transaction. 60 | * 61 | * @param tx - The transaction response (e.g., contract deployment or executed transaction). 62 | * @returns A string containing gas information or null if the transaction is falsy or unsuccessful. 63 | */ 64 | async function getExtraGasInfo(tx) { 65 | if (!tx) { 66 | return null; 67 | } 68 | 69 | const gasPrice = tx.gasPrice; 70 | const gasUsed = tx.gasLimit * gasPrice; 71 | const txReceipt = await tx.wait(); 72 | 73 | if (!txReceipt) { 74 | return null; 75 | } 76 | 77 | const gas = txReceipt.gasUsed; 78 | const extraGasInfo = `${toGwei(gasPrice)} gwei, ${fromWei( 79 | gasUsed 80 | )} ETH, ${gas} gas, txHash: ${tx.hash}`; 81 | 82 | return extraGasInfo; 83 | } 84 | 85 | module.exports = { 86 | sleep, 87 | delayLog, 88 | isAddress, 89 | createRandomChecksumAddress, 90 | getExtraGasInfo, 91 | }; 92 | -------------------------------------------------------------------------------- /utils/string.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | "use strict"; 3 | 4 | // String utils 5 | // 6 | // resources: 7 | // -- mout, https://github.com/mout/mout/tree/master/src/string 8 | 9 | /** 10 | * "Safer" String.toLowerCase() 11 | */ 12 | function lowerCase(str) { 13 | return str.toLowerCase(); 14 | } 15 | 16 | /** 17 | * "Safer" String.toUpperCase() 18 | */ 19 | function upperCase(str) { 20 | return str.toUpperCase(); 21 | } 22 | 23 | /** 24 | * Convert string to camelCase text. 25 | */ 26 | function camelCase(str) { 27 | str = replaceAccents(str); 28 | str = removeNonWord(str) 29 | .replace(/\-/g, " ") //convert all hyphens to spaces 30 | .replace(/\s[a-z]/g, upperCase) //convert first char of each word to UPPERCASE 31 | .replace(/\s+/g, "") //remove spaces 32 | .replace(/^[A-Z]/g, lowerCase); //convert first char to lowercase 33 | return str; 34 | } 35 | 36 | /** 37 | * Add space between camelCase text. 38 | */ 39 | function unCamelCase(str) { 40 | str = str.replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, "$1 $2"); 41 | str = str.toLowerCase(); //add space between camelCase text 42 | return str; 43 | } 44 | 45 | /** 46 | * UPPERCASE first char of each word. 47 | */ 48 | function properCase(str) { 49 | return lowerCase(str).replace(/^\w|\s\w/g, upperCase); 50 | } 51 | 52 | /** 53 | * camelCase + UPPERCASE first char 54 | */ 55 | function pascalCase(str) { 56 | return camelCase(str).replace(/^[a-z]/, upperCase); 57 | } 58 | 59 | /** 60 | * UPPERCASE first char of each sentence and lowercase other chars. 61 | */ 62 | function sentenceCase(str) { 63 | // Replace first char of each sentence (new line or after '.\s+') to 64 | // UPPERCASE 65 | return lowerCase(str).replace(/(^\w)|\.\s+(\w)/gm, upperCase); 66 | } 67 | 68 | /** 69 | * Convert to lower case, remove accents, remove non-word chars and 70 | * replace spaces with the specified delimeter. 71 | * Does not split camelCase text. 72 | */ 73 | function slugify(str, delimeter) { 74 | if (delimeter == null) { 75 | delimeter = "-"; 76 | } 77 | 78 | str = replaceAccents(str); 79 | str = removeNonWord(str); 80 | str = trim(str) //should come after removeNonWord 81 | .replace(/ +/g, delimeter) //replace spaces with delimeter 82 | .toLowerCase(); 83 | 84 | return str; 85 | } 86 | 87 | /** 88 | * Replaces spaces with hyphens, split camelCase text, remove non-word chars, remove accents and convert to lower case. 89 | */ 90 | function hyphenate(str) { 91 | str = unCamelCase(str); 92 | return slugify(str, "-"); 93 | } 94 | 95 | /** 96 | * Replaces hyphens with spaces. (only hyphens between word chars) 97 | */ 98 | function unhyphenate(str) { 99 | return str.replace(/(\w)(-)(\w)/g, "$1 $3"); 100 | } 101 | 102 | /** 103 | * Replaces spaces with underscores, split camelCase text, remove 104 | * non-word chars, remove accents and convert to lower case. 105 | */ 106 | function underscore(str) { 107 | str = unCamelCase(str); 108 | return slugify(str, "_"); 109 | } 110 | 111 | /** 112 | * Remove non-word chars. 113 | */ 114 | function removeNonWord(str) { 115 | return str.replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, ""); 116 | } 117 | 118 | /** 119 | * Convert line-breaks from DOS/MAC to a single standard (UNIX by default) 120 | */ 121 | function normalizeLineBreaks(str, lineEnd) { 122 | lineEnd = lineEnd || "\n"; 123 | 124 | return str 125 | .replace(/\r\n/g, lineEnd) // DOS 126 | .replace(/\r/g, lineEnd) // Mac 127 | .replace(/\n/g, lineEnd); // Unix 128 | } 129 | 130 | /** 131 | * Replaces all accented chars with regular ones 132 | */ 133 | function replaceAccents(str) { 134 | // verifies if the String has accents and replace them 135 | if (str.search(/[\xC0-\xFF]/g) > -1) { 136 | str = str 137 | .replace(/[\xC0-\xC5]/g, "A") 138 | .replace(/[\xC6]/g, "AE") 139 | .replace(/[\xC7]/g, "C") 140 | .replace(/[\xC8-\xCB]/g, "E") 141 | .replace(/[\xCC-\xCF]/g, "I") 142 | .replace(/[\xD0]/g, "D") 143 | .replace(/[\xD1]/g, "N") 144 | .replace(/[\xD2-\xD6\xD8]/g, "O") 145 | .replace(/[\xD9-\xDC]/g, "U") 146 | .replace(/[\xDD]/g, "Y") 147 | .replace(/[\xDE]/g, "P") 148 | .replace(/[\xE0-\xE5]/g, "a") 149 | .replace(/[\xE6]/g, "ae") 150 | .replace(/[\xE7]/g, "c") 151 | .replace(/[\xE8-\xEB]/g, "e") 152 | .replace(/[\xEC-\xEF]/g, "i") 153 | .replace(/[\xF1]/g, "n") 154 | .replace(/[\xF2-\xF6\xF8]/g, "o") 155 | .replace(/[\xF9-\xFC]/g, "u") 156 | .replace(/[\xFE]/g, "p") 157 | .replace(/[\xFD\xFF]/g, "y"); 158 | } 159 | 160 | return str; 161 | } 162 | 163 | /** 164 | * Searches for a given substring 165 | */ 166 | function contains(str, substring, fromIndex) { 167 | return str.indexOf(substring, fromIndex) !== -1; 168 | } 169 | 170 | /** 171 | * Truncate string at full words. 172 | */ 173 | function crop(str, maxChars, append) { 174 | return truncate(str, maxChars, append, true); 175 | } 176 | 177 | /** 178 | * Escape RegExp string chars. 179 | */ 180 | function escapeRegExp(str) { 181 | var ESCAPE_CHARS = /[\\.+*?\^$\[\](){}\/'#]/g; 182 | return str.replace(ESCAPE_CHARS, "\\$&"); 183 | } 184 | 185 | /** 186 | * Escapes a string for insertion into HTML. 187 | */ 188 | function escapeHtml(str) { 189 | str = str 190 | .replace(/&/g, "&") 191 | .replace(//g, ">") 193 | .replace(/'/g, "'") 194 | .replace(/"/g, """); 195 | 196 | return str; 197 | } 198 | 199 | /** 200 | * Unescapes HTML special chars 201 | */ 202 | function unescapeHtml(str) { 203 | str = str 204 | .replace(/&/g, "&") 205 | .replace(/</g, "<") 206 | .replace(/>/g, ">") 207 | .replace(/'/g, "'") 208 | .replace(/"/g, '"'); 209 | return str; 210 | } 211 | 212 | /** 213 | * Escape string into unicode sequences 214 | */ 215 | function escapeUnicode(str, shouldEscapePrintable) { 216 | return str.replace(/[\s\S]/g, function (ch) { 217 | // skip printable ASCII chars if we should not escape them 218 | if (!shouldEscapePrintable && /[\x20-\x7E]/.test(ch)) { 219 | return ch; 220 | } 221 | // we use "000" and slice(-4) for brevity, need to pad zeros, 222 | // unicode escape always have 4 chars after "\u" 223 | return "\\u" + ("000" + ch.charCodeAt(0).toString(16)).slice(-4); 224 | }); 225 | } 226 | 227 | /** 228 | * Remove HTML tags from string. 229 | */ 230 | function stripHtmlTags(str) { 231 | return str.replace(/<[^>]*>/g, ""); 232 | } 233 | 234 | /** 235 | * Remove non-printable ASCII chars 236 | */ 237 | function removeNonASCII(str) { 238 | // Matches non-printable ASCII chars - 239 | // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters 240 | return str.replace(/[^\x20-\x7E]/g, ""); 241 | } 242 | 243 | /** 244 | * String interpolation 245 | */ 246 | function interpolate(template, replacements, syntax) { 247 | var stache = /\{\{(\w+)\}\}/g; //mustache-like 248 | 249 | var replaceFn = function (match, prop) { 250 | return prop in replacements ? replacements[prop] : ""; 251 | }; 252 | 253 | return template.replace(syntax || stache, replaceFn); 254 | } 255 | 256 | /** 257 | * Pad string with `char` if its' length is smaller than `minLen` 258 | */ 259 | function rpad(str, minLen, ch) { 260 | ch = ch || " "; 261 | return str.length < minLen ? str + repeat(ch, minLen - str.length) : str; 262 | } 263 | 264 | /** 265 | * Pad string with `char` if its' length is smaller than `minLen` 266 | */ 267 | function lpad(str, minLen, ch) { 268 | ch = ch || " "; 269 | 270 | return str.length < minLen ? repeat(ch, minLen - str.length) + str : str; 271 | } 272 | 273 | /** 274 | * Repeat string n times 275 | */ 276 | function repeat(str, n) { 277 | return new Array(n + 1).join(str); 278 | } 279 | 280 | /** 281 | * Limit number of chars. 282 | */ 283 | function truncate(str, maxChars, append, onlyFullWords) { 284 | append = append || "..."; 285 | maxChars = onlyFullWords ? maxChars + 1 : maxChars; 286 | 287 | str = trim(str); 288 | if (str.length <= maxChars) { 289 | return str; 290 | } 291 | str = str.substr(0, maxChars - append.length); 292 | //crop at last space or remove trailing whitespace 293 | str = onlyFullWords ? str.substr(0, str.lastIndexOf(" ")) : trim(str); 294 | return str + append; 295 | } 296 | 297 | var WHITE_SPACES = [ 298 | " ", 299 | "\n", 300 | "\r", 301 | "\t", 302 | "\f", 303 | "\v", 304 | "\u00A0", 305 | "\u1680", 306 | "\u180E", 307 | "\u2000", 308 | "\u2001", 309 | "\u2002", 310 | "\u2003", 311 | "\u2004", 312 | "\u2005", 313 | "\u2006", 314 | "\u2007", 315 | "\u2008", 316 | "\u2009", 317 | "\u200A", 318 | "\u2028", 319 | "\u2029", 320 | "\u202F", 321 | "\u205F", 322 | "\u3000", 323 | ]; 324 | 325 | /** 326 | * Remove chars from beginning of string. 327 | */ 328 | function ltrim(str, chars) { 329 | chars = chars || WHITE_SPACES; 330 | 331 | var start = 0, 332 | len = str.length, 333 | charLen = chars.length, 334 | found = true, 335 | i, 336 | c; 337 | 338 | while (found && start < len) { 339 | found = false; 340 | i = -1; 341 | c = str.charAt(start); 342 | 343 | while (++i < charLen) { 344 | if (c === chars[i]) { 345 | found = true; 346 | start++; 347 | break; 348 | } 349 | } 350 | } 351 | 352 | return start >= len ? "" : str.substr(start, len); 353 | } 354 | 355 | /** 356 | * Remove chars from end of string. 357 | */ 358 | function rtrim(str, chars) { 359 | chars = chars || WHITE_SPACES; 360 | 361 | var end = str.length - 1, 362 | charLen = chars.length, 363 | found = true, 364 | i, 365 | c; 366 | 367 | while (found && end >= 0) { 368 | found = false; 369 | i = -1; 370 | c = str.charAt(end); 371 | 372 | while (++i < charLen) { 373 | if (c === chars[i]) { 374 | found = true; 375 | end--; 376 | break; 377 | } 378 | } 379 | } 380 | 381 | return end >= 0 ? str.substring(0, end + 1) : ""; 382 | } 383 | 384 | /** 385 | * Remove white-spaces from beginning and end of string. 386 | */ 387 | function trim(str, chars) { 388 | chars = chars || WHITE_SPACES; 389 | return ltrim(rtrim(str, chars), chars); 390 | } 391 | 392 | /** 393 | * Capture all capital letters following a word boundary (in case the 394 | * input is in all caps) 395 | */ 396 | function abbreviate(str) { 397 | return str.match(/\b([A-Z])/g).join(""); 398 | } 399 | 400 | module.exports = { 401 | lowerCase, 402 | upperCase, 403 | camelCase, 404 | unCamelCase, 405 | properCase, 406 | pascalCase, 407 | sentenceCase, 408 | slugify, 409 | hyphenate, 410 | unhyphenate, 411 | underscore, 412 | removeNonWord, 413 | normalizeLineBreaks, 414 | replaceAccents, 415 | contains, 416 | crop, 417 | escapeRegExp, 418 | escapeHtml, 419 | unescapeHtml, 420 | escapeUnicode, 421 | stripHtmlTags, 422 | removeNonASCII, 423 | interpolate, 424 | rpad, 425 | lpad, 426 | repeat, 427 | truncate, 428 | ltrim, 429 | rtrim, 430 | trim, 431 | abbreviate, 432 | }; 433 | -------------------------------------------------------------------------------- /utils/verify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { run } = require("hardhat"); 4 | const { delayLog } = require("./misc"); 5 | 6 | /** 7 | * Waits for the specified number of confirmations for a given transaction. 8 | * 9 | * @param tx - The transaction response to wait for confirmations. 10 | * @param waitConfirmations - The number of confirmations to wait for. Default is 5. 11 | * @returns A promise that resolves when the specified number of confirmations have been received. 12 | */ 13 | async function waitForConfirmations(tx, waitConfirmations = 5) { 14 | if (!tx) return; 15 | console.log(`waiting for ${waitConfirmations} confirmations ...`); 16 | await tx.wait(waitConfirmations); 17 | } 18 | 19 | /** 20 | * Programmatically verify the given contract using the specified parameters. 21 | * 22 | * @param {*} contractPath contract name in string e.g. `contracts/${contractName}.sol:${contractName}` 23 | * @param {*} contractAddress contract address in string 24 | * @param {*} args constructor args in array 25 | * @param delay delay time in ms 26 | */ 27 | async function verifyContract({ 28 | contractPath, 29 | contractAddress, 30 | args = [], 31 | delay = 60_000, 32 | }) { 33 | await delayLog(delay); 34 | 35 | try { 36 | await run("verify:verify", { 37 | address: contractAddress, 38 | constructorArguments: args, 39 | contract: contractPath, 40 | }); 41 | } catch (error) { 42 | if (error.message.toLowerCase().includes("already verified")) { 43 | console.log("Already verified!"); 44 | } else { 45 | console.log(error); 46 | } 47 | } 48 | } 49 | 50 | module.exports = { 51 | waitForConfirmations, 52 | verifyContract, 53 | }; 54 | --------------------------------------------------------------------------------