├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .solhint.json ├── .solhintignore ├── LICENSE.md ├── README.md ├── contracts └── Greeter.sol ├── frontend ├── .gitignore ├── README.md ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.tsx │ ├── components │ │ ├── ActivateDeactivate.tsx │ │ ├── Greeter.tsx │ │ ├── SectionDivider.tsx │ │ ├── SignMessage.tsx │ │ └── WalletStatus.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── utils │ │ ├── connectors.ts │ │ ├── hooks.ts │ │ └── provider.ts ├── tsconfig.json └── yarn.lock ├── hardhat.config.ts ├── package.json ├── tasks └── deploy.ts ├── test └── Greeter.test.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | ETHERSCAN_API_KEY= 2 | ROPSTEN_URL=https://eth-ropsten.alchemyapi.io/v2/ 3 | TEST_ETH_ACCOUNT_PRIVATE_KEY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | cache 3 | coverage* 4 | gasReporterOutput.json 5 | node_modules 6 | typechain 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true 7 | }, 8 | plugins: ['@typescript-eslint'], 9 | extends: [ 10 | 'standard', 11 | 'plugin:prettier/recommended', 12 | 'plugin:node/recommended', 13 | 'plugin:import/typescript' 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | ecmaVersion: 12 18 | }, 19 | rules: { 20 | 'node/no-unsupported-features/es-syntax': [ 21 | 'error', 22 | { ignores: ['modules'] } 23 | ] 24 | }, 25 | settings: { 26 | node: { 27 | tryExtensions: ['.js', '.json', '.node', '.ts', '.d.ts'] 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Env 3 | .env 4 | 5 | # Node JS 6 | node_modules 7 | 8 | # Coverage 9 | coverage* 10 | coverage.json 11 | 12 | # Typechain 13 | typechain 14 | 15 | # Hardhat 16 | artifacts 17 | cache 18 | gasReporterOutput.json 19 | 20 | # VS Code 21 | vscode-user-settings.json 22 | vscode-workspace-settings.json 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | tasks 3 | test 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | cache 3 | coverage* 4 | gasReporterOutput.json 5 | node_modules 6 | typechain 7 | 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | printWidth: 80, 4 | semi: true, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'none' 8 | }; 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ChainShot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starter React Typescript Ethers.js Hardhat Project 2 | 3 | This repo contains a Hardhat and React Dapp starter project. The React Dapp in the `frontend` dir of this repo interacts with Hardhat's example `Greeter.sol` smart contract running on a local blockchain. The Hardhat `Greeter.sol` example contract is the boilerplate contract that Hardhat creates when creating a new Hardhat project via `yarn hardhat init`. 4 | 5 | The React Dapp in this repo looks like this: 6 | 7 | ![React Dapp](https://res.cloudinary.com/divzjiip8/image/upload/c_scale,w_1280/v1641785505/Screen_Shot_2022-01-03_at_3.52.58_PM_n7ror7.png) 8 | 9 | The Dapp uses the [@web3-react npm package's](https://www.npmjs.com/package/web3-react) injected web3 provider to connect to MetaMask and demonstrates the following functionality: 10 | * Connecting a Dapp to the blockchain 11 | * Reading account data from the blockchain 12 | * Cryptographically signing digital messages 13 | * Deploying new instances of a smart contract 14 | * Reading and writing data to and from the deployed smart contract 15 | 16 | This repo can be useful to anyone looking to get a local Ethereum blockchain running and to get a Dapp up and communicating with the local node quickly. 17 | 18 | Additionally, this repo is a companion project to [ChainShot](https://www.chainshot.com)'s [How to Build a React Dapp with Hardhat and MetaMask](https://medium.com/p/9cec8f6410d3) Medium article. The article and this GitHub repo are recommended for anyone wanting to build up their web3 skills and are helpful resources for anyone interested in joining any of [ChainShot's bootcamps](https://www.chainshot.com/bootcamp). 19 | 20 | The smart contract and Hardhat node part of this project were created by installing the [Hardhat npm package](https://www.npmjs.com/package/hardhat) and bootstrapping a Hardhat project by running: `yarn hardhat init`. For more details you can read more in the [Hardhat README doc](https://github.com/nomiclabs/hardhat). The `frontend` part of this project was created using [Create React App](https://github.com/facebook/create-react-app). 21 | 22 | Pull this project down from GitHub, cd into the project directory and run the following commands to get setup and running. 23 | 24 | ```shell 25 | yarn 26 | yarn compile 27 | yarn hardhat node 28 | ``` 29 | 30 | The commands above will install the project dependencies, compile the sample contract and run a local Hardhat node on port `8545`, using chain id `31337`. 31 | 32 | After running the above tasks checkout the frontend [README.md](https://github.com/ChainShot/hardhat-ethers-react-ts-starter/tree/main/frontend/README.md) to run a React Dapp using ethers.js that will interact with the sample contract on the local Hardhat node. 33 | 34 | Some other hardhat tasks to try out are: 35 | 36 | ```shell 37 | yarn hardhat accounts 38 | yarn hardhat clean 39 | yarn hardhat compile 40 | yarn hardhat deploy 41 | yarn hardhat help 42 | yarn hardhat node 43 | yarn hardhat test 44 | ``` 45 | -------------------------------------------------------------------------------- /contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "hardhat/console.sol"; 5 | 6 | contract Greeter { 7 | string private greeting; 8 | 9 | constructor(string memory _greeting) { 10 | console.log("Deploying a Greeter with greeting:", _greeting); 11 | greeting = _greeting; 12 | } 13 | 14 | function greet() public view returns (string memory) { 15 | return greeting; 16 | } 17 | 18 | function setGreeting(string memory _greeting) public { 19 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); 20 | greeting = _greeting; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate React Typescript Ethers.js Hardhat Project (Frontend) 2 | 3 | If you haven't already read the Hardhat README for this project, checkout the Hardhat [README.md](https://github.com/ChainShot/hardhat-ethers-react-ts-starter/tree/main/README.md) first and then come back to this README file. 4 | 5 | This Dapp was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) using the Typescript template. Additionally it makes use of the popular [@web3-react](https://www.npmjs.com/package/web3-react) npm package. The Metamask integration code found in this Dapp is heavily based on the code found in the [@web3-react example project](https://github.com/NoahZinsmeister/web3-react/tree/v6/example). For simplicity only the Metamask (injected) blockchain provider is used in this Dapp. 6 | 7 | The Dapp is a simple, but complete React Dapp that interacts with a locally run 'Greeter' smart contract for developers new to web3. It introduces the developer to the following: 8 | 1. How to use React and the [@web3-react](https://github.com/NoahZinsmeister/web3-react) npm package to connect to Metamask and display data regarding the connected Metamask wallet in the UI, such as the connected wallet's address, balance and nonce. 9 | 3. How to deploy a new instance of the Greeter contract to the local Hardhat blockchain from the UI. 10 | 4. How to perform a read-only call to the blockchain and display data stored on the blockchain in the UI. 11 | 5. How to update the state of the local blockchain by updating the Greeter contract's `greeting` message via a transaction from the UI. 12 | 6. How to keep the data of the connected wallet up-to-date by listening to new block events via ethers.js as new blocks are mined. 13 | 14 | To start the frontend: 15 | 1. `cd` to the frontend directory of this project 16 | 2. Run `yarn` to install the necessary dependencies 17 | 3. `yarn start` to startup the webserver 18 | 4. Visit `localhost:3000` in your browser to interact with the browser Dapp and the Greeter contract running on your local Hardhat blockchain. 19 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "description": "Typescript starter template for hardhat, ethers and react projects using typescript and @web3-react", 6 | "engines": { 7 | "node": "^16.13.1", 8 | "npm": "8.1.2" 9 | }, 10 | "dependencies": { 11 | "@testing-library/jest-dom": "^5.14.1", 12 | "@testing-library/react": "^12.0.0", 13 | "@testing-library/user-event": "^13.2.1", 14 | "@types/jest": "^27.0.1", 15 | "@types/node": "^16.7.13", 16 | "@types/react": "^17.0.20", 17 | "@types/react-dom": "^17.0.9", 18 | "@types/styled-components": "^5.1.18", 19 | "@web3-react/core": "^6.1.9", 20 | "@web3-react/injected-connector": "^6.0.7", 21 | "ethers": "^5.5.2", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2", 24 | "react-scripts": "5.0.0", 25 | "styled-components": "^5.3.3", 26 | "typescript": "^4.4.2" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@types/styled-components": "^5.1.18" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Ethereum Hardhat Typescript Starter Kit", 3 | "name": "Ethereum Hardhat Typescript Starter Kit", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import { ActivateDeactivate } from './components/ActivateDeactivate'; 4 | import { Greeter } from './components/Greeter'; 5 | import { SectionDivider } from './components/SectionDivider'; 6 | import { SignMessage } from './components/SignMessage'; 7 | import { WalletStatus } from './components/WalletStatus'; 8 | 9 | const StyledAppDiv = styled.div` 10 | display: grid; 11 | grid-gap: 20px; 12 | `; 13 | 14 | export function App(): ReactElement { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/ActivateDeactivate.tsx: -------------------------------------------------------------------------------- 1 | import { AbstractConnector } from '@web3-react/abstract-connector'; 2 | import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'; 3 | import { 4 | NoEthereumProviderError, 5 | UserRejectedRequestError 6 | } from '@web3-react/injected-connector'; 7 | import { MouseEvent, ReactElement, useState } from 'react'; 8 | import styled from 'styled-components'; 9 | import { injected } from '../utils/connectors'; 10 | import { useEagerConnect, useInactiveListener } from '../utils/hooks'; 11 | import { Provider } from '../utils/provider'; 12 | 13 | type ActivateFunction = ( 14 | connector: AbstractConnector, 15 | onError?: (error: Error) => void, 16 | throwErrors?: boolean 17 | ) => Promise; 18 | 19 | function getErrorMessage(error: Error): string { 20 | let errorMessage: string; 21 | 22 | switch (error.constructor) { 23 | case NoEthereumProviderError: 24 | errorMessage = `No Ethereum browser extension detected. Please install MetaMask extension.`; 25 | break; 26 | case UnsupportedChainIdError: 27 | errorMessage = `You're connected to an unsupported network.`; 28 | break; 29 | case UserRejectedRequestError: 30 | errorMessage = `Please authorize this website to access your Ethereum account.`; 31 | break; 32 | default: 33 | errorMessage = error.message; 34 | } 35 | 36 | return errorMessage; 37 | } 38 | 39 | const StyledActivateDeactivateDiv = styled.div` 40 | display: grid; 41 | grid-template-rows: 1fr; 42 | grid-template-columns: 1fr 1fr; 43 | grid-gap: 10px; 44 | place-self: center; 45 | align-items: center; 46 | `; 47 | 48 | const StyledActivateButton = styled.button` 49 | width: 150px; 50 | height: 2rem; 51 | border-radius: 1rem; 52 | border-color: green; 53 | cursor: pointer; 54 | `; 55 | 56 | const StyledDeactivateButton = styled.button` 57 | width: 150px; 58 | height: 2rem; 59 | border-radius: 1rem; 60 | border-color: red; 61 | cursor: pointer; 62 | `; 63 | 64 | function Activate(): ReactElement { 65 | const context = useWeb3React(); 66 | const { activate, active } = context; 67 | 68 | const [activating, setActivating] = useState(false); 69 | 70 | function handleActivate(event: MouseEvent): void { 71 | event.preventDefault(); 72 | 73 | async function _activate(activate: ActivateFunction): Promise { 74 | setActivating(true); 75 | await activate(injected); 76 | setActivating(false); 77 | } 78 | 79 | _activate(activate); 80 | } 81 | 82 | // handle logic to eagerly connect to the injected ethereum provider, if it exists and has 83 | // granted access already 84 | const eagerConnectionSuccessful = useEagerConnect(); 85 | 86 | // handle logic to connect in reaction to certain events on the injected ethereum provider, 87 | // if it exists 88 | useInactiveListener(!eagerConnectionSuccessful); 89 | 90 | return ( 91 | 99 | Connect 100 | 101 | ); 102 | } 103 | 104 | function Deactivate(): ReactElement { 105 | const context = useWeb3React(); 106 | const { deactivate, active } = context; 107 | 108 | function handleDeactivate(event: MouseEvent): void { 109 | event.preventDefault(); 110 | 111 | deactivate(); 112 | } 113 | 114 | return ( 115 | 123 | Disconnect 124 | 125 | ); 126 | } 127 | 128 | export function ActivateDeactivate(): ReactElement { 129 | const context = useWeb3React(); 130 | const { error } = context; 131 | 132 | if (!!error) { 133 | window.alert(getErrorMessage(error)); 134 | } 135 | 136 | return ( 137 | 138 | 139 | 140 | 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /frontend/src/components/Greeter.tsx: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from '@web3-react/core'; 2 | import { Contract, ethers, Signer } from 'ethers'; 3 | import { 4 | ChangeEvent, 5 | MouseEvent, 6 | ReactElement, 7 | useEffect, 8 | useState 9 | } from 'react'; 10 | import styled from 'styled-components'; 11 | import GreeterArtifact from '../artifacts/contracts/Greeter.sol/Greeter.json'; 12 | import { Provider } from '../utils/provider'; 13 | import { SectionDivider } from './SectionDivider'; 14 | 15 | const StyledDeployContractButton = styled.button` 16 | width: 180px; 17 | height: 2rem; 18 | border-radius: 1rem; 19 | border-color: blue; 20 | cursor: pointer; 21 | place-self: center; 22 | `; 23 | 24 | const StyledGreetingDiv = styled.div` 25 | display: grid; 26 | grid-template-rows: 1fr 1fr 1fr; 27 | grid-template-columns: 135px 2.7fr 1fr; 28 | grid-gap: 10px; 29 | place-self: center; 30 | align-items: center; 31 | `; 32 | 33 | const StyledLabel = styled.label` 34 | font-weight: bold; 35 | `; 36 | 37 | const StyledInput = styled.input` 38 | padding: 0.4rem 0.6rem; 39 | line-height: 2fr; 40 | `; 41 | 42 | const StyledButton = styled.button` 43 | width: 150px; 44 | height: 2rem; 45 | border-radius: 1rem; 46 | border-color: blue; 47 | cursor: pointer; 48 | `; 49 | 50 | export function Greeter(): ReactElement { 51 | const context = useWeb3React(); 52 | const { library, active } = context; 53 | 54 | const [signer, setSigner] = useState(); 55 | const [greeterContract, setGreeterContract] = useState(); 56 | const [greeterContractAddr, setGreeterContractAddr] = useState(''); 57 | const [greeting, setGreeting] = useState(''); 58 | const [greetingInput, setGreetingInput] = useState(''); 59 | 60 | useEffect((): void => { 61 | if (!library) { 62 | setSigner(undefined); 63 | return; 64 | } 65 | 66 | setSigner(library.getSigner()); 67 | }, [library]); 68 | 69 | useEffect((): void => { 70 | if (!greeterContract) { 71 | return; 72 | } 73 | 74 | async function getGreeting(greeterContract: Contract): Promise { 75 | const _greeting = await greeterContract.greet(); 76 | 77 | if (_greeting !== greeting) { 78 | setGreeting(_greeting); 79 | } 80 | } 81 | 82 | getGreeting(greeterContract); 83 | }, [greeterContract, greeting]); 84 | 85 | function handleDeployContract(event: MouseEvent) { 86 | event.preventDefault(); 87 | 88 | // only deploy the Greeter contract one time, when a signer is defined 89 | if (greeterContract || !signer) { 90 | return; 91 | } 92 | 93 | async function deployGreeterContract(signer: Signer): Promise { 94 | const Greeter = new ethers.ContractFactory( 95 | GreeterArtifact.abi, 96 | GreeterArtifact.bytecode, 97 | signer 98 | ); 99 | 100 | try { 101 | const greeterContract = await Greeter.deploy('Hello, Hardhat!'); 102 | 103 | await greeterContract.deployed(); 104 | 105 | const greeting = await greeterContract.greet(); 106 | 107 | setGreeterContract(greeterContract); 108 | setGreeting(greeting); 109 | 110 | window.alert(`Greeter deployed to: ${greeterContract.address}`); 111 | 112 | setGreeterContractAddr(greeterContract.address); 113 | } catch (error: any) { 114 | window.alert( 115 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 116 | ); 117 | } 118 | } 119 | 120 | deployGreeterContract(signer); 121 | } 122 | 123 | function handleGreetingChange(event: ChangeEvent): void { 124 | event.preventDefault(); 125 | setGreetingInput(event.target.value); 126 | } 127 | 128 | function handleGreetingSubmit(event: MouseEvent): void { 129 | event.preventDefault(); 130 | 131 | if (!greeterContract) { 132 | window.alert('Undefined greeterContract'); 133 | return; 134 | } 135 | 136 | if (!greetingInput) { 137 | window.alert('Greeting cannot be empty'); 138 | return; 139 | } 140 | 141 | async function submitGreeting(greeterContract: Contract): Promise { 142 | try { 143 | const setGreetingTxn = await greeterContract.setGreeting(greetingInput); 144 | 145 | await setGreetingTxn.wait(); 146 | 147 | const newGreeting = await greeterContract.greet(); 148 | window.alert(`Success!\n\nGreeting is now: ${newGreeting}`); 149 | 150 | if (newGreeting !== greeting) { 151 | setGreeting(newGreeting); 152 | } 153 | } catch (error: any) { 154 | window.alert( 155 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 156 | ); 157 | } 158 | } 159 | 160 | submitGreeting(greeterContract); 161 | } 162 | 163 | return ( 164 | <> 165 | 173 | Deploy Greeter Contract 174 | 175 | 176 | 177 | Contract addr 178 |
179 | {greeterContractAddr ? ( 180 | greeterContractAddr 181 | ) : ( 182 | {``} 183 | )} 184 |
185 | {/* empty placeholder div below to provide empty first row, 3rd col div for a 2x3 grid */} 186 |
187 | Current greeting 188 |
189 | {greeting ? greeting : {``}} 190 |
191 | {/* empty placeholder div below to provide empty first row, 3rd col div for a 2x3 grid */} 192 |
193 | Set new greeting 194 | '} 198 | onChange={handleGreetingChange} 199 | style={{ fontStyle: greeting ? 'normal' : 'italic' }} 200 | > 201 | 209 | Submit 210 | 211 |
212 | 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /frontend/src/components/SectionDivider.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SectionDivider = styled.div` 4 | border-top: 2px solid darkgrey; 5 | grid-column: 1 / 1; /* this code makes the row stretch to entire width of the grid */ 6 | `; 7 | -------------------------------------------------------------------------------- /frontend/src/components/SignMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from '@web3-react/core'; 2 | import { MouseEvent, ReactElement } from 'react'; 3 | import styled from 'styled-components'; 4 | import { Provider } from '../utils/provider'; 5 | 6 | const StyledButton = styled.button` 7 | width: 150px; 8 | height: 2rem; 9 | border-radius: 1rem; 10 | border-color: blue; 11 | cursor: pointer; 12 | place-self: center; 13 | `; 14 | 15 | export function SignMessage(): ReactElement { 16 | const context = useWeb3React(); 17 | const { account, active, library } = context; 18 | 19 | function handleSignMessage(event: MouseEvent): void { 20 | event.preventDefault(); 21 | 22 | if (!library || !account) { 23 | window.alert('Wallet not connected'); 24 | return; 25 | } 26 | 27 | async function signMessage( 28 | library: Provider, 29 | account: string 30 | ): Promise { 31 | try { 32 | const signature = await library.getSigner(account).signMessage('👋'); 33 | window.alert(`Success!\n\n${signature}`); 34 | } catch (error: any) { 35 | window.alert( 36 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 37 | ); 38 | } 39 | } 40 | 41 | signMessage(library, account); 42 | } 43 | 44 | return ( 45 | 53 | Sign Message 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/WalletStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from '@web3-react/core'; 2 | import { ethers } from 'ethers'; 3 | import { ReactElement, useEffect, useState } from 'react'; 4 | import styled from 'styled-components'; 5 | import { Provider } from '../utils/provider'; 6 | 7 | type CleanupFunction = (() => void) | undefined; 8 | 9 | const StyledWalletStatusDiv = styled.div` 10 | display: grid; 11 | grid-template-rows: 1fr; 12 | grid-template-columns: 0.6fr 0.1fr 0.6fr 1fr 0.1fr 0.6fr 0.5fr 0.1fr 1.1fr 0.4fr 0.1fr 1fr 0.9fr 0.1fr 0.7fr 0.1fr; 13 | grid-gap: 10px; 14 | place-self: center; 15 | align-items: center; 16 | `; 17 | 18 | const StyledStatusIcon = styled.h1` 19 | margin: 0px; 20 | `; 21 | 22 | function ChainId(): ReactElement { 23 | const { chainId } = useWeb3React(); 24 | 25 | return ( 26 | <> 27 | 28 | Chain Id 29 | 30 | 31 | ⛓ 32 | 33 | {chainId ?? ''} 34 | 35 | ); 36 | } 37 | 38 | function BlockNumber(): ReactElement { 39 | const { chainId, library } = useWeb3React(); 40 | 41 | const [blockNumber, setBlockNumber] = useState(); 42 | 43 | useEffect((): CleanupFunction => { 44 | if (!library) { 45 | return; 46 | } 47 | 48 | let stale = false; 49 | 50 | async function getBlockNumber(library: Provider): Promise { 51 | try { 52 | const blockNumber: number = await library.getBlockNumber(); 53 | 54 | if (!stale) { 55 | setBlockNumber(blockNumber); 56 | } 57 | } catch (error: any) { 58 | if (!stale) { 59 | setBlockNumber(undefined); 60 | } 61 | 62 | window.alert( 63 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 64 | ); 65 | } 66 | } 67 | 68 | getBlockNumber(library); 69 | 70 | library.on('block', setBlockNumber); 71 | 72 | // cleanup function 73 | return (): void => { 74 | stale = true; 75 | library.removeListener('block', setBlockNumber); 76 | setBlockNumber(undefined); 77 | }; 78 | }, [library, chainId]); // ensures refresh if referential identity of library doesn't change across chainIds 79 | 80 | return ( 81 | <> 82 | 83 | Block Number 84 | 85 | 86 | 🔢 87 | 88 | {blockNumber === null ? 'Error' : blockNumber ?? ''} 89 | 90 | ); 91 | } 92 | 93 | function Account(): ReactElement { 94 | const { account } = useWeb3React(); 95 | 96 | return ( 97 | <> 98 | 99 | Account 100 | 101 | 102 | 🤖 103 | 104 | 105 | {typeof account === 'undefined' 106 | ? '' 107 | : account 108 | ? `${account.substring(0, 6)}...${account.substring( 109 | account.length - 4 110 | )}` 111 | : ''} 112 | 113 | 114 | ); 115 | } 116 | 117 | function Balance(): ReactElement { 118 | const { account, library, chainId } = useWeb3React(); 119 | 120 | const [balance, setBalance] = useState(); 121 | 122 | useEffect((): CleanupFunction => { 123 | if (typeof account === 'undefined' || account === null || !library) { 124 | return; 125 | } 126 | 127 | let stale = false; 128 | 129 | async function getBalance( 130 | library: Provider, 131 | account: string 132 | ): Promise { 133 | const balance: ethers.BigNumber = await library.getBalance(account); 134 | 135 | try { 136 | if (!stale) { 137 | setBalance(balance); 138 | } 139 | } catch (error: any) { 140 | if (!stale) { 141 | setBalance(undefined); 142 | 143 | window.alert( 144 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 145 | ); 146 | } 147 | } 148 | } 149 | 150 | getBalance(library, account); 151 | 152 | // create a named balancer handler function to fetch the balance each block. in the 153 | // cleanup function use the fucntion name to remove the listener 154 | const getBalanceHandler = (): void => { 155 | getBalance(library, account); 156 | }; 157 | 158 | library.on('block', getBalanceHandler); 159 | 160 | // cleanup function 161 | return (): void => { 162 | stale = true; 163 | library.removeListener('block', getBalanceHandler); 164 | setBalance(undefined); 165 | }; 166 | }, [account, library, chainId]); // ensures refresh if referential identity of library doesn't change across chainIds 167 | 168 | return ( 169 | <> 170 | 171 | Balance 172 | 173 | 174 | 💰 175 | 176 | 177 | {balance === null 178 | ? 'Error' 179 | : balance 180 | ? `Ξ${Math.round(+ethers.utils.formatEther(balance) * 1e4) / 1e4}` 181 | : ''} 182 | 183 | 184 | ); 185 | } 186 | 187 | // nonce: aka 'transaction count' 188 | function NextNonce(): ReactElement { 189 | const { account, library, chainId } = useWeb3React(); 190 | 191 | const [nextNonce, setNextNonce] = useState(); 192 | 193 | useEffect((): CleanupFunction => { 194 | if (typeof account === 'undefined' || account === null || !library) { 195 | return; 196 | } 197 | 198 | let stale = false; 199 | 200 | async function getNextNonce( 201 | library: Provider, 202 | account: string 203 | ): Promise { 204 | const nextNonce: number = await library.getTransactionCount(account); 205 | 206 | try { 207 | if (!stale) { 208 | setNextNonce(nextNonce); 209 | } 210 | } catch (error: any) { 211 | if (!stale) { 212 | setNextNonce(undefined); 213 | 214 | window.alert( 215 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 216 | ); 217 | } 218 | } 219 | } 220 | 221 | getNextNonce(library, account); 222 | 223 | // create a named next nonce handler function to fetch the next nonce each block. 224 | // in the cleanup function use the fucntion name to remove the listener 225 | const getNextNonceHandler = (): void => { 226 | getNextNonce(library, account); 227 | }; 228 | 229 | library.on('block', getNextNonceHandler); 230 | 231 | // cleanup function 232 | return (): void => { 233 | stale = true; 234 | setNextNonce(undefined); 235 | }; 236 | }, [account, library, chainId]); // ensures refresh if referential identity of library doesn't change across chainIds 237 | 238 | return ( 239 | <> 240 | 241 | Next Nonce 242 | 243 | 244 | #️⃣ 245 | 246 | {nextNonce === null ? 'Error' : nextNonce ?? ''} 247 | 248 | ); 249 | } 250 | 251 | function StatusIcon(): ReactElement { 252 | const { active, error } = useWeb3React(); 253 | 254 | return ( 255 | {active ? '🟢' : error ? '🔴' : '🟠'} 256 | ); 257 | } 258 | 259 | export function WalletStatus(): ReactElement { 260 | return ( 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ); 270 | } 271 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 30px; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Web3ReactProvider } from '@web3-react/core'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { App } from './App'; 5 | import './index.css'; 6 | import { getProvider } from './utils/provider'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/utils/connectors.ts: -------------------------------------------------------------------------------- 1 | import { InjectedConnector } from '@web3-react/injected-connector'; 2 | 3 | export const injected = new InjectedConnector({ 4 | supportedChainIds: [1, 3, 4, 5, 42, 31337] 5 | }); 6 | -------------------------------------------------------------------------------- /frontend/src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useWeb3React } from '@web3-react/core'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { injected } from './connectors'; 4 | import { Provider } from './provider'; 5 | 6 | export function useEagerConnect(): boolean { 7 | const { activate, active } = useWeb3React(); 8 | 9 | const [tried, setTried] = useState(false); 10 | 11 | // use useCallback() and useEffect() hooks together so that tryActivate() will only 12 | // be called once when attempting eager connection 13 | const tryActivate = useCallback((): void => { 14 | async function _tryActivate() { 15 | const isAuthorized = await injected.isAuthorized(); 16 | 17 | if (isAuthorized) { 18 | try { 19 | await activate(injected, undefined, true); 20 | } catch (error: any) { 21 | window.alert( 22 | 'Error!' + (error && error.message ? `\n\n${error.message}` : '') 23 | ); 24 | } 25 | } 26 | 27 | setTried(true); 28 | } 29 | 30 | _tryActivate(); 31 | }, [activate]); 32 | 33 | useEffect((): void => { 34 | tryActivate(); 35 | }, [tryActivate]); 36 | 37 | // if the connection worked, wait until we get confirmation of that to flip the flag 38 | useEffect((): void => { 39 | if (!tried && active) { 40 | setTried(true); 41 | } 42 | }, [tried, active]); 43 | 44 | return tried; 45 | } 46 | 47 | export function useInactiveListener(suppress: boolean = false): void { 48 | const { active, error, activate } = useWeb3React(); 49 | 50 | useEffect((): (() => void) | undefined => { 51 | const { ethereum } = window as any; 52 | 53 | if (ethereum && ethereum.on && !active && !error && !suppress) { 54 | const handleConnect = (): void => { 55 | console.log("Handling 'connect' event"); 56 | activate(injected); 57 | }; 58 | 59 | const handleChainChanged = (chainId: string | number): void => { 60 | console.log("Handling 'chainChanged' event with payload", chainId); 61 | activate(injected); 62 | }; 63 | 64 | const handleAccountsChanged = (accounts: string[]): void => { 65 | console.log("Handling 'accountsChanged' event with payload", accounts); 66 | if (accounts.length > 0) { 67 | activate(injected); 68 | } 69 | }; 70 | 71 | ethereum.on('connect', handleConnect); 72 | ethereum.on('chainChanged', handleChainChanged); 73 | ethereum.on('accountsChanged', handleAccountsChanged); 74 | 75 | // cleanup function 76 | return (): void => { 77 | if (ethereum.removeListener) { 78 | ethereum.removeListener('connect', handleConnect); 79 | ethereum.removeListener('chainChanged', handleChainChanged); 80 | ethereum.removeListener('accountsChanged', handleAccountsChanged); 81 | } 82 | }; 83 | } 84 | }, [active, error, suppress, activate]); 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/utils/provider.ts: -------------------------------------------------------------------------------- 1 | import type { Web3Provider as ProviderType } from '@ethersproject/providers'; 2 | import { Web3Provider } from '@ethersproject/providers'; 3 | 4 | export function getProvider(provider: any): ProviderType { 5 | const web3Provider = new Web3Provider(provider); 6 | web3Provider.pollingInterval = 1000; 7 | return web3Provider; 8 | } 9 | 10 | export type Provider = ProviderType; 11 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | import { HardhatUserConfig } from 'hardhat/config'; 4 | import '@nomiclabs/hardhat-etherscan'; 5 | import '@nomiclabs/hardhat-waffle'; 6 | import '@typechain/hardhat'; 7 | import 'hardhat-gas-reporter'; 8 | import 'solidity-coverage'; 9 | 10 | import './tasks/deploy'; 11 | 12 | dotenv.config(); 13 | 14 | const config: HardhatUserConfig = { 15 | solidity: '0.8.4', 16 | paths: { 17 | artifacts: './frontend/src/artifacts' 18 | }, 19 | networks: { 20 | hardhat: { 21 | mining: { 22 | auto: false, 23 | interval: 1000 24 | } 25 | }, 26 | ropsten: { 27 | url: process.env.ROPSTEN_URL || '', 28 | accounts: 29 | process.env.TEST_ETH_ACCOUNT_PRIVATE_KEY !== undefined 30 | ? [process.env.TEST_ETH_ACCOUNT_PRIVATE_KEY] 31 | : [] 32 | } 33 | }, 34 | gasReporter: { 35 | enabled: process.env.REPORT_GAS !== undefined, 36 | currency: 'USD' 37 | }, 38 | etherscan: { 39 | apiKey: process.env.ETHERSCAN_API_KEY 40 | } 41 | }; 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-ethers-react-ts-starter", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "description": "Typescript starter template for hardhat, ethers and react projects using typescript and @web3-react", 6 | "engines": { 7 | "node": "^16.13.1", 8 | "npm": "8.1.2" 9 | }, 10 | "devDependencies": { 11 | "@nomiclabs/hardhat-ethers": "^2.0.0", 12 | "@nomiclabs/hardhat-etherscan": "^2.1.3", 13 | "@nomiclabs/hardhat-waffle": "^2.0.0", 14 | "@typechain/ethers-v5": "^7.0.1", 15 | "@typechain/hardhat": "^2.3.0", 16 | "@types/chai": "^4.2.21", 17 | "@types/mocha": "^9.0.0", 18 | "@types/node": "^12.0.0", 19 | "@typescript-eslint/eslint-plugin": "^4.29.1", 20 | "@typescript-eslint/parser": "^5.7.0", 21 | "chai": "^4.2.0", 22 | "dotenv": "^10.0.0", 23 | "eslint": "^7.29.0", 24 | "eslint-config-prettier": "^8.3.0", 25 | "eslint-config-standard": "^16.0.3", 26 | "eslint-plugin-import": "^2.25.3", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-prettier": "^3.4.0", 29 | "eslint-plugin-promise": "^5.1.0", 30 | "ethereum-waffle": "^3.0.0", 31 | "ethers": "^5.0.0", 32 | "hardhat": "^2.8.0", 33 | "hardhat-gas-reporter": "^1.0.4", 34 | "prettier": "^2.3.2", 35 | "prettier-plugin-solidity": "^1.0.0-beta.13", 36 | "solhint": "^3.3.6", 37 | "solidity-coverage": "^0.7.16", 38 | "ts-node": "^10.1.0", 39 | "typechain": "^5.1.2", 40 | "typescript": "^4.5.2" 41 | }, 42 | "scripts": { 43 | "lint": "", 44 | "lint:fix": "", 45 | "test": "echo \"Error: no test specified\" && exit 1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tasks/deploy.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-waffle'; 2 | import { task } from 'hardhat/config'; 3 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 4 | 5 | task('deploy', 'Deploy Greeter contract').setAction( 6 | async (_, hre: HardhatRuntimeEnvironment): Promise => { 7 | const Greeter = await hre.ethers.getContractFactory('Greeter'); 8 | const greeter = await Greeter.deploy('Hello, Hardhat!'); 9 | 10 | await greeter.deployed(); 11 | 12 | console.log('Greeter deployed to:', greeter.address); 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /test/Greeter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | 4 | describe('Greeter', function (): void { 5 | it("Should return the new greeting once it's changed", async function (): Promise { 6 | const Greeter = await ethers.getContractFactory('Greeter'); 7 | const greeter = await Greeter.deploy('Hello, world!'); 8 | await greeter.deployed(); 9 | 10 | expect(await greeter.greet()).to.equal('Hello, world!'); 11 | 12 | const setGreetingTx = await greeter.setGreeting('Hola, mundo!'); 13 | 14 | // wait until the transaction is mined 15 | await setGreetingTx.wait(); 16 | 17 | expect(await greeter.greet()).to.equal('Hola, mundo!'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true 9 | }, 10 | "include": ["./tasks", "./test", "./typechain"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------