├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json ├── packages ├── app │ ├── .env.example │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── abi │ │ │ └── greeter.json │ │ ├── components │ │ │ ├── Greeting.tsx │ │ │ ├── Header.tsx │ │ │ ├── Providers.tsx │ │ │ └── Wrapper.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── hooks │ │ │ └── useGreeting.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── prettierrc.ts │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── tailwind.config.ts │ └── tsconfig.json └── contracts │ ├── .env.example │ ├── .gitignore │ ├── .prettierrc.ts │ ├── contracts │ └── Greeter.sol │ ├── hardhat.config.ts │ ├── package.json │ ├── scripts │ └── deploy.ts │ ├── test │ └── Greeter.test.ts │ └── tsconfig.json ├── screenshot.png ├── why-i-built-this.png └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | A list of versions and notable changes for `solidity-nextjs-starter`. 4 | 5 | ## [2.1.0] - Minor update - 2024-04-03 6 | 7 | ### [2.1.0] - Added 8 | 9 | - Added `@tanstack/react-query` peer dependency 10 | 11 | ### [2.1.0] - Changed 12 | 13 | - Upgraded `next` to v14 14 | - Upgraded `tailwindcss` 15 | - Upgraded `viem`, `wagmi` and `@rainbow-me/rainbowkit` 16 | - Reworked `Providers.tsx` and `useGreeting.ts` to support upgrade 17 | - Rework `app` and `contracts` packages to switch from Goerli ([RIP](https://www.alchemy.com/blog/ethereum-goerli-testnet-deprecation)) to Sepolia testnet 18 | 19 | ## [2.0.0] - Major update - 2023-09-25 20 | 21 | ### [2.0.0] - Added 22 | 23 | - wagmi hooks for better interfacing with `Greeter.sol` 24 | - RainbowKit for better wallet management 25 | - Prettier to consistently format code 26 | 27 | ### [2.0.0] - Changed 28 | 29 | - TypeScript in favour of JavaScript in `packages/contracts` and `packages/app` 30 | - viem in favour of ethers in `packages/app` 31 | - Next.js 13 app directory from pages 32 | 33 | ## [1.0.0] - Initial release - 2021-08-03 34 | 35 | Project launched to help developers create their first Ethereum dApp. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Next.js Starter 2 | 3 | A starter repository for building full stack Ethereum dApps with [Solidity](https://soliditylang.org/) and [Next.js](https://nextjs.org/). 4 | 5 | This code is for anyone looking to quickly bootstrap an EVM dApp using modern best practices. In particular, developers with existing JavaScript/TypeScript experience who're newer to Solidity. 6 | 7 | If you want to learn how to interact with a simple smart contract from the client side, this repository is for you. 8 | 9 | ![Solidity + Next.js Starter](./screenshot.png) 10 | 11 | - [Get started](#getting-started) 12 | - [Read changelog](./CHANGELOG.md) 13 | 14 | ## Packages 15 | 16 | ### Contracts 17 | 18 | `packages/contracts` - All smart contract files. 19 | 20 | #### Contracts Stack 21 | 22 | - [Alchemy](https://www.alchemy.com/) 23 | - [Hardhat](https://hardhat.org/) 24 | - [Mocha](https://mochajs.org/) 25 | - [Chai](https://www.chaijs.com/) 26 | - [Solidity](https://soliditylang.org/) 27 | - [TypeScript](https://www.typescriptlang.org/) 28 | - [Prettier](https://prettier.io/) 29 | 30 | #### Contracts Scripts 31 | 32 | - `yarn start` - Starts your local Hardhat network 33 | - `yarn test` - Tests `Greeter.sol`'s functionality 34 | - `yarn deploy` - Deploys `Greeter.sol` to your local Hardhat network 35 | - `yarn deploy:sepolia` - Deploys `Greeter.sol` to the Sepolia test network 36 | - `yarn format` - Formats all code using Prettier 37 | 38 | ### App 39 | 40 | `packages/app` - All client application files. 41 | 42 | #### App Stack 43 | 44 | - [Alchemy](https://www.alchemy.com/) 45 | - [Next.js](https://nextjs.org/) 46 | - [Tailwind CSS](https://tailwindcss.com/) 47 | - [viem](https://viem.sh/) 48 | - [wagmi](https://wagmi.sh/) 49 | - [RainbowKit](https://www.rainbowkit.com/) 50 | - [TypeScript](https://www.typescriptlang.org/) 51 | - [Prettier](https://prettier.io/) 52 | 53 | #### App Scripts 54 | 55 | - `yarn dev` - Starts the Next.js local development environment 56 | - `yarn build` - Creates an optimised production build of your app 57 | - `yarn start` - Starts the Next.js application in production mode 58 | - `yarn lint` - Checks for problems in your code using ESLint 59 | - `yarn format` - Formats all code using Prettier 60 | 61 | ## Prerequisites 62 | 63 | - [Node](https://nodejs.org/en/download/) 64 | - [MetaMask](https://metamask.io/download.html) 65 | 66 | ## Getting Started 67 | 68 | How to get running on your local machine: 69 | 70 | ### Initial Setup 71 | 72 | Use `git clone https://github.com/tomhirst/solidity-nextjs-starter.git` to clone this repository to your local machine. 73 | 74 | Enter the repository folder with `cd solidity-nextjs-starter`, then install all dependencies using `yarn`. 75 | 76 | Solidity Next.js Starter uses Yarn workspaces, so this will install the relevant dependencies for each packages in one command. 77 | 78 | ### Contracts Setup 79 | 80 | Enter the `contracts` folder with `cd packages/contracts` and start your local hardhat node with `yarn start`. If you're successful, you'll be presented with a number of accounts (one of which you'll need later). Here's an example: 81 | 82 | ```bash 83 | Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH) 84 | Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 85 | ``` 86 | 87 | In a new terminal window, deploy the `Greeter` contract using `yarn deploy`. If you're successful, you'll get a contract address (that you'll also need later) like this: 88 | 89 | ```bash 90 | Greeter with greeting "Hello, world!" deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 91 | ``` 92 | 93 | ### App Setup 94 | 95 | Enter the `app` folder with `cd packages/app` from the root directory. 96 | 97 | You'll need a RainbowKit project ID. You can get one from [WalletConnect Cloud](https://cloud.walletconnect.com/) and it will look something like this: `206a512b7abd9c469123b45fb272b68e` (not a real key). 98 | 99 | Afterwards, duplicate `.env.example` and rename the file `.env`. Then add your RainbowKit project ID like this: `NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID=[your-project-id]`. 100 | 101 | `NEXT_PUBLIC_CHAIN_ID` should already be set to the Hardhat local network ID of `31337` (change this when you want your app to run on other chains). 102 | 103 | Finally, set `NEXT_PUBLIC_CONTRACT_ADDRESS` using the contract address you recieved when you deployed. For example: `NEXT_PUBLIC_CONTRACT_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3` 104 | 105 | Once your environment variables are set, run the application using `yarn dev`. To view, open up `localhost:3000` (or whatever port Next.js has assigned) in your browser. 106 | 107 | ### MetaMask Setup 108 | 109 | To fully demo the apps' features, you'll need a web3 wallet extension. If you don't have MetaMask installed already, you can get it [here](https://metamask.io/download.html). 110 | 111 | If you haven't used Hardhat before, you'll need to add a test account to write to the smart contract that you deployed. Do this by importing one of the accounts you noted down earlier to MetaMask using the accounts' private key (for example, `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`). 112 | 113 | Once connected to the app with the test account, you can set a new greeting on the blockchain by using the form on page. You'll get a confirmation message if you're successful. 114 | 115 | ## Advanced 116 | 117 | Instructions for deploying the smart contract and application to publically viewable environments: 118 | 119 | ### Advanced Contracts 120 | 121 | Up to now, your smart contract has been running locally. The next step is to deploy it to a live test network. We'll use [Sepolia](https://www.alchemy.com/overviews/sepolia-testnet) for this. 122 | 123 | #### Deploying to Sepolia Testnet 124 | 125 | First you need some Sepolia test ETH. You can get some from a [Sepolia Faucet](https://www.alchemy.com/faucets/ethereum-sepolia). 126 | 127 | In the `packages/contracts` directory, duplicate `.env.example` to `.env`. You'll need an [Alchemy API key](https://docs.alchemy.com/docs/alchemy-quickstart-guide#1key-create-an-alchemy-key) and the private key of the wallet you'd like to deploy your Sepolia contract from. I recommend using a burner account that doesn't hold any valuable assets on other chains. 128 | 129 | Set the environment variables like so: 130 | 131 | ```bash 132 | ALCHEMY_API_KEY=[your-api-key] 133 | SEPOLIA_PRIVATE_KEY=[your-private-key] 134 | ``` 135 | 136 | Finally, run `yarn deploy:sepolia`. If you're successful, you'll get a message ike this in your terminal window: 137 | 138 | ```bash 139 | Greeter with greeting "Hello, world!" deployed to 0xE47c47B1db8823BA54aae021cfce03b2d37B52a8 140 | ``` 141 | 142 | Here's a version of the contract I deployed earlier: [0xE47c47B1db8823BA54aae021cfce03b2d37B52a8](https://sepolia.etherscan.io/address/0xE47c47B1db8823BA54aae021cfce03b2d37B52a8) 143 | 144 | #### Verifying Your Contract on Sepolia 145 | 146 | Let's verify your newly deployed contract with Etherscan. First, get an Etherscan API key [here](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics). Then add it to your `.env` file: 147 | 148 | ```bash 149 | ETHERSCAN_API_KEY=[your-api-key] 150 | ``` 151 | 152 | Run `yarn verify:sepolia [your-contract-address] 'Hello, world!'` to verify your contract. Be sure to pass the address of the contract you just deployed and the constructor parameter, which in this case is the default greeting. 153 | 154 | If you're successful, you'll get a message like this: 155 | 156 | ```bash 157 | Successfully verified contract Greeter on the block explorer. 158 | ``` 159 | 160 | ### Advanced App 161 | 162 | Let's look at deploying your application. 163 | 164 | #### Adding an Alchemy API Key 165 | 166 | To interact with smart contracts on a testnet or mainnet from your app, you'll need an Alchemy API key. You can get one [here](https://docs.alchemy.com/docs/alchemy-quickstart-guide#1key-create-an-alchemy-key) if you didn't get one earlier. 167 | 168 | Add this to `.env` in `packages/app` like so: 169 | 170 | ```bash 171 | ALCHEMY_API_KEY=[your-api-key] 172 | ``` 173 | 174 | This will let you point your front end at a publically viewable contract on a network like Sepolia or mainnet. 175 | 176 | #### Deploying to Vercel 177 | 178 | You can deploy the application to Vercel by clicking this button: 179 | 180 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftomhirst%2Fsolidity-nextjs-starter%2Ftree%2Fmain%2Fpackages%2Fapp) 181 | 182 | Be sure to deploy from the `packages/app` directory and set these environment variables: 183 | 184 | ```bash 185 | NEXT_PUBLIC_ALCHEMY_API_KEY=[your-api-key] 186 | NEXT_PUBLIC_CONTRACT_ADDRESS=[your-contract-address] 187 | NEXT_PUBLIC_CHAIN_ID=[your-chain-id] 188 | NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID=[your-project-id] 189 | ``` 190 | 191 | Here's an app I deployed earlier: [https://solidity-nextjs-starter-app.vercel.app/](https://solidity-nextjs-starter-app.vercel.app/) 192 | 193 | ## Why I Built This 194 | 195 | I built this to onboard myself to web3. Since 2021, Solidity Next.js Starter has amassed 100s of GitHub stars and helped devs land dream gigs in the space. 196 | 197 | ![A tweet exchange](./why-i-built-this.png) 198 | 199 | ## Contributions 200 | 201 | All suggestions for improvement are welcome. Please submit a [pull request](https://github.com/tomhirst/solidity-nextjs-starter/pulls) to contribute. 202 | 203 | ## Disclaimer 204 | 205 | All code in this repository is unaudited. Use at your own risk. 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-nextjs-starter", 3 | "version": "2.1.0", 4 | "description": "A starter kit for building full stack Ethereum dApps with Solidity and Next.js", 5 | "author": "Tom Hirst ", 6 | "license": "MIT", 7 | "private": true, 8 | "engines": { 9 | "node": ">=18.0.0" 10 | }, 11 | "workspaces": [ 12 | "packages/*" 13 | ], 14 | "scripts": { 15 | "start:contracts": "yarn workspace solidity-next-js-starter-contracts run start", 16 | "start:app": "yarn workspace solidity-next-js-starter-app run dev" 17 | }, 18 | "devDependencies": { 19 | "@typechain/ethers-v6": "^0.4.3", 20 | "@typechain/hardhat": "^8.0.0", 21 | "@types/chai": "^4.2.0", 22 | "@types/mocha": "^10.0.1", 23 | "@types/node": "^20.6.5", 24 | "ethers": "^6.7.1", 25 | "hardhat": "^2.17.3", 26 | "ts-node": "^10.9.1", 27 | "typechain": "^8.3.1", 28 | "typescript": "^5.2.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/.env.example: -------------------------------------------------------------------------------- 1 | # This is the address of your deployed contract 2 | NEXT_PUBLIC_CONTRACT_ADDRESS= 3 | # This is the chain ID for the network you deloyed your contract to 4 | # 31337 is the chain ID for Hardhat's local network, 11155111 is the chain ID for Sepolia, 1 is the chain ID for mainnet 5 | NEXT_PUBLIC_CHAIN_ID=31337 6 | # This is the API key for your Alchemy project (Get one from https://www.alchemy.com) 7 | NEXT_PUBLIC_ALCHEMY_API_KEY= 8 | # This is the ID for your RainbowKit project (Get one from https://cloud.walletconnect.com) 9 | NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID= -------------------------------------------------------------------------------- /packages/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /packages/app/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /packages/app/app/abi/greeter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "_greeting", 7 | "type": "string" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "inputs": [], 15 | "name": "getGreeting", 16 | "outputs": [ 17 | { 18 | "internalType": "string", 19 | "name": "", 20 | "type": "string" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | }, 26 | { 27 | "inputs": [ 28 | { 29 | "internalType": "string", 30 | "name": "_greeting", 31 | "type": "string" 32 | } 33 | ], 34 | "name": "setGreeting", 35 | "outputs": [], 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /packages/app/app/components/Greeting.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import { useGreeting } from "../hooks/useGreeting"; 5 | import { useConnectModal } from "@rainbow-me/rainbowkit"; 6 | import { toast } from "react-toastify"; 7 | 8 | const Greeting = () => { 9 | const [newGreeting, setNewGreeting] = useState(""); 10 | const newGreetingInputRef = useRef(null); 11 | 12 | const onSetGreetingSuccess = () => { 13 | toast.success(`Successfully set your new greeting`, { 14 | position: toast.POSITION.BOTTOM_CENTER, 15 | autoClose: 3000, 16 | hideProgressBar: true, 17 | closeOnClick: true, 18 | pauseOnHover: true, 19 | theme: "light", 20 | className: "text-sm", 21 | }); 22 | setNewGreeting(""); 23 | newGreetingInputRef.current?.blur(); 24 | }; 25 | 26 | const { 27 | address, 28 | greeting, 29 | getGreetingLoading, 30 | getGreetingError, 31 | setGreeting, 32 | setGreetingLoading, 33 | prepareSetGreetingError, 34 | setGreetingError, 35 | } = useGreeting({ newGreeting, onSetGreetingSuccess }); 36 | 37 | useEffect(() => { 38 | if (!address) { 39 | setNewGreeting(""); 40 | } 41 | }, [address]); 42 | 43 | const { openConnectModal } = useConnectModal(); 44 | 45 | return ( 46 |
47 |
48 |

