├── .cursor
└── rules
│ └── scaffold-eth.mdc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
├── pull_request_template.md
└── workflows
│ └── lint.yaml
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.js
├── .yarn
├── plugins
│ └── @yarnpkg
│ │ ├── plugin-interactive-tools.cjs
│ │ └── plugin-typescript.cjs
└── releases
│ └── yarn-3.2.3.cjs
├── .yarnrc.yml
├── CONTRIBUTING.md
├── LICENCE
├── README.md
├── funding.json
├── package.json
├── packages
├── hardhat
│ ├── .env.example
│ ├── .gitignore
│ ├── .prettierrc.json
│ ├── contracts
│ │ └── YourContract.sol
│ ├── deploy
│ │ └── 00_deploy_your_contract.ts
│ ├── eslint.config.mjs
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── scripts
│ │ ├── generateAccount.ts
│ │ ├── generateTsAbis.ts
│ │ ├── importAccount.ts
│ │ ├── listAccount.ts
│ │ ├── revealPK.ts
│ │ └── runHardhatDeployWithPK.ts
│ ├── test
│ │ └── YourContract.ts
│ └── tsconfig.json
└── nextjs
│ ├── .env.example
│ ├── .gitignore
│ ├── .npmrc
│ ├── .prettierrc.js
│ ├── app
│ ├── blockexplorer
│ │ ├── _components
│ │ │ ├── AddressCodeTab.tsx
│ │ │ ├── AddressComponent.tsx
│ │ │ ├── AddressLogsTab.tsx
│ │ │ ├── AddressStorageTab.tsx
│ │ │ ├── BackButton.tsx
│ │ │ ├── ContractTabs.tsx
│ │ │ ├── PaginationButton.tsx
│ │ │ ├── SearchBar.tsx
│ │ │ ├── TransactionHash.tsx
│ │ │ ├── TransactionsTable.tsx
│ │ │ └── index.tsx
│ │ ├── address
│ │ │ └── [address]
│ │ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── transaction
│ │ │ ├── [txHash]
│ │ │ └── page.tsx
│ │ │ └── _components
│ │ │ └── TransactionComp.tsx
│ ├── debug
│ │ ├── _components
│ │ │ ├── DebugContracts.tsx
│ │ │ └── contract
│ │ │ │ ├── ContractInput.tsx
│ │ │ │ ├── ContractReadMethods.tsx
│ │ │ │ ├── ContractUI.tsx
│ │ │ │ ├── ContractVariables.tsx
│ │ │ │ ├── ContractWriteMethods.tsx
│ │ │ │ ├── DisplayVariable.tsx
│ │ │ │ ├── InheritanceTooltip.tsx
│ │ │ │ ├── ReadOnlyFunctionForm.tsx
│ │ │ │ ├── Tuple.tsx
│ │ │ │ ├── TupleArray.tsx
│ │ │ │ ├── TxReceipt.tsx
│ │ │ │ ├── WriteOnlyFunctionForm.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── utilsContract.tsx
│ │ │ │ └── utilsDisplay.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
│ ├── components
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── ScaffoldEthAppWithProviders.tsx
│ ├── SwitchTheme.tsx
│ ├── ThemeProvider.tsx
│ ├── assets
│ │ └── BuidlGuidlLogo.tsx
│ └── scaffold-eth
│ │ ├── Address
│ │ ├── Address.tsx
│ │ ├── AddressCopyIcon.tsx
│ │ └── AddressLinkWrapper.tsx
│ │ ├── Balance.tsx
│ │ ├── BlockieAvatar.tsx
│ │ ├── Faucet.tsx
│ │ ├── FaucetButton.tsx
│ │ ├── Input
│ │ ├── AddressInput.tsx
│ │ ├── Bytes32Input.tsx
│ │ ├── BytesInput.tsx
│ │ ├── EtherInput.tsx
│ │ ├── InputBase.tsx
│ │ ├── IntegerInput.tsx
│ │ ├── index.ts
│ │ └── utils.ts
│ │ ├── RainbowKitCustomConnectButton
│ │ ├── AddressInfoDropdown.tsx
│ │ ├── AddressQRCodeModal.tsx
│ │ ├── NetworkOptions.tsx
│ │ ├── WrongNetworkDropdown.tsx
│ │ └── index.tsx
│ │ └── index.tsx
│ ├── contracts
│ ├── deployedContracts.ts
│ └── externalContracts.ts
│ ├── eslint.config.mjs
│ ├── hooks
│ └── scaffold-eth
│ │ ├── index.ts
│ │ ├── useAnimationConfig.ts
│ │ ├── useContractLogs.ts
│ │ ├── useCopyToClipboard.ts
│ │ ├── useDeployedContractInfo.ts
│ │ ├── useDisplayUsdMode.ts
│ │ ├── useFetchBlocks.ts
│ │ ├── useInitializeNativeCurrencyPrice.ts
│ │ ├── useNetworkColor.ts
│ │ ├── useOutsideClick.ts
│ │ ├── useScaffoldContract.ts
│ │ ├── useScaffoldEventHistory.ts
│ │ ├── useScaffoldReadContract.ts
│ │ ├── useScaffoldWatchContractEvent.ts
│ │ ├── useScaffoldWriteContract.ts
│ │ ├── useSelectedNetwork.ts
│ │ ├── useTargetNetwork.ts
│ │ ├── useTransactor.tsx
│ │ └── useWatchBalance.ts
│ ├── next-env.d.ts
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ ├── favicon.png
│ ├── logo.svg
│ ├── manifest.json
│ └── thumbnail.jpg
│ ├── scaffold.config.ts
│ ├── services
│ ├── store
│ │ └── store.ts
│ └── web3
│ │ ├── wagmiConfig.tsx
│ │ └── wagmiConnectors.tsx
│ ├── styles
│ └── globals.css
│ ├── tsconfig.json
│ ├── types
│ └── abitype
│ │ └── abi.d.ts
│ ├── utils
│ └── scaffold-eth
│ │ ├── block.ts
│ │ ├── common.ts
│ │ ├── contract.ts
│ │ ├── contractsData.ts
│ │ ├── decodeTxData.ts
│ │ ├── fetchPriceFromUniswap.ts
│ │ ├── getMetadata.ts
│ │ ├── getParsedError.ts
│ │ ├── index.ts
│ │ ├── networks.ts
│ │ └── notification.tsx
│ └── vercel.json
└── yarn.lock
/.cursor/rules/scaffold-eth.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | This codebase contains Scaffold-ETH 2 (SE-2), everything you need to build dApps on Ethereum. Its tech stack is NextJS, RainbowKit, Wagmi and Typescript. Supports Hardhat and Foundry.
7 |
8 | It's a yarn monorepo that contains two main packages:
9 |
10 | - Hardhat (`packages/hardhat`): The solidity framework to write, test and deploy EVM Smart Contracts.
11 | - NextJS (`packages/nextjs`): The UI framework extended with utilities to make interacting with Smart Contracts easy (using Next.js App Router, not Pages Router).
12 |
13 | The usual dev flow is:
14 |
15 | - Start SE-2 locally:
16 | - `yarn chain`: Starts a local blockchain network
17 | - `yarn deploy`: Deploys SE-2 default contract
18 | - `yarn start`: Starts the frontend
19 | - Write a Smart Contract (modify the deployment script in `packages/hardhat/deploy` if needed)
20 | - Deploy it locally (`yarn deploy`)
21 | - Go to the `http://locahost:3000/debug` page to interact with your contract with a nice UI
22 | - Iterate until you get the functionality you want in your contract
23 | - Write tests for the contract in `packages/hardhat/test`
24 | - Create your custom UI using all the SE-2 components, hooks, and utilities.
25 | - Deploy your Smart Contrac to a live network
26 | - Deploy your UI (`yarn vercel` or `yarn ipfs`)
27 | - You can tweak which network the frontend is poiting (and some other configurations) in `scaffold.config.ts`
28 |
29 | ## Smart Contract UI interactions guidelines
30 |
31 | SE-2 provides a set of hooks that facilitates contract interactions from the UI. It reads the contract data from `deployedContracts.ts` and `externalContracts.ts`, located in `packages/nextjs/contracts`.
32 |
33 | ### Reading data from a contract
34 | Use the `useScaffoldReadContract` (`packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts`) hook. Example:
35 |
36 | ```typescript
37 | const { data: someData } = useScaffoldReadContract({
38 | contractName: "YourContract",
39 | functionName: "functionName",
40 | args: [arg1, arg2], // optional
41 | });
42 | ```
43 |
44 | ### Writing data to a contract
45 | Use the `useScaffoldWriteContract` (`packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts`) hook.
46 | 1. Initilize the hook with just the contract name
47 | 2. Call the `writeContractAsync` function.
48 |
49 | Example:
50 |
51 | ```typescript
52 | const { writeContractAsync: writeYourContractAsync } = useScaffoldWriteContract(
53 | { contractName: "YourContract" }
54 | );
55 |
56 | // Usage (this will send a write transaction to the contract)
57 | await writeContractAsync({
58 | functionName: "functionName",
59 | args: [arg1, arg2], // optional
60 | value: parseEther("0.1"), // optional, for payable functions
61 | });
62 | ```
63 |
64 | Never use any other patterns for contract interaction. The hooks are:
65 |
66 | - useScaffoldReadContract (for reading)
67 | - useScaffoldWriteContract (for writing)
68 |
69 | ### Other Hooks
70 | SE-2 also provides other hooks to interact with blockchain data: `useScaffoldWatchContractEvent`, `useScaffoldEventHistory`, `useDeployedContractInfo`, `useScaffoldContract`, `useTransactor`. They live under `packages/nextjs/hooks/scaffold-eth`.
71 |
72 | ## Display Components guidelines
73 | SE-2 provides a set of pre-built React components for common Ethereum use cases:
74 | - `Address`: Always use this when displaying an ETH address
75 | - `AddressInput`: Always use this when users need to input an ETH address
76 | - `Balance`: Display the ETH/USDC balance of a given address
77 | - `EtherInput`: An extended number input with ETH/USD conversion.
78 |
79 | They live under `packages/nextjs/components/scaffold-eth`.
80 |
81 | Find the relevant information from the documentation and the codebase. Think step by step before answering the question.
82 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug/issue
3 | title: 'bug:
'
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for taking the time to fill out this bug report! The more info you provide, the more we can help you 🙌
9 |
10 | - type: checkboxes
11 | attributes:
12 | label: Is there an existing issue for this?
13 | description: Please search to see if an issue already exists for the bug you encountered.
14 | options:
15 | - label: I have looked through the [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues)
16 | required: true
17 |
18 | - type: dropdown
19 | attributes:
20 | label: Which method was used to setup Scaffold-ETH 2 ?
21 | description: You may select both, if the bug is present in both the methods.
22 | multiple: true
23 | options:
24 | - git clone
25 | - npx create-eth@latest
26 | validations:
27 | required: true
28 |
29 | - type: textarea
30 | attributes:
31 | label: Current Behavior
32 | description: A concise description of what you're experiencing.
33 | validations:
34 | required: false
35 |
36 | - type: textarea
37 | attributes:
38 | label: Expected Behavior
39 | description: A concise description of what you expected to happen.
40 | validations:
41 | required: false
42 |
43 | - type: textarea
44 | attributes:
45 | label: Steps To Reproduce
46 | description: Steps or code snippets to reproduce the behavior.
47 | validations:
48 | required: false
49 |
50 | - type: textarea
51 | attributes:
52 | label: Anything else?
53 | description: |
54 | Browser info? Screenshots? Anything that will give us more context about the issue you are encountering!
55 |
56 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
57 | validations:
58 | required: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Ask Question
4 | url: https://github.com/scaffold-eth/scaffold-eth-2/discussions/new?category=q-a
5 | about: Ask questions and discuss with other community members
6 | - name: Request Feature
7 | url: https://github.com/scaffold-eth/scaffold-eth-2/discussions/new?category=ideas
8 | about: Requests features or brainstorm ideas for new functionality
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | _Concise description of proposed changes, We recommend using screenshots and videos for better description_
4 |
5 | ## Additional Information
6 |
7 | - [ ] I have read the [contributing docs](/scaffold-eth/scaffold-eth-2/blob/main/CONTRIBUTING.md) (if this is your first contribution)
8 | - [ ] This is not a duplicate of any [existing pull request](https://github.com/scaffold-eth/scaffold-eth-2/pulls)
9 |
10 | ## Related Issues
11 |
12 | _Closes #{issue number}_
13 |
14 | _Note: If your changes are small and straightforward, you may skip the creation of an issue beforehand and remove this section. However, for medium-to-large changes, it is recommended to have an open issue for discussion and approval prior to submitting a pull request._
15 |
16 | Your ENS/address:
17 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | ci:
13 | runs-on: ${{ matrix.os }}
14 |
15 | strategy:
16 | matrix:
17 | os: [ubuntu-latest]
18 | node: [lts/*]
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@master
23 |
24 | - name: Setup node env
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node }}
28 | cache: yarn
29 |
30 | - name: Install dependencies
31 | run: yarn install --immutable
32 |
33 | - name: Run hardhat node, deploy contracts (& generate contracts typescript output)
34 | run: yarn chain & yarn deploy
35 |
36 | - name: Run nextjs lint
37 | run: yarn next:lint --max-warnings=0
38 |
39 | - name: Check typings on nextjs
40 | run: yarn next:check-types
41 |
42 | - name: Run hardhat lint
43 | run: yarn hardhat:lint --max-warnings=0
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # yarn
5 | .yarn/*
6 | !.yarn/patches
7 | !.yarn/plugins
8 | !.yarn/releases
9 | !.yarn/sdks
10 | !.yarn/versions
11 |
12 | # eslint
13 | .eslintcache
14 |
15 | # misc
16 | .DS_Store
17 |
18 | # IDE
19 | .vscode
20 | .idea
21 |
22 | # cli
23 | dist
24 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | yarn lint-staged --verbose
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const buildNextEslintCommand = (filenames) =>
4 | `yarn next:lint --fix --file ${filenames
5 | .map((f) => path.relative(path.join("packages", "nextjs"), f))
6 | .join(" --file ")}`;
7 |
8 | const checkTypesNextCommand = () => "yarn next:check-types";
9 |
10 | const buildHardhatEslintCommand = (filenames) =>
11 | `yarn hardhat:lint-staged --fix ${filenames
12 | .map((f) => path.relative(path.join("packages", "hardhat"), f))
13 | .join(" ")}`;
14 |
15 | module.exports = {
16 | "packages/nextjs/**/*.{ts,tsx}": [
17 | buildNextEslintCommand,
18 | checkTypesNextCommand,
19 | ],
20 | "packages/hardhat/**/*.{ts,tsx}": [buildHardhatEslintCommand],
21 | };
22 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableColors: true
2 |
3 | nmHoistingLimits: workspaces
4 |
5 | nodeLinker: node-modules
6 |
7 | plugins:
8 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
9 | spec: "@yarnpkg/plugin-typescript"
10 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
11 | spec: "@yarnpkg/plugin-interactive-tools"
12 |
13 | yarnPath: .yarn/releases/yarn-3.2.3.cjs
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Welcome to Scaffold-ETH 2 Contributing Guide
2 |
3 | Thank you for investing your time in contributing to Scaffold-ETH 2!
4 |
5 | This guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved.
6 |
7 | ## About the Project
8 |
9 | Scaffold-ETH 2 is a minimal and forkable repo providing builders with a starter kit to build decentralized applications on Ethereum.
10 |
11 | Read the [README](README.md) to get an overview of the project.
12 |
13 | ### Vision
14 |
15 | The goal of Scaffold-ETH 2 is to provide the primary building blocks for a decentralized application.
16 |
17 | The repo can be forked to include integrations and more features, but we want to keep the `main` branch simple and minimal.
18 |
19 | ### Project Status
20 |
21 | The project is under active development.
22 |
23 | You can view the open Issues, follow the development process, and contribute to the project.
24 |
25 | ### Rules
26 |
27 | 1. All code contributions require an Issue to be created and agreed upon by core contributors before submitting a Pull Request. This ensures proper discussion, alignment, and consensus on the proposed changes.
28 | 2. Contributors must be humans, not bots.
29 | 3. First-time contributions must not contain only spelling or grammatical fixes.
30 |
31 | ## Getting started
32 |
33 | You can contribute to this repo in many ways:
34 |
35 | - Solve open issues
36 | - Report bugs or feature requests
37 | - Improve the documentation
38 |
39 | Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions:
40 |
41 | - Search for existing Issues and PRs before creating your own.
42 | - Contributions should only fix/add the functionality in the issue OR address style issues, not both.
43 | - If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error.
44 | - Please use the same formatting in the code repository. You can configure your IDE to do it by using the prettier / linting config files included in each package.
45 | - If applicable, please edit the README.md file to reflect the changes.
46 |
47 | ### Issues
48 |
49 | Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created.
50 |
51 | #### Solve an issue
52 |
53 | Scan through our [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues) to find one that interests you.
54 |
55 | If a contributor is working on the issue, they will be assigned to the individual. If you find an issue to work on, you are welcome to assign it to yourself and open a PR with a fix for it.
56 |
57 | #### Create a new issue
58 |
59 | If a related issue doesn't exist, you can open a new issue.
60 |
61 | Some tips to follow when you are creating an issue:
62 |
63 | - Provide as much context as possible. Over-communicate to give the most details to the reader.
64 | - Include the steps to reproduce the issue or the reason for adding the feature.
65 | - Screenshots, videos, etc., are highly appreciated.
66 |
67 | ### Pull Requests
68 |
69 | #### Pull Request Process
70 |
71 | We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
72 |
73 | 1. Fork the repo
74 | 2. Clone the project
75 | 3. Create a new branch with a descriptive name
76 | 4. Commit your changes to the new branch
77 | 5. Push changes to your fork
78 | 6. Open a PR in our repository and tag one of the maintainers to review your PR
79 |
80 | Here are some tips for a high-quality pull request:
81 |
82 | - Create a title for the PR that accurately defines the work done.
83 | - Structure the description neatly to make it easy to consume by the readers. For example, you can include bullet points and screenshots instead of having one large paragraph.
84 | - Add the link to the issue if applicable.
85 | - Have a good commit message that summarises the work done.
86 |
87 | Once you submit your PR:
88 |
89 | - We may ask questions, request additional information, or ask for changes to be made before a PR can be merged. Please note that these are to make the PR clear for everyone involved and aim to create a frictionless interaction process.
90 | - As you update your PR and apply changes, mark each conversation resolved.
91 |
92 | Once the PR is approved, we'll "squash-and-merge" to keep the git commit history clean.
93 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 BuidlGuidl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🏗 Scaffold-ETH 2
2 |
3 |
7 |
8 | 🧪 An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts.
9 |
10 | ⚙️ Built using NextJS, RainbowKit, Foundry/Hardhat, Wagmi, Viem, and Typescript.
11 |
12 | - ✅ **Contract Hot Reload**: Your frontend auto-adapts to your smart contract as you edit it.
13 | - 🪝 **[Custom hooks](https://docs.scaffoldeth.io/hooks/)**: Collection of React hooks wrapper around [wagmi](https://wagmi.sh/) to simplify interactions with smart contracts with typescript autocompletion.
14 | - 🧱 [**Components**](https://docs.scaffoldeth.io/components/): Collection of common web3 components to quickly build your frontend.
15 | - 🔥 **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet.
16 | - 🔐 **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network.
17 |
18 | 
19 |
20 | ## Requirements
21 |
22 | Before you begin, you need to install the following tools:
23 |
24 | - [Node (>= v20.18.3)](https://nodejs.org/en/download/)
25 | - Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install))
26 | - [Git](https://git-scm.com/downloads)
27 |
28 | ## Quickstart
29 |
30 | To get started with Scaffold-ETH 2, follow the steps below:
31 |
32 | 1. Install the latest version of Scaffold-ETH 2
33 |
34 | ```
35 | npx create-eth@latest
36 | ```
37 |
38 | This command will install all the necessary packages and dependencies, so it might take a while.
39 |
40 | > [!NOTE]
41 | > You can also initialize your project with one of our extensions to add specific features or starter-kits. Learn more in our [extensions documentation](https://docs.scaffoldeth.io/extensions/).
42 |
43 | 2. Run a local network in the first terminal:
44 |
45 | ```
46 | yarn chain
47 | ```
48 |
49 | This command starts a local Ethereum network that runs on your local machine and can be used for testing and development. Learn how to [customize your network configuration](https://docs.scaffoldeth.io/quick-start/environment#1-initialize-a-local-blockchain).
50 |
51 | 3. On a second terminal, deploy the test contract:
52 |
53 | ```
54 | yarn deploy
55 | ```
56 |
57 | This command deploys a test smart contract to the local network. You can find more information about how to customize your contract and deployment script in our [documentation](https://docs.scaffoldeth.io/quick-start/environment#2-deploy-your-smart-contract).
58 |
59 | 4. On a third terminal, start your NextJS app:
60 |
61 | ```
62 | yarn start
63 | ```
64 |
65 | Visit your app on: `http://localhost:3000`. You can interact with your smart contract using the `Debug Contracts` page. You can tweak the app config in `packages/nextjs/scaffold.config.ts`.
66 |
67 | **What's next**:
68 |
69 | Visit the [What's next section of our docs](https://docs.scaffoldeth.io/quick-start/environment#whats-next) to learn how to:
70 |
71 | - Edit your smart contracts
72 | - Edit your deployment scripts
73 | - Customize your frontend
74 | - Edit the app config
75 | - Writing and running tests
76 | - [Setting up external services and API keys](https://docs.scaffoldeth.io/deploying/deploy-smart-contracts#configuration-of-third-party-services-for-production-grade-apps)
77 |
78 | ## Documentation
79 |
80 | Visit our [docs](https://docs.scaffoldeth.io) to learn all the technical details and guides of Scaffold-ETH 2.
81 |
82 | To know more about its features, check out our [website](https://scaffoldeth.io).
83 |
84 | ## Contributing to Scaffold-ETH 2
85 |
86 | We welcome contributions to Scaffold-ETH 2!
87 |
88 | Please see [CONTRIBUTING.MD](https://github.com/scaffold-eth/scaffold-eth-2/blob/main/CONTRIBUTING.md) for more information and guidelines for contributing to Scaffold-ETH 2.
89 |
--------------------------------------------------------------------------------
/funding.json:
--------------------------------------------------------------------------------
1 | {
2 | "opRetro": {
3 | "projectId": "0x154a42e5ca88d7c2732fda74d6eb611057fc88dbe6f0ff3aae7b89c2cd1666ab"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "se-2",
3 | "version": "0.0.1",
4 | "private": true,
5 | "workspaces": {
6 | "packages": [
7 | "packages/hardhat",
8 | "packages/nextjs"
9 | ]
10 | },
11 | "scripts": {
12 | "account": "yarn hardhat:account",
13 | "account:import": "yarn workspace @se-2/hardhat account:import",
14 | "account:generate": "yarn workspace @se-2/hardhat account:generate",
15 | "account:reveal-pk": "yarn workspace @se-2/hardhat account:reveal-pk",
16 | "chain": "yarn hardhat:chain",
17 | "compile": "yarn hardhat:compile",
18 | "deploy": "yarn hardhat:deploy",
19 | "fork": "yarn hardhat:fork",
20 | "format": "yarn next:format && yarn hardhat:format",
21 | "generate": "yarn account:generate",
22 | "hardhat:account": "yarn workspace @se-2/hardhat account",
23 | "hardhat:chain": "yarn workspace @se-2/hardhat chain",
24 | "hardhat:check-types": "yarn workspace @se-2/hardhat check-types",
25 | "hardhat:clean": "yarn workspace @se-2/hardhat clean",
26 | "hardhat:compile": "yarn workspace @se-2/hardhat compile",
27 | "hardhat:deploy": "yarn workspace @se-2/hardhat deploy",
28 | "hardhat:flatten": "yarn workspace @se-2/hardhat flatten",
29 | "hardhat:fork": "yarn workspace @se-2/hardhat fork",
30 | "hardhat:format": "yarn workspace @se-2/hardhat format",
31 | "hardhat:generate": "yarn workspace @se-2/hardhat generate",
32 | "hardhat:hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify",
33 | "hardhat:lint": "yarn workspace @se-2/hardhat lint",
34 | "hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged",
35 | "hardhat:test": "yarn workspace @se-2/hardhat test",
36 | "hardhat:verify": "yarn workspace @se-2/hardhat verify",
37 | "lint": "yarn next:lint && yarn hardhat:lint",
38 | "next:build": "yarn workspace @se-2/nextjs build",
39 | "next:check-types": "yarn workspace @se-2/nextjs check-types",
40 | "next:format": "yarn workspace @se-2/nextjs format",
41 | "next:lint": "yarn workspace @se-2/nextjs lint",
42 | "next:serve": "yarn workspace @se-2/nextjs serve",
43 | "postinstall": "husky",
44 | "precommit": "lint-staged",
45 | "start": "yarn workspace @se-2/nextjs dev",
46 | "test": "yarn hardhat:test",
47 | "vercel": "yarn workspace @se-2/nextjs vercel",
48 | "vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo",
49 | "ipfs": "yarn workspace @se-2/nextjs ipfs",
50 | "vercel:login": "yarn workspace @se-2/nextjs vercel:login",
51 | "verify": "yarn hardhat:verify"
52 | },
53 | "packageManager": "yarn@3.2.3",
54 | "devDependencies": {
55 | "husky": "^9.1.6",
56 | "lint-staged": "^15.2.10"
57 | },
58 | "engines": {
59 | "node": ">=20.18.3"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/hardhat/.env.example:
--------------------------------------------------------------------------------
1 | # Template for Hardhat environment variables.
2 |
3 | # To use this template, copy this file, rename it .env, and fill in the values.
4 |
5 | # If not set, we provide default values (check `hardhat.config.ts`) so developers can start prototyping out of the box,
6 | # but we recommend getting your own API Keys for Production Apps.
7 |
8 | # To access the values stored in this .env file you can use: process.env.VARIABLENAME
9 | ALCHEMY_API_KEY=
10 | ETHERSCAN_MAINNET_API_KEY=
11 |
12 | # Don't fill this value manually, run yarn generate to generate a new account or yarn account:import to import an existing PK.
13 | DEPLOYER_PRIVATE_KEY_ENCRYPTED=
14 |
--------------------------------------------------------------------------------
/packages/hardhat/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # env files
5 | .env
6 |
7 | # coverage
8 | coverage
9 | coverage.json
10 |
11 | # typechain
12 | typechain
13 | typechain-types
14 |
15 | # hardhat files
16 | cache
17 | artifacts
18 |
19 | # zkSync files
20 | artifacts-zk
21 | cache-zk
22 |
23 | # deployments
24 | deployments/localhost
25 |
26 | # typescript
27 | *.tsbuildinfo
28 |
29 | # other
30 | temp
31 |
--------------------------------------------------------------------------------
/packages/hardhat/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-solidity"],
3 | "arrowParens": "avoid",
4 | "printWidth": 120,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "overrides": [
8 | {
9 | "files": "*.sol",
10 | "options": {
11 | "printWidth": 120,
12 | "tabWidth": 4,
13 | "singleQuote": false,
14 | "bracketSpacing": true
15 | }
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/hardhat/contracts/YourContract.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0 <0.9.0;
3 |
4 | // Useful for debugging. Remove when deploying to a live network.
5 | import "hardhat/console.sol";
6 |
7 | // Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
8 | // import "@openzeppelin/contracts/access/Ownable.sol";
9 |
10 | /**
11 | * A smart contract that allows changing a state variable of the contract and tracking the changes
12 | * It also allows the owner to withdraw the Ether in the contract
13 | * @author BuidlGuidl
14 | */
15 | contract YourContract {
16 | // State Variables
17 | address public immutable owner;
18 | string public greeting = "Building Unstoppable Apps!!!";
19 | bool public premium = false;
20 | uint256 public totalCounter = 0;
21 | mapping(address => uint) public userGreetingCounter;
22 |
23 | // Events: a way to emit log statements from smart contract that can be listened to by external parties
24 | event GreetingChange(address indexed greetingSetter, string newGreeting, bool premium, uint256 value);
25 |
26 | // Constructor: Called once on contract deployment
27 | // Check packages/hardhat/deploy/00_deploy_your_contract.ts
28 | constructor(address _owner) {
29 | owner = _owner;
30 | }
31 |
32 | // Modifier: used to define a set of rules that must be met before or after a function is executed
33 | // Check the withdraw() function
34 | modifier isOwner() {
35 | // msg.sender: predefined variable that represents address of the account that called the current function
36 | require(msg.sender == owner, "Not the Owner");
37 | _;
38 | }
39 |
40 | /**
41 | * Function that allows anyone to change the state variable "greeting" of the contract and increase the counters
42 | *
43 | * @param _newGreeting (string memory) - new greeting to save on the contract
44 | */
45 | function setGreeting(string memory _newGreeting) public payable {
46 | // Print data to the hardhat chain console. Remove when deploying to a live network.
47 | console.log("Setting new greeting '%s' from %s", _newGreeting, msg.sender);
48 |
49 | // Change state variables
50 | greeting = _newGreeting;
51 | totalCounter += 1;
52 | userGreetingCounter[msg.sender] += 1;
53 |
54 | // msg.value: built-in global variable that represents the amount of ether sent with the transaction
55 | if (msg.value > 0) {
56 | premium = true;
57 | } else {
58 | premium = false;
59 | }
60 |
61 | // emit: keyword used to trigger an event
62 | emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value);
63 | }
64 |
65 | /**
66 | * Function that allows the owner to withdraw all the Ether in the contract
67 | * The function can only be called by the owner of the contract as defined by the isOwner modifier
68 | */
69 | function withdraw() public isOwner {
70 | (bool success, ) = owner.call{ value: address(this).balance }("");
71 | require(success, "Failed to send Ether");
72 | }
73 |
74 | /**
75 | * Function that allows the contract to receive ETH
76 | */
77 | receive() external payable {}
78 | }
79 |
--------------------------------------------------------------------------------
/packages/hardhat/deploy/00_deploy_your_contract.ts:
--------------------------------------------------------------------------------
1 | import { HardhatRuntimeEnvironment } from "hardhat/types";
2 | import { DeployFunction } from "hardhat-deploy/types";
3 | import { Contract } from "ethers";
4 |
5 | /**
6 | * Deploys a contract named "YourContract" using the deployer account and
7 | * constructor arguments set to the deployer address
8 | *
9 | * @param hre HardhatRuntimeEnvironment object.
10 | */
11 | const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
12 | /*
13 | On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
14 |
15 | When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
16 | should have sufficient balance to pay for the gas fees for contract creation.
17 |
18 | You can generate a random account with `yarn generate` or `yarn account:import` to import your
19 | existing PK which will fill DEPLOYER_PRIVATE_KEY_ENCRYPTED in the .env file (then used on hardhat.config.ts)
20 | You can run the `yarn account` command to check your balance in every network.
21 | */
22 | const { deployer } = await hre.getNamedAccounts();
23 | const { deploy } = hre.deployments;
24 |
25 | await deploy("YourContract", {
26 | from: deployer,
27 | // Contract constructor arguments
28 | args: [deployer],
29 | log: true,
30 | // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
31 | // automatically mining the contract deployment transaction. There is no effect on live networks.
32 | autoMine: true,
33 | });
34 |
35 | // Get the deployed contract to interact with it after deploying.
36 | const yourContract = await hre.ethers.getContract("YourContract", deployer);
37 | console.log("👋 Initial greeting:", await yourContract.greeting());
38 | };
39 |
40 | export default deployYourContract;
41 |
42 | // Tags are useful if you have multiple deploy files and only want to run one of them.
43 | // e.g. yarn deploy --tags YourContract
44 | deployYourContract.tags = ["YourContract"];
45 |
--------------------------------------------------------------------------------
/packages/hardhat/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig, globalIgnores } from "eslint/config";
2 | import globals from "globals";
3 | import tsParser from "@typescript-eslint/parser";
4 | import prettierPlugin from "eslint-plugin-prettier";
5 |
6 | import path from "node:path";
7 | import { fileURLToPath } from "node:url";
8 | import { FlatCompat } from "@eslint/eslintrc";
9 |
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = path.dirname(__filename);
12 | const compat = new FlatCompat({
13 | baseDirectory: __dirname,
14 | });
15 |
16 | export default defineConfig([
17 | globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]),
18 | {
19 | extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"),
20 |
21 | plugins: {
22 | prettier: prettierPlugin,
23 | },
24 | languageOptions: {
25 | globals: {
26 | ...globals.node,
27 | },
28 |
29 | parser: tsParser,
30 | },
31 |
32 | rules: {
33 | "@typescript-eslint/no-unused-vars": "error",
34 | "@typescript-eslint/no-explicit-any": "off",
35 |
36 | "prettier/prettier": [
37 | "warn",
38 | {
39 | endOfLine: "auto",
40 | },
41 | ],
42 | },
43 | },
44 | ]);
45 |
--------------------------------------------------------------------------------
/packages/hardhat/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@se-2/hardhat",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "account": "hardhat run scripts/listAccount.ts",
6 | "account:generate": "hardhat run scripts/generateAccount.ts",
7 | "account:import": "hardhat run scripts/importAccount.ts",
8 | "account:reveal-pk": "hardhat run scripts/revealPK.ts",
9 | "chain": "hardhat node --network hardhat --no-deploy",
10 | "check-types": "tsc --noEmit --incremental",
11 | "clean": "hardhat clean",
12 | "compile": "hardhat compile",
13 | "deploy": "ts-node scripts/runHardhatDeployWithPK.ts",
14 | "flatten": "hardhat flatten",
15 | "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
16 | "format": "prettier --write './**/*.(ts|sol)'",
17 | "generate": "yarn account:generate",
18 | "hardhat-verify": "hardhat verify",
19 | "lint": "eslint",
20 | "lint-staged": "eslint",
21 | "test": "REPORT_GAS=true hardhat test --network hardhat",
22 | "verify": "hardhat etherscan-verify"
23 | },
24 | "devDependencies": {
25 | "@ethersproject/abi": "^5.7.0",
26 | "@ethersproject/providers": "^5.7.2",
27 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
28 | "@nomicfoundation/hardhat-ethers": "^3.0.8",
29 | "@nomicfoundation/hardhat-network-helpers": "^1.0.11",
30 | "@nomicfoundation/hardhat-verify": "^2.0.10",
31 | "@typechain/ethers-v5": "^11.1.2",
32 | "@typechain/hardhat": "^9.1.0",
33 | "@types/eslint": "^9.6.1",
34 | "@types/mocha": "^10.0.10",
35 | "@types/prettier": "^3.0.0",
36 | "@types/qrcode": "^1.5.5",
37 | "@typescript-eslint/eslint-plugin": "^8.27.0",
38 | "@typescript-eslint/parser": "^8.27.0",
39 | "chai": "^4.5.0",
40 | "eslint": "^9.23.0",
41 | "eslint-config-prettier": "^10.1.1",
42 | "eslint-plugin-prettier": "^5.2.4",
43 | "ethers": "^6.13.2",
44 | "hardhat": "^2.22.10",
45 | "hardhat-deploy": "^0.12.4",
46 | "hardhat-deploy-ethers": "^0.4.2",
47 | "hardhat-gas-reporter": "^2.2.1",
48 | "prettier": "^3.5.3",
49 | "prettier-plugin-solidity": "^1.4.1",
50 | "solidity-coverage": "^0.8.13",
51 | "ts-node": "^10.9.1",
52 | "typechain": "^8.3.2",
53 | "typescript": "^5.8.2"
54 | },
55 | "dependencies": {
56 | "@inquirer/password": "^4.0.2",
57 | "@openzeppelin/contracts": "^5.0.2",
58 | "@typechain/ethers-v6": "^0.5.1",
59 | "dotenv": "^16.4.5",
60 | "envfile": "^7.1.0",
61 | "qrcode": "^1.5.4"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/generateAccount.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { parse, stringify } from "envfile";
3 | import * as fs from "fs";
4 | import password from "@inquirer/password";
5 |
6 | const envFilePath = "./.env";
7 |
8 | const getValidatedPassword = async () => {
9 | while (true) {
10 | const pass = await password({ message: "Enter a password to encrypt your private key:" });
11 | const confirmation = await password({ message: "Confirm password:" });
12 |
13 | if (pass === confirmation) {
14 | return pass;
15 | }
16 | console.log("❌ Passwords don't match. Please try again.");
17 | }
18 | };
19 |
20 | const setNewEnvConfig = async (existingEnvConfig = {}) => {
21 | console.log("👛 Generating new Wallet\n");
22 | const randomWallet = ethers.Wallet.createRandom();
23 |
24 | const pass = await getValidatedPassword();
25 | const encryptedJson = await randomWallet.encrypt(pass);
26 |
27 | const newEnvConfig = {
28 | ...existingEnvConfig,
29 | DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
30 | };
31 |
32 | // Store in .env
33 | fs.writeFileSync(envFilePath, stringify(newEnvConfig));
34 | console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
35 | console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
36 | console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
37 | };
38 |
39 | async function main() {
40 | if (!fs.existsSync(envFilePath)) {
41 | // No .env file yet.
42 | await setNewEnvConfig();
43 | return;
44 | }
45 |
46 | const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
47 | if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
48 | console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
49 | return;
50 | }
51 |
52 | await setNewEnvConfig(existingEnvConfig);
53 | }
54 |
55 | main().catch(error => {
56 | console.error(error);
57 | process.exitCode = 1;
58 | });
59 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/importAccount.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { parse, stringify } from "envfile";
3 | import * as fs from "fs";
4 | import password from "@inquirer/password";
5 |
6 | const envFilePath = "./.env";
7 |
8 | const getValidatedPassword = async () => {
9 | while (true) {
10 | const pass = await password({ message: "Enter a password to encrypt your private key:" });
11 | const confirmation = await password({ message: "Confirm password:" });
12 |
13 | if (pass === confirmation) {
14 | return pass;
15 | }
16 | console.log("❌ Passwords don't match. Please try again.");
17 | }
18 | };
19 |
20 | const getWalletFromPrivateKey = async () => {
21 | while (true) {
22 | const privateKey = await password({ message: "Paste your private key:" });
23 | try {
24 | const wallet = new ethers.Wallet(privateKey);
25 | return wallet;
26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
27 | } catch (e) {
28 | console.log("❌ Invalid private key format. Please try again.");
29 | }
30 | }
31 | };
32 |
33 | const setNewEnvConfig = async (existingEnvConfig = {}) => {
34 | console.log("👛 Importing Wallet\n");
35 |
36 | const wallet = await getWalletFromPrivateKey();
37 |
38 | const pass = await getValidatedPassword();
39 | const encryptedJson = await wallet.encrypt(pass);
40 |
41 | const newEnvConfig = {
42 | ...existingEnvConfig,
43 | DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
44 | };
45 |
46 | // Store in .env
47 | fs.writeFileSync(envFilePath, stringify(newEnvConfig));
48 | console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
49 | console.log("🪄 Imported wallet address:", wallet.address, "\n");
50 | console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
51 | };
52 |
53 | async function main() {
54 | if (!fs.existsSync(envFilePath)) {
55 | // No .env file yet.
56 | await setNewEnvConfig();
57 | return;
58 | }
59 |
60 | const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
61 | if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
62 | console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
63 | return;
64 | }
65 |
66 | await setNewEnvConfig(existingEnvConfig);
67 | }
68 |
69 | main().catch(error => {
70 | console.error(error);
71 | process.exitCode = 1;
72 | });
73 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/listAccount.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 | dotenv.config();
3 | import { ethers, Wallet } from "ethers";
4 | import QRCode from "qrcode";
5 | import { config } from "hardhat";
6 | import password from "@inquirer/password";
7 |
8 | async function main() {
9 | const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
10 |
11 | if (!encryptedKey) {
12 | console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
13 | return;
14 | }
15 |
16 | const pass = await password({ message: "Enter your password to decrypt the private key:" });
17 | let wallet: Wallet;
18 | try {
19 | wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
21 | } catch (e) {
22 | console.log("❌ Failed to decrypt private key. Wrong password?");
23 | return;
24 | }
25 |
26 | const address = wallet.address;
27 | console.log(await QRCode.toString(address, { type: "terminal", small: true }));
28 | console.log("Public address:", address, "\n");
29 |
30 | // Balance on each network
31 | const availableNetworks = config.networks;
32 | for (const networkName in availableNetworks) {
33 | try {
34 | const network = availableNetworks[networkName];
35 | if (!("url" in network)) continue;
36 | const provider = new ethers.JsonRpcProvider(network.url);
37 | await provider._detectNetwork();
38 | const balance = await provider.getBalance(address);
39 | console.log("--", networkName, "-- 📡");
40 | console.log(" balance:", +ethers.formatEther(balance));
41 | console.log(" nonce:", +(await provider.getTransactionCount(address)));
42 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
43 | } catch (e) {
44 | console.log("Can't connect to network", networkName);
45 | }
46 | }
47 | }
48 |
49 | main().catch(error => {
50 | console.error(error);
51 | process.exitCode = 1;
52 | });
53 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/revealPK.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 | dotenv.config();
3 | import { Wallet } from "ethers";
4 | import password from "@inquirer/password";
5 |
6 | async function main() {
7 | const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
8 |
9 | if (!encryptedKey) {
10 | console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
11 | return;
12 | }
13 |
14 | console.log("👀 This will reveal your private key on the console.\n");
15 |
16 | const pass = await password({ message: "Enter your password to decrypt the private key:" });
17 | let wallet: Wallet;
18 | try {
19 | wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
20 | } catch {
21 | console.log("❌ Failed to decrypt private key. Wrong password?");
22 | return;
23 | }
24 |
25 | console.log("\n🔑 Private key:", wallet.privateKey);
26 | }
27 |
28 | main().catch(error => {
29 | console.error(error);
30 | process.exitCode = 1;
31 | });
32 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/runHardhatDeployWithPK.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 | dotenv.config();
3 | import { Wallet } from "ethers";
4 | import password from "@inquirer/password";
5 | import { spawn } from "child_process";
6 | import { config } from "hardhat";
7 |
8 | /**
9 | * Unencrypts the private key and runs the hardhat deploy command
10 | */
11 | async function main() {
12 | const networkIndex = process.argv.indexOf("--network");
13 | const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
14 |
15 | if (networkName === "localhost" || networkName === "hardhat") {
16 | // Deploy command on the localhost network
17 | const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
18 | stdio: "inherit",
19 | env: process.env,
20 | shell: process.platform === "win32",
21 | });
22 |
23 | hardhat.on("exit", code => {
24 | process.exit(code || 0);
25 | });
26 | return;
27 | }
28 |
29 | const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
30 |
31 | if (!encryptedKey) {
32 | console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
33 | return;
34 | }
35 |
36 | const pass = await password({ message: "Enter password to decrypt private key:" });
37 |
38 | try {
39 | const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
40 | process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
41 |
42 | const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
43 | stdio: "inherit",
44 | env: process.env,
45 | shell: process.platform === "win32",
46 | });
47 |
48 | hardhat.on("exit", code => {
49 | process.exit(code || 0);
50 | });
51 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
52 | } catch (e) {
53 | console.error("Failed to decrypt private key. Wrong password?");
54 | process.exit(1);
55 | }
56 | }
57 |
58 | main().catch(console.error);
59 |
--------------------------------------------------------------------------------
/packages/hardhat/test/YourContract.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | import { ethers } from "hardhat";
3 | import { YourContract } from "../typechain-types";
4 |
5 | describe("YourContract", function () {
6 | // We define a fixture to reuse the same setup in every test.
7 |
8 | let yourContract: YourContract;
9 | before(async () => {
10 | const [owner] = await ethers.getSigners();
11 | const yourContractFactory = await ethers.getContractFactory("YourContract");
12 | yourContract = (await yourContractFactory.deploy(owner.address)) as YourContract;
13 | await yourContract.waitForDeployment();
14 | });
15 |
16 | describe("Deployment", function () {
17 | it("Should have the right message on deploy", async function () {
18 | expect(await yourContract.greeting()).to.equal("Building Unstoppable Apps!!!");
19 | });
20 |
21 | it("Should allow setting a new message", async function () {
22 | const newGreeting = "Learn Scaffold-ETH 2! :)";
23 |
24 | await yourContract.setGreeting(newGreeting);
25 | expect(await yourContract.greeting()).to.equal(newGreeting);
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/packages/hardhat/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "resolveJsonModule": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "skipLibCheck": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/nextjs/.env.example:
--------------------------------------------------------------------------------
1 | # Template for NextJS environment variables.
2 |
3 | # For local development, copy this file, rename it to .env.local, and fill in the values.
4 | # When deploying live, you'll need to store the vars in Vercel/System config.
5 |
6 | # If not set, we provide default values (check `scaffold.config.ts`) so developers can start prototyping out of the box,
7 | # but we recommend getting your own API Keys for Production Apps.
8 |
9 | # To access the values stored in this env file you can use: process.env.VARIABLENAME
10 | # You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side.
11 | # More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
12 | NEXT_PUBLIC_ALCHEMY_API_KEY=
13 | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
14 |
--------------------------------------------------------------------------------
/packages/nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 | .vercel
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env
31 | .env.local
32 | .env.development.local
33 | .env.test.local
34 | .env.production.local
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
39 | ipfs-upload.config.json
--------------------------------------------------------------------------------
/packages/nextjs/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies = false
2 |
--------------------------------------------------------------------------------
/packages/nextjs/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "arrowParens": "avoid",
3 | "printWidth": 120,
4 | "tabWidth": 2,
5 | "trailingComma": "all",
6 | "importOrder": ["^react$", "^next/(.*)$", "", "^@heroicons/(.*)$", "^~~/(.*)$"],
7 | "importOrderSortSpecifiers": true,
8 | "plugins": [require.resolve("@trivago/prettier-plugin-sort-imports")],
9 | }
10 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/AddressCodeTab.tsx:
--------------------------------------------------------------------------------
1 | type AddressCodeTabProps = {
2 | bytecode: string;
3 | assembly: string;
4 | };
5 |
6 | export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => {
7 | const formattedAssembly = Array.from(assembly.matchAll(/\w+( 0x[a-fA-F0-9]+)?/g))
8 | .map(it => it[0])
9 | .join("\n");
10 |
11 | return (
12 |
13 | Bytecode
14 |
15 |
16 | {bytecode}
17 |
18 |
19 | Opcodes
20 |
21 |
22 | {formattedAssembly}
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx:
--------------------------------------------------------------------------------
1 | import { BackButton } from "./BackButton";
2 | import { ContractTabs } from "./ContractTabs";
3 | import { Address as AddressType } from "viem";
4 | import { Address, Balance } from "~~/components/scaffold-eth";
5 |
6 | export const AddressComponent = ({
7 | address,
8 | contractData,
9 | }: {
10 | address: AddressType;
11 | contractData: { bytecode: string; assembly: string } | null;
12 | }) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Balance:
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/AddressLogsTab.tsx:
--------------------------------------------------------------------------------
1 | import { Address } from "viem";
2 | import { useContractLogs } from "~~/hooks/scaffold-eth";
3 | import { replacer } from "~~/utils/scaffold-eth/common";
4 |
5 | export const AddressLogsTab = ({ address }: { address: Address }) => {
6 | const contractLogs = useContractLogs(address);
7 |
8 | return (
9 |
10 |
11 |
12 | {contractLogs.map((log, i) => (
13 |
14 | Log: {JSON.stringify(log, replacer, 2)}
15 |
16 | ))}
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/AddressStorageTab.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Address, createPublicClient, http, toHex } from "viem";
5 | import { hardhat } from "viem/chains";
6 |
7 | const publicClient = createPublicClient({
8 | chain: hardhat,
9 | transport: http(),
10 | });
11 |
12 | export const AddressStorageTab = ({ address }: { address: Address }) => {
13 | const [storage, setStorage] = useState([]);
14 |
15 | useEffect(() => {
16 | const fetchStorage = async () => {
17 | try {
18 | const storageData = [];
19 | let idx = 0;
20 |
21 | while (true) {
22 | const storageAtPosition = await publicClient.getStorageAt({
23 | address: address,
24 | slot: toHex(idx),
25 | });
26 |
27 | if (storageAtPosition === "0x" + "0".repeat(64)) break;
28 |
29 | if (storageAtPosition) {
30 | storageData.push(storageAtPosition);
31 | }
32 |
33 | idx++;
34 | }
35 | setStorage(storageData);
36 | } catch (error) {
37 | console.error("Failed to fetch storage:", error);
38 | }
39 | };
40 |
41 | fetchStorage();
42 | }, [address]);
43 |
44 | return (
45 |
46 | {storage.length > 0 ? (
47 |
48 |
49 | {storage.map((data, i) => (
50 |
51 | Storage Slot {i}: {data}
52 |
53 | ))}
54 |
55 |
56 | ) : (
57 |
This contract does not have any variables.
58 | )}
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/BackButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | export const BackButton = () => {
6 | const router = useRouter();
7 | return (
8 | router.back()}>
9 | Back
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { AddressCodeTab } from "./AddressCodeTab";
5 | import { AddressLogsTab } from "./AddressLogsTab";
6 | import { AddressStorageTab } from "./AddressStorageTab";
7 | import { PaginationButton } from "./PaginationButton";
8 | import { TransactionsTable } from "./TransactionsTable";
9 | import { Address, createPublicClient, http } from "viem";
10 | import { hardhat } from "viem/chains";
11 | import { useFetchBlocks } from "~~/hooks/scaffold-eth";
12 |
13 | type AddressCodeTabProps = {
14 | bytecode: string;
15 | assembly: string;
16 | };
17 |
18 | type PageProps = {
19 | address: Address;
20 | contractData: AddressCodeTabProps | null;
21 | };
22 |
23 | const publicClient = createPublicClient({
24 | chain: hardhat,
25 | transport: http(),
26 | });
27 |
28 | export const ContractTabs = ({ address, contractData }: PageProps) => {
29 | const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage } = useFetchBlocks();
30 | const [activeTab, setActiveTab] = useState("transactions");
31 | const [isContract, setIsContract] = useState(false);
32 |
33 | useEffect(() => {
34 | const checkIsContract = async () => {
35 | const contractCode = await publicClient.getBytecode({ address: address });
36 | setIsContract(contractCode !== undefined && contractCode !== "0x");
37 | };
38 |
39 | checkIsContract();
40 | }, [address]);
41 |
42 | const filteredBlocks = blocks.filter(block =>
43 | block.transactions.some(tx => {
44 | if (typeof tx === "string") {
45 | return false;
46 | }
47 | return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase();
48 | }),
49 | );
50 |
51 | return (
52 | <>
53 | {isContract && (
54 |
55 | setActiveTab("transactions")}
59 | >
60 | Transactions
61 |
62 | setActiveTab("code")}
66 | >
67 | Code
68 |
69 | setActiveTab("storage")}
73 | >
74 | Storage
75 |
76 | setActiveTab("logs")}
80 | >
81 | Logs
82 |
83 |
84 | )}
85 | {activeTab === "transactions" && (
86 |
94 | )}
95 | {activeTab === "code" && contractData && (
96 |
97 | )}
98 | {activeTab === "storage" && }
99 | {activeTab === "logs" && }
100 | >
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
2 |
3 | type PaginationButtonProps = {
4 | currentPage: number;
5 | totalItems: number;
6 | setCurrentPage: (page: number) => void;
7 | };
8 |
9 | const ITEMS_PER_PAGE = 20;
10 |
11 | export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => {
12 | const isPrevButtonDisabled = currentPage === 0;
13 | const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE);
14 |
15 | const prevButtonClass = isPrevButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
16 | const nextButtonClass = isNextButtonDisabled ? "btn-disabled cursor-default" : "btn-primary";
17 |
18 | if (isNextButtonDisabled && isPrevButtonDisabled) return null;
19 |
20 | return (
21 |
22 |
setCurrentPage(currentPage - 1)}
26 | >
27 |
28 |
29 |
Page {currentPage + 1}
30 |
setCurrentPage(currentPage + 1)}
34 | >
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { isAddress, isHex } from "viem";
6 | import { hardhat } from "viem/chains";
7 | import { usePublicClient } from "wagmi";
8 |
9 | export const SearchBar = () => {
10 | const [searchInput, setSearchInput] = useState("");
11 | const router = useRouter();
12 |
13 | const client = usePublicClient({ chainId: hardhat.id });
14 |
15 | const handleSearch = async (event: React.FormEvent) => {
16 | event.preventDefault();
17 | if (isHex(searchInput)) {
18 | try {
19 | const tx = await client?.getTransaction({ hash: searchInput });
20 | if (tx) {
21 | router.push(`/blockexplorer/transaction/${searchInput}`);
22 | return;
23 | }
24 | } catch (error) {
25 | console.error("Failed to fetch transaction:", error);
26 | }
27 | }
28 |
29 | if (isAddress(searchInput)) {
30 | router.push(`/blockexplorer/address/${searchInput}`);
31 | return;
32 | }
33 | };
34 |
35 | return (
36 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
3 | import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
4 |
5 | export const TransactionHash = ({ hash }: { hash: string }) => {
6 | const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
7 | useCopyToClipboard();
8 |
9 | return (
10 |
11 |
12 | {hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
13 |
14 | {isAddressCopiedToClipboard ? (
15 |
19 | ) : (
20 | copyAddressToClipboard(hash)}
24 | />
25 | )}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/TransactionsTable.tsx:
--------------------------------------------------------------------------------
1 | import { TransactionHash } from "./TransactionHash";
2 | import { formatEther } from "viem";
3 | import { Address } from "~~/components/scaffold-eth";
4 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
5 | import { TransactionWithFunction } from "~~/utils/scaffold-eth";
6 | import { TransactionsTableProps } from "~~/utils/scaffold-eth/";
7 |
8 | export const TransactionsTable = ({ blocks, transactionReceipts }: TransactionsTableProps) => {
9 | const { targetNetwork } = useTargetNetwork();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | Transaction Hash
18 | Function Called
19 | Block Number
20 | Time Mined
21 | From
22 | To
23 | Value ({targetNetwork.nativeCurrency.symbol})
24 |
25 |
26 |
27 | {blocks.map(block =>
28 | (block.transactions as TransactionWithFunction[]).map(tx => {
29 | const receipt = transactionReceipts[tx.hash];
30 | const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString();
31 | const functionCalled = tx.input.substring(0, 10);
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {tx.functionName === "0x" ? "" : {tx.functionName} }
40 | {functionCalled !== "0x" && (
41 | {functionCalled}
42 | )}
43 |
44 | {block.number?.toString()}
45 | {timeMined}
46 |
47 |
48 |
49 |
50 | {!receipt?.contractAddress ? (
51 | tx.to &&
52 | ) : (
53 |
54 |
55 |
(Contract Creation)
56 |
57 | )}
58 |
59 |
60 | {formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol}
61 |
62 |
63 | );
64 | }),
65 | )}
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/_components/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./SearchBar";
2 | export * from "./BackButton";
3 | export * from "./AddressCodeTab";
4 | export * from "./TransactionHash";
5 | export * from "./ContractTabs";
6 | export * from "./PaginationButton";
7 | export * from "./TransactionsTable";
8 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/address/[address]/page.tsx:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import { Address } from "viem";
4 | import { hardhat } from "viem/chains";
5 | import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
6 | import deployedContracts from "~~/contracts/deployedContracts";
7 | import { isZeroAddress } from "~~/utils/scaffold-eth/common";
8 | import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
9 |
10 | type PageProps = {
11 | params: Promise<{ address: Address }>;
12 | };
13 |
14 | async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath: string) {
15 | const buildInfoFiles = fs.readdirSync(buildInfoDirectory);
16 | let bytecode = "";
17 | let assembly = "";
18 |
19 | for (let i = 0; i < buildInfoFiles.length; i++) {
20 | const filePath = path.join(buildInfoDirectory, buildInfoFiles[i]);
21 |
22 | const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8"));
23 |
24 | if (buildInfo.output.contracts[contractPath]) {
25 | for (const contract in buildInfo.output.contracts[contractPath]) {
26 | bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object;
27 | assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes;
28 | break;
29 | }
30 | }
31 |
32 | if (bytecode && assembly) {
33 | break;
34 | }
35 | }
36 |
37 | return { bytecode, assembly };
38 | }
39 |
40 | const getContractData = async (address: Address) => {
41 | const contracts = deployedContracts as GenericContractsDeclaration | null;
42 | const chainId = hardhat.id;
43 | let contractPath = "";
44 |
45 | const buildInfoDirectory = path.join(
46 | __dirname,
47 | "..",
48 | "..",
49 | "..",
50 | "..",
51 | "..",
52 | "..",
53 | "..",
54 | "hardhat",
55 | "artifacts",
56 | "build-info",
57 | );
58 |
59 | if (!fs.existsSync(buildInfoDirectory)) {
60 | throw new Error(`Directory ${buildInfoDirectory} not found.`);
61 | }
62 |
63 | const deployedContractsOnChain = contracts ? contracts[chainId] : {};
64 | for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) {
65 | if (contractInfo.address.toLowerCase() === address.toLowerCase()) {
66 | contractPath = `contracts/${contractName}.sol`;
67 | break;
68 | }
69 | }
70 |
71 | if (!contractPath) {
72 | // No contract found at this address
73 | return null;
74 | }
75 |
76 | const { bytecode, assembly } = await fetchByteCodeAndAssembly(buildInfoDirectory, contractPath);
77 |
78 | return { bytecode, assembly };
79 | };
80 |
81 | export function generateStaticParams() {
82 | // An workaround to enable static exports in Next.js, generating single dummy page.
83 | return [{ address: "0x0000000000000000000000000000000000000000" }];
84 | }
85 |
86 | const AddressPage = async (props: PageProps) => {
87 | const params = await props.params;
88 | const address = params?.address as Address;
89 |
90 | if (isZeroAddress(address)) return null;
91 |
92 | const contractData: { bytecode: string; assembly: string } | null = await getContractData(address);
93 | return ;
94 | };
95 |
96 | export default AddressPage;
97 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/layout.tsx:
--------------------------------------------------------------------------------
1 | import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
2 |
3 | export const metadata = getMetadata({
4 | title: "Block Explorer",
5 | description: "Block Explorer created with 🏗 Scaffold-ETH 2",
6 | });
7 |
8 | const BlockExplorerLayout = ({ children }: { children: React.ReactNode }) => {
9 | return <>{children}>;
10 | };
11 |
12 | export default BlockExplorerLayout;
13 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { PaginationButton, SearchBar, TransactionsTable } from "./_components";
5 | import type { NextPage } from "next";
6 | import { hardhat } from "viem/chains";
7 | import { useFetchBlocks } from "~~/hooks/scaffold-eth";
8 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
9 | import { notification } from "~~/utils/scaffold-eth";
10 |
11 | const BlockExplorer: NextPage = () => {
12 | const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks();
13 | const { targetNetwork } = useTargetNetwork();
14 | const [isLocalNetwork, setIsLocalNetwork] = useState(true);
15 | const [hasError, setHasError] = useState(false);
16 |
17 | useEffect(() => {
18 | if (targetNetwork.id !== hardhat.id) {
19 | setIsLocalNetwork(false);
20 | }
21 | }, [targetNetwork.id]);
22 |
23 | useEffect(() => {
24 | if (targetNetwork.id === hardhat.id && error) {
25 | setHasError(true);
26 | }
27 | }, [targetNetwork.id, error]);
28 |
29 | useEffect(() => {
30 | if (!isLocalNetwork) {
31 | notification.error(
32 | <>
33 |
34 | targetNetwork
is not localhost
35 |
36 |
37 | - You are on {targetNetwork.name}
.This
38 | block explorer is only for localhost
.
39 |
40 |
41 | - You can use{" "}
42 |
43 | {targetNetwork.blockExplorers?.default.name}
44 | {" "}
45 | instead
46 |
47 | >,
48 | );
49 | }
50 | }, [
51 | isLocalNetwork,
52 | targetNetwork.blockExplorers?.default.name,
53 | targetNetwork.blockExplorers?.default.url,
54 | targetNetwork.name,
55 | ]);
56 |
57 | useEffect(() => {
58 | if (hasError) {
59 | notification.error(
60 | <>
61 | Cannot connect to local provider
62 |
63 | - Did you forget to run yarn chain
?
64 |
65 |
66 | - Or you can change targetNetwork
in{" "}
67 | scaffold.config.ts
68 |
69 | >,
70 | );
71 | }
72 | }, [hasError]);
73 |
74 | return (
75 |
80 | );
81 | };
82 |
83 | export default BlockExplorer;
84 |
--------------------------------------------------------------------------------
/packages/nextjs/app/blockexplorer/transaction/[txHash]/page.tsx:
--------------------------------------------------------------------------------
1 | import TransactionComp from "../_components/TransactionComp";
2 | import type { NextPage } from "next";
3 | import { Hash } from "viem";
4 | import { isZeroAddress } from "~~/utils/scaffold-eth/common";
5 |
6 | type PageProps = {
7 | params: Promise<{ txHash?: Hash }>;
8 | };
9 |
10 | export function generateStaticParams() {
11 | // An workaround to enable static exports in Next.js, generating single dummy page.
12 | return [{ txHash: "0x0000000000000000000000000000000000000000" }];
13 | }
14 | const TransactionPage: NextPage = async (props: PageProps) => {
15 | const params = await props.params;
16 | const txHash = params?.txHash as Hash;
17 |
18 | if (isZeroAddress(txHash)) return null;
19 |
20 | return ;
21 | };
22 |
23 | export default TransactionPage;
24 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/DebugContracts.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useMemo } from "react";
4 | import { useSessionStorage } from "usehooks-ts";
5 | import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
6 | import { ContractUI } from "~~/app/debug/_components/contract";
7 | import { ContractName, GenericContract } from "~~/utils/scaffold-eth/contract";
8 | import { useAllContracts } from "~~/utils/scaffold-eth/contractsData";
9 |
10 | const selectedContractStorageKey = "scaffoldEth2.selectedContract";
11 |
12 | export function DebugContracts() {
13 | const contractsData = useAllContracts();
14 | const contractNames = useMemo(
15 | () =>
16 | Object.keys(contractsData).sort((a, b) => {
17 | return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
18 | }) as ContractName[],
19 | [contractsData],
20 | );
21 |
22 | const [selectedContract, setSelectedContract] = useSessionStorage(
23 | selectedContractStorageKey,
24 | contractNames[0],
25 | { initializeWithValue: false },
26 | );
27 |
28 | useEffect(() => {
29 | if (!contractNames.includes(selectedContract)) {
30 | setSelectedContract(contractNames[0]);
31 | }
32 | }, [contractNames, selectedContract, setSelectedContract]);
33 |
34 | return (
35 |
36 | {contractNames.length === 0 ? (
37 |
No contracts found!
38 | ) : (
39 | <>
40 | {contractNames.length > 1 && (
41 |
42 | {contractNames.map(contractName => (
43 | setSelectedContract(contractName)}
51 | >
52 | {contractName}
53 | {(contractsData[contractName] as GenericContract)?.external && (
54 |
55 |
56 |
57 | )}
58 |
59 | ))}
60 |
61 | )}
62 | {contractNames.map(contractName => (
63 |
68 | ))}
69 | >
70 | )}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ContractInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Dispatch, SetStateAction } from "react";
4 | import { Tuple } from "./Tuple";
5 | import { TupleArray } from "./TupleArray";
6 | import { AbiParameter } from "abitype";
7 | import {
8 | AddressInput,
9 | Bytes32Input,
10 | BytesInput,
11 | InputBase,
12 | IntegerInput,
13 | IntegerVariant,
14 | } from "~~/components/scaffold-eth";
15 | import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
16 |
17 | type ContractInputProps = {
18 | setForm: Dispatch>>;
19 | form: Record | undefined;
20 | stateObjectKey: string;
21 | paramType: AbiParameter;
22 | };
23 |
24 | /**
25 | * Generic Input component to handle input's based on their function param type
26 | */
27 | export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => {
28 | const inputProps = {
29 | name: stateObjectKey,
30 | value: form?.[stateObjectKey],
31 | placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type,
32 | onChange: (value: any) => {
33 | setForm(form => ({ ...form, [stateObjectKey]: value }));
34 | },
35 | };
36 |
37 | const renderInput = () => {
38 | switch (paramType.type) {
39 | case "address":
40 | return ;
41 | case "bytes32":
42 | return ;
43 | case "bytes":
44 | return ;
45 | case "string":
46 | return ;
47 | case "tuple":
48 | return (
49 |
55 | );
56 | default:
57 | // Handling 'int' types and 'tuple[]' types
58 | if (paramType.type.includes("int") && !paramType.type.includes("[")) {
59 | return ;
60 | } else if (paramType.type.startsWith("tuple[")) {
61 | return (
62 |
68 | );
69 | } else {
70 | return ;
71 | }
72 | }
73 | };
74 |
75 | return (
76 |
77 |
78 | {paramType.name && {paramType.name} }
79 | {paramType.type}
80 |
81 | {renderInput()}
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx:
--------------------------------------------------------------------------------
1 | import { Abi, AbiFunction } from "abitype";
2 | import { ReadOnlyFunctionForm } from "~~/app/debug/_components/contract";
3 | import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
4 |
5 | export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract }) => {
6 | if (!deployedContractData) {
7 | return null;
8 | }
9 |
10 | const functionsToDisplay = (
11 | ((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[]
12 | )
13 | .filter(fn => {
14 | const isQueryableWithParams =
15 | (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0;
16 | return isQueryableWithParams;
17 | })
18 | .map(fn => {
19 | return {
20 | fn,
21 | inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
22 | };
23 | })
24 | .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
25 |
26 | if (!functionsToDisplay.length) {
27 | return <>No read methods>;
28 | }
29 |
30 | return (
31 | <>
32 | {functionsToDisplay.map(({ fn, inheritedFrom }) => (
33 |
40 | ))}
41 | >
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ContractUI.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // @refresh reset
4 | import { useReducer } from "react";
5 | import { ContractReadMethods } from "./ContractReadMethods";
6 | import { ContractVariables } from "./ContractVariables";
7 | import { ContractWriteMethods } from "./ContractWriteMethods";
8 | import { Address, Balance } from "~~/components/scaffold-eth";
9 | import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth";
10 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
11 | import { ContractName } from "~~/utils/scaffold-eth/contract";
12 |
13 | type ContractUIProps = {
14 | contractName: ContractName;
15 | className?: string;
16 | };
17 |
18 | /**
19 | * UI component to interface with deployed contracts.
20 | **/
21 | export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => {
22 | const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false);
23 | const { targetNetwork } = useTargetNetwork();
24 | const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName });
25 | const networkColor = useNetworkColor();
26 |
27 | if (deployedContractLoading) {
28 | return (
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | if (!deployedContractData) {
36 | return (
37 |
38 | {`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`}
39 |
40 | );
41 | }
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
{contractName}
51 |
52 |
53 | Balance:
54 |
55 |
56 |
57 |
58 | {targetNetwork && (
59 |
60 | Network :{" "}
61 | {targetNetwork.name}
62 |
63 | )}
64 |
65 |
66 |
70 |
71 |
72 |
101 |
102 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx:
--------------------------------------------------------------------------------
1 | import { DisplayVariable } from "./DisplayVariable";
2 | import { Abi, AbiFunction } from "abitype";
3 | import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
4 |
5 | export const ContractVariables = ({
6 | refreshDisplayVariables,
7 | deployedContractData,
8 | }: {
9 | refreshDisplayVariables: boolean;
10 | deployedContractData: Contract;
11 | }) => {
12 | if (!deployedContractData) {
13 | return null;
14 | }
15 |
16 | const functionsToDisplay = (
17 | (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
18 | )
19 | .filter(fn => {
20 | const isQueryableWithNoParams =
21 | (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
22 | return isQueryableWithNoParams;
23 | })
24 | .map(fn => {
25 | return {
26 | fn,
27 | inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
28 | };
29 | })
30 | .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
31 |
32 | if (!functionsToDisplay.length) {
33 | return <>No contract variables>;
34 | }
35 |
36 | return (
37 | <>
38 | {functionsToDisplay.map(({ fn, inheritedFrom }) => (
39 |
47 | ))}
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx:
--------------------------------------------------------------------------------
1 | import { Abi, AbiFunction } from "abitype";
2 | import { WriteOnlyFunctionForm } from "~~/app/debug/_components/contract";
3 | import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
4 |
5 | export const ContractWriteMethods = ({
6 | onChange,
7 | deployedContractData,
8 | }: {
9 | onChange: () => void;
10 | deployedContractData: Contract;
11 | }) => {
12 | if (!deployedContractData) {
13 | return null;
14 | }
15 |
16 | const functionsToDisplay = (
17 | (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
18 | )
19 | .filter(fn => {
20 | const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure";
21 | return isWriteableFunction;
22 | })
23 | .map(fn => {
24 | return {
25 | fn,
26 | inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
27 | };
28 | })
29 | .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
30 |
31 | if (!functionsToDisplay.length) {
32 | return <>No write methods>;
33 | }
34 |
35 | return (
36 | <>
37 | {functionsToDisplay.map(({ fn, inheritedFrom }, idx) => (
38 |
46 | ))}
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/DisplayVariable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { InheritanceTooltip } from "./InheritanceTooltip";
5 | import { displayTxResult } from "./utilsDisplay";
6 | import { Abi, AbiFunction } from "abitype";
7 | import { Address } from "viem";
8 | import { useReadContract } from "wagmi";
9 | import { ArrowPathIcon } from "@heroicons/react/24/outline";
10 | import { useAnimationConfig } from "~~/hooks/scaffold-eth";
11 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
12 | import { getParsedError, notification } from "~~/utils/scaffold-eth";
13 |
14 | type DisplayVariableProps = {
15 | contractAddress: Address;
16 | abiFunction: AbiFunction;
17 | refreshDisplayVariables: boolean;
18 | inheritedFrom?: string;
19 | abi: Abi;
20 | };
21 |
22 | export const DisplayVariable = ({
23 | contractAddress,
24 | abiFunction,
25 | refreshDisplayVariables,
26 | abi,
27 | inheritedFrom,
28 | }: DisplayVariableProps) => {
29 | const { targetNetwork } = useTargetNetwork();
30 |
31 | const {
32 | data: result,
33 | isFetching,
34 | refetch,
35 | error,
36 | } = useReadContract({
37 | address: contractAddress,
38 | functionName: abiFunction.name,
39 | abi: abi,
40 | chainId: targetNetwork.id,
41 | query: {
42 | retry: false,
43 | },
44 | });
45 |
46 | const { showAnimation } = useAnimationConfig(result);
47 |
48 | useEffect(() => {
49 | refetch();
50 | }, [refetch, refreshDisplayVariables]);
51 |
52 | useEffect(() => {
53 | if (error) {
54 | const parsedError = getParsedError(error);
55 | notification.error(parsedError);
56 | }
57 | }, [error]);
58 |
59 | return (
60 |
61 |
62 |
{abiFunction.name}
63 |
await refetch()}>
64 | {isFetching ? (
65 |
66 | ) : (
67 |
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
79 | {displayTxResult(result)}
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/InheritanceTooltip.tsx:
--------------------------------------------------------------------------------
1 | import { InformationCircleIcon } from "@heroicons/react/20/solid";
2 |
3 | export const InheritanceTooltip = ({ inheritedFrom }: { inheritedFrom?: string }) => (
4 | <>
5 | {inheritedFrom && (
6 |
10 |
11 |
12 | )}
13 | >
14 | );
15 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { InheritanceTooltip } from "./InheritanceTooltip";
5 | import { Abi, AbiFunction } from "abitype";
6 | import { Address } from "viem";
7 | import { useReadContract } from "wagmi";
8 | import {
9 | ContractInput,
10 | displayTxResult,
11 | getFunctionInputKey,
12 | getInitialFormState,
13 | getParsedContractFunctionArgs,
14 | transformAbiFunction,
15 | } from "~~/app/debug/_components/contract";
16 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
17 | import { getParsedError, notification } from "~~/utils/scaffold-eth";
18 |
19 | type ReadOnlyFunctionFormProps = {
20 | contractAddress: Address;
21 | abiFunction: AbiFunction;
22 | inheritedFrom?: string;
23 | abi: Abi;
24 | };
25 |
26 | export const ReadOnlyFunctionForm = ({
27 | contractAddress,
28 | abiFunction,
29 | inheritedFrom,
30 | abi,
31 | }: ReadOnlyFunctionFormProps) => {
32 | const [form, setForm] = useState>(() => getInitialFormState(abiFunction));
33 | const [result, setResult] = useState();
34 | const { targetNetwork } = useTargetNetwork();
35 |
36 | const { isFetching, refetch, error } = useReadContract({
37 | address: contractAddress,
38 | functionName: abiFunction.name,
39 | abi: abi,
40 | args: getParsedContractFunctionArgs(form),
41 | chainId: targetNetwork.id,
42 | query: {
43 | enabled: false,
44 | retry: false,
45 | },
46 | });
47 |
48 | useEffect(() => {
49 | if (error) {
50 | const parsedError = getParsedError(error);
51 | notification.error(parsedError);
52 | }
53 | }, [error]);
54 |
55 | const transformedFunction = transformAbiFunction(abiFunction);
56 | const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
57 | const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
58 | return (
59 | {
62 | setResult(undefined);
63 | setForm(updatedFormValue);
64 | }}
65 | form={form}
66 | stateObjectKey={key}
67 | paramType={input}
68 | />
69 | );
70 | });
71 |
72 | return (
73 |
74 |
75 | {abiFunction.name}
76 |
77 |
78 | {inputElements}
79 |
80 |
81 | {result !== null && result !== undefined && (
82 |
83 |
Result:
84 |
{displayTxResult(result, "sm")}
85 |
86 | )}
87 |
88 |
{
91 | const { data } = await refetch();
92 | setResult(data);
93 | }}
94 | disabled={isFetching}
95 | >
96 | {isFetching && }
97 | Read 📡
98 |
99 |
100 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/Tuple.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useEffect, useState } from "react";
2 | import { ContractInput } from "./ContractInput";
3 | import { getFunctionInputKey, getInitialTupleFormState } from "./utilsContract";
4 | import { replacer } from "~~/utils/scaffold-eth/common";
5 | import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
6 |
7 | type TupleProps = {
8 | abiTupleParameter: AbiParameterTuple;
9 | setParentForm: Dispatch>>;
10 | parentStateObjectKey: string;
11 | parentForm: Record | undefined;
12 | };
13 |
14 | export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
15 | const [form, setForm] = useState>(() => getInitialTupleFormState(abiTupleParameter));
16 |
17 | useEffect(() => {
18 | const values = Object.values(form);
19 | const argsStruct: Record = {};
20 | abiTupleParameter.components.forEach((component, componentIndex) => {
21 | argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
22 | });
23 |
24 | setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
25 | // eslint-disable-next-line react-hooks/exhaustive-deps
26 | }, [JSON.stringify(form, replacer)]);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
{abiTupleParameter.internalType}
34 |
35 |
36 | {abiTupleParameter?.components?.map((param, index) => {
37 | const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
38 | return ;
39 | })}
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx:
--------------------------------------------------------------------------------
1 | import { TransactionReceipt } from "viem";
2 | import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
3 | import { ObjectFieldDisplay } from "~~/app/debug/_components/contract";
4 | import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
5 | import { replacer } from "~~/utils/scaffold-eth/common";
6 |
7 | export const TxReceipt = ({ txResult }: { txResult: TransactionReceipt }) => {
8 | const { copyToClipboard: copyTxResultToClipboard, isCopiedToClipboard: isTxResultCopiedToClipboard } =
9 | useCopyToClipboard();
10 |
11 | return (
12 |
13 |
14 | {isTxResultCopiedToClipboard ? (
15 |
19 | ) : (
20 | copyTxResultToClipboard(JSON.stringify(txResult, replacer, 2))}
24 | />
25 | )}
26 |
27 |
28 |
29 |
30 | Transaction Receipt
31 |
32 |
33 |
34 | {Object.entries(txResult).map(([k, v]) => (
35 |
36 | ))}
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./ContractInput";
2 | export * from "./ContractUI";
3 | export * from "./DisplayVariable";
4 | export * from "./ReadOnlyFunctionForm";
5 | export * from "./TxReceipt";
6 | export * from "./utilsContract";
7 | export * from "./utilsDisplay";
8 | export * from "./WriteOnlyFunctionForm";
9 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, useState } from "react";
2 | import { TransactionBase, TransactionReceipt, formatEther, isAddress, isHex } from "viem";
3 | import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
4 | import { Address } from "~~/components/scaffold-eth";
5 | import { replacer } from "~~/utils/scaffold-eth/common";
6 |
7 | type DisplayContent =
8 | | string
9 | | number
10 | | bigint
11 | | Record
12 | | TransactionBase
13 | | TransactionReceipt
14 | | undefined
15 | | unknown;
16 |
17 | type ResultFontSize = "sm" | "base" | "xs" | "lg" | "xl" | "2xl" | "3xl";
18 |
19 | export const displayTxResult = (
20 | displayContent: DisplayContent | DisplayContent[],
21 | fontSize: ResultFontSize = "base",
22 | ): string | ReactElement | number => {
23 | if (displayContent == null) {
24 | return "";
25 | }
26 |
27 | if (typeof displayContent === "bigint") {
28 | return ;
29 | }
30 |
31 | if (typeof displayContent === "string") {
32 | if (isAddress(displayContent)) {
33 | return ;
34 | }
35 |
36 | if (isHex(displayContent)) {
37 | return displayContent; // don't add quotes
38 | }
39 | }
40 |
41 | if (Array.isArray(displayContent)) {
42 | return ;
43 | }
44 |
45 | if (typeof displayContent === "object") {
46 | return ;
47 | }
48 |
49 | return JSON.stringify(displayContent, replacer, 2);
50 | };
51 |
52 | const NumberDisplay = ({ value }: { value: bigint }) => {
53 | const [isEther, setIsEther] = useState(false);
54 |
55 | const asNumber = Number(value);
56 | if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) {
57 | return String(value);
58 | }
59 |
60 | return (
61 |
62 | {isEther ? "Ξ" + formatEther(value) : String(value)}
63 |
67 | setIsEther(!isEther)}>
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export const ObjectFieldDisplay = ({
76 | name,
77 | value,
78 | size,
79 | leftPad = true,
80 | }: {
81 | name: string;
82 | value: DisplayContent;
83 | size: ResultFontSize;
84 | leftPad?: boolean;
85 | }) => {
86 | return (
87 |
88 | {name}:
89 | {displayTxResult(value, size)}
90 |
91 | );
92 | };
93 |
94 | const ArrayDisplay = ({ values, size }: { values: DisplayContent[]; size: ResultFontSize }) => {
95 | return (
96 |
97 | {values.length ? "array" : "[]"}
98 | {values.map((v, i) => (
99 |
100 | ))}
101 |
102 | );
103 | };
104 |
105 | const StructDisplay = ({ struct, size }: { struct: Record; size: ResultFontSize }) => {
106 | return (
107 |
108 | struct
109 | {Object.entries(struct).map(([k, v]) => (
110 |
111 | ))}
112 |
113 | );
114 | };
115 |
--------------------------------------------------------------------------------
/packages/nextjs/app/debug/page.tsx:
--------------------------------------------------------------------------------
1 | import { DebugContracts } from "./_components/DebugContracts";
2 | import type { NextPage } from "next";
3 | import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
4 |
5 | export const metadata = getMetadata({
6 | title: "Debug Contracts",
7 | description: "Debug your deployed 🏗 Scaffold-ETH 2 contracts in an easy way",
8 | });
9 |
10 | const Debug: NextPage = () => {
11 | return (
12 | <>
13 |
14 |
15 |
Debug Contracts
16 |
17 | You can debug & interact with your deployed contracts here.
18 | Check{" "}
19 |
20 | packages / nextjs / app / debug / page.tsx
21 |
{" "}
22 |
23 |
24 | >
25 | );
26 | };
27 |
28 | export default Debug;
29 |
--------------------------------------------------------------------------------
/packages/nextjs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@rainbow-me/rainbowkit/styles.css";
2 | import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
3 | import { ThemeProvider } from "~~/components/ThemeProvider";
4 | import "~~/styles/globals.css";
5 | import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
6 |
7 | export const metadata = getMetadata({
8 | title: "Scaffold-ETH 2 App",
9 | description: "Built with 🏗 Scaffold-ETH 2",
10 | });
11 |
12 | const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
13 | return (
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default ScaffoldEthApp;
25 |
--------------------------------------------------------------------------------
/packages/nextjs/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import type { NextPage } from "next";
5 | import { useAccount } from "wagmi";
6 | import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
7 | import { Address } from "~~/components/scaffold-eth";
8 |
9 | const Home: NextPage = () => {
10 | const { address: connectedAddress } = useAccount();
11 |
12 | return (
13 | <>
14 |
15 |
16 |
17 | Welcome to
18 | Scaffold-ETH 2
19 |
20 |
21 |
Connected Address:
22 |
23 |
24 |
25 | Get started by editing{" "}
26 |
27 | packages/nextjs/app/page.tsx
28 |
29 |
30 |
31 | Edit your smart contract{" "}
32 |
33 | YourContract.sol
34 |
{" "}
35 | in{" "}
36 |
37 | packages/hardhat/contracts
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Tinker with your smart contract using the{" "}
48 |
49 | Debug Contracts
50 | {" "}
51 | tab.
52 |
53 |
54 |
55 |
56 |
57 | Explore your local transactions with the{" "}
58 |
59 | Block Explorer
60 | {" "}
61 | tab.
62 |
63 |
64 |
65 |
66 |
67 | >
68 | );
69 | };
70 |
71 | export default Home;
72 |
--------------------------------------------------------------------------------
/packages/nextjs/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { hardhat } from "viem/chains";
4 | import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
5 | import { HeartIcon } from "@heroicons/react/24/outline";
6 | import { SwitchTheme } from "~~/components/SwitchTheme";
7 | import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo";
8 | import { Faucet } from "~~/components/scaffold-eth";
9 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
10 | import { useGlobalState } from "~~/services/store/store";
11 |
12 | /**
13 | * Site footer
14 | */
15 | export const Footer = () => {
16 | const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
17 | const { targetNetwork } = useTargetNetwork();
18 | const isLocalNetwork = targetNetwork.id === hardhat.id;
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | {nativeCurrencyPrice > 0 && (
26 |
27 |
28 |
29 | {nativeCurrencyPrice.toFixed(2)}
30 |
31 |
32 | )}
33 | {isLocalNetwork && (
34 | <>
35 |
36 |
37 |
38 |
Block Explorer
39 |
40 | >
41 | )}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
·
55 |
69 |
·
70 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/packages/nextjs/components/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useRef } from "react";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 | import { usePathname } from "next/navigation";
7 | import { hardhat } from "viem/chains";
8 | import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
9 | import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
10 | import { useOutsideClick, useTargetNetwork } from "~~/hooks/scaffold-eth";
11 |
12 | type HeaderMenuLink = {
13 | label: string;
14 | href: string;
15 | icon?: React.ReactNode;
16 | };
17 |
18 | export const menuLinks: HeaderMenuLink[] = [
19 | {
20 | label: "Home",
21 | href: "/",
22 | },
23 | {
24 | label: "Debug Contracts",
25 | href: "/debug",
26 | icon: ,
27 | },
28 | ];
29 |
30 | export const HeaderMenuLinks = () => {
31 | const pathname = usePathname();
32 |
33 | return (
34 | <>
35 | {menuLinks.map(({ label, href, icon }) => {
36 | const isActive = pathname === href;
37 | return (
38 |
39 |
46 | {icon}
47 | {label}
48 |
49 |
50 | );
51 | })}
52 | >
53 | );
54 | };
55 |
56 | /**
57 | * Site header
58 | */
59 | export const Header = () => {
60 | const { targetNetwork } = useTargetNetwork();
61 | const isLocalNetwork = targetNetwork.id === hardhat.id;
62 |
63 | const burgerMenuRef = useRef(null);
64 | useOutsideClick(burgerMenuRef, () => {
65 | burgerMenuRef?.current?.removeAttribute("open");
66 | });
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 | {
78 | burgerMenuRef?.current?.removeAttribute("open");
79 | }}
80 | >
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Scaffold-ETH
90 | Ethereum dev stack
91 |
92 |
93 |
96 |
97 |
98 |
99 | {isLocalNetwork && }
100 |
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6 | import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
7 | import { useTheme } from "next-themes";
8 | import { Toaster } from "react-hot-toast";
9 | import { WagmiProvider } from "wagmi";
10 | import { Footer } from "~~/components/Footer";
11 | import { Header } from "~~/components/Header";
12 | import { BlockieAvatar } from "~~/components/scaffold-eth";
13 | import { useInitializeNativeCurrencyPrice } from "~~/hooks/scaffold-eth";
14 | import { wagmiConfig } from "~~/services/web3/wagmiConfig";
15 |
16 | const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
17 | useInitializeNativeCurrencyPrice();
18 |
19 | return (
20 | <>
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | >
28 | );
29 | };
30 |
31 | export const queryClient = new QueryClient({
32 | defaultOptions: {
33 | queries: {
34 | refetchOnWindowFocus: false,
35 | },
36 | },
37 | });
38 |
39 | export const ScaffoldEthAppWithProviders = ({ children }: { children: React.ReactNode }) => {
40 | const { resolvedTheme } = useTheme();
41 | const isDarkMode = resolvedTheme === "dark";
42 | const [mounted, setMounted] = useState(false);
43 |
44 | useEffect(() => {
45 | setMounted(true);
46 | }, []);
47 |
48 | return (
49 |
50 |
51 |
52 |
56 | {children}
57 |
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/packages/nextjs/components/SwitchTheme.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useTheme } from "next-themes";
5 | import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
6 |
7 | export const SwitchTheme = ({ className }: { className?: string }) => {
8 | const { setTheme, resolvedTheme } = useTheme();
9 | const [mounted, setMounted] = useState(false);
10 |
11 | const isDarkMode = resolvedTheme === "dark";
12 |
13 | const handleToggle = () => {
14 | if (isDarkMode) {
15 | setTheme("light");
16 | return;
17 | }
18 | setTheme("dark");
19 | };
20 |
21 | useEffect(() => {
22 | setMounted(true);
23 | }, []);
24 |
25 | if (!mounted) return null;
26 |
27 | return (
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/nextjs/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
8 | return {children} ;
9 | };
10 |
--------------------------------------------------------------------------------
/packages/nextjs/components/assets/BuidlGuidlLogo.tsx:
--------------------------------------------------------------------------------
1 | export const BuidlGuidlLogo = ({ className }: { className: string }) => {
2 | return (
3 |
11 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Address/AddressCopyIcon.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
2 | import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
3 |
4 | export const AddressCopyIcon = ({ className, address }: { className?: string; address: string }) => {
5 | const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
6 | useCopyToClipboard();
7 |
8 | return (
9 | {
11 | e.stopPropagation();
12 | copyAddressToClipboard(address);
13 | }}
14 | type="button"
15 | >
16 | {isAddressCopiedToClipboard ? (
17 |
18 | ) : (
19 |
20 | )}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Address/AddressLinkWrapper.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { hardhat } from "viem/chains";
3 | import { useTargetNetwork } from "~~/hooks/scaffold-eth";
4 |
5 | type AddressLinkWrapperProps = {
6 | children: React.ReactNode;
7 | disableAddressLink?: boolean;
8 | blockExplorerAddressLink: string;
9 | };
10 |
11 | export const AddressLinkWrapper = ({
12 | children,
13 | disableAddressLink,
14 | blockExplorerAddressLink,
15 | }: AddressLinkWrapperProps) => {
16 | const { targetNetwork } = useTargetNetwork();
17 |
18 | return disableAddressLink ? (
19 | <>{children}>
20 | ) : (
21 |
26 | {children}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Balance.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Address, formatEther } from "viem";
4 | import { useDisplayUsdMode } from "~~/hooks/scaffold-eth/useDisplayUsdMode";
5 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
6 | import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
7 | import { useGlobalState } from "~~/services/store/store";
8 |
9 | type BalanceProps = {
10 | address?: Address;
11 | className?: string;
12 | usdMode?: boolean;
13 | };
14 |
15 | /**
16 | * Display (ETH & USD) balance of an ETH address.
17 | */
18 | export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
19 | const { targetNetwork } = useTargetNetwork();
20 | const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
21 | const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching);
22 |
23 | const {
24 | data: balance,
25 | isError,
26 | isLoading,
27 | } = useWatchBalance({
28 | address,
29 | });
30 |
31 | const { displayUsdMode, toggleDisplayUsdMode } = useDisplayUsdMode({ defaultUsdMode: usdMode });
32 |
33 | if (!address || isLoading || balance === null || (isNativeCurrencyPriceFetching && nativeCurrencyPrice === 0)) {
34 | return (
35 |
41 | );
42 | }
43 |
44 | if (isError) {
45 | return (
46 |
49 | );
50 | }
51 |
52 | const formattedBalance = balance ? Number(formatEther(balance.value)) : 0;
53 |
54 | return (
55 |
60 |
61 | {displayUsdMode ? (
62 | <>
63 | $
64 | {(formattedBalance * nativeCurrencyPrice).toFixed(2)}
65 | >
66 | ) : (
67 | <>
68 | {formattedBalance.toFixed(4)}
69 | {targetNetwork.nativeCurrency.symbol}
70 | >
71 | )}
72 |
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AvatarComponent } from "@rainbow-me/rainbowkit";
4 | import { blo } from "blo";
5 |
6 | // Custom Avatar for RainbowKit
7 | export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
8 | // Don't want to use nextJS Image here (and adding remote patterns for the URL)
9 | // eslint-disable-next-line @next/next/no-img-element
10 |
17 | );
18 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/FaucetButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { createWalletClient, http, parseEther } from "viem";
5 | import { hardhat } from "viem/chains";
6 | import { useAccount } from "wagmi";
7 | import { BanknotesIcon } from "@heroicons/react/24/outline";
8 | import { useTransactor } from "~~/hooks/scaffold-eth";
9 | import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
10 |
11 | // Number of ETH faucet sends to an address
12 | const NUM_OF_ETH = "1";
13 | const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
14 |
15 | const localWalletClient = createWalletClient({
16 | chain: hardhat,
17 | transport: http(),
18 | });
19 |
20 | /**
21 | * FaucetButton button which lets you grab eth.
22 | */
23 | export const FaucetButton = () => {
24 | const { address, chain: ConnectedChain } = useAccount();
25 |
26 | const { data: balance } = useWatchBalance({ address });
27 |
28 | const [loading, setLoading] = useState(false);
29 |
30 | const faucetTxn = useTransactor(localWalletClient);
31 |
32 | const sendETH = async () => {
33 | if (!address) return;
34 | try {
35 | setLoading(true);
36 | await faucetTxn({
37 | account: FAUCET_ADDRESS,
38 | to: address,
39 | value: parseEther(NUM_OF_ETH),
40 | });
41 | setLoading(false);
42 | } catch (error) {
43 | console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error);
44 | setLoading(false);
45 | }
46 | };
47 |
48 | // Render only on local chain
49 | if (ConnectedChain?.id !== hardhat.id) {
50 | return null;
51 | }
52 |
53 | const isBalanceZero = balance && balance.value === 0n;
54 |
55 | return (
56 |
64 |
65 | {!loading ? (
66 |
67 | ) : (
68 |
69 | )}
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { blo } from "blo";
3 | import { useDebounceValue } from "usehooks-ts";
4 | import { Address, isAddress } from "viem";
5 | import { normalize } from "viem/ens";
6 | import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi";
7 | import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
8 |
9 | /**
10 | * Address input with ENS name resolution
11 | */
12 | export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps) => {
13 | // Debounce the input to keep clean RPC calls when resolving ENS names
14 | // If the input is an address, we don't need to debounce it
15 | const [_debouncedValue] = useDebounceValue(value, 500);
16 | const debouncedValue = isAddress(value) ? value : _debouncedValue;
17 | const isDebouncedValueLive = debouncedValue === value;
18 |
19 | // If the user changes the input after an ENS name is already resolved, we want to remove the stale result
20 | const settledValue = isDebouncedValueLive ? debouncedValue : undefined;
21 |
22 | const {
23 | data: ensAddress,
24 | isLoading: isEnsAddressLoading,
25 | isError: isEnsAddressError,
26 | isSuccess: isEnsAddressSuccess,
27 | } = useEnsAddress({
28 | name: settledValue,
29 | chainId: 1,
30 | query: {
31 | gcTime: 30_000,
32 | enabled: isDebouncedValueLive && isENS(debouncedValue),
33 | },
34 | });
35 |
36 | const [enteredEnsName, setEnteredEnsName] = useState();
37 | const {
38 | data: ensName,
39 | isLoading: isEnsNameLoading,
40 | isError: isEnsNameError,
41 | isSuccess: isEnsNameSuccess,
42 | } = useEnsName({
43 | address: settledValue as Address,
44 | chainId: 1,
45 | query: {
46 | enabled: isAddress(debouncedValue),
47 | gcTime: 30_000,
48 | },
49 | });
50 |
51 | const { data: ensAvatar, isLoading: isEnsAvatarLoading } = useEnsAvatar({
52 | name: ensName ? normalize(ensName) : undefined,
53 | chainId: 1,
54 | query: {
55 | enabled: Boolean(ensName),
56 | gcTime: 30_000,
57 | },
58 | });
59 |
60 | // ens => address
61 | useEffect(() => {
62 | if (!ensAddress) return;
63 |
64 | // ENS resolved successfully
65 | setEnteredEnsName(debouncedValue);
66 | onChange(ensAddress);
67 | }, [ensAddress, onChange, debouncedValue]);
68 |
69 | useEffect(() => {
70 | setEnteredEnsName(undefined);
71 | }, [value]);
72 |
73 | const reFocus =
74 | isEnsAddressError ||
75 | isEnsNameError ||
76 | isEnsNameSuccess ||
77 | isEnsAddressSuccess ||
78 | ensName === null ||
79 | ensAddress === null;
80 |
81 | return (
82 |
83 | name={name}
84 | placeholder={placeholder}
85 | error={ensAddress === null}
86 | value={value as Address}
87 | onChange={onChange}
88 | disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
89 | reFocus={reFocus}
90 | prefix={
91 | ensName ? (
92 |
93 | {isEnsAvatarLoading &&
}
94 | {ensAvatar ? (
95 |
96 | {
97 | // eslint-disable-next-line
98 |
99 | }
100 |
101 | ) : null}
102 |
{enteredEnsName ?? ensName}
103 |
104 | ) : (
105 | (isEnsNameLoading || isEnsAddressLoading) && (
106 |
110 | )
111 | )
112 | }
113 | suffix={
114 | // Don't want to use nextJS Image here (and adding remote patterns for the URL)
115 | // eslint-disable-next-line @next/next/no-img-element
116 | value &&
117 | }
118 | />
119 | );
120 | };
121 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { hexToString, isHex, stringToHex } from "viem";
3 | import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
4 |
5 | export const Bytes32Input = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
6 | const convertStringToBytes32 = useCallback(() => {
7 | if (!value) {
8 | return;
9 | }
10 | onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 }));
11 | }, [onChange, value]);
12 |
13 | return (
14 |
26 | #
27 |
28 | }
29 | />
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { bytesToString, isHex, toBytes, toHex } from "viem";
3 | import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
4 |
5 | export const BytesInput = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
6 | const convertStringToBytes = useCallback(() => {
7 | onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value)));
8 | }, [onChange, value]);
9 |
10 | return (
11 |
23 | #
24 |
25 | }
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, FocusEvent, ReactNode, useCallback, useEffect, useRef } from "react";
2 | import { CommonInputProps } from "~~/components/scaffold-eth";
3 |
4 | type InputBaseProps = CommonInputProps & {
5 | error?: boolean;
6 | prefix?: ReactNode;
7 | suffix?: ReactNode;
8 | reFocus?: boolean;
9 | };
10 |
11 | export const InputBase = string } | undefined = string>({
12 | name,
13 | value,
14 | onChange,
15 | placeholder,
16 | error,
17 | disabled,
18 | prefix,
19 | suffix,
20 | reFocus,
21 | }: InputBaseProps) => {
22 | const inputReft = useRef(null);
23 |
24 | let modifier = "";
25 | if (error) {
26 | modifier = "border-error";
27 | } else if (disabled) {
28 | modifier = "border-disabled bg-base-300";
29 | }
30 |
31 | const handleChange = useCallback(
32 | (e: ChangeEvent) => {
33 | onChange(e.target.value as unknown as T);
34 | },
35 | [onChange],
36 | );
37 |
38 | // Runs only when reFocus prop is passed, useful for setting the cursor
39 | // at the end of the input. Example AddressInput
40 | const onFocus = (e: FocusEvent) => {
41 | if (reFocus !== undefined) {
42 | e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length);
43 | }
44 | };
45 | useEffect(() => {
46 | if (reFocus !== undefined && reFocus === true) inputReft.current?.focus();
47 | }, [reFocus]);
48 |
49 | return (
50 |
51 | {prefix}
52 |
63 | {suffix}
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { parseEther } from "viem";
3 | import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth";
4 |
5 | type IntegerInputProps = CommonInputProps & {
6 | variant?: IntegerVariant;
7 | disableMultiplyBy1e18?: boolean;
8 | };
9 |
10 | export const IntegerInput = ({
11 | value,
12 | onChange,
13 | name,
14 | placeholder,
15 | disabled,
16 | variant = IntegerVariant.UINT256,
17 | disableMultiplyBy1e18 = false,
18 | }: IntegerInputProps) => {
19 | const [inputError, setInputError] = useState(false);
20 | const multiplyBy1e18 = useCallback(() => {
21 | if (!value) {
22 | return;
23 | }
24 | return onChange(parseEther(value).toString());
25 | }, [onChange, value]);
26 |
27 | useEffect(() => {
28 | if (isValidInteger(variant, value)) {
29 | setInputError(false);
30 | } else {
31 | setInputError(true);
32 | }
33 | }, [value, variant]);
34 |
35 | return (
36 |
50 |
56 | ∗
57 |
58 |
59 | )
60 | }
61 | />
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/index.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export * from "./AddressInput";
4 | export * from "./Bytes32Input";
5 | export * from "./BytesInput";
6 | export * from "./EtherInput";
7 | export * from "./InputBase";
8 | export * from "./IntegerInput";
9 | export * from "./utils";
10 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/Input/utils.ts:
--------------------------------------------------------------------------------
1 | export type CommonInputProps = {
2 | value: T;
3 | onChange: (newValue: T) => void;
4 | name?: string;
5 | placeholder?: string;
6 | disabled?: boolean;
7 | };
8 |
9 | export enum IntegerVariant {
10 | UINT8 = "uint8",
11 | UINT16 = "uint16",
12 | UINT24 = "uint24",
13 | UINT32 = "uint32",
14 | UINT40 = "uint40",
15 | UINT48 = "uint48",
16 | UINT56 = "uint56",
17 | UINT64 = "uint64",
18 | UINT72 = "uint72",
19 | UINT80 = "uint80",
20 | UINT88 = "uint88",
21 | UINT96 = "uint96",
22 | UINT104 = "uint104",
23 | UINT112 = "uint112",
24 | UINT120 = "uint120",
25 | UINT128 = "uint128",
26 | UINT136 = "uint136",
27 | UINT144 = "uint144",
28 | UINT152 = "uint152",
29 | UINT160 = "uint160",
30 | UINT168 = "uint168",
31 | UINT176 = "uint176",
32 | UINT184 = "uint184",
33 | UINT192 = "uint192",
34 | UINT200 = "uint200",
35 | UINT208 = "uint208",
36 | UINT216 = "uint216",
37 | UINT224 = "uint224",
38 | UINT232 = "uint232",
39 | UINT240 = "uint240",
40 | UINT248 = "uint248",
41 | UINT256 = "uint256",
42 | INT8 = "int8",
43 | INT16 = "int16",
44 | INT24 = "int24",
45 | INT32 = "int32",
46 | INT40 = "int40",
47 | INT48 = "int48",
48 | INT56 = "int56",
49 | INT64 = "int64",
50 | INT72 = "int72",
51 | INT80 = "int80",
52 | INT88 = "int88",
53 | INT96 = "int96",
54 | INT104 = "int104",
55 | INT112 = "int112",
56 | INT120 = "int120",
57 | INT128 = "int128",
58 | INT136 = "int136",
59 | INT144 = "int144",
60 | INT152 = "int152",
61 | INT160 = "int160",
62 | INT168 = "int168",
63 | INT176 = "int176",
64 | INT184 = "int184",
65 | INT192 = "int192",
66 | INT200 = "int200",
67 | INT208 = "int208",
68 | INT216 = "int216",
69 | INT224 = "int224",
70 | INT232 = "int232",
71 | INT240 = "int240",
72 | INT248 = "int248",
73 | INT256 = "int256",
74 | }
75 |
76 | export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/;
77 | export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/;
78 |
79 | export const isValidInteger = (dataType: IntegerVariant, value: string) => {
80 | const isSigned = dataType.startsWith("i");
81 | const bitcount = Number(dataType.substring(isSigned ? 3 : 4));
82 |
83 | let valueAsBigInt;
84 | try {
85 | valueAsBigInt = BigInt(value);
86 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
87 | } catch (e) {}
88 | if (typeof valueAsBigInt !== "bigint") {
89 | if (!value || typeof value !== "string") {
90 | return true;
91 | }
92 | return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value);
93 | } else if (!isSigned && valueAsBigInt < 0) {
94 | return false;
95 | }
96 | const hexString = valueAsBigInt.toString(16);
97 | const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? "";
98 | if (
99 | significantHexDigits.length * 4 > bitcount ||
100 | (isSigned && significantHexDigits.length * 4 === bitcount && parseInt(significantHexDigits.slice(-1)?.[0], 16) < 8)
101 | ) {
102 | return false;
103 | }
104 | return true;
105 | };
106 |
107 | // Treat any dot-separated string as a potential ENS name
108 | const ensRegex = /.+\..+/;
109 | export const isENS = (address = "") => ensRegex.test(address);
110 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressQRCodeModal.tsx:
--------------------------------------------------------------------------------
1 | import { QRCodeSVG } from "qrcode.react";
2 | import { Address as AddressType } from "viem";
3 | import { Address } from "~~/components/scaffold-eth";
4 |
5 | type AddressQRCodeModalProps = {
6 | address: AddressType;
7 | modalId: string;
8 | };
9 |
10 | export const AddressQRCodeModal = ({ address, modalId }: AddressQRCodeModalProps) => {
11 | return (
12 | <>
13 |
31 | >
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/NetworkOptions.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { useAccount, useSwitchChain } from "wagmi";
3 | import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
4 | import { getNetworkColor } from "~~/hooks/scaffold-eth";
5 | import { getTargetNetworks } from "~~/utils/scaffold-eth";
6 |
7 | const allowedNetworks = getTargetNetworks();
8 |
9 | type NetworkOptionsProps = {
10 | hidden?: boolean;
11 | };
12 |
13 | export const NetworkOptions = ({ hidden = false }: NetworkOptionsProps) => {
14 | const { switchChain } = useSwitchChain();
15 | const { chain } = useAccount();
16 | const { resolvedTheme } = useTheme();
17 | const isDarkMode = resolvedTheme === "dark";
18 |
19 | return (
20 | <>
21 | {allowedNetworks
22 | .filter(allowedNetwork => allowedNetwork.id !== chain?.id)
23 | .map(allowedNetwork => (
24 |
25 | {
29 | switchChain?.({ chainId: allowedNetwork.id });
30 | }}
31 | >
32 |
33 |
34 | Switch to{" "}
35 |
40 | {allowedNetwork.name}
41 |
42 |
43 |
44 |
45 | ))}
46 | >
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/WrongNetworkDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { NetworkOptions } from "./NetworkOptions";
2 | import { useDisconnect } from "wagmi";
3 | import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
4 |
5 | export const WrongNetworkDropdown = () => {
6 | const { disconnect } = useDisconnect();
7 |
8 | return (
9 |
10 |
11 | Wrong network
12 |
13 |
14 |
18 |
19 |
20 | disconnect()}
24 | >
25 |
26 | Disconnect
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // @refresh reset
4 | import { Balance } from "../Balance";
5 | import { AddressInfoDropdown } from "./AddressInfoDropdown";
6 | import { AddressQRCodeModal } from "./AddressQRCodeModal";
7 | import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
8 | import { ConnectButton } from "@rainbow-me/rainbowkit";
9 | import { Address } from "viem";
10 | import { useNetworkColor } from "~~/hooks/scaffold-eth";
11 | import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
12 | import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
13 |
14 | /**
15 | * Custom Wagmi Connect Button (watch balance + custom design)
16 | */
17 | export const RainbowKitCustomConnectButton = () => {
18 | const networkColor = useNetworkColor();
19 | const { targetNetwork } = useTargetNetwork();
20 |
21 | return (
22 |
23 | {({ account, chain, openConnectModal, mounted }) => {
24 | const connected = mounted && account && chain;
25 | const blockExplorerAddressLink = account
26 | ? getBlockExplorerAddressLink(targetNetwork, account.address)
27 | : undefined;
28 |
29 | return (
30 | <>
31 | {(() => {
32 | if (!connected) {
33 | return (
34 |
35 | Connect Wallet
36 |
37 | );
38 | }
39 |
40 | if (chain.unsupported || chain.id !== targetNetwork.id) {
41 | return ;
42 | }
43 |
44 | return (
45 | <>
46 |
47 |
48 |
49 | {chain.name}
50 |
51 |
52 |
58 |
59 | >
60 | );
61 | })()}
62 | >
63 | );
64 | }}
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/packages/nextjs/components/scaffold-eth/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Address/Address";
2 | export * from "./Balance";
3 | export * from "./BlockieAvatar";
4 | export * from "./Faucet";
5 | export * from "./FaucetButton";
6 | export * from "./Input";
7 | export * from "./RainbowKitCustomConnectButton";
8 |
--------------------------------------------------------------------------------
/packages/nextjs/contracts/deployedContracts.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is autogenerated by Scaffold-ETH.
3 | * You should not edit it manually or your changes might be overwritten.
4 | */
5 | import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
6 |
7 | const deployedContracts = {} as const;
8 |
9 | export default deployedContracts satisfies GenericContractsDeclaration;
10 |
--------------------------------------------------------------------------------
/packages/nextjs/contracts/externalContracts.ts:
--------------------------------------------------------------------------------
1 | import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
2 |
3 | /**
4 | * @example
5 | * const externalContracts = {
6 | * 1: {
7 | * DAI: {
8 | * address: "0x...",
9 | * abi: [...],
10 | * },
11 | * },
12 | * } as const;
13 | */
14 | const externalContracts = {} as const;
15 |
16 | export default externalContracts satisfies GenericContractsDeclaration;
17 |
--------------------------------------------------------------------------------
/packages/nextjs/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { FlatCompat } from "@eslint/eslintrc";
2 | import prettierPlugin from "eslint-plugin-prettier";
3 | import { defineConfig } from "eslint/config";
4 | import path from "node:path";
5 | import { fileURLToPath } from "node:url";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | });
12 |
13 | export default defineConfig([
14 | {
15 | plugins: {
16 | prettier: prettierPlugin,
17 | },
18 | extends: compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
19 |
20 | rules: {
21 | "@typescript-eslint/no-explicit-any": "off",
22 | "@typescript-eslint/ban-ts-comment": "off",
23 |
24 | "prettier/prettier": [
25 | "warn",
26 | {
27 | endOfLine: "auto",
28 | },
29 | ],
30 | },
31 | },
32 | ]);
33 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useAnimationConfig";
2 | export * from "./useContractLogs";
3 | export * from "./useCopyToClipboard";
4 | export * from "./useDeployedContractInfo";
5 | export * from "./useFetchBlocks";
6 | export * from "./useInitializeNativeCurrencyPrice";
7 | export * from "./useNetworkColor";
8 | export * from "./useOutsideClick";
9 | export * from "./useScaffoldContract";
10 | export * from "./useScaffoldEventHistory";
11 | export * from "./useScaffoldReadContract";
12 | export * from "./useScaffoldWatchContractEvent";
13 | export * from "./useScaffoldWriteContract";
14 | export * from "./useTargetNetwork";
15 | export * from "./useTransactor";
16 | export * from "./useWatchBalance";
17 | export * from "./useSelectedNetwork";
18 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const ANIMATION_TIME = 2000;
4 |
5 | export function useAnimationConfig(data: any) {
6 | const [showAnimation, setShowAnimation] = useState(false);
7 | const [prevData, setPrevData] = useState();
8 |
9 | useEffect(() => {
10 | if (prevData !== undefined && prevData !== data) {
11 | setShowAnimation(true);
12 | setTimeout(() => setShowAnimation(false), ANIMATION_TIME);
13 | }
14 | setPrevData(data);
15 | }, [data, prevData]);
16 |
17 | return {
18 | showAnimation,
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useTargetNetwork } from "./useTargetNetwork";
3 | import { Address, Log } from "viem";
4 | import { usePublicClient } from "wagmi";
5 |
6 | export const useContractLogs = (address: Address) => {
7 | const [logs, setLogs] = useState([]);
8 | const { targetNetwork } = useTargetNetwork();
9 | const client = usePublicClient({ chainId: targetNetwork.id });
10 |
11 | useEffect(() => {
12 | const fetchLogs = async () => {
13 | if (!client) return console.error("Client not found");
14 | try {
15 | const existingLogs = await client.getLogs({
16 | address: address,
17 | fromBlock: 0n,
18 | toBlock: "latest",
19 | });
20 | setLogs(existingLogs);
21 | } catch (error) {
22 | console.error("Failed to fetch logs:", error);
23 | }
24 | };
25 | fetchLogs();
26 |
27 | return client?.watchBlockNumber({
28 | onBlockNumber: async (_blockNumber, prevBlockNumber) => {
29 | const newLogs = await client.getLogs({
30 | address: address,
31 | fromBlock: prevBlockNumber,
32 | toBlock: "latest",
33 | });
34 | setLogs(prevLogs => [...prevLogs, ...newLogs]);
35 | },
36 | });
37 | }, [address, client]);
38 |
39 | return logs;
40 | };
41 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export const useCopyToClipboard = () => {
4 | const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
5 |
6 | const copyToClipboard = async (text: string) => {
7 | try {
8 | await navigator.clipboard.writeText(text);
9 | setIsCopiedToClipboard(true);
10 | setTimeout(() => {
11 | setIsCopiedToClipboard(false);
12 | }, 800);
13 | } catch (err) {
14 | console.error("Failed to copy text:", err);
15 | }
16 | };
17 |
18 | return { copyToClipboard, isCopiedToClipboard };
19 | };
20 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useIsMounted } from "usehooks-ts";
3 | import { usePublicClient } from "wagmi";
4 | import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
5 | import {
6 | Contract,
7 | ContractCodeStatus,
8 | ContractName,
9 | UseDeployedContractConfig,
10 | contracts,
11 | } from "~~/utils/scaffold-eth/contract";
12 |
13 | type DeployedContractData = {
14 | data: Contract | undefined;
15 | isLoading: boolean;
16 | };
17 |
18 | /**
19 | * Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts
20 | * and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
21 | */
22 | export function useDeployedContractInfo(
23 | config: UseDeployedContractConfig,
24 | ): DeployedContractData;
25 | /**
26 | * @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
27 | */
28 | export function useDeployedContractInfo(
29 | contractName: TContractName,
30 | ): DeployedContractData;
31 |
32 | export function useDeployedContractInfo(
33 | configOrName: UseDeployedContractConfig | TContractName,
34 | ): DeployedContractData {
35 | const isMounted = useIsMounted();
36 |
37 | const finalConfig: UseDeployedContractConfig =
38 | typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);
39 |
40 | useEffect(() => {
41 | if (typeof configOrName === "string") {
42 | console.warn(
43 | "Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.",
44 | );
45 | }
46 | }, [configOrName]);
47 | const { contractName, chainId } = finalConfig;
48 | const selectedNetwork = useSelectedNetwork(chainId);
49 | const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract;
50 | const [status, setStatus] = useState(ContractCodeStatus.LOADING);
51 | const publicClient = usePublicClient({ chainId: selectedNetwork.id });
52 |
53 | useEffect(() => {
54 | const checkContractDeployment = async () => {
55 | try {
56 | if (!isMounted() || !publicClient) return;
57 |
58 | if (!deployedContract) {
59 | setStatus(ContractCodeStatus.NOT_FOUND);
60 | return;
61 | }
62 |
63 | const code = await publicClient.getBytecode({
64 | address: deployedContract.address,
65 | });
66 |
67 | // If contract code is `0x` => no contract deployed on that address
68 | if (code === "0x") {
69 | setStatus(ContractCodeStatus.NOT_FOUND);
70 | return;
71 | }
72 | setStatus(ContractCodeStatus.DEPLOYED);
73 | } catch (e) {
74 | console.error(e);
75 | setStatus(ContractCodeStatus.NOT_FOUND);
76 | }
77 | };
78 |
79 | checkContractDeployment();
80 | }, [isMounted, contractName, deployedContract, publicClient]);
81 |
82 | return {
83 | data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
84 | isLoading: status === ContractCodeStatus.LOADING,
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { useGlobalState } from "~~/services/store/store";
3 |
4 | export const useDisplayUsdMode = ({ defaultUsdMode = false }: { defaultUsdMode?: boolean }) => {
5 | const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
6 | const isPriceFetched = nativeCurrencyPrice > 0;
7 | const predefinedUsdMode = isPriceFetched ? Boolean(defaultUsdMode) : false;
8 | const [displayUsdMode, setDisplayUsdMode] = useState(predefinedUsdMode);
9 |
10 | useEffect(() => {
11 | setDisplayUsdMode(predefinedUsdMode);
12 | }, [predefinedUsdMode]);
13 |
14 | const toggleDisplayUsdMode = useCallback(() => {
15 | if (isPriceFetched) {
16 | setDisplayUsdMode(!displayUsdMode);
17 | }
18 | }, [displayUsdMode, isPriceFetched]);
19 |
20 | return { displayUsdMode, toggleDisplayUsdMode };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import {
3 | Block,
4 | Hash,
5 | Transaction,
6 | TransactionReceipt,
7 | createTestClient,
8 | publicActions,
9 | walletActions,
10 | webSocket,
11 | } from "viem";
12 | import { hardhat } from "viem/chains";
13 | import { decodeTransactionData } from "~~/utils/scaffold-eth";
14 |
15 | const BLOCKS_PER_PAGE = 20;
16 |
17 | export const testClient = createTestClient({
18 | chain: hardhat,
19 | mode: "hardhat",
20 | transport: webSocket("ws://127.0.0.1:8545"),
21 | })
22 | .extend(publicActions)
23 | .extend(walletActions);
24 |
25 | export const useFetchBlocks = () => {
26 | const [blocks, setBlocks] = useState([]);
27 | const [transactionReceipts, setTransactionReceipts] = useState<{
28 | [key: string]: TransactionReceipt;
29 | }>({});
30 | const [currentPage, setCurrentPage] = useState(0);
31 | const [totalBlocks, setTotalBlocks] = useState(0n);
32 | const [error, setError] = useState(null);
33 |
34 | const fetchBlocks = useCallback(async () => {
35 | setError(null);
36 |
37 | try {
38 | const blockNumber = await testClient.getBlockNumber();
39 | setTotalBlocks(blockNumber);
40 |
41 | const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE);
42 | const blockNumbersToFetch = Array.from(
43 | { length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) },
44 | (_, i) => startingBlock - BigInt(i),
45 | );
46 |
47 | const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => {
48 | try {
49 | return testClient.getBlock({ blockNumber, includeTransactions: true });
50 | } catch (err) {
51 | setError(err instanceof Error ? err : new Error("An error occurred."));
52 | throw err;
53 | }
54 | });
55 | const fetchedBlocks = await Promise.all(blocksWithTransactions);
56 |
57 | fetchedBlocks.forEach(block => {
58 | block.transactions.forEach(tx => decodeTransactionData(tx as Transaction));
59 | });
60 |
61 | const txReceipts = await Promise.all(
62 | fetchedBlocks.flatMap(block =>
63 | block.transactions.map(async tx => {
64 | try {
65 | const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
66 | return { [(tx as Transaction).hash]: receipt };
67 | } catch (err) {
68 | setError(err instanceof Error ? err : new Error("An error occurred."));
69 | throw err;
70 | }
71 | }),
72 | ),
73 | );
74 |
75 | setBlocks(fetchedBlocks);
76 | setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) }));
77 | } catch (err) {
78 | setError(err instanceof Error ? err : new Error("An error occurred."));
79 | }
80 | }, [currentPage]);
81 |
82 | useEffect(() => {
83 | fetchBlocks();
84 | }, [fetchBlocks]);
85 |
86 | useEffect(() => {
87 | const handleNewBlock = async (newBlock: any) => {
88 | try {
89 | if (currentPage === 0) {
90 | if (newBlock.transactions.length > 0) {
91 | const transactionsDetails = await Promise.all(
92 | newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })),
93 | );
94 | newBlock.transactions = transactionsDetails;
95 | }
96 |
97 | newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction));
98 |
99 | const receipts = await Promise.all(
100 | newBlock.transactions.map(async (tx: Transaction) => {
101 | try {
102 | const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
103 | return { [(tx as Transaction).hash]: receipt };
104 | } catch (err) {
105 | setError(err instanceof Error ? err : new Error("An error occurred fetching receipt."));
106 | throw err;
107 | }
108 | }),
109 | );
110 |
111 | setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]);
112 | setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) }));
113 | }
114 | if (newBlock.number) {
115 | setTotalBlocks(newBlock.number);
116 | }
117 | } catch (err) {
118 | setError(err instanceof Error ? err : new Error("An error occurred."));
119 | }
120 | };
121 |
122 | return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true });
123 | }, [currentPage]);
124 |
125 | return {
126 | blocks,
127 | transactionReceipts,
128 | currentPage,
129 | totalBlocks,
130 | setCurrentPage,
131 | error,
132 | };
133 | };
134 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useInitializeNativeCurrencyPrice.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from "react";
2 | import { useTargetNetwork } from "./useTargetNetwork";
3 | import { useInterval } from "usehooks-ts";
4 | import scaffoldConfig from "~~/scaffold.config";
5 | import { useGlobalState } from "~~/services/store/store";
6 | import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth";
7 |
8 | const enablePolling = false;
9 |
10 | /**
11 | * Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK
12 | */
13 | export const useInitializeNativeCurrencyPrice = () => {
14 | const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);
15 | const setIsNativeCurrencyFetching = useGlobalState(state => state.setIsNativeCurrencyFetching);
16 | const { targetNetwork } = useTargetNetwork();
17 |
18 | const fetchPrice = useCallback(async () => {
19 | setIsNativeCurrencyFetching(true);
20 | const price = await fetchPriceFromUniswap(targetNetwork);
21 | setNativeCurrencyPrice(price);
22 | setIsNativeCurrencyFetching(false);
23 | }, [setIsNativeCurrencyFetching, setNativeCurrencyPrice, targetNetwork]);
24 |
25 | // Get the price of ETH from Uniswap on mount
26 | useEffect(() => {
27 | fetchPrice();
28 | }, [fetchPrice]);
29 |
30 | // Get the price of ETH from Uniswap at a given interval
31 | useInterval(fetchPrice, enablePolling ? scaffoldConfig.pollingInterval : null);
32 | };
33 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
3 | import { AllowedChainIds, ChainWithAttributes } from "~~/utils/scaffold-eth";
4 |
5 | export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"];
6 |
7 | export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolean) {
8 | const colorConfig = network.color ?? DEFAULT_NETWORK_COLOR;
9 | return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig;
10 | }
11 |
12 | /**
13 | * Gets the color of the target network
14 | */
15 | export const useNetworkColor = (chainId?: AllowedChainIds) => {
16 | const { resolvedTheme } = useTheme();
17 |
18 | const chain = useSelectedNetwork(chainId);
19 | const isDarkMode = resolvedTheme === "dark";
20 |
21 | return getNetworkColor(chain, isDarkMode);
22 | };
23 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | /**
4 | * Handles clicks outside of passed ref element
5 | * @param ref - react ref of the element
6 | * @param callback - callback function to call when clicked outside
7 | */
8 | export const useOutsideClick = (ref: React.RefObject, callback: { (): void }) => {
9 | useEffect(() => {
10 | function handleOutsideClick(event: MouseEvent) {
11 | if (!(event.target instanceof Element)) {
12 | return;
13 | }
14 |
15 | if (ref.current && !ref.current.contains(event.target)) {
16 | callback();
17 | }
18 | }
19 |
20 | document.addEventListener("click", handleOutsideClick);
21 | return () => document.removeEventListener("click", handleOutsideClick);
22 | }, [ref, callback]);
23 | };
24 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts:
--------------------------------------------------------------------------------
1 | import { Account, Address, Chain, Client, Transport, getContract } from "viem";
2 | import { usePublicClient } from "wagmi";
3 | import { GetWalletClientReturnType } from "wagmi/actions";
4 | import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
5 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
6 | import { AllowedChainIds } from "~~/utils/scaffold-eth";
7 | import { Contract, ContractName } from "~~/utils/scaffold-eth/contract";
8 |
9 | /**
10 | * Gets a viem instance of the contract present in deployedContracts.ts or externalContracts.ts corresponding to
11 | * targetNetworks configured in scaffold.config.ts. Optional walletClient can be passed for doing write transactions.
12 | * @param config - The config settings for the hook
13 | * @param config.contractName - deployed contract name
14 | * @param config.walletClient - optional walletClient from wagmi useWalletClient hook can be passed for doing write transactions
15 | * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
16 | */
17 | export const useScaffoldContract = <
18 | TContractName extends ContractName,
19 | TWalletClient extends Exclude | undefined,
20 | >({
21 | contractName,
22 | walletClient,
23 | chainId,
24 | }: {
25 | contractName: TContractName;
26 | walletClient?: TWalletClient | null;
27 | chainId?: AllowedChainIds;
28 | }) => {
29 | const selectedNetwork = useSelectedNetwork(chainId);
30 | const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({
31 | contractName,
32 | chainId: selectedNetwork?.id as AllowedChainIds,
33 | });
34 |
35 | const publicClient = usePublicClient({ chainId: selectedNetwork?.id });
36 |
37 | let contract = undefined;
38 | if (deployedContractData && publicClient) {
39 | contract = getContract<
40 | Transport,
41 | Address,
42 | Contract["abi"],
43 | TWalletClient extends Exclude
44 | ? {
45 | public: Client;
46 | wallet: TWalletClient;
47 | }
48 | : { public: Client },
49 | Chain,
50 | Account
51 | >({
52 | address: deployedContractData.address,
53 | abi: deployedContractData.abi as Contract["abi"],
54 | client: {
55 | public: publicClient,
56 | wallet: walletClient ? walletClient : undefined,
57 | } as any,
58 | });
59 | }
60 |
61 | return {
62 | data: contract,
63 | isLoading: deployedContractLoading,
64 | };
65 | };
66 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { QueryObserverResult, RefetchOptions, useQueryClient } from "@tanstack/react-query";
3 | import type { ExtractAbiFunctionNames } from "abitype";
4 | import { ReadContractErrorType } from "viem";
5 | import { useBlockNumber, useReadContract } from "wagmi";
6 | import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
7 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
8 | import { AllowedChainIds } from "~~/utils/scaffold-eth";
9 | import {
10 | AbiFunctionReturnType,
11 | ContractAbi,
12 | ContractName,
13 | UseScaffoldReadConfig,
14 | } from "~~/utils/scaffold-eth/contract";
15 |
16 | /**
17 | * Wrapper around wagmi's useContractRead hook which automatically loads (by name) the contract ABI and address from
18 | * the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
19 | * @param config - The config settings, including extra wagmi configuration
20 | * @param config.contractName - deployed contract name
21 | * @param config.functionName - name of the function to be called
22 | * @param config.args - args to be passed to the function call
23 | * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
24 | */
25 | export const useScaffoldReadContract = <
26 | TContractName extends ContractName,
27 | TFunctionName extends ExtractAbiFunctionNames, "pure" | "view">,
28 | >({
29 | contractName,
30 | functionName,
31 | args,
32 | chainId,
33 | ...readConfig
34 | }: UseScaffoldReadConfig) => {
35 | const selectedNetwork = useSelectedNetwork(chainId);
36 | const { data: deployedContract } = useDeployedContractInfo({
37 | contractName,
38 | chainId: selectedNetwork.id as AllowedChainIds,
39 | });
40 |
41 | const { query: queryOptions, watch, ...readContractConfig } = readConfig;
42 | // set watch to true by default
43 | const defaultWatch = watch ?? true;
44 |
45 | const readContractHookRes = useReadContract({
46 | chainId: selectedNetwork.id,
47 | functionName,
48 | address: deployedContract?.address,
49 | abi: deployedContract?.abi,
50 | args,
51 | ...(readContractConfig as any),
52 | query: {
53 | enabled: !Array.isArray(args) || !args.some(arg => arg === undefined),
54 | ...queryOptions,
55 | },
56 | }) as Omit, "data" | "refetch"> & {
57 | data: AbiFunctionReturnType | undefined;
58 | refetch: (
59 | options?: RefetchOptions | undefined,
60 | ) => Promise, ReadContractErrorType>>;
61 | };
62 |
63 | const queryClient = useQueryClient();
64 | const { data: blockNumber } = useBlockNumber({
65 | watch: defaultWatch,
66 | chainId: selectedNetwork.id,
67 | query: {
68 | enabled: defaultWatch,
69 | },
70 | });
71 |
72 | useEffect(() => {
73 | if (defaultWatch) {
74 | queryClient.invalidateQueries({ queryKey: readContractHookRes.queryKey });
75 | }
76 | // eslint-disable-next-line react-hooks/exhaustive-deps
77 | }, [blockNumber]);
78 |
79 | return readContractHookRes;
80 | };
81 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useScaffoldWatchContractEvent.ts:
--------------------------------------------------------------------------------
1 | import { Abi, ExtractAbiEventNames } from "abitype";
2 | import { Log } from "viem";
3 | import { useWatchContractEvent } from "wagmi";
4 | import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
5 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
6 | import { AllowedChainIds } from "~~/utils/scaffold-eth";
7 | import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract";
8 |
9 | /**
10 | * Wrapper around wagmi's useEventSubscriber hook which automatically loads (by name) the contract ABI and
11 | * address from the contracts present in deployedContracts.ts & externalContracts.ts
12 | * @param config - The config settings
13 | * @param config.contractName - deployed contract name
14 | * @param config.eventName - name of the event to listen for
15 | * @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
16 | * @param config.onLogs - the callback that receives events.
17 | */
18 | export const useScaffoldWatchContractEvent = <
19 | TContractName extends ContractName,
20 | TEventName extends ExtractAbiEventNames>,
21 | >({
22 | contractName,
23 | eventName,
24 | chainId,
25 | onLogs,
26 | }: UseScaffoldEventConfig) => {
27 | const selectedNetwork = useSelectedNetwork(chainId);
28 | const { data: deployedContractData } = useDeployedContractInfo({
29 | contractName,
30 | chainId: selectedNetwork.id as AllowedChainIds,
31 | });
32 |
33 | return useWatchContractEvent({
34 | address: deployedContractData?.address,
35 | abi: deployedContractData?.abi as Abi,
36 | chainId: selectedNetwork.id,
37 | onLogs: (logs: Log[]) => onLogs(logs as Parameters[0]),
38 | eventName,
39 | });
40 | };
41 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts:
--------------------------------------------------------------------------------
1 | import scaffoldConfig from "~~/scaffold.config";
2 | import { useGlobalState } from "~~/services/store/store";
3 | import { AllowedChainIds } from "~~/utils/scaffold-eth";
4 | import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth/networks";
5 |
6 | /**
7 | * Given a chainId, retrives the network object from `scaffold.config`,
8 | * if not found default to network set by `useTargetNetwork` hook
9 | */
10 | export function useSelectedNetwork(chainId?: AllowedChainIds): ChainWithAttributes {
11 | const globalTargetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
12 | const targetNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chainId);
13 |
14 | if (targetNetwork) {
15 | return { ...targetNetwork, ...NETWORKS_EXTRA_DATA[targetNetwork.id] };
16 | }
17 |
18 | return globalTargetNetwork;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from "react";
2 | import { useAccount } from "wagmi";
3 | import scaffoldConfig from "~~/scaffold.config";
4 | import { useGlobalState } from "~~/services/store/store";
5 | import { ChainWithAttributes } from "~~/utils/scaffold-eth";
6 | import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
7 |
8 | /**
9 | * Retrieves the connected wallet's network from scaffold.config or defaults to the 0th network in the list if the wallet is not connected.
10 | */
11 | export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } {
12 | const { chain } = useAccount();
13 | const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
14 | const setTargetNetwork = useGlobalState(({ setTargetNetwork }) => setTargetNetwork);
15 |
16 | useEffect(() => {
17 | const newSelectedNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chain?.id);
18 | if (newSelectedNetwork && newSelectedNetwork.id !== targetNetwork.id) {
19 | setTargetNetwork({ ...newSelectedNetwork, ...NETWORKS_EXTRA_DATA[newSelectedNetwork.id] });
20 | }
21 | }, [chain?.id, setTargetNetwork, targetNetwork.id]);
22 |
23 | return useMemo(() => ({ targetNetwork }), [targetNetwork]);
24 | }
25 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx:
--------------------------------------------------------------------------------
1 | import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem";
2 | import { Config, useWalletClient } from "wagmi";
3 | import { getPublicClient } from "wagmi/actions";
4 | import { SendTransactionMutate } from "wagmi/query";
5 | import { wagmiConfig } from "~~/services/web3/wagmiConfig";
6 | import { getBlockExplorerTxLink, getParsedError, notification } from "~~/utils/scaffold-eth";
7 | import { TransactorFuncOptions } from "~~/utils/scaffold-eth/contract";
8 |
9 | type TransactionFunc = (
10 | tx: (() => Promise) | Parameters>[0],
11 | options?: TransactorFuncOptions,
12 | ) => Promise;
13 |
14 | /**
15 | * Custom notification content for TXs.
16 | */
17 | const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => {
18 | return (
19 |
27 | );
28 | };
29 |
30 | /**
31 | * Runs Transaction passed in to returned function showing UI feedback.
32 | * @param _walletClient - Optional wallet client to use. If not provided, will use the one from useWalletClient.
33 | * @returns function that takes in transaction function as callback, shows UI feedback for transaction and returns a promise of the transaction hash
34 | */
35 | export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => {
36 | let walletClient = _walletClient;
37 | const { data } = useWalletClient();
38 | if (walletClient === undefined && data) {
39 | walletClient = data;
40 | }
41 |
42 | const result: TransactionFunc = async (tx, options) => {
43 | if (!walletClient) {
44 | notification.error("Cannot access account");
45 | console.error("⚡️ ~ file: useTransactor.tsx ~ error");
46 | return;
47 | }
48 |
49 | let notificationId = null;
50 | let transactionHash: Hash | undefined = undefined;
51 | let transactionReceipt: TransactionReceipt | undefined;
52 | let blockExplorerTxURL = "";
53 | try {
54 | const network = await walletClient.getChainId();
55 | // Get full transaction from public client
56 | const publicClient = getPublicClient(wagmiConfig);
57 |
58 | notificationId = notification.loading( );
59 | if (typeof tx === "function") {
60 | // Tx is already prepared by the caller
61 | const result = await tx();
62 | transactionHash = result;
63 | } else if (tx != null) {
64 | transactionHash = await walletClient.sendTransaction(tx as SendTransactionParameters);
65 | } else {
66 | throw new Error("Incorrect transaction passed to transactor");
67 | }
68 | notification.remove(notificationId);
69 |
70 | blockExplorerTxURL = network ? getBlockExplorerTxLink(network, transactionHash) : "";
71 |
72 | notificationId = notification.loading(
73 | ,
74 | );
75 |
76 | transactionReceipt = await publicClient.waitForTransactionReceipt({
77 | hash: transactionHash,
78 | confirmations: options?.blockConfirmations,
79 | });
80 | notification.remove(notificationId);
81 |
82 | if (transactionReceipt.status === "reverted") throw new Error("Transaction reverted");
83 |
84 | notification.success(
85 | ,
86 | {
87 | icon: "🎉",
88 | },
89 | );
90 |
91 | if (options?.onBlockConfirmation) options.onBlockConfirmation(transactionReceipt);
92 | } catch (error: any) {
93 | if (notificationId) {
94 | notification.remove(notificationId);
95 | }
96 | console.error("⚡️ ~ file: useTransactor.ts ~ error", error);
97 | const message = getParsedError(error);
98 |
99 | // if receipt was reverted, show notification with block explorer link and return error
100 | if (transactionReceipt?.status === "reverted") {
101 | notification.error( );
102 | throw error;
103 | }
104 |
105 | notification.error(message);
106 | throw error;
107 | }
108 |
109 | return transactionHash;
110 | };
111 |
112 | return result;
113 | };
114 |
--------------------------------------------------------------------------------
/packages/nextjs/hooks/scaffold-eth/useWatchBalance.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useTargetNetwork } from "./useTargetNetwork";
3 | import { useQueryClient } from "@tanstack/react-query";
4 | import { UseBalanceParameters, useBalance, useBlockNumber } from "wagmi";
5 |
6 | /**
7 | * Wrapper around wagmi's useBalance hook. Updates data on every block change.
8 | */
9 | export const useWatchBalance = (useBalanceParameters: UseBalanceParameters) => {
10 | const { targetNetwork } = useTargetNetwork();
11 | const queryClient = useQueryClient();
12 | const { data: blockNumber } = useBlockNumber({ watch: true, chainId: targetNetwork.id });
13 | const { queryKey, ...restUseBalanceReturn } = useBalance(useBalanceParameters);
14 |
15 | useEffect(() => {
16 | queryClient.invalidateQueries({ queryKey });
17 | // eslint-disable-next-line react-hooks/exhaustive-deps
18 | }, [blockNumber]);
19 |
20 | return restUseBalanceReturn;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/nextjs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/packages/nextjs/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | reactStrictMode: true,
5 | devIndicators: false,
6 | typescript: {
7 | ignoreBuildErrors: process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true",
8 | },
9 | eslint: {
10 | ignoreDuringBuilds: process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true",
11 | },
12 | webpack: config => {
13 | config.resolve.fallback = { fs: false, net: false, tls: false };
14 | config.externals.push("pino-pretty", "lokijs", "encoding");
15 | return config;
16 | },
17 | };
18 |
19 | const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
20 |
21 | if (isIpfs) {
22 | nextConfig.output = "export";
23 | nextConfig.trailingSlash = true;
24 | nextConfig.images = {
25 | unoptimized: true,
26 | };
27 | }
28 |
29 | module.exports = nextConfig;
30 |
--------------------------------------------------------------------------------
/packages/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@se-2/nextjs",
3 | "private": true,
4 | "version": "0.1.0",
5 | "scripts": {
6 | "build": "next build",
7 | "check-types": "tsc --noEmit --incremental",
8 | "dev": "next dev",
9 | "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'",
10 | "lint": "next lint",
11 | "serve": "next start",
12 | "start": "next dev",
13 | "vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env VERCEL_TELEMETRY_DISABLED=1",
14 | "vercel:yolo": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --build-env VERCEL_TELEMETRY_DISABLED=1",
15 | "ipfs": "NEXT_PUBLIC_IPFS_BUILD=true yarn build && yarn bgipfs upload config init -u https://upload.bgipfs.com && CID=$(yarn bgipfs upload out | grep -o 'CID: [^ ]*' | cut -d' ' -f2) && [ ! -z \"$CID\" ] && echo '🚀 Upload complete! Your site is now available at: https://community.bgipfs.com/ipfs/'$CID || echo '❌ Upload failed'",
16 | "vercel:login": "vercel login"
17 | },
18 | "dependencies": {
19 | "@heroicons/react": "^2.1.5",
20 | "@rainbow-me/rainbowkit": "2.2.5",
21 | "@tanstack/react-query": "^5.59.15",
22 | "@uniswap/sdk-core": "^5.8.2",
23 | "@uniswap/v2-sdk": "^4.6.1",
24 | "blo": "^1.2.0",
25 | "burner-connector": "0.0.14",
26 | "daisyui": "^5.0.9",
27 | "kubo-rpc-client": "^5.0.2",
28 | "next": "^15.2.3",
29 | "next-nprogress-bar": "^2.3.13",
30 | "next-themes": "^0.3.0",
31 | "qrcode.react": "^4.0.1",
32 | "react": "^19.0.0",
33 | "react-dom": "^19.0.0",
34 | "react-hot-toast": "^2.4.0",
35 | "usehooks-ts": "^3.1.0",
36 | "viem": "2.30.0",
37 | "wagmi": "2.15.4",
38 | "zustand": "^5.0.0"
39 | },
40 | "devDependencies": {
41 | "@tailwindcss/postcss": "latest",
42 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
43 | "@types/node": "^18.19.50",
44 | "@types/react": "^19.0.7",
45 | "abitype": "1.0.6",
46 | "bgipfs": "^0.0.12",
47 | "eslint": "^9.23.0",
48 | "eslint-config-next": "^15.2.3",
49 | "eslint-config-prettier": "^10.1.1",
50 | "eslint-plugin-prettier": "^5.2.4",
51 | "postcss": "^8.4.45",
52 | "prettier": "^3.5.3",
53 | "tailwindcss": "^4.1.3",
54 | "type-fest": "^4.26.1",
55 | "typescript": "^5.8.2",
56 | "vercel": "^39.1.3"
57 | },
58 | "packageManager": "yarn@3.2.3"
59 | }
60 |
--------------------------------------------------------------------------------
/packages/nextjs/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/packages/nextjs/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-2/ba6cf8c52609bbb717dc9f928cd32897d11c2a68/packages/nextjs/public/favicon.png
--------------------------------------------------------------------------------
/packages/nextjs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/nextjs/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Scaffold-ETH 2 DApp",
3 | "description": "A DApp built with Scaffold-ETH",
4 | "iconPath": "logo.svg"
5 | }
6 |
--------------------------------------------------------------------------------
/packages/nextjs/public/thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-2/ba6cf8c52609bbb717dc9f928cd32897d11c2a68/packages/nextjs/public/thumbnail.jpg
--------------------------------------------------------------------------------
/packages/nextjs/scaffold.config.ts:
--------------------------------------------------------------------------------
1 | import * as chains from "viem/chains";
2 |
3 | export type ScaffoldConfig = {
4 | targetNetworks: readonly chains.Chain[];
5 | pollingInterval: number;
6 | alchemyApiKey: string;
7 | rpcOverrides?: Record;
8 | walletConnectProjectId: string;
9 | onlyLocalBurnerWallet: boolean;
10 | };
11 |
12 | export const DEFAULT_ALCHEMY_API_KEY = "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
13 |
14 | const scaffoldConfig = {
15 | // The networks on which your DApp is live
16 | targetNetworks: [chains.hardhat],
17 |
18 | // The interval at which your front-end polls the RPC servers for new data
19 | // it has no effect if you only target the local network (default is 4000)
20 | pollingInterval: 30000,
21 |
22 | // This is ours Alchemy's default API key.
23 | // You can get your own at https://dashboard.alchemyapi.io
24 | // It's recommended to store it in an env variable:
25 | // .env.local for local testing, and in the Vercel/system env config for live apps.
26 | alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || DEFAULT_ALCHEMY_API_KEY,
27 |
28 | // If you want to use a different RPC for a specific network, you can add it here.
29 | // The key is the chain ID, and the value is the HTTP RPC URL
30 | rpcOverrides: {
31 | // Example:
32 | // [chains.mainnet.id]: "https://mainnet.buidlguidl.com",
33 | },
34 |
35 | // This is ours WalletConnect's default project ID.
36 | // You can get your own at https://cloud.walletconnect.com
37 | // It's recommended to store it in an env variable:
38 | // .env.local for local testing, and in the Vercel/system env config for live apps.
39 | walletConnectProjectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || "3a8170812b534d0ff9d794f19a901d64",
40 |
41 | // Only show the Burner Wallet when running on hardhat network
42 | onlyLocalBurnerWallet: true,
43 | } as const satisfies ScaffoldConfig;
44 |
45 | export default scaffoldConfig;
46 |
--------------------------------------------------------------------------------
/packages/nextjs/services/store/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import scaffoldConfig from "~~/scaffold.config";
3 | import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
4 |
5 | /**
6 | * Zustand Store
7 | *
8 | * You can add global state to the app using this useGlobalState, to get & set
9 | * values from anywhere in the app.
10 | *
11 | * Think about it as a global useState.
12 | */
13 |
14 | type GlobalState = {
15 | nativeCurrency: {
16 | price: number;
17 | isFetching: boolean;
18 | };
19 | setNativeCurrencyPrice: (newNativeCurrencyPriceState: number) => void;
20 | setIsNativeCurrencyFetching: (newIsNativeCurrencyFetching: boolean) => void;
21 | targetNetwork: ChainWithAttributes;
22 | setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => void;
23 | };
24 |
25 | export const useGlobalState = create(set => ({
26 | nativeCurrency: {
27 | price: 0,
28 | isFetching: true,
29 | },
30 | setNativeCurrencyPrice: (newValue: number): void =>
31 | set(state => ({ nativeCurrency: { ...state.nativeCurrency, price: newValue } })),
32 | setIsNativeCurrencyFetching: (newValue: boolean): void =>
33 | set(state => ({ nativeCurrency: { ...state.nativeCurrency, isFetching: newValue } })),
34 | targetNetwork: {
35 | ...scaffoldConfig.targetNetworks[0],
36 | ...NETWORKS_EXTRA_DATA[scaffoldConfig.targetNetworks[0].id],
37 | },
38 | setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => set(() => ({ targetNetwork: newTargetNetwork })),
39 | }));
40 |
--------------------------------------------------------------------------------
/packages/nextjs/services/web3/wagmiConfig.tsx:
--------------------------------------------------------------------------------
1 | import { wagmiConnectors } from "./wagmiConnectors";
2 | import { Chain, createClient, fallback, http } from "viem";
3 | import { hardhat, mainnet } from "viem/chains";
4 | import { createConfig } from "wagmi";
5 | import scaffoldConfig, { DEFAULT_ALCHEMY_API_KEY, ScaffoldConfig } from "~~/scaffold.config";
6 | import { getAlchemyHttpUrl } from "~~/utils/scaffold-eth";
7 |
8 | const { targetNetworks } = scaffoldConfig;
9 |
10 | // We always want to have mainnet enabled (ENS resolution, ETH price, etc). But only once.
11 | export const enabledChains = targetNetworks.find((network: Chain) => network.id === 1)
12 | ? targetNetworks
13 | : ([...targetNetworks, mainnet] as const);
14 |
15 | export const wagmiConfig = createConfig({
16 | chains: enabledChains,
17 | connectors: wagmiConnectors,
18 | ssr: true,
19 | client({ chain }) {
20 | let rpcFallbacks = [http()];
21 |
22 | const rpcOverrideUrl = (scaffoldConfig.rpcOverrides as ScaffoldConfig["rpcOverrides"])?.[chain.id];
23 | if (rpcOverrideUrl) {
24 | rpcFallbacks = [http(rpcOverrideUrl), http()];
25 | } else {
26 | const alchemyHttpUrl = getAlchemyHttpUrl(chain.id);
27 | if (alchemyHttpUrl) {
28 | const isUsingDefaultKey = scaffoldConfig.alchemyApiKey === DEFAULT_ALCHEMY_API_KEY;
29 | // If using default Scaffold-ETH 2 API key, we prioritize the default RPC
30 | rpcFallbacks = isUsingDefaultKey ? [http(), http(alchemyHttpUrl)] : [http(alchemyHttpUrl), http()];
31 | }
32 | }
33 |
34 | return createClient({
35 | chain,
36 | transport: fallback(rpcFallbacks),
37 | ...(chain.id !== (hardhat as Chain).id
38 | ? {
39 | pollingInterval: scaffoldConfig.pollingInterval,
40 | }
41 | : {}),
42 | });
43 | },
44 | });
45 |
--------------------------------------------------------------------------------
/packages/nextjs/services/web3/wagmiConnectors.tsx:
--------------------------------------------------------------------------------
1 | import { connectorsForWallets } from "@rainbow-me/rainbowkit";
2 | import {
3 | coinbaseWallet,
4 | ledgerWallet,
5 | metaMaskWallet,
6 | rainbowWallet,
7 | safeWallet,
8 | walletConnectWallet,
9 | } from "@rainbow-me/rainbowkit/wallets";
10 | import { rainbowkitBurnerWallet } from "burner-connector";
11 | import * as chains from "viem/chains";
12 | import scaffoldConfig from "~~/scaffold.config";
13 |
14 | const { onlyLocalBurnerWallet, targetNetworks } = scaffoldConfig;
15 |
16 | const wallets = [
17 | metaMaskWallet,
18 | walletConnectWallet,
19 | ledgerWallet,
20 | coinbaseWallet,
21 | rainbowWallet,
22 | safeWallet,
23 | ...(!targetNetworks.some(network => network.id !== (chains.hardhat as chains.Chain).id) || !onlyLocalBurnerWallet
24 | ? [rainbowkitBurnerWallet]
25 | : []),
26 | ];
27 |
28 | /**
29 | * wagmi connectors for the wagmi context
30 | */
31 | export const wagmiConnectors = connectorsForWallets(
32 | [
33 | {
34 | groupName: "Supported Wallets",
35 | wallets,
36 | },
37 | ],
38 |
39 | {
40 | appName: "scaffold-eth-2",
41 | projectId: scaffoldConfig.walletConnectProjectId,
42 | },
43 | );
44 |
--------------------------------------------------------------------------------
/packages/nextjs/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
4 |
5 | @theme {
6 | --shadow-center: 0 0 12px -2px rgb(0 0 0 / 0.05);
7 | --animate-pulse-fast: pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
8 | }
9 |
10 | @plugin "daisyui" {
11 | themes:
12 | light,
13 | dark --prefersdark;
14 | }
15 |
16 | @plugin "daisyui/theme" {
17 | name: "light";
18 |
19 | --color-primary: #93bbfb;
20 | --color-primary-content: #212638;
21 | --color-secondary: #dae8ff;
22 | --color-secondary-content: #212638;
23 | --color-accent: #93bbfb;
24 | --color-accent-content: #212638;
25 | --color-neutral: #212638;
26 | --color-neutral-content: #ffffff;
27 | --color-base-100: #ffffff;
28 | --color-base-200: #f4f8ff;
29 | --color-base-300: #dae8ff;
30 | --color-base-content: #212638;
31 | --color-info: #93bbfb;
32 | --color-success: #34eeb6;
33 | --color-warning: #ffcf72;
34 | --color-error: #ff8863;
35 |
36 | --radius-field: 9999rem;
37 | --radius-box: 1rem;
38 | --tt-tailw: 6px;
39 | }
40 |
41 | @plugin "daisyui/theme" {
42 | name: "dark";
43 |
44 | --color-primary: #212638;
45 | --color-primary-content: #f9fbff;
46 | --color-secondary: #323f61;
47 | --color-secondary-content: #f9fbff;
48 | --color-accent: #4969a6;
49 | --color-accent-content: #f9fbff;
50 | --color-neutral: #f9fbff;
51 | --color-neutral-content: #385183;
52 | --color-base-100: #385183;
53 | --color-base-200: #2a3655;
54 | --color-base-300: #212638;
55 | --color-base-content: #f9fbff;
56 | --color-info: #385183;
57 | --color-success: #34eeb6;
58 | --color-warning: #ffcf72;
59 | --color-error: #ff8863;
60 |
61 | --radius-field: 9999rem;
62 | --radius-box: 1rem;
63 |
64 | --tt-tailw: 6px;
65 | --tt-bg: var(--color-primary);
66 | }
67 |
68 | /*
69 | The default border color has changed to `currentColor` in Tailwind CSS v4,
70 | so we've added these compatibility styles to make sure everything still
71 | looks the same as it did with Tailwind CSS v3.
72 |
73 | If we ever want to remove these styles, we need to add an explicit border
74 | color utility to any element that depends on these defaults.
75 | */
76 | @layer base {
77 | *,
78 | ::after,
79 | ::before,
80 | ::backdrop,
81 | ::file-selector-button {
82 | border-color: var(--color-gray-200, currentColor);
83 | }
84 |
85 | p {
86 | margin: 1rem 0;
87 | }
88 |
89 | body {
90 | min-height: 100vh;
91 | }
92 |
93 | h1,
94 | h2,
95 | h3,
96 | h4 {
97 | margin-bottom: 0.5rem;
98 | line-height: 1;
99 | }
100 | }
101 |
102 | :root,
103 | [data-theme] {
104 | background: var(--color-base-200);
105 | }
106 |
107 | .btn {
108 | @apply shadow-md;
109 | }
110 |
111 | .btn.btn-ghost {
112 | @apply shadow-none;
113 | }
114 |
115 | .link {
116 | text-underline-offset: 2px;
117 | }
118 |
119 | .link:hover {
120 | opacity: 80%;
121 | }
122 |
--------------------------------------------------------------------------------
/packages/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "Bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "~~/*": ["./*"]
19 | },
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ]
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/nextjs/types/abitype/abi.d.ts:
--------------------------------------------------------------------------------
1 | import "abitype";
2 | import "~~/node_modules/viem/node_modules/abitype";
3 |
4 | type AddressType = string;
5 |
6 | declare module "abitype" {
7 | export interface Register {
8 | AddressType: AddressType;
9 | }
10 | }
11 |
12 | declare module "~~/node_modules/viem/node_modules/abitype" {
13 | export interface Register {
14 | AddressType: AddressType;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/block.ts:
--------------------------------------------------------------------------------
1 | import { Block, Transaction, TransactionReceipt } from "viem";
2 |
3 | export type TransactionWithFunction = Transaction & {
4 | functionName?: string;
5 | functionArgs?: any[];
6 | functionArgNames?: string[];
7 | functionArgTypes?: string[];
8 | };
9 |
10 | type TransactionReceipts = {
11 | [key: string]: TransactionReceipt;
12 | };
13 |
14 | export type TransactionsTableProps = {
15 | blocks: Block[];
16 | transactionReceipts: TransactionReceipts;
17 | };
18 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/common.ts:
--------------------------------------------------------------------------------
1 | // To be used in JSON.stringify when a field might be bigint
2 |
3 | // https://wagmi.sh/react/faq#bigint-serialization
4 | export const replacer = (_key: string, value: unknown) => (typeof value === "bigint" ? value.toString() : value);
5 |
6 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
7 |
8 | export const isZeroAddress = (address: string) => address === ZERO_ADDRESS;
9 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/contractsData.ts:
--------------------------------------------------------------------------------
1 | import { useTargetNetwork } from "~~/hooks/scaffold-eth";
2 | import { GenericContractsDeclaration, contracts } from "~~/utils/scaffold-eth/contract";
3 |
4 | const DEFAULT_ALL_CONTRACTS: GenericContractsDeclaration[number] = {};
5 |
6 | export function useAllContracts() {
7 | const { targetNetwork } = useTargetNetwork();
8 | const contractsData = contracts?.[targetNetwork.id];
9 | // using constant to avoid creating a new object on every call
10 | return contractsData || DEFAULT_ALL_CONTRACTS;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/decodeTxData.ts:
--------------------------------------------------------------------------------
1 | import { TransactionWithFunction } from "./block";
2 | import { GenericContractsDeclaration } from "./contract";
3 | import { Abi, AbiFunction, decodeFunctionData, getAbiItem } from "viem";
4 | import { hardhat } from "viem/chains";
5 | import contractData from "~~/contracts/deployedContracts";
6 |
7 | type ContractsInterfaces = Record;
8 | type TransactionType = TransactionWithFunction | null;
9 |
10 | const deployedContracts = contractData as GenericContractsDeclaration | null;
11 | const chainMetaData = deployedContracts?.[hardhat.id];
12 | const interfaces = chainMetaData
13 | ? Object.entries(chainMetaData).reduce((finalInterfacesObj, [contractName, contract]) => {
14 | finalInterfacesObj[contractName] = contract.abi;
15 | return finalInterfacesObj;
16 | }, {} as ContractsInterfaces)
17 | : {};
18 |
19 | export const decodeTransactionData = (tx: TransactionWithFunction) => {
20 | if (tx.input.length >= 10 && !tx.input.startsWith("0x60e06040")) {
21 | let foundInterface = false;
22 | for (const [, contractAbi] of Object.entries(interfaces)) {
23 | try {
24 | const { functionName, args } = decodeFunctionData({
25 | abi: contractAbi,
26 | data: tx.input,
27 | });
28 | tx.functionName = functionName;
29 | tx.functionArgs = args as any[];
30 | tx.functionArgNames = getAbiItem({
31 | abi: contractAbi as AbiFunction[],
32 | name: functionName,
33 | })?.inputs?.map((input: any) => input.name);
34 | tx.functionArgTypes = getAbiItem({
35 | abi: contractAbi as AbiFunction[],
36 | name: functionName,
37 | })?.inputs.map((input: any) => input.type);
38 | foundInterface = true;
39 | break;
40 | } catch {
41 | // do nothing
42 | }
43 | }
44 | if (!foundInterface) {
45 | tx.functionName = "⚠️ Unknown";
46 | }
47 | }
48 | return tx;
49 | };
50 |
51 | export const getFunctionDetails = (transaction: TransactionType) => {
52 | if (
53 | transaction &&
54 | transaction.functionName &&
55 | transaction.functionArgNames &&
56 | transaction.functionArgTypes &&
57 | transaction.functionArgs
58 | ) {
59 | const details = transaction.functionArgNames.map(
60 | (name, i) => `${transaction.functionArgTypes?.[i] || ""} ${name} = ${transaction.functionArgs?.[i] ?? ""}`,
61 | );
62 | return `${transaction.functionName}(${details.join(", ")})`;
63 | }
64 | return "";
65 | };
66 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/fetchPriceFromUniswap.ts:
--------------------------------------------------------------------------------
1 | import { ChainWithAttributes, getAlchemyHttpUrl } from "./networks";
2 | import { CurrencyAmount, Token } from "@uniswap/sdk-core";
3 | import { Pair, Route } from "@uniswap/v2-sdk";
4 | import { Address, createPublicClient, fallback, http, parseAbi } from "viem";
5 | import { mainnet } from "viem/chains";
6 |
7 | const alchemyHttpUrl = getAlchemyHttpUrl(mainnet.id);
8 | const rpcFallbacks = alchemyHttpUrl ? [http(alchemyHttpUrl), http()] : [http()];
9 | const publicClient = createPublicClient({
10 | chain: mainnet,
11 | transport: fallback(rpcFallbacks),
12 | });
13 |
14 | const ABI = parseAbi([
15 | "function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
16 | "function token0() external view returns (address)",
17 | "function token1() external view returns (address)",
18 | ]);
19 |
20 | export const fetchPriceFromUniswap = async (targetNetwork: ChainWithAttributes): Promise => {
21 | if (
22 | targetNetwork.nativeCurrency.symbol !== "ETH" &&
23 | targetNetwork.nativeCurrency.symbol !== "SEP" &&
24 | !targetNetwork.nativeCurrencyTokenAddress
25 | ) {
26 | return 0;
27 | }
28 | try {
29 | const DAI = new Token(1, "0x6B175474E89094C44Da98b954EedeAC495271d0F", 18);
30 | const TOKEN = new Token(
31 | 1,
32 | targetNetwork.nativeCurrencyTokenAddress || "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
33 | 18,
34 | );
35 | const pairAddress = Pair.getAddress(TOKEN, DAI) as Address;
36 |
37 | const wagmiConfig = {
38 | address: pairAddress,
39 | abi: ABI,
40 | };
41 |
42 | const reserves = await publicClient.readContract({
43 | ...wagmiConfig,
44 | functionName: "getReserves",
45 | });
46 |
47 | const token0Address = await publicClient.readContract({
48 | ...wagmiConfig,
49 | functionName: "token0",
50 | });
51 |
52 | const token1Address = await publicClient.readContract({
53 | ...wagmiConfig,
54 | functionName: "token1",
55 | });
56 | const token0 = [TOKEN, DAI].find(token => token.address === token0Address) as Token;
57 | const token1 = [TOKEN, DAI].find(token => token.address === token1Address) as Token;
58 | const pair = new Pair(
59 | CurrencyAmount.fromRawAmount(token0, reserves[0].toString()),
60 | CurrencyAmount.fromRawAmount(token1, reserves[1].toString()),
61 | );
62 | const route = new Route([pair], TOKEN, DAI);
63 | const price = parseFloat(route.midPrice.toSignificant(6));
64 | return price;
65 | } catch (error) {
66 | console.error(
67 | `useNativeCurrencyPrice - Error fetching ${targetNetwork.nativeCurrency.symbol} price from Uniswap: `,
68 | error,
69 | );
70 | return 0;
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/getMetadata.ts:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
4 | ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
5 | : `http://localhost:${process.env.PORT || 3000}`;
6 | const titleTemplate = "%s | Scaffold-ETH 2";
7 |
8 | export const getMetadata = ({
9 | title,
10 | description,
11 | imageRelativePath = "/thumbnail.jpg",
12 | }: {
13 | title: string;
14 | description: string;
15 | imageRelativePath?: string;
16 | }): Metadata => {
17 | const imageUrl = `${baseUrl}${imageRelativePath}`;
18 |
19 | return {
20 | metadataBase: new URL(baseUrl),
21 | title: {
22 | default: title,
23 | template: titleTemplate,
24 | },
25 | description: description,
26 | openGraph: {
27 | title: {
28 | default: title,
29 | template: titleTemplate,
30 | },
31 | description: description,
32 | images: [
33 | {
34 | url: imageUrl,
35 | },
36 | ],
37 | },
38 | twitter: {
39 | title: {
40 | default: title,
41 | template: titleTemplate,
42 | },
43 | description: description,
44 | images: [imageUrl],
45 | },
46 | icons: {
47 | icon: [{ url: "/favicon.png", sizes: "32x32", type: "image/png" }],
48 | },
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/getParsedError.ts:
--------------------------------------------------------------------------------
1 | import { BaseError as BaseViemError, ContractFunctionRevertedError } from "viem";
2 |
3 | /**
4 | * Parses an viem/wagmi error to get a displayable string
5 | * @param e - error object
6 | * @returns parsed error string
7 | */
8 | export const getParsedError = (error: any): string => {
9 | const parsedError = error?.walk ? error.walk() : error;
10 |
11 | if (parsedError instanceof BaseViemError) {
12 | if (parsedError.details) {
13 | return parsedError.details;
14 | }
15 |
16 | if (parsedError.shortMessage) {
17 | if (
18 | parsedError instanceof ContractFunctionRevertedError &&
19 | parsedError.data &&
20 | parsedError.data.errorName !== "Error"
21 | ) {
22 | const customErrorArgs = parsedError.data.args?.toString() ?? "";
23 | return `${parsedError.shortMessage.replace(/reverted\.$/, "reverted with the following reason:")}\n${
24 | parsedError.data.errorName
25 | }(${customErrorArgs})`;
26 | }
27 |
28 | return parsedError.shortMessage;
29 | }
30 |
31 | return parsedError.message ?? parsedError.name ?? "An unknown error occurred";
32 | }
33 |
34 | return parsedError?.message ?? "An unknown error occurred";
35 | };
36 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./fetchPriceFromUniswap";
2 | export * from "./networks";
3 | export * from "./notification";
4 | export * from "./block";
5 | export * from "./decodeTxData";
6 | export * from "./getParsedError";
7 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/networks.ts:
--------------------------------------------------------------------------------
1 | import * as chains from "viem/chains";
2 | import scaffoldConfig from "~~/scaffold.config";
3 |
4 | type ChainAttributes = {
5 | // color | [lightThemeColor, darkThemeColor]
6 | color: string | [string, string];
7 | // Used to fetch price by providing mainnet token address
8 | // for networks having native currency other than ETH
9 | nativeCurrencyTokenAddress?: string;
10 | };
11 |
12 | export type ChainWithAttributes = chains.Chain & Partial;
13 | export type AllowedChainIds = (typeof scaffoldConfig.targetNetworks)[number]["id"];
14 |
15 | // Mapping of chainId to RPC chain name an format followed by alchemy and infura
16 | export const RPC_CHAIN_NAMES: Record = {
17 | [chains.mainnet.id]: "eth-mainnet",
18 | [chains.goerli.id]: "eth-goerli",
19 | [chains.sepolia.id]: "eth-sepolia",
20 | [chains.optimism.id]: "opt-mainnet",
21 | [chains.optimismGoerli.id]: "opt-goerli",
22 | [chains.optimismSepolia.id]: "opt-sepolia",
23 | [chains.arbitrum.id]: "arb-mainnet",
24 | [chains.arbitrumGoerli.id]: "arb-goerli",
25 | [chains.arbitrumSepolia.id]: "arb-sepolia",
26 | [chains.polygon.id]: "polygon-mainnet",
27 | [chains.polygonMumbai.id]: "polygon-mumbai",
28 | [chains.polygonAmoy.id]: "polygon-amoy",
29 | [chains.astar.id]: "astar-mainnet",
30 | [chains.polygonZkEvm.id]: "polygonzkevm-mainnet",
31 | [chains.polygonZkEvmTestnet.id]: "polygonzkevm-testnet",
32 | [chains.base.id]: "base-mainnet",
33 | [chains.baseGoerli.id]: "base-goerli",
34 | [chains.baseSepolia.id]: "base-sepolia",
35 | [chains.celo.id]: "celo-mainnet",
36 | [chains.celoAlfajores.id]: "celo-alfajores",
37 | };
38 |
39 | export const getAlchemyHttpUrl = (chainId: number) => {
40 | return scaffoldConfig.alchemyApiKey && RPC_CHAIN_NAMES[chainId]
41 | ? `https://${RPC_CHAIN_NAMES[chainId]}.g.alchemy.com/v2/${scaffoldConfig.alchemyApiKey}`
42 | : undefined;
43 | };
44 |
45 | export const NETWORKS_EXTRA_DATA: Record = {
46 | [chains.hardhat.id]: {
47 | color: "#b8af0c",
48 | },
49 | [chains.mainnet.id]: {
50 | color: "#ff8b9e",
51 | },
52 | [chains.sepolia.id]: {
53 | color: ["#5f4bb6", "#87ff65"],
54 | },
55 | [chains.gnosis.id]: {
56 | color: "#48a9a6",
57 | },
58 | [chains.polygon.id]: {
59 | color: "#2bbdf7",
60 | nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
61 | },
62 | [chains.polygonMumbai.id]: {
63 | color: "#92D9FA",
64 | nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
65 | },
66 | [chains.optimismSepolia.id]: {
67 | color: "#f01a37",
68 | },
69 | [chains.optimism.id]: {
70 | color: "#f01a37",
71 | },
72 | [chains.arbitrumSepolia.id]: {
73 | color: "#28a0f0",
74 | },
75 | [chains.arbitrum.id]: {
76 | color: "#28a0f0",
77 | },
78 | [chains.fantom.id]: {
79 | color: "#1969ff",
80 | },
81 | [chains.fantomTestnet.id]: {
82 | color: "#1969ff",
83 | },
84 | [chains.scrollSepolia.id]: {
85 | color: "#fbebd4",
86 | },
87 | [chains.celo.id]: {
88 | color: "#FCFF52",
89 | },
90 | [chains.celoAlfajores.id]: {
91 | color: "#476520",
92 | },
93 | };
94 |
95 | /**
96 | * Gives the block explorer transaction URL, returns empty string if the network is a local chain
97 | */
98 | export function getBlockExplorerTxLink(chainId: number, txnHash: string) {
99 | const chainNames = Object.keys(chains);
100 |
101 | const targetChainArr = chainNames.filter(chainName => {
102 | const wagmiChain = chains[chainName as keyof typeof chains];
103 | return wagmiChain.id === chainId;
104 | });
105 |
106 | if (targetChainArr.length === 0) {
107 | return "";
108 | }
109 |
110 | const targetChain = targetChainArr[0] as keyof typeof chains;
111 | const blockExplorerTxURL = chains[targetChain]?.blockExplorers?.default?.url;
112 |
113 | if (!blockExplorerTxURL) {
114 | return "";
115 | }
116 |
117 | return `${blockExplorerTxURL}/tx/${txnHash}`;
118 | }
119 |
120 | /**
121 | * Gives the block explorer URL for a given address.
122 | * Defaults to Etherscan if no (wagmi) block explorer is configured for the network.
123 | */
124 | export function getBlockExplorerAddressLink(network: chains.Chain, address: string) {
125 | const blockExplorerBaseURL = network.blockExplorers?.default?.url;
126 | if (network.id === chains.hardhat.id) {
127 | return `/blockexplorer/address/${address}`;
128 | }
129 |
130 | if (!blockExplorerBaseURL) {
131 | return `https://etherscan.io/address/${address}`;
132 | }
133 |
134 | return `${blockExplorerBaseURL}/address/${address}`;
135 | }
136 |
137 | /**
138 | * @returns targetNetworks array containing networks configured in scaffold.config including extra network metadata
139 | */
140 | export function getTargetNetworks(): ChainWithAttributes[] {
141 | return scaffoldConfig.targetNetworks.map(targetNetwork => ({
142 | ...targetNetwork,
143 | ...NETWORKS_EXTRA_DATA[targetNetwork.id],
144 | }));
145 | }
146 |
--------------------------------------------------------------------------------
/packages/nextjs/utils/scaffold-eth/notification.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Toast, ToastPosition, toast } from "react-hot-toast";
3 | import { XMarkIcon } from "@heroicons/react/20/solid";
4 | import {
5 | CheckCircleIcon,
6 | ExclamationCircleIcon,
7 | ExclamationTriangleIcon,
8 | InformationCircleIcon,
9 | } from "@heroicons/react/24/solid";
10 |
11 | type NotificationProps = {
12 | content: React.ReactNode;
13 | status: "success" | "info" | "loading" | "error" | "warning";
14 | duration?: number;
15 | icon?: string;
16 | position?: ToastPosition;
17 | };
18 |
19 | type NotificationOptions = {
20 | duration?: number;
21 | icon?: string;
22 | position?: ToastPosition;
23 | };
24 |
25 | const ENUM_STATUSES = {
26 | success: ,
27 | loading: ,
28 | error: ,
29 | info: ,
30 | warning: ,
31 | };
32 |
33 | const DEFAULT_DURATION = 3000;
34 | const DEFAULT_POSITION: ToastPosition = "top-center";
35 |
36 | /**
37 | * Custom Notification
38 | */
39 | const Notification = ({
40 | content,
41 | status,
42 | duration = DEFAULT_DURATION,
43 | icon,
44 | position = DEFAULT_POSITION,
45 | }: NotificationProps) => {
46 | return toast.custom(
47 | (t: Toast) => (
48 |
56 |
{icon ? icon : ENUM_STATUSES[status]}
57 |
{content}
58 |
59 |
toast.dismiss(t.id)}>
60 | toast.remove(t.id)} />
61 |
62 |
63 | ),
64 | {
65 | duration: status === "loading" ? Infinity : duration,
66 | position,
67 | },
68 | );
69 | };
70 |
71 | export const notification = {
72 | success: (content: React.ReactNode, options?: NotificationOptions) => {
73 | return Notification({ content, status: "success", ...options });
74 | },
75 | info: (content: React.ReactNode, options?: NotificationOptions) => {
76 | return Notification({ content, status: "info", ...options });
77 | },
78 | warning: (content: React.ReactNode, options?: NotificationOptions) => {
79 | return Notification({ content, status: "warning", ...options });
80 | },
81 | error: (content: React.ReactNode, options?: NotificationOptions) => {
82 | return Notification({ content, status: "error", ...options });
83 | },
84 | loading: (content: React.ReactNode, options?: NotificationOptions) => {
85 | return Notification({ content, status: "loading", ...options });
86 | },
87 | remove: (toastId: string) => {
88 | toast.remove(toastId);
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/packages/nextjs/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "installCommand": "yarn install"
3 | }
4 |
--------------------------------------------------------------------------------