├── .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 |
105 |
106 | ))
107 |
108 | return (
109 |
110 |
111 |
112 | {/* Mobile Navigation */}
113 |
114 |
122 |
123 |
124 |
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 |
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 |
--------------------------------------------------------------------------------