├── .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 | <h4 align="center"> 4 | <a href="https://docs.scaffoldeth.io">Documentation</a> | 5 | <a href="https://scaffoldeth.io">Website</a> 6 | </h4> 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 | ![Debug Contracts tab](https://github.com/scaffold-eth/scaffold-eth-2/assets/55535804/b237af0c-5027-4849-a5c1-2e31495cccb1) 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<Contract>("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/(.*)$", "<THIRD_PARTY_MODULES>", "^@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 | <div className="flex flex-col gap-3 p-4"> 13 | Bytecode 14 | <div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]"> 15 | <pre className="px-5"> 16 | <code className="whitespace-pre-wrap overflow-auto break-words">{bytecode}</code> 17 | </pre> 18 | </div> 19 | Opcodes 20 | <div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]"> 21 | <pre className="px-5"> 22 | <code>{formattedAssembly}</code> 23 | </pre> 24 | </div> 25 | </div> 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 | <div className="m-10 mb-20"> 15 | <div className="flex justify-start mb-5"> 16 | <BackButton /> 17 | </div> 18 | <div className="col-span-5 grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-10"> 19 | <div className="col-span-1 flex flex-col"> 20 | <div className="bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-6 lg:px-8 mb-6 space-y-1 py-4 overflow-x-auto"> 21 | <div className="flex"> 22 | <div className="flex flex-col gap-1"> 23 | <Address address={address} format="long" onlyEnsOrAddress /> 24 | <div className="flex gap-1 items-center"> 25 | <span className="font-bold text-sm">Balance:</span> 26 | <Balance address={address} className="text" /> 27 | </div> 28 | </div> 29 | </div> 30 | </div> 31 | </div> 32 | </div> 33 | <ContractTabs address={address} contractData={contractData} /> 34 | </div> 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 | <div className="flex flex-col gap-3 p-4"> 10 | <div className="mockup-code overflow-auto max-h-[500px]"> 11 | <pre className="px-5 whitespace-pre-wrap break-words"> 12 | {contractLogs.map((log, i) => ( 13 | <div key={i}> 14 | <strong>Log:</strong> {JSON.stringify(log, replacer, 2)} 15 | </div> 16 | ))} 17 | </pre> 18 | </div> 19 | </div> 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<string[]>([]); 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 | <div className="flex flex-col gap-3 p-4"> 46 | {storage.length > 0 ? ( 47 | <div className="mockup-code overflow-auto max-h-[500px]"> 48 | <pre className="px-5 whitespace-pre-wrap break-words"> 49 | {storage.map((data, i) => ( 50 | <div key={i}> 51 | <strong>Storage Slot {i}:</strong> {data} 52 | </div> 53 | ))} 54 | </pre> 55 | </div> 56 | ) : ( 57 | <div className="text-lg">This contract does not have any variables.</div> 58 | )} 59 | </div> 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 | <button className="btn btn-sm btn-primary" onClick={() => router.back()}> 9 | Back 10 | </button> 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 | <div role="tablist" className="tabs tabs-lift"> 55 | <button 56 | role="tab" 57 | className={`tab ${activeTab === "transactions" ? "tab-active" : ""}`} 58 | onClick={() => setActiveTab("transactions")} 59 | > 60 | Transactions 61 | </button> 62 | <button 63 | role="tab" 64 | className={`tab ${activeTab === "code" ? "tab-active" : ""}`} 65 | onClick={() => setActiveTab("code")} 66 | > 67 | Code 68 | </button> 69 | <button 70 | role="tab" 71 | className={`tab ${activeTab === "storage" ? "tab-active" : ""}`} 72 | onClick={() => setActiveTab("storage")} 73 | > 74 | Storage 75 | </button> 76 | <button 77 | role="tab" 78 | className={`tab ${activeTab === "logs" ? "tab-active" : ""}`} 79 | onClick={() => setActiveTab("logs")} 80 | > 81 | Logs 82 | </button> 83 | </div> 84 | )} 85 | {activeTab === "transactions" && ( 86 | <div className="pt-4"> 87 | <TransactionsTable blocks={filteredBlocks} transactionReceipts={transactionReceipts} /> 88 | <PaginationButton 89 | currentPage={currentPage} 90 | totalItems={Number(totalBlocks)} 91 | setCurrentPage={setCurrentPage} 92 | /> 93 | </div> 94 | )} 95 | {activeTab === "code" && contractData && ( 96 | <AddressCodeTab bytecode={contractData.bytecode} assembly={contractData.assembly} /> 97 | )} 98 | {activeTab === "storage" && <AddressStorageTab address={address} />} 99 | {activeTab === "logs" && <AddressLogsTab address={address} />} 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 | <div className="mt-5 justify-end flex gap-3 mx-5"> 22 | <button 23 | className={`btn btn-sm ${prevButtonClass}`} 24 | disabled={isPrevButtonDisabled} 25 | onClick={() => setCurrentPage(currentPage - 1)} 26 | > 27 | <ArrowLeftIcon className="h-4 w-4" /> 28 | </button> 29 | <span className="self-center text-primary-content font-medium">Page {currentPage + 1}</span> 30 | <button 31 | className={`btn btn-sm ${nextButtonClass}`} 32 | disabled={isNextButtonDisabled} 33 | onClick={() => setCurrentPage(currentPage + 1)} 34 | > 35 | <ArrowRightIcon className="h-4 w-4" /> 36 | </button> 37 | </div> 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 | <form onSubmit={handleSearch} className="flex items-center justify-end mb-5 space-x-3 mx-5"> 37 | <input 38 | className="border-primary bg-base-100 text-base-content placeholder:text-base-content/50 p-2 mr-2 w-full md:w-1/2 lg:w-1/3 rounded-md shadow-md focus:outline-hidden focus:ring-2 focus:ring-accent" 39 | type="text" 40 | value={searchInput} 41 | placeholder="Search by hash or address" 42 | onChange={e => setSearchInput(e.target.value)} 43 | /> 44 | <button className="btn btn-sm btn-primary" type="submit"> 45 | Search 46 | </button> 47 | </form> 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 | <div className="flex items-center"> 11 | <Link href={`/blockexplorer/transaction/${hash}`}> 12 | {hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)} 13 | </Link> 14 | {isAddressCopiedToClipboard ? ( 15 | <CheckCircleIcon 16 | className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer" 17 | aria-hidden="true" 18 | /> 19 | ) : ( 20 | <DocumentDuplicateIcon 21 | className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer" 22 | aria-hidden="true" 23 | onClick={() => copyAddressToClipboard(hash)} 24 | /> 25 | )} 26 | </div> 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 | <div className="flex justify-center px-4 md:px-0"> 13 | <div className="overflow-x-auto w-full shadow-2xl rounded-xl"> 14 | <table className="table text-xl bg-base-100 table-zebra w-full md:table-md table-sm"> 15 | <thead> 16 | <tr className="rounded-xl text-sm text-base-content"> 17 | <th className="bg-primary">Transaction Hash</th> 18 | <th className="bg-primary">Function Called</th> 19 | <th className="bg-primary">Block Number</th> 20 | <th className="bg-primary">Time Mined</th> 21 | <th className="bg-primary">From</th> 22 | <th className="bg-primary">To</th> 23 | <th className="bg-primary text-end">Value ({targetNetwork.nativeCurrency.symbol})</th> 24 | </tr> 25 | </thead> 26 | <tbody> 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 | <tr key={tx.hash} className="hover text-sm"> 35 | <td className="w-1/12 md:py-4"> 36 | <TransactionHash hash={tx.hash} /> 37 | </td> 38 | <td className="w-2/12 md:py-4"> 39 | {tx.functionName === "0x" ? "" : <span className="mr-1">{tx.functionName}</span>} 40 | {functionCalled !== "0x" && ( 41 | <span className="badge badge-primary font-bold text-xs">{functionCalled}</span> 42 | )} 43 | </td> 44 | <td className="w-1/12 md:py-4">{block.number?.toString()}</td> 45 | <td className="w-2/12 md:py-4">{timeMined}</td> 46 | <td className="w-2/12 md:py-4"> 47 | <Address address={tx.from} size="sm" onlyEnsOrAddress /> 48 | </td> 49 | <td className="w-2/12 md:py-4"> 50 | {!receipt?.contractAddress ? ( 51 | tx.to && <Address address={tx.to} size="sm" onlyEnsOrAddress /> 52 | ) : ( 53 | <div className="relative"> 54 | <Address address={receipt.contractAddress} size="sm" onlyEnsOrAddress /> 55 | <small className="absolute top-4 left-4">(Contract Creation)</small> 56 | </div> 57 | )} 58 | </td> 59 | <td className="text-right md:py-4"> 60 | {formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol} 61 | </td> 62 | </tr> 63 | ); 64 | }), 65 | )} 66 | </tbody> 67 | </table> 68 | </div> 69 | </div> 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 <AddressComponent address={address} contractData={contractData} />; 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 | <p className="font-bold mt-0 mb-1"> 34 | <code className="italic bg-base-300 text-base font-bold"> targetNetwork </code> is not localhost 35 | </p> 36 | <p className="m-0"> 37 | - You are on <code className="italic bg-base-300 text-base font-bold">{targetNetwork.name}</code> .This 38 | block explorer is only for <code className="italic bg-base-300 text-base font-bold">localhost</code>. 39 | </p> 40 | <p className="mt-1 break-normal"> 41 | - You can use{" "} 42 | <a className="text-accent" href={targetNetwork.blockExplorers?.default.url}> 43 | {targetNetwork.blockExplorers?.default.name} 44 | </a>{" "} 45 | instead 46 | </p> 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 | <p className="font-bold mt-0 mb-1">Cannot connect to local provider</p> 62 | <p className="m-0"> 63 | - Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ? 64 | </p> 65 | <p className="mt-1 break-normal"> 66 | - Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "} 67 | <code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code> 68 | </p> 69 | </>, 70 | ); 71 | } 72 | }, [hasError]); 73 | 74 | return ( 75 | <div className="container mx-auto my-10"> 76 | <SearchBar /> 77 | <TransactionsTable blocks={blocks} transactionReceipts={transactionReceipts} /> 78 | <PaginationButton currentPage={currentPage} totalItems={Number(totalBlocks)} setCurrentPage={setCurrentPage} /> 79 | </div> 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<PageProps> = 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 <TransactionComp txHash={txHash} />; 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<ContractName>( 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 | <div className="flex flex-col gap-y-6 lg:gap-y-8 py-8 lg:py-12 justify-center items-center"> 36 | {contractNames.length === 0 ? ( 37 | <p className="text-3xl mt-14">No contracts found!</p> 38 | ) : ( 39 | <> 40 | {contractNames.length > 1 && ( 41 | <div className="flex flex-row gap-2 w-full max-w-7xl pb-1 px-6 lg:px-10 flex-wrap"> 42 | {contractNames.map(contractName => ( 43 | <button 44 | className={`btn btn-secondary btn-sm font-light hover:border-transparent ${ 45 | contractName === selectedContract 46 | ? "bg-base-300 hover:bg-base-300 no-animation" 47 | : "bg-base-100 hover:bg-secondary" 48 | }`} 49 | key={contractName} 50 | onClick={() => setSelectedContract(contractName)} 51 | > 52 | {contractName} 53 | {(contractsData[contractName] as GenericContract)?.external && ( 54 | <span className="tooltip tooltip-top tooltip-accent" data-tip="External contract"> 55 | <BarsArrowUpIcon className="h-4 w-4 cursor-pointer" /> 56 | </span> 57 | )} 58 | </button> 59 | ))} 60 | </div> 61 | )} 62 | {contractNames.map(contractName => ( 63 | <ContractUI 64 | key={contractName} 65 | contractName={contractName} 66 | className={contractName === selectedContract ? "" : "hidden"} 67 | /> 68 | ))} 69 | </> 70 | )} 71 | </div> 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<SetStateAction<Record<string, any>>>; 19 | form: Record<string, any> | 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 <AddressInput {...inputProps} />; 41 | case "bytes32": 42 | return <Bytes32Input {...inputProps} />; 43 | case "bytes": 44 | return <BytesInput {...inputProps} />; 45 | case "string": 46 | return <InputBase {...inputProps} />; 47 | case "tuple": 48 | return ( 49 | <Tuple 50 | setParentForm={setForm} 51 | parentForm={form} 52 | abiTupleParameter={paramType as AbiParameterTuple} 53 | parentStateObjectKey={stateObjectKey} 54 | /> 55 | ); 56 | default: 57 | // Handling 'int' types and 'tuple[]' types 58 | if (paramType.type.includes("int") && !paramType.type.includes("[")) { 59 | return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />; 60 | } else if (paramType.type.startsWith("tuple[")) { 61 | return ( 62 | <TupleArray 63 | setParentForm={setForm} 64 | parentForm={form} 65 | abiTupleParameter={paramType as AbiParameterTuple} 66 | parentStateObjectKey={stateObjectKey} 67 | /> 68 | ); 69 | } else { 70 | return <InputBase {...inputProps} />; 71 | } 72 | } 73 | }; 74 | 75 | return ( 76 | <div className="flex flex-col gap-1.5 w-full"> 77 | <div className="flex items-center ml-2"> 78 | {paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>} 79 | <span className="block text-xs font-extralight leading-none">{paramType.type}</span> 80 | </div> 81 | {renderInput()} 82 | </div> 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<ContractName> }) => { 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 | <ReadOnlyFunctionForm 34 | abi={deployedContractData.abi as Abi} 35 | contractAddress={deployedContractData.address} 36 | abiFunction={fn} 37 | key={fn.name} 38 | inheritedFrom={inheritedFrom} 39 | /> 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 | <div className="mt-14"> 30 | <span className="loading loading-spinner loading-lg"></span> 31 | </div> 32 | ); 33 | } 34 | 35 | if (!deployedContractData) { 36 | return ( 37 | <p className="text-3xl mt-14"> 38 | {`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`} 39 | </p> 40 | ); 41 | } 42 | 43 | return ( 44 | <div className={`grid grid-cols-1 lg:grid-cols-6 px-6 lg:px-10 lg:gap-12 w-full max-w-7xl my-0 ${className}`}> 45 | <div className="col-span-5 grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10"> 46 | <div className="col-span-1 flex flex-col"> 47 | <div className="bg-base-100 border-base-300 border shadow-md shadow-secondary rounded-3xl px-6 lg:px-8 mb-6 space-y-1 py-4"> 48 | <div className="flex"> 49 | <div className="flex flex-col gap-1"> 50 | <span className="font-bold">{contractName}</span> 51 | <Address address={deployedContractData.address} onlyEnsOrAddress /> 52 | <div className="flex gap-1 items-center"> 53 | <span className="font-bold text-sm">Balance:</span> 54 | <Balance address={deployedContractData.address} className="px-0 h-1.5 min-h-[0.375rem]" /> 55 | </div> 56 | </div> 57 | </div> 58 | {targetNetwork && ( 59 | <p className="my-0 text-sm"> 60 | <span className="font-bold">Network</span>:{" "} 61 | <span style={{ color: networkColor }}>{targetNetwork.name}</span> 62 | </p> 63 | )} 64 | </div> 65 | <div className="bg-base-300 rounded-3xl px-6 lg:px-8 py-4 shadow-lg shadow-base-300"> 66 | <ContractVariables 67 | refreshDisplayVariables={refreshDisplayVariables} 68 | deployedContractData={deployedContractData} 69 | /> 70 | </div> 71 | </div> 72 | <div className="col-span-1 lg:col-span-2 flex flex-col gap-6"> 73 | <div className="z-10"> 74 | <div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative"> 75 | <div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300"> 76 | <div className="flex items-center justify-center space-x-2"> 77 | <p className="my-0 text-sm">Read</p> 78 | </div> 79 | </div> 80 | <div className="p-5 divide-y divide-base-300"> 81 | <ContractReadMethods deployedContractData={deployedContractData} /> 82 | </div> 83 | </div> 84 | </div> 85 | <div className="z-10"> 86 | <div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative"> 87 | <div className="h-[5rem] w-[5.5rem] bg-base-300 absolute self-start rounded-[22px] -top-[38px] -left-[1px] -z-10 py-[0.65rem] shadow-lg shadow-base-300"> 88 | <div className="flex items-center justify-center space-x-2"> 89 | <p className="my-0 text-sm">Write</p> 90 | </div> 91 | </div> 92 | <div className="p-5 divide-y divide-base-300"> 93 | <ContractWriteMethods 94 | deployedContractData={deployedContractData} 95 | onChange={triggerRefreshDisplayVariables} 96 | /> 97 | </div> 98 | </div> 99 | </div> 100 | </div> 101 | </div> 102 | </div> 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<ContractName>; 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 | <DisplayVariable 40 | abi={deployedContractData.abi as Abi} 41 | abiFunction={fn} 42 | contractAddress={deployedContractData.address} 43 | key={fn.name} 44 | refreshDisplayVariables={refreshDisplayVariables} 45 | inheritedFrom={inheritedFrom} 46 | /> 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<ContractName>; 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 | <WriteOnlyFunctionForm 39 | abi={deployedContractData.abi as Abi} 40 | key={`${fn.name}-${idx}}`} 41 | abiFunction={fn} 42 | onChange={onChange} 43 | contractAddress={deployedContractData.address} 44 | inheritedFrom={inheritedFrom} 45 | /> 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 | <div className="space-y-1 pb-2"> 61 | <div className="flex items-center"> 62 | <h3 className="font-medium text-lg mb-0 break-all">{abiFunction.name}</h3> 63 | <button className="btn btn-ghost btn-xs" onClick={async () => await refetch()}> 64 | {isFetching ? ( 65 | <span className="loading loading-spinner loading-xs"></span> 66 | ) : ( 67 | <ArrowPathIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" /> 68 | )} 69 | </button> 70 | <InheritanceTooltip inheritedFrom={inheritedFrom} /> 71 | </div> 72 | <div className="text-base-content/80 flex flex-col items-start"> 73 | <div> 74 | <div 75 | className={`break-all block transition bg-transparent ${ 76 | showAnimation ? "bg-warning rounded-xs animate-pulse-fast" : "" 77 | }`} 78 | > 79 | {displayTxResult(result)} 80 | </div> 81 | </div> 82 | </div> 83 | </div> 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 | <span 7 | className="tooltip tooltip-top tooltip-accent px-2 md:break-normal" 8 | data-tip={`Inherited from: ${inheritedFrom}`} 9 | > 10 | <InformationCircleIcon className="h-4 w-4" aria-hidden="true" /> 11 | </span> 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<Record<string, any>>(() => getInitialFormState(abiFunction)); 33 | const [result, setResult] = useState<unknown>(); 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 | <ContractInput 60 | key={key} 61 | setForm={updatedFormValue => { 62 | setResult(undefined); 63 | setForm(updatedFormValue); 64 | }} 65 | form={form} 66 | stateObjectKey={key} 67 | paramType={input} 68 | /> 69 | ); 70 | }); 71 | 72 | return ( 73 | <div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-1"> 74 | <p className="font-medium my-0 break-words"> 75 | {abiFunction.name} 76 | <InheritanceTooltip inheritedFrom={inheritedFrom} /> 77 | </p> 78 | {inputElements} 79 | <div className="flex flex-col md:flex-row justify-between gap-2 flex-wrap"> 80 | <div className="grow w-full md:max-w-[80%]"> 81 | {result !== null && result !== undefined && ( 82 | <div className="bg-secondary rounded-3xl text-sm px-4 py-1.5 break-words overflow-auto"> 83 | <p className="font-bold m-0 mb-1">Result:</p> 84 | <pre className="whitespace-pre-wrap break-words">{displayTxResult(result, "sm")}</pre> 85 | </div> 86 | )} 87 | </div> 88 | <button 89 | className="btn btn-secondary btn-sm self-end md:self-start" 90 | onClick={async () => { 91 | const { data } = await refetch(); 92 | setResult(data); 93 | }} 94 | disabled={isFetching} 95 | > 96 | {isFetching && <span className="loading loading-spinner loading-xs"></span>} 97 | Read 📡 98 | </button> 99 | </div> 100 | </div> 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<SetStateAction<Record<string, any>>>; 10 | parentStateObjectKey: string; 11 | parentForm: Record<string, any> | undefined; 12 | }; 13 | 14 | export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => { 15 | const [form, setForm] = useState<Record<string, any>>(() => getInitialTupleFormState(abiTupleParameter)); 16 | 17 | useEffect(() => { 18 | const values = Object.values(form); 19 | const argsStruct: Record<string, any> = {}; 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 | <div> 30 | <div tabIndex={0} className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary"> 31 | <input type="checkbox" className="min-h-fit! peer" /> 32 | <div className="collapse-title after:top-3.5! p-0 min-h-fit! peer-checked:mb-2 text-primary-content/50"> 33 | <p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p> 34 | </div> 35 | <div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content"> 36 | {abiTupleParameter?.components?.map((param, index) => { 37 | const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index); 38 | return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />; 39 | })} 40 | </div> 41 | </div> 42 | </div> 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 | <div className="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0"> 13 | <div className="mt-1 pl-2"> 14 | {isTxResultCopiedToClipboard ? ( 15 | <CheckCircleIcon 16 | className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer" 17 | aria-hidden="true" 18 | /> 19 | ) : ( 20 | <DocumentDuplicateIcon 21 | className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer" 22 | aria-hidden="true" 23 | onClick={() => copyTxResultToClipboard(JSON.stringify(txResult, replacer, 2))} 24 | /> 25 | )} 26 | </div> 27 | <div tabIndex={0} className="flex-wrap collapse collapse-arrow"> 28 | <input type="checkbox" className="min-h-0! peer" /> 29 | <div className="collapse-title text-sm min-h-0! py-1.5 pl-1 after:top-4!"> 30 | <strong>Transaction Receipt</strong> 31 | </div> 32 | <div className="collapse-content overflow-auto bg-secondary rounded-t-none rounded-3xl pl-0!"> 33 | <pre className="text-xs"> 34 | {Object.entries(txResult).map(([k, v]) => ( 35 | <ObjectFieldDisplay name={k} value={v} size="xs" leftPad={false} key={k} /> 36 | ))} 37 | </pre> 38 | </div> 39 | </div> 40 | </div> 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<string, any> 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 <NumberDisplay value={displayContent} />; 29 | } 30 | 31 | if (typeof displayContent === "string") { 32 | if (isAddress(displayContent)) { 33 | return <Address address={displayContent} size={fontSize} onlyEnsOrAddress />; 34 | } 35 | 36 | if (isHex(displayContent)) { 37 | return displayContent; // don't add quotes 38 | } 39 | } 40 | 41 | if (Array.isArray(displayContent)) { 42 | return <ArrayDisplay values={displayContent} size={fontSize} />; 43 | } 44 | 45 | if (typeof displayContent === "object") { 46 | return <StructDisplay struct={displayContent} size={fontSize} />; 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 | <div className="flex items-baseline"> 62 | {isEther ? "Ξ" + formatEther(value) : String(value)} 63 | <span 64 | className="tooltip tooltip-secondary font-sans ml-2" 65 | data-tip={isEther ? "Multiply by 1e18" : "Divide by 1e18"} 66 | > 67 | <button className="btn btn-ghost btn-circle btn-xs" onClick={() => setIsEther(!isEther)}> 68 | <ArrowsRightLeftIcon className="h-3 w-3 opacity-65" /> 69 | </button> 70 | </span> 71 | </div> 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 | <div className={`flex flex-row items-baseline ${leftPad ? "ml-4" : ""}`}> 88 | <span className="text-base-content/60 mr-2">{name}:</span> 89 | <span className="text-base-content">{displayTxResult(value, size)}</span> 90 | </div> 91 | ); 92 | }; 93 | 94 | const ArrayDisplay = ({ values, size }: { values: DisplayContent[]; size: ResultFontSize }) => { 95 | return ( 96 | <div className="flex flex-col gap-y-1"> 97 | {values.length ? "array" : "[]"} 98 | {values.map((v, i) => ( 99 | <ObjectFieldDisplay key={i} name={`[${i}]`} value={v} size={size} /> 100 | ))} 101 | </div> 102 | ); 103 | }; 104 | 105 | const StructDisplay = ({ struct, size }: { struct: Record<string, any>; size: ResultFontSize }) => { 106 | return ( 107 | <div className="flex flex-col gap-y-1"> 108 | struct 109 | {Object.entries(struct).map(([k, v]) => ( 110 | <ObjectFieldDisplay key={k} name={k} value={v} size={size} /> 111 | ))} 112 | </div> 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 | <DebugContracts /> 14 | <div className="text-center mt-8 bg-secondary p-10"> 15 | <h1 className="text-4xl my-0">Debug Contracts</h1> 16 | <p className="text-neutral"> 17 | You can debug & interact with your deployed contracts here. 18 | <br /> Check{" "} 19 | <code className="italic bg-base-300 text-base font-bold [word-spacing:-0.5rem] px-1"> 20 | packages / nextjs / app / debug / page.tsx 21 | </code>{" "} 22 | </p> 23 | </div> 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 | <html suppressHydrationWarning> 15 | <body> 16 | <ThemeProvider enableSystem> 17 | <ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders> 18 | </ThemeProvider> 19 | </body> 20 | </html> 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 | <div className="flex items-center flex-col grow pt-10"> 15 | <div className="px-5"> 16 | <h1 className="text-center"> 17 | <span className="block text-2xl mb-2">Welcome to</span> 18 | <span className="block text-4xl font-bold">Scaffold-ETH 2</span> 19 | </h1> 20 | <div className="flex justify-center items-center space-x-2 flex-col"> 21 | <p className="my-2 font-medium">Connected Address:</p> 22 | <Address address={connectedAddress} /> 23 | </div> 24 | <p className="text-center text-lg"> 25 | Get started by editing{" "} 26 | <code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block"> 27 | packages/nextjs/app/page.tsx 28 | </code> 29 | </p> 30 | <p className="text-center text-lg"> 31 | Edit your smart contract{" "} 32 | <code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block"> 33 | YourContract.sol 34 | </code>{" "} 35 | in{" "} 36 | <code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block"> 37 | packages/hardhat/contracts 38 | </code> 39 | </p> 40 | </div> 41 | 42 | <div className="grow bg-base-300 w-full mt-16 px-8 py-12"> 43 | <div className="flex justify-center items-center gap-12 flex-col md:flex-row"> 44 | <div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl"> 45 | <BugAntIcon className="h-8 w-8 fill-secondary" /> 46 | <p> 47 | Tinker with your smart contract using the{" "} 48 | <Link href="/debug" passHref className="link"> 49 | Debug Contracts 50 | </Link>{" "} 51 | tab. 52 | </p> 53 | </div> 54 | <div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl"> 55 | <MagnifyingGlassIcon className="h-8 w-8 fill-secondary" /> 56 | <p> 57 | Explore your local transactions with the{" "} 58 | <Link href="/blockexplorer" passHref className="link"> 59 | Block Explorer 60 | </Link>{" "} 61 | tab. 62 | </p> 63 | </div> 64 | </div> 65 | </div> 66 | </div> 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 | <div className="min-h-0 py-5 px-1 mb-11 lg:mb-0"> 22 | <div> 23 | <div className="fixed flex justify-between items-center w-full z-10 p-4 bottom-0 left-0 pointer-events-none"> 24 | <div className="flex flex-col md:flex-row gap-2 pointer-events-auto"> 25 | {nativeCurrencyPrice > 0 && ( 26 | <div> 27 | <div className="btn btn-primary btn-sm font-normal gap-1 cursor-auto"> 28 | <CurrencyDollarIcon className="h-4 w-4" /> 29 | <span>{nativeCurrencyPrice.toFixed(2)}</span> 30 | </div> 31 | </div> 32 | )} 33 | {isLocalNetwork && ( 34 | <> 35 | <Faucet /> 36 | <Link href="/blockexplorer" passHref className="btn btn-primary btn-sm font-normal gap-1"> 37 | <MagnifyingGlassIcon className="h-4 w-4" /> 38 | <span>Block Explorer</span> 39 | </Link> 40 | </> 41 | )} 42 | </div> 43 | <SwitchTheme className={`pointer-events-auto ${isLocalNetwork ? "self-end md:self-auto" : ""}`} /> 44 | </div> 45 | </div> 46 | <div className="w-full"> 47 | <ul className="menu menu-horizontal w-full"> 48 | <div className="flex justify-center items-center gap-2 text-sm w-full"> 49 | <div className="text-center"> 50 | <a href="https://github.com/scaffold-eth/se-2" target="_blank" rel="noreferrer" className="link"> 51 | Fork me 52 | </a> 53 | </div> 54 | <span>·</span> 55 | <div className="flex justify-center items-center gap-2"> 56 | <p className="m-0 text-center"> 57 | Built with <HeartIcon className="inline-block h-4 w-4" /> at 58 | </p> 59 | <a 60 | className="flex justify-center items-center gap-1" 61 | href="https://buidlguidl.com/" 62 | target="_blank" 63 | rel="noreferrer" 64 | > 65 | <BuidlGuidlLogo className="w-3 h-5 pb-1" /> 66 | <span className="link">BuidlGuidl</span> 67 | </a> 68 | </div> 69 | <span>·</span> 70 | <div className="text-center"> 71 | <a href="https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA" target="_blank" rel="noreferrer" className="link"> 72 | Support 73 | </a> 74 | </div> 75 | </div> 76 | </ul> 77 | </div> 78 | </div> 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: <BugAntIcon className="h-4 w-4" />, 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 | <li key={href}> 39 | <Link 40 | href={href} 41 | passHref 42 | className={`${ 43 | isActive ? "bg-secondary shadow-md" : "" 44 | } hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 px-3 text-sm rounded-full gap-2 grid grid-flow-col`} 45 | > 46 | {icon} 47 | <span>{label}</span> 48 | </Link> 49 | </li> 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<HTMLDetailsElement>(null); 64 | useOutsideClick(burgerMenuRef, () => { 65 | burgerMenuRef?.current?.removeAttribute("open"); 66 | }); 67 | 68 | return ( 69 | <div className="sticky lg:static top-0 navbar bg-base-100 min-h-0 shrink-0 justify-between z-20 shadow-md shadow-secondary px-0 sm:px-2"> 70 | <div className="navbar-start w-auto lg:w-1/2"> 71 | <details className="dropdown" ref={burgerMenuRef}> 72 | <summary className="ml-1 btn btn-ghost lg:hidden hover:bg-transparent"> 73 | <Bars3Icon className="h-1/2" /> 74 | </summary> 75 | <ul 76 | className="menu menu-compact dropdown-content mt-3 p-2 shadow-sm bg-base-100 rounded-box w-52" 77 | onClick={() => { 78 | burgerMenuRef?.current?.removeAttribute("open"); 79 | }} 80 | > 81 | <HeaderMenuLinks /> 82 | </ul> 83 | </details> 84 | <Link href="/" passHref className="hidden lg:flex items-center gap-2 ml-4 mr-6 shrink-0"> 85 | <div className="flex relative w-10 h-10"> 86 | <Image alt="SE2 logo" className="cursor-pointer" fill src="/logo.svg" /> 87 | </div> 88 | <div className="flex flex-col"> 89 | <span className="font-bold leading-tight">Scaffold-ETH</span> 90 | <span className="text-xs">Ethereum dev stack</span> 91 | </div> 92 | </Link> 93 | <ul className="hidden lg:flex lg:flex-nowrap menu menu-horizontal px-1 gap-2"> 94 | <HeaderMenuLinks /> 95 | </ul> 96 | </div> 97 | <div className="navbar-end grow mr-4"> 98 | <RainbowKitCustomConnectButton /> 99 | {isLocalNetwork && <FaucetButton />} 100 | </div> 101 | </div> 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 | <div className="flex flex-col min-h-screen"> 22 | <Header /> 23 | <main className="relative flex flex-col flex-1">{children}</main> 24 | <Footer /> 25 | </div> 26 | <Toaster /> 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 | <WagmiProvider config={wagmiConfig}> 50 | <QueryClientProvider client={queryClient}> 51 | <ProgressBar height="3px" color="#2299dd" /> 52 | <RainbowKitProvider 53 | avatar={BlockieAvatar} 54 | theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()} 55 | > 56 | <ScaffoldEthApp>{children}</ScaffoldEthApp> 57 | </RainbowKitProvider> 58 | </QueryClientProvider> 59 | </WagmiProvider> 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 | <div className={`flex space-x-2 h-8 items-center justify-center text-sm ${className}`}> 29 | <input 30 | id="theme-toggle" 31 | type="checkbox" 32 | className="toggle bg-secondary toggle-primary hover:bg-accent transition-all" 33 | onChange={handleToggle} 34 | checked={isDarkMode} 35 | /> 36 | <label htmlFor="theme-toggle" className={`swap swap-rotate ${!isDarkMode ? "swap-active" : ""}`}> 37 | <SunIcon className="swap-on h-5 w-5" /> 38 | <MoonIcon className="swap-off h-5 w-5" /> 39 | </label> 40 | </div> 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 <NextThemesProvider {...props}>{children}</NextThemesProvider>; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/nextjs/components/assets/BuidlGuidlLogo.tsx: -------------------------------------------------------------------------------- 1 | export const BuidlGuidlLogo = ({ className }: { className: string }) => { 2 | return ( 3 | <svg 4 | className={className} 5 | width="53" 6 | height="72" 7 | viewBox="0 0 53 72" 8 | fill="currentColor" 9 | xmlns="http://www.w3.org/2000/svg" 10 | > 11 | <path 12 | fillRule="evenodd" 13 | d="M25.9 17.434v15.638h3.927v9.04h9.718v-9.04h6.745v18.08l-10.607 19.88-12.11-.182-12.11.183L.856 51.152v-18.08h6.713v9.04h9.75v-9.04h4.329V2.46a2.126 2.126 0 0 1 4.047-.914c1.074.412 2.157 1.5 3.276 2.626 1.33 1.337 2.711 2.726 4.193 3.095 1.496.373 2.605-.026 3.855-.475 1.31-.47 2.776-.997 5.005-.747 1.67.197 2.557 1.289 3.548 2.509 1.317 1.623 2.82 3.473 6.599 3.752l-.024.017c-2.42 1.709-5.726 4.043-10.86 3.587-1.605-.139-2.736-.656-3.82-1.153-1.546-.707-2.997-1.37-5.59-.832-2.809.563-4.227 1.892-5.306 2.903-.236.221-.456.427-.67.606Z" 14 | clipRule="evenodd" 15 | /> 16 | </svg> 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 | <button 10 | onClick={e => { 11 | e.stopPropagation(); 12 | copyAddressToClipboard(address); 13 | }} 14 | type="button" 15 | > 16 | {isAddressCopiedToClipboard ? ( 17 | <CheckCircleIcon className={className} aria-hidden="true" /> 18 | ) : ( 19 | <DocumentDuplicateIcon className={className} aria-hidden="true" /> 20 | )} 21 | </button> 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 | <Link 22 | href={blockExplorerAddressLink} 23 | target={targetNetwork.id === hardhat.id ? undefined : "_blank"} 24 | rel={targetNetwork.id === hardhat.id ? undefined : "noopener noreferrer"} 25 | > 26 | {children} 27 | </Link> 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 | <div className="animate-pulse flex space-x-4"> 36 | <div className="rounded-md bg-slate-300 h-6 w-6"></div> 37 | <div className="flex items-center space-y-6"> 38 | <div className="h-2 w-28 bg-slate-300 rounded-sm"></div> 39 | </div> 40 | </div> 41 | ); 42 | } 43 | 44 | if (isError) { 45 | return ( 46 | <div className="border-2 border-base-content/30 rounded-md px-2 flex flex-col items-center max-w-fit cursor-pointer"> 47 | <div className="text-warning">Error</div> 48 | </div> 49 | ); 50 | } 51 | 52 | const formattedBalance = balance ? Number(formatEther(balance.value)) : 0; 53 | 54 | return ( 55 | <button 56 | className={`btn btn-sm btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`} 57 | onClick={toggleDisplayUsdMode} 58 | type="button" 59 | > 60 | <div className="w-full flex items-center justify-center"> 61 | {displayUsdMode ? ( 62 | <> 63 | <span className="text-[0.8em] font-bold mr-1">$</span> 64 | <span>{(formattedBalance * nativeCurrencyPrice).toFixed(2)}</span> 65 | </> 66 | ) : ( 67 | <> 68 | <span>{formattedBalance.toFixed(4)}</span> 69 | <span className="text-[0.8em] font-bold ml-1">{targetNetwork.nativeCurrency.symbol}</span> 70 | </> 71 | )} 72 | </div> 73 | </button> 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 | <img 11 | className="rounded-full" 12 | src={ensImage || blo(address as `0x${string}`)} 13 | width={size} 14 | height={size} 15 | alt={`${address} avatar`} 16 | /> 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 | <div 57 | className={ 58 | !isBalanceZero 59 | ? "ml-1" 60 | : "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:-translate-x-2/5" 61 | } 62 | data-tip="Grab funds from faucet" 63 | > 64 | <button className="btn btn-secondary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}> 65 | {!loading ? ( 66 | <BanknotesIcon className="h-4 w-4" /> 67 | ) : ( 68 | <span className="loading loading-spinner loading-xs"></span> 69 | )} 70 | </button> 71 | </div> 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<Address | string>) => { 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<string>(); 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 | <InputBase<Address> 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 | <div className="flex bg-base-300 rounded-l-full items-center"> 93 | {isEnsAvatarLoading && <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>} 94 | {ensAvatar ? ( 95 | <span className="w-[35px]"> 96 | { 97 | // eslint-disable-next-line 98 | <img className="w-full rounded-full" src={ensAvatar} alt={`${ensAddress} avatar`} /> 99 | } 100 | </span> 101 | ) : null} 102 | <span className="text-accent px-2">{enteredEnsName ?? ensName}</span> 103 | </div> 104 | ) : ( 105 | (isEnsNameLoading || isEnsAddressLoading) && ( 106 | <div className="flex bg-base-300 rounded-l-full items-center gap-2 pr-2"> 107 | <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div> 108 | <div className="skeleton bg-base-200 h-3 w-20"></div> 109 | </div> 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 && <img alt="" className="rounded-full!" src={blo(value as `0x${string}`)} width="35" height="35" /> 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 | <InputBase 15 | name={name} 16 | value={value} 17 | placeholder={placeholder} 18 | onChange={onChange} 19 | disabled={disabled} 20 | suffix={ 21 | <button 22 | className="self-center cursor-pointer text-xl font-semibold px-4 text-accent" 23 | onClick={convertStringToBytes32} 24 | type="button" 25 | > 26 | # 27 | </button> 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 | <InputBase 12 | name={name} 13 | value={value} 14 | placeholder={placeholder} 15 | onChange={onChange} 16 | disabled={disabled} 17 | suffix={ 18 | <button 19 | className="self-center cursor-pointer text-xl font-semibold px-4 text-accent" 20 | onClick={convertStringToBytes} 21 | type="button" 22 | > 23 | # 24 | </button> 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<T> = CommonInputProps<T> & { 5 | error?: boolean; 6 | prefix?: ReactNode; 7 | suffix?: ReactNode; 8 | reFocus?: boolean; 9 | }; 10 | 11 | export const InputBase = <T extends { toString: () => string } | undefined = string>({ 12 | name, 13 | value, 14 | onChange, 15 | placeholder, 16 | error, 17 | disabled, 18 | prefix, 19 | suffix, 20 | reFocus, 21 | }: InputBaseProps<T>) => { 22 | const inputReft = useRef<HTMLInputElement>(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<HTMLInputElement>) => { 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<HTMLInputElement, Element>) => { 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 | <div className={`flex border-2 border-base-300 bg-base-200 rounded-full text-accent ${modifier}`}> 51 | {prefix} 52 | <input 53 | className="input input-ghost focus-within:border-transparent focus:outline-hidden focus:bg-transparent h-[2.2rem] min-h-[2.2rem] px-4 border w-full font-medium placeholder:text-accent/70 text-base-content/70 focus:text-base-content/70" 54 | placeholder={placeholder} 55 | name={name} 56 | value={value?.toString()} 57 | onChange={handleChange} 58 | disabled={disabled} 59 | autoComplete="off" 60 | ref={inputReft} 61 | onFocus={onFocus} 62 | /> 63 | {suffix} 64 | </div> 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<string> & { 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 | <InputBase 37 | name={name} 38 | value={value} 39 | placeholder={placeholder} 40 | error={inputError} 41 | onChange={onChange} 42 | disabled={disabled} 43 | suffix={ 44 | !inputError && 45 | !disableMultiplyBy1e18 && ( 46 | <div 47 | className="space-x-4 flex tooltip tooltip-top tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none" 48 | data-tip="Multiply by 1e18 (wei)" 49 | > 50 | <button 51 | className={`${disabled ? "cursor-not-allowed" : "cursor-pointer"} font-semibold px-4 text-accent`} 52 | onClick={multiplyBy1e18} 53 | disabled={disabled} 54 | type="button" 55 | > 56 | ∗ 57 | </button> 58 | </div> 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<T = string> = { 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 | <div> 14 | <input type="checkbox" id={`${modalId}`} className="modal-toggle" /> 15 | <label htmlFor={`${modalId}`} className="modal cursor-pointer"> 16 | <label className="modal-box relative"> 17 | {/* dummy input to capture event onclick on modal box */} 18 | <input className="h-0 w-0 absolute top-0 left-0" /> 19 | <label htmlFor={`${modalId}`} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3"> 20 | ✕ 21 | </label> 22 | <div className="space-y-3 py-6"> 23 | <div className="flex flex-col items-center gap-6"> 24 | <QRCodeSVG value={address} size={256} /> 25 | <Address address={address} format="long" disableAddressLink onlyEnsOrAddress /> 26 | </div> 27 | </div> 28 | </label> 29 | </label> 30 | </div> 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 | <li key={allowedNetwork.id} className={hidden ? "hidden" : ""}> 25 | <button 26 | className="menu-item btn-sm rounded-xl! flex gap-3 py-3 whitespace-nowrap" 27 | type="button" 28 | onClick={() => { 29 | switchChain?.({ chainId: allowedNetwork.id }); 30 | }} 31 | > 32 | <ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" /> 33 | <span> 34 | Switch to{" "} 35 | <span 36 | style={{ 37 | color: getNetworkColor(allowedNetwork, isDarkMode), 38 | }} 39 | > 40 | {allowedNetwork.name} 41 | </span> 42 | </span> 43 | </button> 44 | </li> 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 | <div className="dropdown dropdown-end mr-2"> 10 | <label tabIndex={0} className="btn btn-error btn-sm dropdown-toggle gap-1"> 11 | <span>Wrong network</span> 12 | <ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" /> 13 | </label> 14 | <ul 15 | tabIndex={0} 16 | className="dropdown-content menu p-2 mt-1 shadow-center shadow-accent bg-base-200 rounded-box gap-1" 17 | > 18 | <NetworkOptions /> 19 | <li> 20 | <button 21 | className="menu-item text-error btn-sm rounded-xl! flex gap-3 py-3" 22 | type="button" 23 | onClick={() => disconnect()} 24 | > 25 | <ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" /> 26 | <span>Disconnect</span> 27 | </button> 28 | </li> 29 | </ul> 30 | </div> 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 | <ConnectButton.Custom> 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 | <button className="btn btn-primary btn-sm" onClick={openConnectModal} type="button"> 35 | Connect Wallet 36 | </button> 37 | ); 38 | } 39 | 40 | if (chain.unsupported || chain.id !== targetNetwork.id) { 41 | return <WrongNetworkDropdown />; 42 | } 43 | 44 | return ( 45 | <> 46 | <div className="flex flex-col items-center mr-1"> 47 | <Balance address={account.address as Address} className="min-h-0 h-auto" /> 48 | <span className="text-xs" style={{ color: networkColor }}> 49 | {chain.name} 50 | </span> 51 | </div> 52 | <AddressInfoDropdown 53 | address={account.address as Address} 54 | displayName={account.displayName} 55 | ensAvatar={account.ensAvatar} 56 | blockExplorerAddressLink={blockExplorerAddressLink} 57 | /> 58 | <AddressQRCodeModal address={account.address as Address} modalId="qrcode-modal" /> 59 | </> 60 | ); 61 | })()} 62 | </> 63 | ); 64 | }} 65 | </ConnectButton.Custom> 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<Log[]>([]); 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<TContractName extends ContractName> = { 14 | data: Contract<TContractName> | 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<TContractName extends ContractName>( 23 | config: UseDeployedContractConfig<TContractName>, 24 | ): DeployedContractData<TContractName>; 25 | /** 26 | * @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" }) 27 | */ 28 | export function useDeployedContractInfo<TContractName extends ContractName>( 29 | contractName: TContractName, 30 | ): DeployedContractData<TContractName>; 31 | 32 | export function useDeployedContractInfo<TContractName extends ContractName>( 33 | configOrName: UseDeployedContractConfig<TContractName> | TContractName, 34 | ): DeployedContractData<TContractName> { 35 | const isMounted = useIsMounted(); 36 | 37 | const finalConfig: UseDeployedContractConfig<TContractName> = 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<TContractName>; 50 | const [status, setStatus] = useState<ContractCodeStatus>(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<Block[]>([]); 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<Error | null>(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<HTMLElement | null>, 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<GetWalletClientReturnType, null> | 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<TContractName>["abi"], 43 | TWalletClient extends Exclude<GetWalletClientReturnType, null> 44 | ? { 45 | public: Client<Transport, Chain>; 46 | wallet: TWalletClient; 47 | } 48 | : { public: Client<Transport, Chain> }, 49 | Chain, 50 | Account 51 | >({ 52 | address: deployedContractData.address, 53 | abi: deployedContractData.abi as Contract<TContractName>["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<ContractAbi<TContractName>, "pure" | "view">, 28 | >({ 29 | contractName, 30 | functionName, 31 | args, 32 | chainId, 33 | ...readConfig 34 | }: UseScaffoldReadConfig<TContractName, TFunctionName>) => { 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<ReturnType<typeof useReadContract>, "data" | "refetch"> & { 57 | data: AbiFunctionReturnType<ContractAbi, TFunctionName> | undefined; 58 | refetch: ( 59 | options?: RefetchOptions | undefined, 60 | ) => Promise<QueryObserverResult<AbiFunctionReturnType<ContractAbi, TFunctionName>, 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<ContractAbi<TContractName>>, 21 | >({ 22 | contractName, 23 | eventName, 24 | chainId, 25 | onLogs, 26 | }: UseScaffoldEventConfig<TContractName, TEventName>) => { 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<typeof onLogs>[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<Hash>) | Parameters<SendTransactionMutate<Config, undefined>>[0], 11 | options?: TransactorFuncOptions, 12 | ) => Promise<Hash | undefined>; 13 | 14 | /** 15 | * Custom notification content for TXs. 16 | */ 17 | const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => { 18 | return ( 19 | <div className={`flex flex-col ml-1 cursor-default`}> 20 | <p className="my-0">{message}</p> 21 | {blockExplorerLink && blockExplorerLink.length > 0 ? ( 22 | <a href={blockExplorerLink} target="_blank" rel="noreferrer" className="block link text-md"> 23 | check out transaction 24 | </a> 25 | ) : null} 26 | </div> 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(<TxnNotification message="Awaiting for user confirmation" />); 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 | <TxnNotification message="Waiting for transaction to complete." blockExplorerLink={blockExplorerTxURL} />, 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 | <TxnNotification message="Transaction completed successfully!" blockExplorerLink={blockExplorerTxURL} />, 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(<TxnNotification message={message} blockExplorerLink={blockExplorerTxURL} />); 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 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 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 | <svg width="103" height="102" viewBox="0 0 103 102" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="0.72229" y="0.99707" width="101.901" height="100.925" rx="19.6606" fill="black"/> 3 | <path d="M69.4149 42.5046L70.9118 39.812L72.5438 42.5046L83.0241 59.1906L70.9533 65.9361L59.1193 59.1906L69.4149 42.5046Z" fill="white"/> 4 | <path d="M70.9533 69.2496L60.6577 63.6876L70.9533 79.0719L81.184 63.4985L70.9533 69.2496Z" fill="white"/> 5 | <path d="M70.953 65.9259V41.8533V39.8499L83.063 59.1785L70.953 65.9259Z" fill="#DFDFDF"/> 6 | <path d="M70.9617 79.0499L71.0409 69.2969L81.2062 63.4629L70.9617 79.0499Z" fill="#DFDFDF"/> 7 | <path d="M34.409 21.6931V24.6125H34.4132L34.4124 27.747H26.8093L26.8093 27.7566H25.5383L21.3839 36.4914H34.4135V39.9723H34.4091L34.4135 69.4549C34.4135 73.5268 31.1126 76.8277 27.0408 76.8277H24.7346L19.8064 84.0772H62.4172L57.3539 76.8277H51.7795C47.7076 76.8277 44.4067 73.5268 44.4067 69.4549L44.4024 43.665C44.5071 39.7076 47.7304 36.5273 51.7046 36.4914H79.5481L74.6584 27.7566H53.6021L53.6021 27.747L44.3987 27.7469L44.3994 24.6125H44.4022V18.3245L34.409 21.6931Z" fill="white"/> 8 | <path d="M39.882 19.8517V76.5496C39.9731 74.9642 41.0475 70.8554 44.3648 69.1027V50.8665L44.4703 50.8998V43.8309C44.4703 39.7591 47.7712 36.4582 51.843 36.4582H79.5812L76.9508 31.8656H49.9083C46.1286 31.8656 44.3648 34.556 44.3648 34.556L44.4435 18.3066L39.882 19.8517Z" fill="#DFDFDF"/> 9 | <path d="M23.622 31.7927L21.3295 36.5083H34.4247V31.7927H23.622Z" fill="#DFDFDF"/> 10 | </svg> 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<number, string>; 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<GlobalState>(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<string, Abi>; 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<AbiFunction[], string>({ 31 | abi: contractAbi as AbiFunction[], 32 | name: functionName, 33 | })?.inputs?.map((input: any) => input.name); 34 | tx.functionArgTypes = getAbiItem<AbiFunction[], string>({ 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<number> => { 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<ChainAttributes>; 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<number, string> = { 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<string, ChainAttributes> = { 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: <CheckCircleIcon className="w-7 text-success" />, 27 | loading: <span className="w-6 loading loading-spinner"></span>, 28 | error: <ExclamationCircleIcon className="w-7 text-error" />, 29 | info: <InformationCircleIcon className="w-7 text-info" />, 30 | warning: <ExclamationTriangleIcon className="w-7 text-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 | <div 49 | className={`flex flex-row items-start justify-between max-w-sm rounded-xl shadow-center shadow-accent bg-base-200 p-4 transform-gpu relative transition-all duration-500 ease-in-out space-x-2 50 | ${ 51 | position.substring(0, 3) == "top" 52 | ? `hover:translate-y-1 ${t.visible ? "top-0" : "-top-96"}` 53 | : `hover:-translate-y-1 ${t.visible ? "bottom-0" : "-bottom-96"}` 54 | }`} 55 | > 56 | <div className="leading-[0] self-center">{icon ? icon : ENUM_STATUSES[status]}</div> 57 | <div className={`overflow-x-hidden break-words whitespace-pre-line ${icon ? "mt-1" : ""}`}>{content}</div> 58 | 59 | <div className={`cursor-pointer text-lg ${icon ? "mt-1" : ""}`} onClick={() => toast.dismiss(t.id)}> 60 | <XMarkIcon className="w-6 cursor-pointer" onClick={() => toast.remove(t.id)} /> 61 | </div> 62 | </div> 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 | --------------------------------------------------------------------------------