49 | Greeting from the blockchain: 50 |

51 | {getGreetingLoading ? ( 52 |

Loading...

53 | ) : ( 54 |

61 | {!getGreetingError 62 | ? greeting 63 | : `There was an error getting the greeting`} 64 |

65 | )} 66 |
67 |
68 |
69 | setNewGreeting(e.target.value)} 72 | placeholder="Write a new greeting" 73 | ref={newGreetingInputRef} 74 | disabled={!address} 75 | value={newGreeting} 76 | /> 77 | 91 | {!address && ( 92 | 98 | )} 99 | {address && !newGreeting && ( 100 |

101 | Type something to set a new greeting 102 |

103 | )} 104 | {setGreetingError && ( 105 |

106 | There was an error setting your new greeting 107 |

108 | )} 109 | {newGreeting && prepareSetGreetingError && ( 110 |

111 | Sorry, only the contract owner can set a greeting 112 |

113 | )} 114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export { Greeting }; 121 | -------------------------------------------------------------------------------- /packages/app/app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 2 | import { Wrapper } from "./Wrapper"; 3 | 4 | const Header = () => { 5 | return ( 6 |
7 | 8 |
9 |

10 | Solidity Next.js Starter 11 |

12 | 17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export { Header }; 24 | -------------------------------------------------------------------------------- /packages/app/app/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { WagmiProvider } from "wagmi"; 6 | import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit"; 7 | import { hardhat, sepolia } from "wagmi/chains"; 8 | 9 | const queryClient = new QueryClient(); 10 | 11 | const config = getDefaultConfig({ 12 | appName: 'Solidity Next.js Starter', 13 | projectId: process.env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID ?? "", 14 | chains: [hardhat, sepolia], 15 | ssr: true, 16 | }); 17 | 18 | const Providers = ({ children }: { children: ReactNode }) => ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | 26 | export { Providers }; 27 | -------------------------------------------------------------------------------- /packages/app/app/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | const Wrapper = ({ children }: { children: ReactNode }) => ( 4 |
{children}
5 | ); 6 | 7 | export { Wrapper }; 8 | -------------------------------------------------------------------------------- /packages/app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhirst/solidity-nextjs-starter/d7f236ecdb8c3d4692c17da210e39599ee617289/packages/app/app/favicon.ico -------------------------------------------------------------------------------- /packages/app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/app/app/hooks/useGreeting.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { 5 | useAccount, 6 | useReadContract, 7 | useWriteContract, 8 | useWaitForTransactionReceipt, 9 | } from "wagmi"; 10 | import abi from "../abi/greeter.json"; 11 | 12 | const useGreeting = ({ 13 | newGreeting, 14 | onSetGreetingSuccess, 15 | }: { 16 | newGreeting?: string; 17 | onSetGreetingSuccess?: () => void; 18 | }): { 19 | address: `0x${string}` | undefined; 20 | greeting: string | null; 21 | getGreetingLoading: boolean; 22 | getGreetingError: boolean; 23 | setGreeting: (() => void) | undefined; 24 | setGreetingLoading: boolean; 25 | prepareSetGreetingError: boolean; 26 | setGreetingError: boolean; 27 | } => { 28 | const { address } = useAccount(); 29 | 30 | const { 31 | data: greeting, 32 | isLoading: getGreetingLoading, 33 | isError: getGreetingError, 34 | refetch: refetchGreeting, 35 | } = useReadContract({ 36 | address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x${string}`, 37 | abi, 38 | functionName: "getGreeting", 39 | }); 40 | 41 | const { 42 | data: setGreetingHash, 43 | writeContract: setGreeting, 44 | isPending: setGreetingLoading, 45 | isError: setGreetingError, 46 | } = useWriteContract(); 47 | 48 | const { 49 | isSuccess: txSuccess, 50 | isLoading: txLoading, 51 | } = useWaitForTransactionReceipt({ 52 | hash: setGreetingHash, 53 | query: { 54 | enabled: Boolean(setGreetingHash), 55 | } 56 | }); 57 | 58 | useEffect(() => { 59 | if (txSuccess) { 60 | onSetGreetingSuccess?.(); 61 | refetchGreeting(); 62 | } 63 | }, [txSuccess]); // eslint-disable-line react-hooks/exhaustive-deps 64 | 65 | return { 66 | address, 67 | greeting: greeting as string, 68 | getGreetingLoading, 69 | getGreetingError, 70 | setGreeting: () => setGreeting?.({ 71 | address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x${string}`, 72 | abi, 73 | functionName: "setGreeting", 74 | args: [newGreeting], 75 | }), 76 | setGreetingLoading: setGreetingLoading || txLoading, 77 | prepareSetGreetingError: newGreeting === undefined, 78 | setGreetingError, 79 | }; 80 | }; 81 | 82 | export { useGreeting }; 83 | -------------------------------------------------------------------------------- /packages/app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import "@rainbow-me/rainbowkit/styles.css"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | import type { Metadata } from "next"; 5 | import { Inter } from "next/font/google"; 6 | import { Providers } from "./components/Providers"; 7 | import { Header } from "./components/Header"; 8 | import { ToastContainer } from "react-toastify"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | export const metadata: Metadata = { 12 | title: "Solidity Next.js Starter", 13 | description: 14 | "A starter kit for building full stack Ethereum dApps with Solidity and Next.js", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) { 22 | return ( 23 | 24 | 25 | 26 |
27 | {children} 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Wrapper } from "./components/Wrapper"; 2 | import { Greeting } from "./components/Greeting"; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Home; 15 | -------------------------------------------------------------------------------- /packages/app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: (config) => { 4 | config.resolve.fallback = { fs: false, net: false, tls: false }; 5 | config.externals.push("pino-pretty", "lokijs", "encoding"); 6 | return config; 7 | }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-next-js-starter-app", 3 | "version": "2.1.0", 4 | "description": "A basic Next.js setup for starting to build full stack Ethereum dApps", 5 | "main": "index.js", 6 | "author": "Tom Hirst ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint", 13 | "format": "prettier --write ." 14 | }, 15 | "dependencies": { 16 | "@rainbow-me/rainbowkit": "^2.0.4", 17 | "@tanstack/react-query": "^5.28.14", 18 | "@types/node": "20.6.3", 19 | "@types/react": "18.2.22", 20 | "@types/react-dom": "18.2.7", 21 | "autoprefixer": "10.4.15", 22 | "eslint": "8.49.0", 23 | "eslint-config-next": "13.5.1", 24 | "next": "^14.1.4", 25 | "postcss": "8.4.30", 26 | "prettier": "^3.0.3", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-toastify": "^9.1.3", 30 | "tailwindcss": "^3.4.3", 31 | "typescript": "5.2.2", 32 | "viem": "^2.9.8", 33 | "wagmi": "^2.5.18" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/app/prettierrc.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | trailingComma: "es5", 6 | bracketSpacing: true, 7 | arrowParens: "always", 8 | bracketSameLine: false, 9 | printWidth: 80, 10 | useTabs: false, 11 | quoteProps: "as-needed", 12 | }; 13 | -------------------------------------------------------------------------------- /packages/app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/contracts/.env.example: -------------------------------------------------------------------------------- 1 | # This is the API key for your Alchemy project (Get one from https://www.alchemy.com) 2 | ALCHEMY_API_KEY= 3 | # This is the private key of the EOA you'd like to deploy your Sepolia contract from 4 | SEPOLIA_PRIVATE_KEY= 5 | # This is your Etherscan API Key used for verifying contracts you've dpeloyed 6 | ETHERSCAN_API_KEY= -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /packages/contracts/.prettierrc.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | trailingComma: "es5", 6 | bracketSpacing: true, 7 | arrowParens: "always", 8 | bracketSameLine: false, 9 | printWidth: 80, 10 | useTabs: false, 11 | quoteProps: "as-needed", 12 | overrides: [ 13 | { 14 | files: "*.sol", 15 | options: { 16 | semi: true, 17 | singleQuote: false, 18 | tabWidth: 4, 19 | trailingComma: "none", 20 | bracketSpacing: true, 21 | arrowParens: "avoid", 22 | printWidth: 120, 23 | useTabs: false, 24 | explicitTypes: "always", 25 | compiler: "^0.8.9", 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /packages/contracts/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | // @dev Remove the console import if you don't want to use it 5 | import "hardhat/console.sol"; 6 | 7 | /** 8 | * @title Greeter 9 | * @author @tom_hirst 10 | * @notice Simple contract to demonstrate basic Solidity features 11 | */ 12 | contract Greeter { 13 | /** 14 | * @dev Stores a greeting string 15 | */ 16 | string greeting; 17 | 18 | /** 19 | * @dev Constructor sets the greeting string 20 | * @param _greeting The greeting string to store 21 | */ 22 | constructor(string memory _greeting) { 23 | // @dev Remove the console log if you don't want to use it 24 | console.log("Deploying a Greeter with greeting:", _greeting); 25 | 26 | greeting = _greeting; 27 | } 28 | 29 | /** 30 | * @dev Returns the greeting string 31 | * @return The greeting string 32 | */ 33 | function getGreeting() public view returns (string memory) { 34 | return greeting; 35 | } 36 | 37 | /** 38 | * @dev Sets the greeting string 39 | * @param _greeting The greeting string to store 40 | */ 41 | function setGreeting(string memory _greeting) public { 42 | // @dev Remove the console log if you don't want to use it 43 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); 44 | 45 | greeting = _greeting; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/contracts/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import { config as dotenvConfig } from "dotenv"; 4 | dotenvConfig({ path: __dirname + "/.env" }); 5 | 6 | const config: HardhatUserConfig = { 7 | solidity: "0.8.19", 8 | networks: { 9 | sepolia: { 10 | url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`, 11 | accounts: [process.env.SEPOLIA_PRIVATE_KEY ?? ''], 12 | }, 13 | }, 14 | etherscan: { 15 | apiKey: process.env.ETHERSCAN_API_KEY, 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-next-js-starter-contracts", 3 | "version": "2.1.0", 4 | "description": "A basic contract for starting to build full stack Ethereum dApps", 5 | "main": "index.js", 6 | "author": "Tom Hirst ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "hardhat node", 10 | "test": "hardhat test", 11 | "deploy": "hardhat run --network localhost scripts/deploy.ts", 12 | "deploy:sepolia": "hardhat run --network sepolia scripts/deploy.ts", 13 | "verify:sepolia": "hardhat verify --network sepolia", 14 | "format": "prettier --write \"./contracts/**/*.sol\" --plugin prettier-plugin-solidity && prettier --write \"./**/*.ts\"" 15 | }, 16 | "devDependencies": { 17 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", 18 | "@nomicfoundation/hardhat-ethers": "^3.0.4", 19 | "@nomicfoundation/hardhat-network-helpers": "^1.0.9", 20 | "@nomicfoundation/hardhat-toolbox": "^3.0.0", 21 | "@nomicfoundation/hardhat-verify": "^1.1.1", 22 | "@typechain/ethers-v6": "^0.5.0", 23 | "@typechain/hardhat": "^9.0.0", 24 | "chai": "^4.3.8", 25 | "ethers": "^6.7.1", 26 | "hardhat": "^2.17.3", 27 | "hardhat-gas-reporter": "^1.0.9", 28 | "prettier": "^3.0.3", 29 | "prettier-plugin-solidity": "^1.1.3", 30 | "solidity-coverage": "^0.8.4", 31 | "typechain": "^8.3.1" 32 | }, 33 | "dependencies": { 34 | "dotenv": "^16.3.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/contracts/scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const greeting = "Hello, world!"; 5 | const greeter = await ethers.deployContract("Greeter", [ 6 | greeting, 7 | ]); 8 | await greeter.waitForDeployment(); 9 | console.log( 10 | `Greeter with greeting "${greeting}" deployed to ${greeter.target}`, 11 | ); 12 | } 13 | 14 | main().catch((error) => { 15 | console.error(error); 16 | process.exitCode = 1; 17 | }); 18 | -------------------------------------------------------------------------------- /packages/contracts/test/Greeter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 4 | 5 | describe("Greeter", function () { 6 | // This fixture deploys the contract and returns it 7 | const deploy = async () => { 8 | const Greeter = await ethers.getContractFactory("Greeter"); 9 | const greeter = await Greeter.deploy("Hello world!"); 10 | 11 | return { greeter }; 12 | }; 13 | 14 | it("Should deploy with the right greeting", async function () { 15 | const { greeter } = await loadFixture(deploy); 16 | expect(await greeter.getGreeting()).to.equal("Hello world!"); 17 | }); 18 | 19 | it("Should return the new greeting once it's changed", async function () { 20 | const { greeter } = await loadFixture(deploy); 21 | await greeter.setGreeting("Hey there!"); 22 | expect(await greeter.getGreeting()).to.equal("Hey there!"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhirst/solidity-nextjs-starter/d7f236ecdb8c3d4692c17da210e39599ee617289/screenshot.png -------------------------------------------------------------------------------- /why-i-built-this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhirst/solidity-nextjs-starter/d7f236ecdb8c3d4692c17da210e39599ee617289/why-i-built-this.png --------------------------------------------------------------------------------