├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── README.md ├── contracts └── Counter.sol ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts └── deploy.ts ├── test └── counter.ts └── tsconfig.json /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache/ 3 | build/ 4 | typechain/ 5 | artifacts/ 6 | coverage* 7 | .env 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typescript Solidity Dev Starter Kit 2 | 3 | _Updated to use Hardhat!_ 4 | 5 | This is a starter kit for developing, testing, and deploying smart contracts with a full Typescript environment. This stack uses [Hardhat](https://hardhat.org) as the platform layer to orchestrate all the tasks. [Ethers](https://docs.ethers.io/v5/) is used for all Ethereum interactions and testing. 6 | 7 | [Blog Post](https://medium.com/@rahulsethuram/the-new-solidity-dev-stack-buidler-ethers-waffle-typescript-tutorial-f07917de48ae) 8 | 9 | ## Using this Project 10 | 11 | Clone this repository, then install the dependencies with `npm install`. Build everything with `npm run build`. https://hardhat.org has excellent docs, and can be used as reference for extending this project. 12 | 13 | ## Available Functionality 14 | 15 | ### Build Contracts and Generate Typechain Typeings 16 | 17 | `npm run compile` 18 | 19 | ### Run Contract Tests & Get Callstacks 20 | 21 | In one terminal run `npx hardhat node` 22 | 23 | Then in another run `npm run test` 24 | 25 | Notes: 26 | 27 | - The gas usage table may be incomplete (the gas report currently needs to run with the `--network localhost` flag; see below). 28 | 29 | ### Run Contract Tests and Generate Gas Usage Report 30 | 31 | In one terminal run `npx hardhat node` 32 | 33 | Then in another run `npm run test -- --network localhost` 34 | 35 | Notes: 36 | 37 | - When running with this `localhost` option, you get a gas report but may not get good callstacks 38 | - See [here](https://github.com/cgewecke/eth-gas-reporter#installation-and-config) for how to configure the gas usage report. 39 | 40 | ### Run Coverage Report for Tests 41 | 42 | `npm run coverage` 43 | 44 | Notes: 45 | 46 | - running a coverage report currently deletes artifacts, so after each coverage run you will then need to run `npx hardhat clean` followed by `npm run build` before re-running tests 47 | - the branch coverage is 75% 48 | 49 | ### Deploy to Ethereum 50 | 51 | Create/modify network config in `hardhat.config.ts` and add API key and private key, then run: 52 | 53 | `npx hardhat run --network rinkeby scripts/deploy.ts` 54 | 55 | ### Verify on Etherscan 56 | 57 | Using the [hardhat-etherscan plugin](https://hardhat.org/plugins/nomiclabs-hardhat-etherscan.html), add Etherscan API key to `hardhat.config.ts`, then run: 58 | 59 | `npx hardhat verify --network rinkeby ` 60 | 61 | PRs and feedback welcome! 62 | -------------------------------------------------------------------------------- /contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.6; 2 | 3 | import "hardhat/console.sol"; 4 | 5 | contract Counter { 6 | uint256 count = 0; 7 | 8 | event CountedTo(uint256 number); 9 | 10 | function getCount() public view returns (uint256) { 11 | return count; 12 | } 13 | 14 | function countUp() public returns (uint256) { 15 | console.log("countUp: count =", count); 16 | uint256 newCount = count + 1; 17 | require(newCount > count, "Uint256 overflow"); 18 | 19 | count = newCount; 20 | 21 | emit CountedTo(count); 22 | return count; 23 | } 24 | 25 | function countDown() public returns (uint256) { 26 | console.log("countDown: count =", count); 27 | uint256 newCount = count - 1; 28 | require(newCount < count, "Uint256 underflow"); 29 | 30 | count = newCount; 31 | 32 | emit CountedTo(count); 33 | return count; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotEnvConfig } from "dotenv"; 2 | dotEnvConfig(); 3 | 4 | import { HardhatUserConfig } from "hardhat/types"; 5 | 6 | import "@nomiclabs/hardhat-waffle"; 7 | import "@typechain/hardhat"; 8 | import "@nomiclabs/hardhat-etherscan"; 9 | import "solidity-coverage"; 10 | 11 | interface Etherscan { 12 | etherscan: { apiKey: string | undefined }; 13 | } 14 | 15 | type HardhatUserEtherscanConfig = HardhatUserConfig & Etherscan; 16 | 17 | 18 | const INFURA_API_KEY = process.env.INFURA_API_KEY || ""; 19 | const RINKEBY_PRIVATE_KEY = 20 | process.env.RINKEBY_PRIVATE_KEY! || 21 | "0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"; // well known private key 22 | const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; 23 | 24 | const config: HardhatUserEtherscanConfig = { 25 | defaultNetwork: "hardhat", 26 | solidity: { 27 | compilers: [{ version: "0.7.6", settings: {} }], 28 | }, 29 | networks: { 30 | hardhat: {}, 31 | localhost: {}, 32 | rinkeby: { 33 | url: `https://rinkeby.infura.io/v3/${INFURA_API_KEY}`, 34 | accounts: [RINKEBY_PRIVATE_KEY], 35 | }, 36 | coverage: { 37 | url: "http://127.0.0.1:8555", // Coverage launches its own ganache-cli client 38 | }, 39 | }, 40 | etherscan: { 41 | // Your API key for Etherscan 42 | // Obtain one at https://etherscan.io/ 43 | apiKey: ETHERSCAN_API_KEY, 44 | }, 45 | }; 46 | 47 | export default config; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-ts-dev-stack-example", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run clean && npm run compile", 8 | "clean": "npx hardhat clean", 9 | "compile": "npx hardhat compile", 10 | "test": "npx hardhat test", 11 | "coverage": "npm run build && npx hardhat coverage --temp artifacts --network coverage" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/rhlsthrm/solidity-ts-dev-stack-example.git" 16 | }, 17 | "author": "rhlsthrm", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/rhlsthrm/solidity-ts-dev-stack-example/issues" 21 | }, 22 | "homepage": "https://github.com/rhlsthrm/solidity-ts-dev-stack-example#readme", 23 | "devDependencies": { 24 | "@nomiclabs/hardhat-ethers": "^2.0.2", 25 | "@nomiclabs/hardhat-etherscan": "^2.1.2", 26 | "@nomiclabs/hardhat-waffle": "^2.0.1", 27 | "@typechain/ethers-v5": "^7.0.0", 28 | "@typechain/hardhat": "^2.0.0", 29 | "@types/chai": "^4.2.18", 30 | "@types/chai-as-promised": "^7.1.1", 31 | "@types/mocha": "^8.2.2", 32 | "@types/node": "^15.0.3", 33 | "chai": "^4.3.4", 34 | "chai-as-promised": "^7.1.1", 35 | "dotenv": "^9.0.2", 36 | "ethereum-waffle": "^3.3.0", 37 | "ethers": "^5.1.4", 38 | "hardhat": "^2.2.1", 39 | "solidity-coverage": "^0.7.16", 40 | "ts-generator": "^0.1.1", 41 | "ts-node": "^9.1.1", 42 | "typechain": "^5.0.0", 43 | "typescript": "^4.2.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const factory = await ethers.getContractFactory("Counter"); 5 | 6 | // If we had constructor arguments, they would be passed into deploy() 7 | let contract = await factory.deploy(); 8 | 9 | console.log( 10 | `The address the Contract WILL have once mined: ${contract.address}` 11 | ); 12 | 13 | console.log( 14 | `The transaction that was sent to the network to deploy the Contract: ${ 15 | contract.deployTransaction.hash 16 | }` 17 | ); 18 | 19 | console.log( 20 | 'The contract is NOT deployed yet; we must wait until it is mined...' 21 | ); 22 | await contract.deployed(); 23 | console.log('Mined!'); 24 | } 25 | 26 | main() 27 | .then(() => process.exit(0)) 28 | .catch((error) => { 29 | console.error(error); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /test/counter.ts: -------------------------------------------------------------------------------- 1 | import { ethers} from "hardhat"; 2 | import chai from "chai"; 3 | import chaiAsPromised from "chai-as-promised"; 4 | import { Counter__factory, Counter } from "../typechain"; 5 | 6 | chai.use(chaiAsPromised); 7 | const { expect } = chai; 8 | 9 | describe("Counter", () => { 10 | let counter: Counter; 11 | 12 | beforeEach(async () => { 13 | // 1 14 | const signers = await ethers.getSigners(); 15 | 16 | // 2 17 | const counterFactory = (await ethers.getContractFactory( 18 | "Counter", 19 | signers[0] 20 | )) as Counter__factory; 21 | counter = await counterFactory.deploy(); 22 | await counter.deployed(); 23 | const initialCount = await counter.getCount(); 24 | 25 | // 3 26 | expect(initialCount).to.eq(0); 27 | expect(counter.address).to.properAddress; 28 | }); 29 | 30 | // 4 31 | describe("count up", async () => { 32 | it("should count up", async () => { 33 | await counter.countUp(); 34 | let count = await counter.getCount(); 35 | expect(count).to.eq(1); 36 | }); 37 | }); 38 | 39 | describe("count down", async () => { 40 | // 5 41 | it("should fail due to underflow exception", () => { 42 | return expect(counter.countDown()).to.eventually.be.rejectedWith(Error, 'Uint256 underflow'); 43 | }); 44 | 45 | it("should count down", async () => { 46 | await counter.countUp(); 47 | 48 | await counter.countDown(); 49 | const count = await counter.getCount(); 50 | expect(count).to.eq(0); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["./scripts", "./test"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------