├── .editorconfig ├── .env ├── .eslintrc.json ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc.js ├── .yarnrc.yml ├── README.md ├── abis ├── README.md ├── SimpleNFT.ts └── index.ts ├── commitlint.config.js ├── next.config.js ├── package.json ├── pre-commit ├── public ├── ledger.png ├── metamask.png ├── next.svg ├── vercel.svg └── walletconnect.png ├── src ├── app │ ├── api │ │ └── route.ts │ ├── dashboard │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── AppFooter.tsx │ ├── AppHeader.tsx │ ├── ConnectWalletButton.tsx │ ├── ContractProvider.tsx │ ├── Dashboard.tsx │ └── layouts │ │ └── PrimaryLayout.tsx ├── constants │ ├── contractAddresses.ts │ └── index.ts ├── lib │ ├── muiTheme.ts │ └── wagmiConfig.ts └── utils │ ├── formatAddress.ts │ └── serverResponses.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # RPC Providers 2 | NEXT_PUBLIC_INFURA_RPC_KEY= 3 | NEXT_PUBLIC_ALCHEMY_RPC_KEY= 4 | 5 | # Wallet Providers 6 | NEXT_PUBLIC_LEDGER_PROJECT_ID= 7 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "next", 5 | "next/core-web-vitals", 6 | "plugin:react/recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:import/recommended", 9 | "plugin:import/typescript", 10 | "prettier", // Disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier. 11 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always used last. 12 | ], 13 | "plugins": ["prettier", "import", "simple-import-sort", "@typescript-eslint"], 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module", 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | }, 21 | "rules": { 22 | "@typescript-eslint/ban-ts-comment": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-var-requires": "off", 25 | "@typescript-eslint/no-unused-vars": "warn", 26 | "react/react-in-jsx-scope": "off", // Next.js includes it by default 27 | "prettier/prettier": "warn", 28 | "import/first": "error", 29 | "import/newline-after-import": "error", 30 | "import/no-duplicates": "error", 31 | "import/no-named-as-default": "error", 32 | "import/no-unresolved": "warn", 33 | "simple-import-sort/imports": "error", 34 | "simple-import-sort/exports": "warn" 35 | }, 36 | "overrides": [ 37 | // Turn off prop type errors for Next.js pages, due to complexity with TS 38 | { 39 | "files": ["src/app/**/*.tsx"], 40 | "rules": { 41 | "react/prop-types": "off" 42 | } 43 | } 44 | ], 45 | "settings": { 46 | "react": { 47 | "version": "detect" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.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 | .yarn/ 8 | !.yarn/cache 9 | !.yarn/patches 10 | !.yarn/plugins 11 | !.yarn/releases 12 | !.yarn/sdks 13 | !.yarn/versions 14 | 15 | # testing 16 | /coverage 17 | 18 | # next.js 19 | /.next/ 20 | /out/ 21 | 22 | # production 23 | /build 24 | 25 | # misc 26 | .DS_Store 27 | *.pem 28 | 29 | # debug 30 | npm-debug.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint:fix 5 | git add . 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('altheajs-prettier-config'), 3 | printWidth: 120, 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextjs Web3 Template 2 | 3 | View the [demo app](https://template.dco.dev/). 4 | 5 | This is a template for building a robust frontend application designed to interact with Ethereum-based smart contracts. 6 | 7 | It uses the following features: 8 | 9 | - Next 13 10 | - React 18 11 | - TypeScript 12 | - Material UI 13 | - Emotion 14 | - Viem 15 | - Wagmi 16 | - WalletConnect v3 17 | - Infura & Alchemy RPC Providers 18 | - ESLint 19 | - Prettier 20 | - Commitlint 21 | - Yarn 22 | - Husky Git Hooks 23 | 24 | ## Getting Started 25 | 26 | ### Prerequisites 27 | 28 | This template relies on WalletConnect and an RPC provider to connect to Ethereum-compatible networks. Therefore, you must perform the following steps prior to running the app locally: 29 | 30 | 1. **WalletConnect Project ID** - Set up a new [WalletConnect Project](https://cloud.walletconnect.com/) to obtain the Project ID. Add it to `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` in the `.env` file. 31 | 2. **Infura RPC API Key** - For a simple setup, create a new [Infura API Key](https://app.infura.io/dashboard), and add it to `NEXT_PUBLIC_INFURA_RPC_KEY` in the `.env` file. 32 | 3. **Alternate: Alchemy RPC API Key** - If you prefer to use Alchemy as an RPC provider instead of Infura, set up a new [Alchemy API Key](https://dashboard.alchemyapi.io/) for the network that you wish to use, and add it to `NEXT_PUBLIC_ALCHEMY_RPC_KEY` in the `.env` file. 33 | 34 | ### Running the App Locally 35 | 36 | 1. Install dependencies: `yarn install` 37 | 2. Setup environment variables: `cp .env .env.local` 38 | 1. Update values with appropriate keys 39 | 2. Or, comment them out in `src/lib/wagmiConfig.ts` 40 | 3. Run development server: `yarn dev` 41 | 4. Open browser: `http://localhost:3000` 42 | 1. It will hot reload on each file save 43 | 5. Follow the steps this [README](./abis/README.md) to add contract ABIs and configurations for deployment addresses 44 | 6. Start editing: `src/app/page.tsx` to update the UI (visit the Dashboard page to mint an example NFT) 45 | -------------------------------------------------------------------------------- /abis/README.md: -------------------------------------------------------------------------------- 1 | # ABIs 2 | 3 | This holds some common ABIs as well as any ABIs intending to be used within this client application. 4 | 5 | ## Follow These Steps 6 | 7 | 1. Copy over the ABI and paste in as a `.json` file for each contract intending to be used. 8 | 2. Import and export them from the `./index.ts` barrel file. 9 | 3. Add in the contract deployment addresses for the given network(s) to `/src/app/constants/contractAddresses.tsx` 10 | 4. Update `/src/app/components/ContractProvider.tsx` to include configurations for each contract ABI. 11 | -------------------------------------------------------------------------------- /abis/SimpleNFT.ts: -------------------------------------------------------------------------------- 1 | import { Abi } from 'viem' 2 | 3 | export const simpleNftABI = [ 4 | { 5 | inputs: [], 6 | stateMutability: 'nonpayable', 7 | type: 'constructor', 8 | }, 9 | { 10 | inputs: [ 11 | { 12 | internalType: 'address', 13 | name: 'sender', 14 | type: 'address', 15 | }, 16 | { 17 | internalType: 'uint256', 18 | name: 'tokenId', 19 | type: 'uint256', 20 | }, 21 | { 22 | internalType: 'address', 23 | name: 'owner', 24 | type: 'address', 25 | }, 26 | ], 27 | name: 'ERC721IncorrectOwner', 28 | type: 'error', 29 | }, 30 | { 31 | inputs: [ 32 | { 33 | internalType: 'address', 34 | name: 'operator', 35 | type: 'address', 36 | }, 37 | { 38 | internalType: 'uint256', 39 | name: 'tokenId', 40 | type: 'uint256', 41 | }, 42 | ], 43 | name: 'ERC721InsufficientApproval', 44 | type: 'error', 45 | }, 46 | { 47 | inputs: [ 48 | { 49 | internalType: 'address', 50 | name: 'approver', 51 | type: 'address', 52 | }, 53 | ], 54 | name: 'ERC721InvalidApprover', 55 | type: 'error', 56 | }, 57 | { 58 | inputs: [ 59 | { 60 | internalType: 'address', 61 | name: 'operator', 62 | type: 'address', 63 | }, 64 | ], 65 | name: 'ERC721InvalidOperator', 66 | type: 'error', 67 | }, 68 | { 69 | inputs: [ 70 | { 71 | internalType: 'address', 72 | name: 'owner', 73 | type: 'address', 74 | }, 75 | ], 76 | name: 'ERC721InvalidOwner', 77 | type: 'error', 78 | }, 79 | { 80 | inputs: [ 81 | { 82 | internalType: 'address', 83 | name: 'receiver', 84 | type: 'address', 85 | }, 86 | ], 87 | name: 'ERC721InvalidReceiver', 88 | type: 'error', 89 | }, 90 | { 91 | inputs: [ 92 | { 93 | internalType: 'address', 94 | name: 'sender', 95 | type: 'address', 96 | }, 97 | ], 98 | name: 'ERC721InvalidSender', 99 | type: 'error', 100 | }, 101 | { 102 | inputs: [ 103 | { 104 | internalType: 'uint256', 105 | name: 'tokenId', 106 | type: 'uint256', 107 | }, 108 | ], 109 | name: 'ERC721NonexistentToken', 110 | type: 'error', 111 | }, 112 | { 113 | anonymous: false, 114 | inputs: [ 115 | { 116 | indexed: true, 117 | internalType: 'address', 118 | name: 'owner', 119 | type: 'address', 120 | }, 121 | { 122 | indexed: true, 123 | internalType: 'address', 124 | name: 'approved', 125 | type: 'address', 126 | }, 127 | { 128 | indexed: true, 129 | internalType: 'uint256', 130 | name: 'tokenId', 131 | type: 'uint256', 132 | }, 133 | ], 134 | name: 'Approval', 135 | type: 'event', 136 | }, 137 | { 138 | anonymous: false, 139 | inputs: [ 140 | { 141 | indexed: true, 142 | internalType: 'address', 143 | name: 'owner', 144 | type: 'address', 145 | }, 146 | { 147 | indexed: true, 148 | internalType: 'address', 149 | name: 'operator', 150 | type: 'address', 151 | }, 152 | { 153 | indexed: false, 154 | internalType: 'bool', 155 | name: 'approved', 156 | type: 'bool', 157 | }, 158 | ], 159 | name: 'ApprovalForAll', 160 | type: 'event', 161 | }, 162 | { 163 | anonymous: false, 164 | inputs: [ 165 | { 166 | indexed: true, 167 | internalType: 'address', 168 | name: 'from', 169 | type: 'address', 170 | }, 171 | { 172 | indexed: true, 173 | internalType: 'address', 174 | name: 'to', 175 | type: 'address', 176 | }, 177 | { 178 | indexed: true, 179 | internalType: 'uint256', 180 | name: 'tokenId', 181 | type: 'uint256', 182 | }, 183 | ], 184 | name: 'Transfer', 185 | type: 'event', 186 | }, 187 | { 188 | inputs: [ 189 | { 190 | internalType: 'address', 191 | name: 'to', 192 | type: 'address', 193 | }, 194 | { 195 | internalType: 'uint256', 196 | name: 'tokenId', 197 | type: 'uint256', 198 | }, 199 | ], 200 | name: 'approve', 201 | outputs: [], 202 | stateMutability: 'nonpayable', 203 | type: 'function', 204 | }, 205 | { 206 | inputs: [ 207 | { 208 | internalType: 'address', 209 | name: 'owner', 210 | type: 'address', 211 | }, 212 | ], 213 | name: 'balanceOf', 214 | outputs: [ 215 | { 216 | internalType: 'uint256', 217 | name: '', 218 | type: 'uint256', 219 | }, 220 | ], 221 | stateMutability: 'view', 222 | type: 'function', 223 | }, 224 | { 225 | inputs: [ 226 | { 227 | internalType: 'uint256', 228 | name: 'tokenId', 229 | type: 'uint256', 230 | }, 231 | ], 232 | name: 'getApproved', 233 | outputs: [ 234 | { 235 | internalType: 'address', 236 | name: '', 237 | type: 'address', 238 | }, 239 | ], 240 | stateMutability: 'view', 241 | type: 'function', 242 | }, 243 | { 244 | inputs: [ 245 | { 246 | internalType: 'address', 247 | name: 'owner', 248 | type: 'address', 249 | }, 250 | { 251 | internalType: 'address', 252 | name: 'operator', 253 | type: 'address', 254 | }, 255 | ], 256 | name: 'isApprovedForAll', 257 | outputs: [ 258 | { 259 | internalType: 'bool', 260 | name: '', 261 | type: 'bool', 262 | }, 263 | ], 264 | stateMutability: 'view', 265 | type: 'function', 266 | }, 267 | { 268 | inputs: [ 269 | { 270 | internalType: 'string', 271 | name: '_tokenUri', 272 | type: 'string', 273 | }, 274 | ], 275 | name: 'mint', 276 | outputs: [], 277 | stateMutability: 'nonpayable', 278 | type: 'function', 279 | }, 280 | { 281 | inputs: [], 282 | name: 'name', 283 | outputs: [ 284 | { 285 | internalType: 'string', 286 | name: '', 287 | type: 'string', 288 | }, 289 | ], 290 | stateMutability: 'view', 291 | type: 'function', 292 | }, 293 | { 294 | inputs: [ 295 | { 296 | internalType: 'uint256', 297 | name: 'tokenId', 298 | type: 'uint256', 299 | }, 300 | ], 301 | name: 'ownerOf', 302 | outputs: [ 303 | { 304 | internalType: 'address', 305 | name: '', 306 | type: 'address', 307 | }, 308 | ], 309 | stateMutability: 'view', 310 | type: 'function', 311 | }, 312 | { 313 | inputs: [ 314 | { 315 | internalType: 'address', 316 | name: 'from', 317 | type: 'address', 318 | }, 319 | { 320 | internalType: 'address', 321 | name: 'to', 322 | type: 'address', 323 | }, 324 | { 325 | internalType: 'uint256', 326 | name: 'tokenId', 327 | type: 'uint256', 328 | }, 329 | ], 330 | name: 'safeTransferFrom', 331 | outputs: [], 332 | stateMutability: 'nonpayable', 333 | type: 'function', 334 | }, 335 | { 336 | inputs: [ 337 | { 338 | internalType: 'address', 339 | name: 'from', 340 | type: 'address', 341 | }, 342 | { 343 | internalType: 'address', 344 | name: 'to', 345 | type: 'address', 346 | }, 347 | { 348 | internalType: 'uint256', 349 | name: 'tokenId', 350 | type: 'uint256', 351 | }, 352 | { 353 | internalType: 'bytes', 354 | name: 'data', 355 | type: 'bytes', 356 | }, 357 | ], 358 | name: 'safeTransferFrom', 359 | outputs: [], 360 | stateMutability: 'nonpayable', 361 | type: 'function', 362 | }, 363 | { 364 | inputs: [ 365 | { 366 | internalType: 'address', 367 | name: 'operator', 368 | type: 'address', 369 | }, 370 | { 371 | internalType: 'bool', 372 | name: 'approved', 373 | type: 'bool', 374 | }, 375 | ], 376 | name: 'setApprovalForAll', 377 | outputs: [], 378 | stateMutability: 'nonpayable', 379 | type: 'function', 380 | }, 381 | { 382 | inputs: [ 383 | { 384 | internalType: 'bytes4', 385 | name: 'interfaceId', 386 | type: 'bytes4', 387 | }, 388 | ], 389 | name: 'supportsInterface', 390 | outputs: [ 391 | { 392 | internalType: 'bool', 393 | name: '', 394 | type: 'bool', 395 | }, 396 | ], 397 | stateMutability: 'view', 398 | type: 'function', 399 | }, 400 | { 401 | inputs: [], 402 | name: 'symbol', 403 | outputs: [ 404 | { 405 | internalType: 'string', 406 | name: '', 407 | type: 'string', 408 | }, 409 | ], 410 | stateMutability: 'view', 411 | type: 'function', 412 | }, 413 | { 414 | inputs: [ 415 | { 416 | internalType: 'uint256', 417 | name: '_tokenId', 418 | type: 'uint256', 419 | }, 420 | ], 421 | name: 'tokenURI', 422 | outputs: [ 423 | { 424 | internalType: 'string', 425 | name: '', 426 | type: 'string', 427 | }, 428 | ], 429 | stateMutability: 'view', 430 | type: 'function', 431 | }, 432 | { 433 | inputs: [ 434 | { 435 | internalType: 'address', 436 | name: 'from', 437 | type: 'address', 438 | }, 439 | { 440 | internalType: 'address', 441 | name: 'to', 442 | type: 'address', 443 | }, 444 | { 445 | internalType: 'uint256', 446 | name: 'tokenId', 447 | type: 'uint256', 448 | }, 449 | ], 450 | name: 'transferFrom', 451 | outputs: [], 452 | stateMutability: 'nonpayable', 453 | type: 'function', 454 | }, 455 | ] as Abi 456 | -------------------------------------------------------------------------------- /abis/index.ts: -------------------------------------------------------------------------------- 1 | import { erc20ABI, erc721ABI } from 'wagmi' 2 | 3 | import { simpleNftABI } from './SimpleNFT' 4 | 5 | export { erc20ABI, erc721ABI, simpleNftABI } 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // See https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional 3 | extends: ['@commitlint/config-conventional'], 4 | } 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | headers: async () => { 7 | return [ 8 | { 9 | source: '/(.*)', 10 | headers: [ 11 | { 12 | key: 'Content-Security-Policy', 13 | value: `frame-ancestors verify.walletconnect.org verify.walletconnect.com;`, 14 | }, 15 | ], 16 | }, 17 | ] 18 | }, 19 | webpack: config => { 20 | config.externals.push('pino-pretty', 'lokijs', 'encoding') 21 | return config 22 | }, 23 | } 24 | 25 | module.exports = nextConfig 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-web3-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "yarn lint:code && yarn lint:formatting", 10 | "lint:fix": "yarn lint:code:fix && yarn lint:formatting:fix", 11 | "lint:code": "next lint", 12 | "lint:code:fix": "next lint --fix", 13 | "lint:formatting": "prettier . --check", 14 | "lint:formatting:fix": "prettier . --write --log-level=silent", 15 | "postinstall": "husky install" 16 | }, 17 | "dependencies": { 18 | "@emotion/react": "^11.11.1", 19 | "@emotion/styled": "^11.11.0", 20 | "@fontsource/roboto": "^5.0.8", 21 | "@mui/icons-material": "^5.14.8", 22 | "@mui/material": "^5.14.8", 23 | "@types/node": "20.5.9", 24 | "@types/react": "18.2.21", 25 | "@types/react-dom": "18.2.7", 26 | "@web3modal/wagmi": "^3.1.0", 27 | "eslint": "8.48.0", 28 | "eslint-config-next": "13.4.19", 29 | "next": "13.4.19", 30 | "react": "18.2.0", 31 | "react-dom": "18.2.0", 32 | "typescript": "5.2.2", 33 | "viem": "^1.10.4", 34 | "wagmi": "^1.4.1" 35 | }, 36 | "devDependencies": { 37 | "@commitlint/cli": "^17.7.1", 38 | "@commitlint/config-conventional": "^17.7.0", 39 | "@typescript-eslint/eslint-plugin": "^6.6.0", 40 | "@typescript-eslint/parser": "^6.6.0", 41 | "altheajs-prettier-config": "^1.3.0", 42 | "eslint-config-prettier": "^9.0.0", 43 | "eslint-plugin-import": "^2.28.1", 44 | "eslint-plugin-prettier": "^5.0.0", 45 | "eslint-plugin-simple-import-sort": "^10.0.0", 46 | "husky": "^8.0.0", 47 | "prettier": "^3.0.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | undefined 5 | undefined 6 | -------------------------------------------------------------------------------- /public/ledger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewcook/nextjs-web3-template/2c68c7dd016ab2d35db9dc81baad17829b953743/public/ledger.png -------------------------------------------------------------------------------- /public/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewcook/nextjs-web3-template/2c68c7dd016ab2d35db9dc81baad17829b953743/public/metamask.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/walletconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewcook/nextjs-web3-template/2c68c7dd016ab2d35db9dc81baad17829b953743/public/walletconnect.png -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { getErrorResponse, getSuccessResponse } from '@/utils/serverResponses' 2 | 3 | // GET /api 4 | export async function GET(req: Request) { 5 | try { 6 | // Get params 7 | const { searchParams } = new URL(req.url) 8 | console.log({ searchParams }) 9 | 10 | // Return success 11 | return getSuccessResponse(null) 12 | } catch (error: any) { 13 | return getErrorResponse(500, error.message, error) 14 | } 15 | } 16 | 17 | // POST /api 18 | export async function POST(req: Request) { 19 | try { 20 | // Get params 21 | const params = await req.json() 22 | console.log({ params }) 23 | 24 | // Return success 25 | return getSuccessResponse(null) 26 | } catch (error: any) { 27 | return getErrorResponse(500, error.message, error) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | import Dashboard from '@/components/Dashboard' 4 | 5 | export const metadata: Metadata = { 6 | title: 'Next DApp', 7 | description: 8 | 'A template for building Ethereum-based dApps using Next.js, Material UI, Wagmi/Viem, and WalletConnect.', 9 | } 10 | 11 | const DashboardPage = () => { 12 | return 13 | } 14 | 15 | export default DashboardPage 16 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewcook/nextjs-web3-template/2c68c7dd016ab2d35db9dc81baad17829b953743/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* TODO: Support both dark & light themes */ 2 | 3 | :root { 4 | /* CSS Vars */ 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | padding: 0; 10 | margin: 0; 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | font-family: 'Roboto'; 18 | } 19 | 20 | a { 21 | color: inherit; 22 | text-decoration: none; 23 | } 24 | 25 | /* Dark Mode */ 26 | @media (prefers-color-scheme: dark) { 27 | :root { 28 | /* CSS Var Overrides for Dark Mode */ 29 | } 30 | 31 | html { 32 | color-scheme: dark; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' // Tradeoff, for the benefit of having in one place and "globally" 2 | import './globals.css' 3 | import '@fontsource/roboto/300.css' 4 | import '@fontsource/roboto/400.css' 5 | import '@fontsource/roboto/500.css' 6 | import '@fontsource/roboto/700.css' 7 | 8 | import { ThemeProvider } from '@mui/material' 9 | import { WagmiConfig } from 'wagmi' 10 | 11 | import { ContractProvider } from '@/components/ContractProvider' 12 | import PrimaryLayout from '@/components/layouts/PrimaryLayout' 13 | import muiTheme from '@/lib/muiTheme' 14 | import wagmiConfig from '@/lib/wagmiConfig' 15 | 16 | // Primarily to hold all context providers 17 | // For the layout, see '/src/components/layouts/PrimaryLayout.tsx' 18 | const RootLayout = ({ children }: { children: React.ReactNode }) => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default RootLayout 35 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Typography } from '@mui/material' 2 | import type { Metadata } from 'next' 3 | 4 | export const metadata: Metadata = { 5 | title: 'Next DApp', 6 | description: 7 | 'A template for building Ethereum-based dApps using Next.js, Material UI, Wagmi/Viem, and WalletConnect.', 8 | } 9 | 10 | const styles = { 11 | paper: { 12 | p: 4, 13 | textAlign: 'center', 14 | }, 15 | } 16 | 17 | const DefaultPage = () => { 18 | return ( 19 | 20 | 21 | Home Page 22 | 23 | Put some info here 24 | 25 | ) 26 | } 27 | 28 | export default DefaultPage 29 | -------------------------------------------------------------------------------- /src/components/AppFooter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Box, Link, Typography } from '@mui/material' 3 | import { grey } from '@mui/material/colors' 4 | 5 | const styles = { 6 | wrap: { 7 | p: 2, 8 | display: 'flex', 9 | flexDirection: 'column', 10 | alignItems: 'center', 11 | justifyContent: 'center', 12 | height: '72px', 13 | backgroundColor: grey[900], 14 | }, 15 | } 16 | 17 | const AppFooter = () => { 18 | return ( 19 | 20 | 21 | ©{new Date().getFullYear()} | made with ♡ by{' '} 22 | 23 | dco 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default AppFooter 31 | -------------------------------------------------------------------------------- /src/components/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import MenuIcon from '@mui/icons-material/Menu' 3 | import { 4 | AppBar, 5 | Avatar, 6 | Box, 7 | Button, 8 | Container, 9 | IconButton, 10 | Menu, 11 | MenuItem, 12 | Toolbar, 13 | Tooltip, 14 | Typography, 15 | } from '@mui/material' 16 | import { grey } from '@mui/material/colors' 17 | import { useWeb3Modal } from '@web3modal/wagmi/react' 18 | import Link from 'next/link' 19 | import { useState } from 'react' 20 | import { useAccount, useDisconnect, useEnsAvatar, useEnsName } from 'wagmi' 21 | 22 | import formatAddress from '@/utils/formatAddress' 23 | 24 | import ConnectWalletButton from './ConnectWalletButton' 25 | 26 | const styles = { 27 | appBar: { backgroundColor: grey[900] }, 28 | navigationMobileWrap: { display: { xs: 'flex', md: 'none' }, flexGrow: 1, alignItems: 'center', mr: 1 }, 29 | navigationMobileMenu: { display: { xs: 'block', md: 'none' } }, 30 | navigationDesktopWrap: { display: { xs: 'none', md: 'flex' }, flexGrow: 1, alignItems: 'center' }, 31 | logoMobile: { 32 | mx: 2, 33 | display: { xs: 'flex', md: 'none' }, 34 | fontFamily: 'monospace', 35 | fontWeight: 700, 36 | letterSpacing: '.3rem', 37 | color: 'inherit', 38 | textDecoration: 'none', 39 | }, 40 | logoDesktop: { 41 | mr: 2, 42 | display: { xs: 'none', md: 'flex' }, 43 | fontFamily: 'monospace', 44 | fontWeight: 700, 45 | letterSpacing: '.3rem', 46 | color: 'inherit', 47 | textDecoration: 'none', 48 | }, 49 | navigationLink: { my: 2, color: 'white', display: 'block' }, 50 | userConnectedButton: { px: 2, py: 0.75 }, 51 | userAvatar: { ml: 1, width: '24px', height: '24px', flexGrow: 0, fontSize: '12px' }, 52 | userMenuWrap: { flexGrow: 0 }, 53 | userMenu: { mt: '45px' }, 54 | } 55 | 56 | const AppHeader = () => { 57 | // App Title 58 | const dappTitleText = 'WEB3DAPP' 59 | 60 | // Navigation Pages 61 | const pages = [ 62 | { 63 | text: 'Dashboard', 64 | href: '/dashboard', 65 | }, 66 | ] 67 | const userMenuItems = ['Switch Network', 'Switch Wallet', 'Disconnect'] 68 | 69 | // State 70 | const [anchorElNav, setAnchorElNav] = useState(null) 71 | const [anchorElUser, setAnchorElUser] = useState(null) 72 | 73 | // Hooks 74 | const { address, isConnected } = useAccount() 75 | const { disconnect } = useDisconnect() 76 | const { data: ensName } = useEnsName({ address }) 77 | const { data: ensAvatar } = useEnsAvatar({ name: ensName }) 78 | const { open } = useWeb3Modal() 79 | 80 | // Handlers 81 | const handleOpenNavMenu = (event: React.MouseEvent) => { 82 | setAnchorElNav(event.currentTarget) 83 | } 84 | const handleOpenUserMenu = (event: React.MouseEvent) => { 85 | setAnchorElUser(event.currentTarget) 86 | } 87 | 88 | const handleCloseNavMenu = () => { 89 | setAnchorElNav(null) 90 | } 91 | 92 | const handleCloseUserMenu = (setting: string) => { 93 | if (setting === 'Switch Network') open({ view: 'Networks' }) 94 | if (setting === 'Switch Wallet') open() 95 | if (setting === 'Disconnect') disconnect() 96 | setAnchorElUser(null) 97 | } 98 | 99 | // Components 100 | const MenuNavigationItems = pages.map(page => ( 101 | 102 | 103 | {page.text} 104 | 105 | 106 | )) 107 | 108 | return ( 109 | 110 | 111 | 112 | {/* Mobile Navigation */} 113 | 114 | 122 | 123 | 124 | 140 | {MenuNavigationItems} 141 | 142 | 143 | 144 | {dappTitleText} 145 | 146 | 147 | 148 | 149 | {/* Desktop Navigation */} 150 | 151 | 152 | 153 | {dappTitleText} 154 | 155 | 156 | {MenuNavigationItems} 157 | 158 | 159 | {/* User Menu */} 160 | 161 | {isConnected ? ( 162 | <> 163 | 164 | 168 | 169 | 185 | {userMenuItems.map(item => ( 186 | handleCloseUserMenu(item)}> 187 | {item} 188 | 189 | ))} 190 | 191 | 192 | ) : ( 193 | 194 | )} 195 | 196 | 197 | 198 | 199 | ) 200 | } 201 | 202 | export default AppHeader 203 | -------------------------------------------------------------------------------- /src/components/ConnectWalletButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@mui/material' 2 | import { useWeb3Modal } from '@web3modal/wagmi/react' 3 | import { useConnect } from 'wagmi' 4 | 5 | const styles = { 6 | button: { 7 | py: 1, 8 | }, 9 | walletText: { 10 | pl: 1, 11 | }, 12 | } 13 | 14 | const ConnectWalletButton = (): JSX.Element => { 15 | const { error } = useConnect() 16 | const { open } = useWeb3Modal() 17 | 18 | return ( 19 | <> 20 | {error && ( 21 | 22 | {error.message} 23 | 24 | )} 25 | 35 | 36 | ) 37 | } 38 | 39 | export default ConnectWalletButton 40 | -------------------------------------------------------------------------------- /src/components/ContractProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useEffect, useState } from 'react' 2 | import { Abi, Address, getContract, GetContractReturnType, parseEther } from 'viem' 3 | import { useAccount, usePublicClient, useWalletClient } from 'wagmi' 4 | 5 | import { CONTRACTS } from '@/constants' 6 | 7 | // Types 8 | type TxHash = Address | undefined 9 | type ContractReadArgs = { address: Address; abi: Abi; functionName: string; args?: unknown[] } 10 | type ContractWriteArgs = { address: Address; abi: Abi; functionName: string; args: unknown[]; value?: number } 11 | type ContractContextValues = { 12 | executeContractRead: (args: ContractReadArgs) => Promise 13 | executeContractWrite: (args: ContractWriteArgs) => Promise<[unknown, TxHash]> 14 | txSuccess: boolean 15 | txError: string | null 16 | resetTxNotifications: () => void 17 | // TODO: Add in fields representing each contract being used 18 | nft: GetContractReturnType 19 | } 20 | type ContractProviderProps = { 21 | children: React.ReactNode 22 | } 23 | 24 | // Create context with initial values 25 | const ContractContext = createContext({ 26 | executeContractRead: () => Promise.resolve(undefined), 27 | executeContractWrite: () => Promise.resolve([undefined, undefined]), 28 | txSuccess: false, 29 | txError: null, 30 | resetTxNotifications: () => {}, 31 | nft: {} as GetContractReturnType, 32 | }) 33 | 34 | // Context provider component 35 | export const ContractProvider: React.FC = ({ children }: ContractProviderProps) => { 36 | // State 37 | const [txSuccess, setTxSuccess] = useState(false) 38 | const [txError, setTxError] = useState(null) 39 | const [nft, setNft] = useState({} as GetContractReturnType) 40 | 41 | // Hooks 42 | const publicClient = usePublicClient() 43 | const { data: walletClient } = useWalletClient() 44 | const { address: account } = useAccount() 45 | 46 | // Provide a way to reset notification states 47 | const resetTxNotifications = () => { 48 | setTxSuccess(false) 49 | setTxError(null) 50 | } 51 | 52 | // Provide contract read helper 53 | const executeContractRead = useCallback( 54 | async ({ address, abi, functionName, args }: ContractReadArgs): Promise => { 55 | try { 56 | if (functionName === 'balance') return await publicClient.getBalance({ address }) 57 | else 58 | return await publicClient.readContract({ 59 | address, 60 | abi, 61 | functionName, 62 | args, 63 | }) 64 | } catch (error: any) { 65 | throw error 66 | } 67 | }, 68 | [publicClient], 69 | ) 70 | 71 | // Provide contract write helper 72 | const executeContractWrite = useCallback( 73 | async ({ address, abi, functionName, args, value }: ContractWriteArgs): Promise<[unknown, TxHash]> => { 74 | try { 75 | const { request, result } = await publicClient.simulateContract({ 76 | account, 77 | address, 78 | abi, 79 | functionName, 80 | args, 81 | value: value ? parseEther(`${value}`) : undefined, 82 | }) 83 | const txHash = await walletClient?.writeContract(request) 84 | setTxSuccess(true) 85 | setTxError(null) 86 | return [result, txHash] 87 | } catch (error: any) { 88 | setTxSuccess(false) 89 | setTxError(error.message) 90 | throw error 91 | } 92 | }, 93 | [publicClient, walletClient, account], 94 | ) 95 | 96 | // Instantiate the contract instance(s) when a new wallet/public client is detected 97 | useEffect(() => { 98 | if (walletClient && publicClient) { 99 | setNft( 100 | getContract({ 101 | address: CONTRACTS.SEPOLIA.NFT_COLLECTION.ADDRESS, 102 | abi: CONTRACTS.SEPOLIA.NFT_COLLECTION.ABI, 103 | publicClient, 104 | walletClient, 105 | }), 106 | ) 107 | } 108 | }, [walletClient, publicClient]) 109 | 110 | return ( 111 | 121 | {children} 122 | 123 | ) 124 | } 125 | 126 | // Context hook 127 | export const useContract = () => { 128 | const context: ContractContextValues = useContext(ContractContext) 129 | if (context === undefined) { 130 | throw new Error('useContract must be used within a ContractProvider component.') 131 | } 132 | return context 133 | } 134 | -------------------------------------------------------------------------------- /src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button, Grid, Paper, Typography } from '@mui/material' 3 | import { useWeb3Modal } from '@web3modal/wagmi/react' 4 | import { useState } from 'react' 5 | import { useAccount } from 'wagmi' 6 | 7 | import { useContract } from '@/components/ContractProvider' 8 | 9 | const styles = { 10 | paper: { 11 | p: 4, 12 | textAlign: 'center', 13 | }, 14 | button: { 15 | display: 'block', 16 | my: 2, 17 | mx: 'auto', 18 | }, 19 | } 20 | 21 | const Dashboard: React.FC = () => { 22 | // State 23 | const [nftName, setNftName] = useState('') 24 | const [tokenUri, setTokenUri] = useState('') 25 | 26 | // Hooks 27 | const { nft, executeContractRead, executeContractWrite } = useContract() 28 | const { isConnected } = useAccount() 29 | const { open } = useWeb3Modal() 30 | 31 | // Handlers 32 | const handleMint = async () => { 33 | try { 34 | if (!isConnected) return open() 35 | 36 | const [result, hash] = await executeContractWrite({ 37 | address: nft.address, 38 | abi: nft.abi, 39 | functionName: 'mint', 40 | args: ['exampleTokenURI'], 41 | }) 42 | 43 | console.log({ result, hash }) 44 | } catch (e) { 45 | console.error(e) 46 | } 47 | } 48 | 49 | const handleGetName = async () => { 50 | try { 51 | setNftName('') 52 | const result = (await executeContractRead({ address: nft.address, abi: nft.abi, functionName: 'name' })) as string 53 | setNftName(result) 54 | } catch (e) { 55 | console.error(e) 56 | } 57 | } 58 | 59 | const handleGetTokenURI = async (tokenId: number) => { 60 | try { 61 | setTokenUri('') 62 | const result = (await executeContractRead({ 63 | address: nft.address, 64 | abi: nft.abi, 65 | functionName: 'tokenURI', 66 | args: [tokenId], 67 | })) as string 68 | setTokenUri(result) 69 | } catch (e) { 70 | console.error(e) 71 | } 72 | } 73 | 74 | return ( 75 | <> 76 | 77 | 78 | 79 | 80 | Your Dashboard 81 | 82 | 85 | {nft.address && ( 86 | <> 87 | 90 | 93 | 94 | )} 95 | 96 | 97 | 98 | 99 | 100 | More Information 101 | 102 | NFT Name: {nftName || 'n/a'} 103 | TokenURI: {tokenUri || 'n/a'} 104 | 105 | 106 | 107 | 108 | ) 109 | } 110 | 111 | export default Dashboard 112 | -------------------------------------------------------------------------------- /src/components/layouts/PrimaryLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Alert, Box, Container, Snackbar, SnackbarOrigin } from '@mui/material' 3 | 4 | import AppFooter from '@/components/AppFooter' 5 | import AppHeader from '@/components/AppHeader' 6 | import { useContract } from '@/components/ContractProvider' 7 | 8 | const styles = { 9 | main: { 10 | display: 'flex', 11 | flexDirection: 'column', 12 | justifyContent: 'space-between', 13 | alignItems: 'center', 14 | py: 6, 15 | minHeight: 'calc(100vh - calc(64px + 72px))', 16 | }, 17 | alert: { 18 | color: '#fff', 19 | }, 20 | } 21 | 22 | const PrimaryLayout = ({ children }: { children: React.ReactNode }): React.ReactNode => { 23 | // Constants 24 | const NOTIFICATION_TIMEOUT: number = 10000 25 | const NOTIFICATION_POSITION: SnackbarOrigin = { vertical: 'top', horizontal: 'center' } 26 | // Hooks 27 | const { txSuccess, txError, resetTxNotifications } = useContract() 28 | 29 | return ( 30 | <> 31 | {/* Header/Body/Footer */} 32 | 33 | 34 | {children} 35 | 36 | 37 | {/* Tx Notifications */} 38 | 44 | 45 | Successfully sent transaction 46 | 47 | 48 | 54 | 55 | {txError} 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export default PrimaryLayout 63 | -------------------------------------------------------------------------------- /src/constants/contractAddresses.ts: -------------------------------------------------------------------------------- 1 | import { Abi, Address, getAddress } from 'viem' 2 | import { sepolia } from 'wagmi' 3 | 4 | import { simpleNftABI } from '../../abis/SimpleNFT' 5 | 6 | export type ContractABIPair = { 7 | ADDRESS: Address 8 | ABI: Abi 9 | } 10 | 11 | // TODO: Add in contract deployments and their ABIs for each network supported 12 | type ContractDeployments = { 13 | NFT_COLLECTION: ContractABIPair 14 | } 15 | 16 | const SEPOLIA: ContractDeployments = { 17 | // SimpleNFT: https://sepolia.etherscan.io/address/0x1cfD246a218b35e359584979dDBeAD1f567d9C88 18 | NFT_COLLECTION: { 19 | ADDRESS: getAddress('0x1cfD246a218b35e359584979dDBeAD1f567d9C88', sepolia.id), 20 | ABI: simpleNftABI, 21 | }, 22 | } 23 | 24 | const CONTRACTS = { 25 | SEPOLIA, 26 | } 27 | 28 | export default CONTRACTS 29 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import CONTRACTS from './contractAddresses' 2 | 3 | export { CONTRACTS } 4 | -------------------------------------------------------------------------------- /src/lib/muiTheme.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { grey } from '@mui/material/colors' 3 | import { createTheme, ThemeOptions } from '@mui/material/styles' 4 | 5 | const themeOptions: ThemeOptions = { 6 | palette: { 7 | mode: 'dark', 8 | primary: { 9 | main: '#20feff', 10 | }, 11 | secondary: { 12 | main: '#9c75ff', 13 | }, 14 | text: { 15 | primary: '#ffffff', 16 | secondary: 'rgba(255,255,255,0.7)', 17 | disabled: 'rgba(255,255,255,0.4)', 18 | // hint: '#c9c9ff', 19 | }, 20 | background: { 21 | default: '#000404', 22 | paper: grey[900], 23 | }, 24 | error: { 25 | main: '#fb1870', 26 | }, 27 | warning: { 28 | main: '#ff9131', 29 | }, 30 | info: { 31 | main: '#5e82ea', 32 | }, 33 | success: { 34 | main: '#38ff65', 35 | }, 36 | divider: 'rgba(103,103,103,0.7)', 37 | }, 38 | } 39 | 40 | const muiTheme = createTheme(themeOptions) 41 | 42 | export default muiTheme 43 | -------------------------------------------------------------------------------- /src/lib/wagmiConfig.ts: -------------------------------------------------------------------------------- 1 | import { createWeb3Modal } from '@web3modal/wagmi/react' 2 | import { configureChains, Connector, createConfig } from 'wagmi' 3 | import { 4 | arbitrum, 5 | arbitrumSepolia, 6 | mainnet, 7 | optimism, 8 | optimismSepolia, 9 | polygon, 10 | polygonMumbai, 11 | sepolia, 12 | } from 'wagmi/chains' 13 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet' 14 | import { InjectedConnector } from 'wagmi/connectors/injected' 15 | import { LedgerConnector } from 'wagmi/connectors/ledger' 16 | import { WalletConnectConnector } from 'wagmi/connectors/walletConnect' 17 | import { alchemyProvider } from 'wagmi/providers/alchemy' 18 | import { infuraProvider } from 'wagmi/providers/infura' 19 | import { publicProvider } from 'wagmi/providers/public' 20 | 21 | // WalletConnect options 22 | const projectId = `${process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID}` 23 | const metadata = { 24 | name: 'Next DApp', 25 | description: 'A simple boilerplate app template for building web3 applications.', 26 | url: 'https://dco.dev', 27 | icons: ['https://avatars.githubusercontent.com/u/37784886'], 28 | } 29 | 30 | export const { chains, publicClient, webSocketPublicClient } = configureChains( 31 | // Support several networks 32 | [mainnet, polygon, arbitrum, optimism, sepolia, polygonMumbai, arbitrumSepolia, optimismSepolia], 33 | 34 | // Prefer Alchemy, then Infura, then fallback 35 | [ 36 | infuraProvider({ apiKey: `${process.env.NEXT_PUBLIC_INFURA_RPC_KEY}` }), 37 | alchemyProvider({ apiKey: `${process.env.NEXT_PUBLIC_ALCHEMY_RPC_KEY}` }), 38 | publicProvider(), 39 | ], 40 | ) 41 | 42 | // Setup wallet connectors with many options 43 | const connectors: Connector[] = [ 44 | new WalletConnectConnector({ 45 | chains, 46 | options: { 47 | projectId, 48 | showQrModal: false, 49 | metadata, 50 | }, 51 | }), 52 | new InjectedConnector({ 53 | chains, 54 | options: { 55 | name: 'Browser Wallet', 56 | shimDisconnect: true, 57 | }, 58 | }), 59 | new LedgerConnector({ 60 | chains, 61 | options: { 62 | projectId: `${process.env.NEXT_PUBLIC_LEDGER_PROJECT_ID}`, 63 | }, 64 | }), 65 | new CoinbaseWalletConnector({ chains, options: { appName: metadata.name } }), 66 | ] 67 | 68 | // Stitch together the wagmi config 69 | const wagmiConfig = createConfig({ 70 | autoConnect: false, 71 | connectors, 72 | publicClient, 73 | webSocketPublicClient, 74 | }) 75 | 76 | // Create WalletConnect modal 77 | export const usingWalletcConnect: boolean = !!projectId 78 | if (usingWalletcConnect) { 79 | const defaultChain = sepolia 80 | createWeb3Modal({ wagmiConfig, projectId, chains, defaultChain }) 81 | } 82 | 83 | export default wagmiConfig 84 | -------------------------------------------------------------------------------- /src/utils/formatAddress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a long wallet address to a somewhat hidden one, showing the first two characters and the last 4 characters 3 | * @param address The wallet address abcdefg12345678 4 | * @returns An obfuscated address ab.................5678 5 | */ 6 | const formatAddress = (address: string | undefined): string => { 7 | if (!address) return '' 8 | return address.substring(0, 2) + '..........' + address.substring(address.length - 4) 9 | } 10 | 11 | // Variant, good for displaying longer hashes, like the commitment hash 12 | export const formatAddressLong = (address: string): string => { 13 | if (!address) return '' 14 | return ( 15 | address.substring(0, 6) + 16 | '................................................................................................' + 17 | address.substring(address.length - 4) 18 | ) 19 | } 20 | 21 | export default formatAddress 22 | -------------------------------------------------------------------------------- /src/utils/serverResponses.ts: -------------------------------------------------------------------------------- 1 | // Utility for success responses 2 | export const getSuccessResponse = (data: any, status = 200) => { 3 | return new Response( 4 | JSON.stringify({ 5 | status: 'success', 6 | data, 7 | }), 8 | { 9 | status, 10 | headers: { 'Content-Type': 'application/json' }, 11 | }, 12 | ) 13 | } 14 | 15 | // Utility for error responses 16 | export const getErrorResponse = (status = 500, message: string, error: Error | null = null) => { 17 | return new Response( 18 | JSON.stringify({ 19 | status: status < 500 ? 'fail' : 'error', 20 | message, 21 | error, 22 | }), 23 | { 24 | status, 25 | headers: { 'Content-Type': 'application/json' }, 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /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 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------