mirror of
https://github.com/yashgo0018/maci-wrapper.git
synced 2026-01-10 12:07:56 -05:00
Initial commit
This commit is contained in:
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Bug Report
|
||||
description: File a bug/issue
|
||||
title: 'bug: <title>'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! The more info you provide, the more we can help you 🙌
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have looked through the [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues)
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Which method was used to setup Scaffold-ETH 2 ?
|
||||
description: You may select both, if the bug is present in both the methods.
|
||||
multiple: true
|
||||
options:
|
||||
- git clone
|
||||
- npx create-eth@latest
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps or code snippets to reproduce the behavior.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Browser info? Screenshots? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask Question
|
||||
url: https://github.com/scaffold-eth/scaffold-eth-2/discussions/new?category=q-a
|
||||
about: Ask questions and discuss with other community members
|
||||
- name: Request Feature
|
||||
url: https://github.com/scaffold-eth/scaffold-eth-2/discussions/new?category=ideas
|
||||
about: Requests features or brainstorm ideas for new functionality
|
||||
16
.github/pull_request_template.md
vendored
Normal file
16
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
## Description
|
||||
|
||||
_Concise description of proposed changes, We recommend using screenshots and videos for better description_
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [ ] I have read the [contributing docs](/scaffold-eth/scaffold-eth-2/blob/main/CONTRIBUTING.md) (if this is your first contribution)
|
||||
- [ ] This is not a duplicate of any [existing pull request](https://github.com/scaffold-eth/scaffold-eth-2/pulls)
|
||||
|
||||
## Related Issues
|
||||
|
||||
_Closes #{issue number}_
|
||||
|
||||
_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._
|
||||
|
||||
Your ENS/address:
|
||||
43
.github/workflows/lint.yaml
vendored
Normal file
43
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node: [lts/*]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run hardhat node, deploy contracts (& generate contracts typescript output)
|
||||
run: yarn chain & yarn deploy
|
||||
|
||||
- name: Run nextjs lint
|
||||
run: yarn next:lint --max-warnings=0
|
||||
|
||||
- name: Check typings on nextjs
|
||||
run: yarn next:check-types
|
||||
|
||||
- name: Run hardhat lint
|
||||
run: yarn hardhat:lint --max-warnings=0
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
node_modules
|
||||
|
||||
# dependencies, yarn, etc
|
||||
# yarn / eslint
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.eslintcache
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
.vercel
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --verbose
|
||||
21
.lintstagedrc.js
Normal file
21
.lintstagedrc.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require("path");
|
||||
|
||||
const buildNextEslintCommand = (filenames) =>
|
||||
`yarn next:lint --fix --file ${filenames
|
||||
.map((f) => path.relative(path.join("packages", "nextjs"), f))
|
||||
.join(" --file ")}`;
|
||||
|
||||
const checkTypesNextCommand = () => "yarn next:check-types";
|
||||
|
||||
const buildHardhatEslintCommand = (filenames) =>
|
||||
`yarn hardhat:lint-staged --fix ${filenames
|
||||
.map((f) => path.relative(path.join("packages", "hardhat"), f))
|
||||
.join(" ")}`;
|
||||
|
||||
module.exports = {
|
||||
"packages/nextjs/**/*.{ts,tsx}": [
|
||||
buildNextEslintCommand,
|
||||
checkTypesNextCommand,
|
||||
],
|
||||
"packages/hardhat/**/*.{ts,tsx}": [buildHardhatEslintCommand],
|
||||
};
|
||||
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
11
.yarnrc.yml
Normal file
11
.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
enableColors: true
|
||||
|
||||
nmHoistingLimits: workspaces
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||
spec: "@yarnpkg/plugin-typescript"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.3.cjs
|
||||
86
CONTRIBUTING.md
Normal file
86
CONTRIBUTING.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Welcome to Scaffold-ETH 2 Contributing Guide
|
||||
|
||||
Thank you for investing your time in contributing to Scaffold-ETH 2!
|
||||
|
||||
This guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved.
|
||||
|
||||
## About the Project
|
||||
|
||||
Scaffold-ETH 2 is a minimal and forkable repo providing builders with a starter kit to build decentralized applications on Ethereum.
|
||||
|
||||
Read the [README](README.md) to get an overview of the project.
|
||||
|
||||
### Vision
|
||||
|
||||
The goal of Scaffold-ETH 2 is to provide the primary building blocks for a decentralized application.
|
||||
|
||||
The repo can be forked to include integrations and more features, but we want to keep the master branch simple and minimal.
|
||||
|
||||
### Project Status
|
||||
|
||||
The project is under active development.
|
||||
|
||||
You can view the open Issues, follow the development process and contribute to the project.
|
||||
|
||||
## Getting started
|
||||
|
||||
You can contribute to this repo in many ways:
|
||||
|
||||
- Solve open issues
|
||||
- Report bugs or feature requests
|
||||
- Improve the documentation
|
||||
|
||||
Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions:
|
||||
|
||||
- Search for existing Issues and PRs before creating your own.
|
||||
- Contributions should only fix/add the functionality in the issue OR address style issues, not both.
|
||||
- If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error.
|
||||
- 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.
|
||||
- If applicable, please edit the README.md file to reflect the changes.
|
||||
|
||||
### Issues
|
||||
|
||||
Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created.
|
||||
|
||||
#### Solve an issue
|
||||
|
||||
Scan through our [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues) to find one that interests you.
|
||||
|
||||
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.
|
||||
|
||||
#### Create a new issue
|
||||
|
||||
If a related issue doesn't exist, you can open a new issue.
|
||||
|
||||
Some tips to follow when you are creating an issue:
|
||||
|
||||
- Provide as much context as possible. Over-communicate to give the most details to the reader.
|
||||
- Include the steps to reproduce the issue or the reason for adding the feature.
|
||||
- Screenshots, videos etc., are highly appreciated.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
#### Pull Request Process
|
||||
|
||||
We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
|
||||
|
||||
1. Fork the repo
|
||||
2. Clone the project
|
||||
3. Create a new branch with a descriptive name
|
||||
4. Commit your changes to the new branch
|
||||
5. Push changes to your fork
|
||||
6. Open a PR in our repository and tag one of the maintainers to review your PR
|
||||
|
||||
Here are some tips for a high-quality pull request:
|
||||
|
||||
- Create a title for the PR that accurately defines the work done.
|
||||
- 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.
|
||||
- Add the link to the issue if applicable.
|
||||
- Have a good commit message that summarises the work done.
|
||||
|
||||
Once you submit your PR:
|
||||
|
||||
- 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 aims to create a frictionless interaction process.
|
||||
- As you update your PR and apply changes, mark each conversation resolved.
|
||||
|
||||
Once the PR is approved, we'll "squash-and-merge" to keep the git commit history clean.
|
||||
21
LICENCE
Normal file
21
LICENCE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 BuidlGuidl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
80
README.md
Normal file
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 🏗 Scaffold-ETH 2
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://docs.scaffoldeth.io">Documentation</a> |
|
||||
<a href="https://scaffoldeth.io">Website</a>
|
||||
</h4>
|
||||
|
||||
🧪 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.
|
||||
|
||||
⚙️ Built using NextJS, RainbowKit, Hardhat, Wagmi, Viem, and Typescript.
|
||||
|
||||
- ✅ **Contract Hot Reload**: Your frontend auto-adapts to your smart contract as you edit it.
|
||||
- 🪝 **[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.
|
||||
- 🧱 [**Components**](https://docs.scaffoldeth.io/components/): Collection of common web3 components to quickly build your frontend.
|
||||
- 🔥 **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet.
|
||||
- 🔐 **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network.
|
||||
|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
Before you begin, you need to install the following tools:
|
||||
|
||||
- [Node (>= v18.17)](https://nodejs.org/en/download/)
|
||||
- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install))
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Quickstart
|
||||
|
||||
To get started with Scaffold-ETH 2, follow the steps below:
|
||||
|
||||
1. Clone this repo & install dependencies
|
||||
|
||||
```
|
||||
git clone https://github.com/scaffold-eth/scaffold-eth-2.git
|
||||
cd scaffold-eth-2
|
||||
yarn install
|
||||
```
|
||||
|
||||
2. Run a local network in the first terminal:
|
||||
|
||||
```
|
||||
yarn chain
|
||||
```
|
||||
|
||||
This command starts a local Ethereum network using Hardhat. The network runs on your local machine and can be used for testing and development. You can customize the network configuration in `hardhat.config.ts`.
|
||||
|
||||
3. On a second terminal, deploy the test contract:
|
||||
|
||||
```
|
||||
yarn deploy
|
||||
```
|
||||
|
||||
This command deploys a test smart contract to the local network. The contract is located in `packages/hardhat/contracts` and can be modified to suit your needs. The `yarn deploy` command uses the deploy script located in `packages/hardhat/deploy` to deploy the contract to the network. You can also customize the deploy script.
|
||||
|
||||
4. On a third terminal, start your NextJS app:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
Run smart contract test with `yarn hardhat:test`
|
||||
|
||||
- Edit your smart contract `YourContract.sol` in `packages/hardhat/contracts`
|
||||
- Edit your frontend in `packages/nextjs/pages`
|
||||
- Edit your deployment scripts in `packages/hardhat/deploy`
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit our [docs](https://docs.scaffoldeth.io) to learn how to start building with Scaffold-ETH 2.
|
||||
|
||||
To know more about its features, check out our [website](https://scaffoldeth.io).
|
||||
|
||||
## Contributing to Scaffold-ETH 2
|
||||
|
||||
We welcome contributions to Scaffold-ETH 2!
|
||||
|
||||
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.
|
||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "se-2",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/hardhat",
|
||||
"packages/nextjs"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"account": "yarn workspace @se-2/hardhat account",
|
||||
"chain": "yarn workspace @se-2/hardhat chain",
|
||||
"fork": "yarn workspace @se-2/hardhat fork",
|
||||
"deploy": "yarn workspace @se-2/hardhat deploy",
|
||||
"verify": "yarn workspace @se-2/hardhat verify",
|
||||
"hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify",
|
||||
"compile": "yarn workspace @se-2/hardhat compile",
|
||||
"generate": "yarn workspace @se-2/hardhat generate",
|
||||
"flatten": "yarn workspace @se-2/hardhat flatten",
|
||||
"hardhat:lint": "yarn workspace @se-2/hardhat lint",
|
||||
"hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged",
|
||||
"hardhat:format": "yarn workspace @se-2/hardhat format",
|
||||
"hardhat:test": "yarn workspace @se-2/hardhat test",
|
||||
"test": "yarn hardhat:test",
|
||||
"format": "yarn next:format && yarn hardhat:format",
|
||||
"start": "yarn workspace @se-2/nextjs dev",
|
||||
"next:lint": "yarn workspace @se-2/nextjs lint",
|
||||
"next:format": "yarn workspace @se-2/nextjs format",
|
||||
"next:check-types": "yarn workspace @se-2/nextjs check-types",
|
||||
"next:build": "yarn workspace @se-2/nextjs build",
|
||||
"postinstall": "husky install",
|
||||
"precommit": "lint-staged",
|
||||
"vercel": "yarn workspace @se-2/nextjs vercel",
|
||||
"vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo"
|
||||
},
|
||||
"packageManager": "yarn@3.2.3",
|
||||
"devDependencies": {
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3"
|
||||
}
|
||||
}
|
||||
11
packages/hardhat/.env.example
Normal file
11
packages/hardhat/.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# Template for Hardhat environment variables.
|
||||
|
||||
# To use this template, copy this file, rename it .env, and fill in the values.
|
||||
|
||||
# If not set, we provide default values (check `hardhat.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this .env file you can use: process.env.VARIABLENAME
|
||||
ALCHEMY_API_KEY=
|
||||
DEPLOYER_PRIVATE_KEY=
|
||||
ETHERSCAN_API_KEY=
|
||||
8
packages/hardhat/.eslintignore
Normal file
8
packages/hardhat/.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# folders
|
||||
artifacts
|
||||
cache
|
||||
contracts
|
||||
node_modules/
|
||||
typechain-types
|
||||
# files
|
||||
**/*.json
|
||||
17
packages/hardhat/.eslintrc.json
Normal file
17
packages/hardhat/.eslintrc.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
packages/hardhat/.gitignore
vendored
Normal file
17
packages/hardhat/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
node_modules
|
||||
.env
|
||||
coverage
|
||||
coverage.json
|
||||
typechain
|
||||
typechain-types
|
||||
temp
|
||||
|
||||
#Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
#zkSync files
|
||||
artifacts-zk
|
||||
cache-zk
|
||||
|
||||
deployments/localhost
|
||||
19
packages/hardhat/.prettierrc.json
Normal file
19
packages/hardhat/.prettierrc.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sol",
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"explicitTypes": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
87
packages/hardhat/contracts/YourContract.sol
Normal file
87
packages/hardhat/contracts/YourContract.sol
Normal file
@@ -0,0 +1,87 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
// Useful for debugging. Remove when deploying to a live network.
|
||||
import "hardhat/console.sol";
|
||||
|
||||
// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
|
||||
// import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* A smart contract that allows changing a state variable of the contract and tracking the changes
|
||||
* It also allows the owner to withdraw the Ether in the contract
|
||||
* @author BuidlGuidl
|
||||
*/
|
||||
contract YourContract {
|
||||
// State Variables
|
||||
address public immutable owner;
|
||||
string public greeting = "Building Unstoppable Apps!!!";
|
||||
bool public premium = false;
|
||||
uint256 public totalCounter = 0;
|
||||
mapping(address => uint) public userGreetingCounter;
|
||||
|
||||
// Events: a way to emit log statements from smart contract that can be listened to by external parties
|
||||
event GreetingChange(
|
||||
address indexed greetingSetter,
|
||||
string newGreeting,
|
||||
bool premium,
|
||||
uint256 value
|
||||
);
|
||||
|
||||
// Constructor: Called once on contract deployment
|
||||
// Check packages/hardhat/deploy/00_deploy_your_contract.ts
|
||||
constructor(address _owner) {
|
||||
owner = _owner;
|
||||
}
|
||||
|
||||
// Modifier: used to define a set of rules that must be met before or after a function is executed
|
||||
// Check the withdraw() function
|
||||
modifier isOwner() {
|
||||
// msg.sender: predefined variable that represents address of the account that called the current function
|
||||
require(msg.sender == owner, "Not the Owner");
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that allows anyone to change the state variable "greeting" of the contract and increase the counters
|
||||
*
|
||||
* @param _newGreeting (string memory) - new greeting to save on the contract
|
||||
*/
|
||||
function setGreeting(string memory _newGreeting) public payable {
|
||||
// Print data to the hardhat chain console. Remove when deploying to a live network.
|
||||
console.log(
|
||||
"Setting new greeting '%s' from %s",
|
||||
_newGreeting,
|
||||
msg.sender
|
||||
);
|
||||
|
||||
// Change state variables
|
||||
greeting = _newGreeting;
|
||||
totalCounter += 1;
|
||||
userGreetingCounter[msg.sender] += 1;
|
||||
|
||||
// msg.value: built-in global variable that represents the amount of ether sent with the transaction
|
||||
if (msg.value > 0) {
|
||||
premium = true;
|
||||
} else {
|
||||
premium = false;
|
||||
}
|
||||
|
||||
// emit: keyword used to trigger an event
|
||||
emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that allows the owner to withdraw all the Ether in the contract
|
||||
* The function can only be called by the owner of the contract as defined by the isOwner modifier
|
||||
*/
|
||||
function withdraw() public isOwner {
|
||||
(bool success, ) = owner.call{ value: address(this).balance }("");
|
||||
require(success, "Failed to send Ether");
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that allows the contract to receive ETH
|
||||
*/
|
||||
receive() external payable {}
|
||||
}
|
||||
44
packages/hardhat/deploy/00_deploy_your_contract.ts
Normal file
44
packages/hardhat/deploy/00_deploy_your_contract.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
import { Contract } from "ethers";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "YourContract" using the deployer account and
|
||||
* constructor arguments set to the deployer address
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
/*
|
||||
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
|
||||
|
||||
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
|
||||
should have sufficient balance to pay for the gas fees for contract creation.
|
||||
|
||||
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
|
||||
with a random private key in the .env file (then used on hardhat.config.ts)
|
||||
You can run the `yarn account` command to check your balance in every network.
|
||||
*/
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy } = hre.deployments;
|
||||
|
||||
await deploy("YourContract", {
|
||||
from: deployer,
|
||||
// Contract constructor arguments
|
||||
args: [deployer],
|
||||
log: true,
|
||||
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
|
||||
// automatically mining the contract deployment transaction. There is no effect on live networks.
|
||||
autoMine: true,
|
||||
});
|
||||
|
||||
// Get the deployed contract to interact with it after deploying.
|
||||
const yourContract = await hre.ethers.getContract<Contract>("YourContract", deployer);
|
||||
console.log("👋 Initial greeting:", await yourContract.greeting());
|
||||
};
|
||||
|
||||
export default deployYourContract;
|
||||
|
||||
// Tags are useful if you have multiple deploy files and only want to run one of them.
|
||||
// e.g. yarn deploy --tags YourContract
|
||||
deployYourContract.tags = ["YourContract"];
|
||||
133
packages/hardhat/deploy/99_generateTsAbis.ts
Normal file
133
packages/hardhat/deploy/99_generateTsAbis.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing)
|
||||
*
|
||||
* This script generates the file containing the contracts Abi definitions.
|
||||
* These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
|
||||
* This script should run as the last deploy script.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import prettier from "prettier";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const generatedContractComment = `
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
`;
|
||||
|
||||
const DEPLOYMENTS_DIR = "./deployments";
|
||||
const ARTIFACTS_DIR = "./artifacts";
|
||||
|
||||
function getDirectories(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
function getContractNames(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
|
||||
.map(dirent => dirent.name.split(".")[0]);
|
||||
}
|
||||
|
||||
function getActualSourcesForContract(sources: Record<string, any>, contractName: string) {
|
||||
for (const sourcePath of Object.keys(sources)) {
|
||||
const sourceName = sourcePath.split("/").pop()?.split(".sol")[0];
|
||||
if (sourceName === contractName) {
|
||||
const contractContent = sources[sourcePath].content as string;
|
||||
const regex = /contract\s+(\w+)\s+is\s+([^{}]+)\{/;
|
||||
const match = contractContent.match(regex);
|
||||
|
||||
if (match) {
|
||||
const inheritancePart = match[2];
|
||||
// Split the inherited contracts by commas to get the list of inherited contracts
|
||||
const inheritedContracts = inheritancePart.split(",").map(contract => `${contract.trim()}.sol`);
|
||||
|
||||
return inheritedContracts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getInheritedFunctions(sources: Record<string, any>, contractName: string) {
|
||||
const actualSources = getActualSourcesForContract(sources, contractName);
|
||||
const inheritedFunctions = {} as Record<string, any>;
|
||||
|
||||
for (const sourceContractName of actualSources) {
|
||||
const sourcePath = Object.keys(sources).find(key => key.includes(`/${sourceContractName}`));
|
||||
if (sourcePath) {
|
||||
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
|
||||
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
|
||||
for (const functionAbi of abi) {
|
||||
if (functionAbi.type === "function") {
|
||||
inheritedFunctions[functionAbi.name] = sourcePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedFunctions;
|
||||
}
|
||||
|
||||
function getContractDataFromDeployments() {
|
||||
if (!fs.existsSync(DEPLOYMENTS_DIR)) {
|
||||
throw Error("At least one other deployment script should exist to generate an actual contract.");
|
||||
}
|
||||
const output = {} as Record<string, any>;
|
||||
for (const chainName of getDirectories(DEPLOYMENTS_DIR)) {
|
||||
const chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
|
||||
const contracts = {} as Record<string, any>;
|
||||
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
|
||||
const { abi, address, metadata } = JSON.parse(
|
||||
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
|
||||
);
|
||||
const inheritedFunctions = getInheritedFunctions(JSON.parse(metadata).sources, contractName);
|
||||
contracts[contractName] = { address, abi, inheritedFunctions };
|
||||
}
|
||||
output[chainId] = contracts;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the TypeScript contract definition file based on the json output of the contract deployment scripts
|
||||
* This script should be run last.
|
||||
*/
|
||||
const generateTsAbis: DeployFunction = async function () {
|
||||
const TARGET_DIR = "../nextjs/contracts/";
|
||||
const allContractsData = getContractDataFromDeployments();
|
||||
|
||||
const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => {
|
||||
return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`;
|
||||
}, "");
|
||||
|
||||
if (!fs.existsSync(TARGET_DIR)) {
|
||||
fs.mkdirSync(TARGET_DIR);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${TARGET_DIR}deployedContracts.ts`,
|
||||
prettier.format(
|
||||
`${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n
|
||||
const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`,
|
||||
{
|
||||
parser: "typescript",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`);
|
||||
};
|
||||
|
||||
export default generateTsAbis;
|
||||
|
||||
// Tags are useful if you have multiple deploy files and only want to run one of them.
|
||||
// e.g. yarn deploy --tags generateTsAbis
|
||||
generateTsAbis.tags = ["generateTsAbis"];
|
||||
|
||||
generateTsAbis.runAtTheEnd = true;
|
||||
137
packages/hardhat/hardhat.config.ts
Normal file
137
packages/hardhat/hardhat.config.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-ethers";
|
||||
import "@nomicfoundation/hardhat-chai-matchers";
|
||||
import "@typechain/hardhat";
|
||||
import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
import "@nomicfoundation/hardhat-verify";
|
||||
import "hardhat-deploy";
|
||||
import "hardhat-deploy-ethers";
|
||||
|
||||
// If not set, it uses ours Alchemy's default API key.
|
||||
// You can get your own at https://dashboard.alchemyapi.io
|
||||
const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
|
||||
// If not set, it uses the hardhat account 0 private key.
|
||||
const deployerPrivateKey =
|
||||
process.env.DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
// If not set, it uses ours Etherscan default API key.
|
||||
const etherscanApiKey = process.env.ETHERSCAN_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
version: "0.8.17",
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
// https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultNetwork: "localhost",
|
||||
namedAccounts: {
|
||||
deployer: {
|
||||
// By default, it will take the first Hardhat account as the deployer
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
// View the networks that are pre-configured.
|
||||
// If the network you are looking for is not here you can add new network settings
|
||||
hardhat: {
|
||||
forking: {
|
||||
url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`,
|
||||
enabled: process.env.MAINNET_FORKING_ENABLED === "true",
|
||||
},
|
||||
},
|
||||
mainnet: {
|
||||
url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
sepolia: {
|
||||
url: `https://eth-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrum: {
|
||||
url: `https://arb-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
arbitrumSepolia: {
|
||||
url: `https://arb-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimism: {
|
||||
url: `https://opt-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
optimismSepolia: {
|
||||
url: `https://opt-sepolia.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygon: {
|
||||
url: `https://polygon-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonMumbai: {
|
||||
url: `https://polygon-mumbai.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvm: {
|
||||
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
polygonZkEvmTestnet: {
|
||||
url: `https://polygonzkevm-testnet.g.alchemy.com/v2/${providerApiKey}`,
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
gnosis: {
|
||||
url: "https://rpc.gnosischain.com",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
chiado: {
|
||||
url: "https://rpc.chiadochain.net",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
base: {
|
||||
url: "https://mainnet.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
baseSepolia: {
|
||||
url: "https://sepolia.base.org",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scrollSepolia: {
|
||||
url: "https://sepolia-rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
scroll: {
|
||||
url: "https://rpc.scroll.io",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
pgn: {
|
||||
url: "https://rpc.publicgoods.network",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
pgnTestnet: {
|
||||
url: "https://sepolia.publicgoods.network",
|
||||
accounts: [deployerPrivateKey],
|
||||
},
|
||||
},
|
||||
// configuration for harhdat-verify plugin
|
||||
etherscan: {
|
||||
apiKey: `${etherscanApiKey}`,
|
||||
},
|
||||
// configuration for etherscan-verify from hardhat-deploy plugin
|
||||
verify: {
|
||||
etherscan: {
|
||||
apiKey: `${etherscanApiKey}`,
|
||||
},
|
||||
},
|
||||
sourcify: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
56
packages/hardhat/package.json
Normal file
56
packages/hardhat/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@se-2/hardhat",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"account": "hardhat run scripts/listAccount.ts",
|
||||
"chain": "hardhat node --network hardhat --no-deploy",
|
||||
"compile": "hardhat compile",
|
||||
"deploy": "hardhat deploy",
|
||||
"fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
|
||||
"generate": "hardhat run scripts/generateAccount.ts",
|
||||
"flatten": "hardhat flatten",
|
||||
"lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts",
|
||||
"lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore",
|
||||
"format": "prettier --write ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts",
|
||||
"test": "REPORT_GAS=true hardhat test --network hardhat",
|
||||
"verify": "hardhat etherscan-verify",
|
||||
"hardhat-verify": "hardhat verify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethersproject/abi": "^5.7.0",
|
||||
"@ethersproject/providers": "^5.7.1",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
|
||||
"@nomicfoundation/hardhat-ethers": "^3.0.5",
|
||||
"@nomicfoundation/hardhat-network-helpers": "^1.0.6",
|
||||
"@nomicfoundation/hardhat-verify": "^2.0.3",
|
||||
"@typechain/ethers-v5": "^10.1.0",
|
||||
"@typechain/hardhat": "^9.1.0",
|
||||
"@types/eslint": "^8",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/prettier": "^2",
|
||||
"@types/qrcode": "^1",
|
||||
"@typescript-eslint/eslint-plugin": "latest",
|
||||
"@typescript-eslint/parser": "latest",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"ethers": "^6.10.0",
|
||||
"hardhat": "^2.19.4",
|
||||
"hardhat-deploy": "^0.11.45",
|
||||
"hardhat-deploy-ethers": "^0.4.1",
|
||||
"hardhat-gas-reporter": "^1.0.9",
|
||||
"prettier": "^2.8.4",
|
||||
"solidity-coverage": "^0.8.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typechain": "^8.1.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.8.1",
|
||||
"@typechain/ethers-v6": "^0.5.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"envfile": "^6.18.0",
|
||||
"qrcode": "^1.5.1"
|
||||
}
|
||||
}
|
||||
45
packages/hardhat/scripts/generateAccount.ts
Normal file
45
packages/hardhat/scripts/generateAccount.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
/**
|
||||
* Generate a new random private key and write it to the .env file
|
||||
*/
|
||||
const setNewEnvConfig = (existingEnvConfig = {}) => {
|
||||
console.log("👛 Generating new Wallet");
|
||||
const randomWallet = ethers.Wallet.createRandom();
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY: randomWallet.privateKey,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("📄 Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Generated wallet address:", randomWallet.address);
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// .env file exists
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
42
packages/hardhat/scripts/listAccount.ts
Normal file
42
packages/hardhat/scripts/listAccount.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { ethers, Wallet } from "ethers";
|
||||
import QRCode from "qrcode";
|
||||
import { config } from "hardhat";
|
||||
|
||||
async function main() {
|
||||
const privateKey = process.env.DEPLOYER_PRIVATE_KEY;
|
||||
|
||||
if (!privateKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` first");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get account from private key.
|
||||
const wallet = new Wallet(privateKey);
|
||||
const address = wallet.address;
|
||||
console.log(await QRCode.toString(address, { type: "terminal", small: true }));
|
||||
console.log("Public address:", address, "\n");
|
||||
|
||||
// Balance on each network
|
||||
const availableNetworks = config.networks;
|
||||
for (const networkName in availableNetworks) {
|
||||
try {
|
||||
const network = availableNetworks[networkName];
|
||||
if (!("url" in network)) continue;
|
||||
const provider = new ethers.JsonRpcProvider(network.url);
|
||||
await provider._detectNetwork();
|
||||
const balance = await provider.getBalance(address);
|
||||
console.log("--", networkName, "-- 📡");
|
||||
console.log(" balance:", +ethers.formatEther(balance));
|
||||
console.log(" nonce:", +(await provider.getTransactionCount(address)));
|
||||
} catch (e) {
|
||||
console.log("Can't connect to network", networkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
28
packages/hardhat/test/YourContract.ts
Normal file
28
packages/hardhat/test/YourContract.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "hardhat";
|
||||
import { YourContract } from "../typechain-types";
|
||||
|
||||
describe("YourContract", function () {
|
||||
// We define a fixture to reuse the same setup in every test.
|
||||
|
||||
let yourContract: YourContract;
|
||||
before(async () => {
|
||||
const [owner] = await ethers.getSigners();
|
||||
const yourContractFactory = await ethers.getContractFactory("YourContract");
|
||||
yourContract = (await yourContractFactory.deploy(owner.address)) as YourContract;
|
||||
await yourContract.waitForDeployment();
|
||||
});
|
||||
|
||||
describe("Deployment", function () {
|
||||
it("Should have the right message on deploy", async function () {
|
||||
expect(await yourContract.greeting()).to.equal("Building Unstoppable Apps!!!");
|
||||
});
|
||||
|
||||
it("Should allow setting a new message", async function () {
|
||||
const newGreeting = "Learn Scaffold-ETH 2! :)";
|
||||
|
||||
await yourContract.setGreeting(newGreeting);
|
||||
expect(await yourContract.greeting()).to.equal(newGreeting);
|
||||
});
|
||||
});
|
||||
});
|
||||
10
packages/hardhat/tsconfig.json
Normal file
10
packages/hardhat/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
13
packages/nextjs/.env.example
Normal file
13
packages/nextjs/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Template for NextJS environment variables.
|
||||
|
||||
# For local development, copy this file, rename it to .env.local, and fill in the values.
|
||||
# When deploying live, you'll need to store the vars in Vercel/System config.
|
||||
|
||||
# If not set, we provide default values (check `scaffold.config.ts`) so developers can start prototyping out of the box,
|
||||
# but we recommend getting your own API Keys for Production Apps.
|
||||
|
||||
# To access the values stored in this env file you can use: process.env.VARIABLENAME
|
||||
# You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side.
|
||||
# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
|
||||
NEXT_PUBLIC_ALCHEMY_API_KEY=
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
|
||||
11
packages/nextjs/.eslintignore
Normal file
11
packages/nextjs/.eslintignore
Normal file
@@ -0,0 +1,11 @@
|
||||
# folders
|
||||
.next
|
||||
node_modules/
|
||||
# files
|
||||
**/*.less
|
||||
**/*.css
|
||||
**/*.scss
|
||||
**/*.json
|
||||
**/*.png
|
||||
**/*.svg
|
||||
**/generated/**/*
|
||||
15
packages/nextjs/.eslintrc.json
Normal file
15
packages/nextjs/.eslintrc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["next/core-web-vitals", "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"@typescript-eslint/ban-ts-comment": ["off"],
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
36
packages/nextjs/.gitignore
vendored
Normal file
36
packages/nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
.vercel
|
||||
1
packages/nextjs/.npmrc
Normal file
1
packages/nextjs/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
strict-peer-dependencies = false
|
||||
8
packages/nextjs/.prettierrc.json
Normal file
8
packages/nextjs/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"importOrder": ["^react$", "^next/(.*)$", "<THIRD_PARTY_MODULES>", "^@heroicons/(.*)$", "^~~/(.*)$"],
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
type AddressCodeTabProps = {
|
||||
bytecode: string;
|
||||
assembly: string;
|
||||
};
|
||||
|
||||
export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => {
|
||||
const formattedAssembly = assembly.split(" ").join("\n");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
Bytecode
|
||||
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
|
||||
<pre className="px-5">
|
||||
<code className="whitespace-pre-wrap overflow-auto break-words">{bytecode}</code>
|
||||
</pre>
|
||||
</div>
|
||||
Opcodes
|
||||
<div className="mockup-code -indent-5 overflow-y-auto max-h-[500px]">
|
||||
<pre className="px-5">
|
||||
<code>{formattedAssembly}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BackButton } from "./BackButton";
|
||||
import { ContractTabs } from "./ContractTabs";
|
||||
import { Address, Balance } from "~~/components/scaffold-eth";
|
||||
|
||||
export const AddressComponent = ({
|
||||
address,
|
||||
contractData,
|
||||
}: {
|
||||
address: string;
|
||||
contractData: { bytecode: string; assembly: string } | null;
|
||||
}) => {
|
||||
return (
|
||||
<div className="m-10 mb-20">
|
||||
<div className="flex justify-start mb-5">
|
||||
<BackButton />
|
||||
</div>
|
||||
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-10">
|
||||
<div className="col-span-1 flex flex-col">
|
||||
<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">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Address address={address} format="long" />
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="font-bold text-sm">Balance:</span>
|
||||
<Balance address={address} className="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContractTabs address={address} contractData={contractData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Address } from "viem";
|
||||
import { useContractLogs } from "~~/hooks/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
export const AddressLogsTab = ({ address }: { address: Address }) => {
|
||||
const contractLogs = useContractLogs(address);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{contractLogs.map((log, i) => (
|
||||
<div key={i}>
|
||||
<strong>Log:</strong> {JSON.stringify(log, replacer, 2)}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Address, createPublicClient, http, toHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
export const AddressStorageTab = ({ address }: { address: Address }) => {
|
||||
const [storage, setStorage] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStorage = async () => {
|
||||
try {
|
||||
const storageData = [];
|
||||
let idx = 0;
|
||||
|
||||
while (true) {
|
||||
const storageAtPosition = await publicClient.getStorageAt({
|
||||
address: address,
|
||||
slot: toHex(idx),
|
||||
});
|
||||
|
||||
if (storageAtPosition === "0x" + "0".repeat(64)) break;
|
||||
|
||||
if (storageAtPosition) {
|
||||
storageData.push(storageAtPosition);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
setStorage(storageData);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch storage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStorage();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
{storage.length > 0 ? (
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{storage.map((data, i) => (
|
||||
<div key={i}>
|
||||
<strong>Storage Slot {i}:</strong> {data}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-lg">This contract does not have any variables.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const BackButton = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
|
||||
Back
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AddressCodeTab } from "./AddressCodeTab";
|
||||
import { AddressLogsTab } from "./AddressLogsTab";
|
||||
import { AddressStorageTab } from "./AddressStorageTab";
|
||||
import { PaginationButton } from "./PaginationButton";
|
||||
import { TransactionsTable } from "./TransactionsTable";
|
||||
import { createPublicClient, http } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
|
||||
|
||||
type AddressCodeTabProps = {
|
||||
bytecode: string;
|
||||
assembly: string;
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
address: string;
|
||||
contractData: AddressCodeTabProps | null;
|
||||
};
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
export const ContractTabs = ({ address, contractData }: PageProps) => {
|
||||
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage } = useFetchBlocks();
|
||||
const [activeTab, setActiveTab] = useState("transactions");
|
||||
const [isContract, setIsContract] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIsContract = async () => {
|
||||
const contractCode = await publicClient.getBytecode({ address: address });
|
||||
setIsContract(contractCode !== undefined && contractCode !== "0x");
|
||||
};
|
||||
|
||||
checkIsContract();
|
||||
}, [address]);
|
||||
|
||||
const filteredBlocks = blocks.filter(block =>
|
||||
block.transactions.some(tx => {
|
||||
if (typeof tx === "string") {
|
||||
return false;
|
||||
}
|
||||
return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase();
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isContract && (
|
||||
<div className="tabs tabs-lifted w-min">
|
||||
<button
|
||||
className={`tab ${activeTab === "transactions" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("transactions")}
|
||||
>
|
||||
Transactions
|
||||
</button>
|
||||
<button className={`tab ${activeTab === "code" ? "tab-active" : ""}`} onClick={() => setActiveTab("code")}>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === "storage" ? "tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("storage")}
|
||||
>
|
||||
Storage
|
||||
</button>
|
||||
<button className={`tab ${activeTab === "logs" ? "tab-active" : ""}`} onClick={() => setActiveTab("logs")}>
|
||||
Logs
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "transactions" && (
|
||||
<div className="pt-4">
|
||||
<TransactionsTable blocks={filteredBlocks} transactionReceipts={transactionReceipts} />
|
||||
<PaginationButton
|
||||
currentPage={currentPage}
|
||||
totalItems={Number(totalBlocks)}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "code" && contractData && (
|
||||
<AddressCodeTab bytecode={contractData.bytecode} assembly={contractData.assembly} />
|
||||
)}
|
||||
{activeTab === "storage" && <AddressStorageTab address={address} />}
|
||||
{activeTab === "logs" && <AddressLogsTab address={address} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type PaginationButtonProps = {
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
setCurrentPage: (page: number) => void;
|
||||
};
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => {
|
||||
const isPrevButtonDisabled = currentPage === 0;
|
||||
const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE);
|
||||
|
||||
const prevButtonClass = isPrevButtonDisabled ? "bg-gray-200 cursor-default" : "btn btn-primary";
|
||||
const nextButtonClass = isNextButtonDisabled ? "bg-gray-200 cursor-default" : "btn btn-primary";
|
||||
|
||||
if (isNextButtonDisabled && isPrevButtonDisabled) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-5 justify-end flex gap-3 mx-5">
|
||||
<button
|
||||
className={`btn btn-sm ${prevButtonClass}`}
|
||||
disabled={isPrevButtonDisabled}
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="self-center text-primary-content font-medium">Page {currentPage + 1}</span>
|
||||
<button
|
||||
className={`btn btn-sm ${nextButtonClass}`}
|
||||
disabled={isNextButtonDisabled}
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isAddress, isHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { usePublicClient } from "wagmi";
|
||||
|
||||
export const SearchBar = () => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const client = usePublicClient({ chainId: hardhat.id });
|
||||
|
||||
const handleSearch = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (isHex(searchInput)) {
|
||||
try {
|
||||
const tx = await client.getTransaction({ hash: searchInput });
|
||||
if (tx) {
|
||||
router.push(`/blockexplorer/transaction/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAddress(searchInput)) {
|
||||
router.push(`/blockexplorer/address/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearch} className="flex items-center justify-end mb-5 space-x-3 mx-5">
|
||||
<input
|
||||
className="border-primary bg-base-100 text-base-content p-2 mr-2 w-full md:w-1/2 lg:w-1/3 rounded-md shadow-md focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
type="text"
|
||||
value={searchInput}
|
||||
placeholder="Search by hash or address"
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-sm btn-primary" type="submit">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const TransactionHash = ({ hash }: { hash: string }) => {
|
||||
const [addressCopied, setAddressCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Link href={`/blockexplorer/transaction/${hash}`}>
|
||||
{hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
|
||||
</Link>
|
||||
{addressCopied ? (
|
||||
<CheckCircleIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<CopyToClipboard
|
||||
text={hash as string}
|
||||
onCopy={() => {
|
||||
setAddressCopied(true);
|
||||
setTimeout(() => {
|
||||
setAddressCopied(false);
|
||||
}, 800);
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicateIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TransactionHash } from "./TransactionHash";
|
||||
import { formatEther } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { TransactionWithFunction } from "~~/utils/scaffold-eth";
|
||||
import { TransactionsTableProps } from "~~/utils/scaffold-eth/";
|
||||
|
||||
export const TransactionsTable = ({ blocks, transactionReceipts }: TransactionsTableProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 md:px-0">
|
||||
<div className="overflow-x-auto w-full shadow-2xl rounded-xl">
|
||||
<table className="table text-xl bg-base-100 table-zebra w-full md:table-md table-sm">
|
||||
<thead>
|
||||
<tr className="rounded-xl text-sm text-base-content">
|
||||
<th className="bg-primary">Transaction Hash</th>
|
||||
<th className="bg-primary">Function Called</th>
|
||||
<th className="bg-primary">Block Number</th>
|
||||
<th className="bg-primary">Time Mined</th>
|
||||
<th className="bg-primary">From</th>
|
||||
<th className="bg-primary">To</th>
|
||||
<th className="bg-primary text-end">Value ({targetNetwork.nativeCurrency.symbol})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{blocks.map(block =>
|
||||
(block.transactions as TransactionWithFunction[]).map(tx => {
|
||||
const receipt = transactionReceipts[tx.hash];
|
||||
const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString();
|
||||
const functionCalled = tx.input.substring(0, 10);
|
||||
|
||||
return (
|
||||
<tr key={tx.hash} className="hover text-sm">
|
||||
<td className="w-1/12 md:py-4">
|
||||
<TransactionHash hash={tx.hash} />
|
||||
</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
{tx.functionName === "0x" ? "" : <span className="mr-1">{tx.functionName}</span>}
|
||||
{functionCalled !== "0x" && (
|
||||
<span className="badge badge-primary font-bold text-xs">{functionCalled}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="w-1/12 md:py-4">{block.number?.toString()}</td>
|
||||
<td className="w-2/1 md:py-4">{timeMined}</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
<Address address={tx.from} size="sm" />
|
||||
</td>
|
||||
<td className="w-2/12 md:py-4">
|
||||
{!receipt?.contractAddress ? (
|
||||
tx.to && <Address address={tx.to} size="sm" />
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Address address={receipt.contractAddress} size="sm" />
|
||||
<small className="absolute top-4 left-4">(Contract Creation)</small>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right md:py-4">
|
||||
{formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./SearchBar";
|
||||
export * from "./BackButton";
|
||||
export * from "./AddressCodeTab";
|
||||
export * from "./TransactionHash";
|
||||
export * from "./ContractTabs";
|
||||
export * from "./PaginationButton";
|
||||
export * from "./TransactionsTable";
|
||||
85
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
85
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
|
||||
import deployedContracts from "~~/contracts/deployedContracts";
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type PageProps = {
|
||||
params: { address: string };
|
||||
};
|
||||
|
||||
async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath: string) {
|
||||
const buildInfoFiles = fs.readdirSync(buildInfoDirectory);
|
||||
let bytecode = "";
|
||||
let assembly = "";
|
||||
|
||||
for (let i = 0; i < buildInfoFiles.length; i++) {
|
||||
const filePath = path.join(buildInfoDirectory, buildInfoFiles[i]);
|
||||
|
||||
const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
|
||||
if (buildInfo.output.contracts[contractPath]) {
|
||||
for (const contract in buildInfo.output.contracts[contractPath]) {
|
||||
bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object;
|
||||
assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytecode && assembly) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { bytecode, assembly };
|
||||
}
|
||||
|
||||
const getContractData = async (address: string) => {
|
||||
const contracts = deployedContracts as GenericContractsDeclaration | null;
|
||||
const chainId = hardhat.id;
|
||||
let contractPath = "";
|
||||
|
||||
const buildInfoDirectory = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"hardhat",
|
||||
"artifacts",
|
||||
"build-info",
|
||||
);
|
||||
|
||||
if (!fs.existsSync(buildInfoDirectory)) {
|
||||
throw new Error(`Directory ${buildInfoDirectory} not found.`);
|
||||
}
|
||||
|
||||
const deployedContractsOnChain = contracts ? contracts[chainId] : {};
|
||||
for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) {
|
||||
if (contractInfo.address.toLowerCase() === address.toLowerCase()) {
|
||||
contractPath = `contracts/${contractName}.sol`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contractPath) {
|
||||
// No contract found at this address
|
||||
return null;
|
||||
}
|
||||
|
||||
const { bytecode, assembly } = await fetchByteCodeAndAssembly(buildInfoDirectory, contractPath);
|
||||
|
||||
return { bytecode, assembly };
|
||||
};
|
||||
|
||||
const AddressPage = async ({ params }: PageProps) => {
|
||||
const address = params?.address as string;
|
||||
const contractData: { bytecode: string; assembly: string } | null = await getContractData(address);
|
||||
return <AddressComponent address={address} contractData={contractData} />;
|
||||
};
|
||||
|
||||
export default AddressPage;
|
||||
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Block Explorer",
|
||||
description: "Block Explorer created with 🏗 Scaffold-ETH 2",
|
||||
});
|
||||
|
||||
const BlockExplorerLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default BlockExplorerLayout;
|
||||
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { PaginationButton, SearchBar, TransactionsTable } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BlockExplorer: NextPage = () => {
|
||||
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const [isLocalNetwork, setIsLocalNetwork] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id !== hardhat.id) {
|
||||
setIsLocalNetwork(false);
|
||||
}
|
||||
}, [targetNetwork.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id === hardhat.id && error) {
|
||||
setHasError(true);
|
||||
}
|
||||
}, [targetNetwork.id, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLocalNetwork) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">
|
||||
<code className="italic bg-base-300 text-base font-bold"> targeNetwork </code> is not localhost
|
||||
</p>
|
||||
<p className="m-0">
|
||||
- You are on <code className="italic bg-base-300 text-base font-bold">{targetNetwork.name}</code> .This
|
||||
block explorer is only for <code className="italic bg-base-300 text-base font-bold">localhost</code>.
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- You can use{" "}
|
||||
<a className="text-accent" href={targetNetwork.blockExplorers?.default.url}>
|
||||
{targetNetwork.blockExplorers?.default.name}
|
||||
</a>{" "}
|
||||
instead
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isLocalNetwork,
|
||||
targetNetwork.blockExplorers?.default.name,
|
||||
targetNetwork.blockExplorers?.default.url,
|
||||
targetNetwork.name,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">Cannot connect to local provider</p>
|
||||
<p className="m-0">
|
||||
- Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ?
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code>
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
}, [hasError]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto my-10">
|
||||
<SearchBar />
|
||||
<TransactionsTable blocks={blocks} transactionReceipts={transactionReceipts} />
|
||||
<PaginationButton currentPage={currentPage} totalItems={Number(totalBlocks)} setCurrentPage={setCurrentPage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockExplorer;
|
||||
153
packages/nextjs/app/blockexplorer/transaction/[txHash]/page.tsx
Normal file
153
packages/nextjs/app/blockexplorer/transaction/[txHash]/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { NextPage } from "next";
|
||||
import { Hash, Transaction, TransactionReceipt, formatEther, formatUnits } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { usePublicClient } from "wagmi";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { decodeTransactionData, getFunctionDetails } from "~~/utils/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
type PageProps = {
|
||||
params: { txHash?: Hash };
|
||||
};
|
||||
const TransactionPage: NextPage<PageProps> = ({ params }: PageProps) => {
|
||||
const client = usePublicClient({ chainId: hardhat.id });
|
||||
const txHash = params?.txHash as Hash;
|
||||
const router = useRouter();
|
||||
const [transaction, setTransaction] = useState<Transaction>();
|
||||
const [receipt, setReceipt] = useState<TransactionReceipt>();
|
||||
const [functionCalled, setFunctionCalled] = useState<string>();
|
||||
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
useEffect(() => {
|
||||
if (txHash) {
|
||||
const fetchTransaction = async () => {
|
||||
const tx = await client.getTransaction({ hash: txHash });
|
||||
const receipt = await client.getTransactionReceipt({ hash: txHash });
|
||||
|
||||
const transactionWithDecodedData = decodeTransactionData(tx);
|
||||
setTransaction(transactionWithDecodedData);
|
||||
setReceipt(receipt);
|
||||
|
||||
const functionCalled = transactionWithDecodedData.input.substring(0, 10);
|
||||
setFunctionCalled(functionCalled);
|
||||
};
|
||||
|
||||
fetchTransaction();
|
||||
}
|
||||
}, [client, txHash]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto mt-10 mb-20 px-10 md:px-0">
|
||||
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
|
||||
Back
|
||||
</button>
|
||||
{transaction ? (
|
||||
<div className="overflow-x-auto">
|
||||
<h2 className="text-3xl font-bold mb-4 text-center text-primary-content">Transaction Details</h2>{" "}
|
||||
<table className="table rounded-lg bg-base-100 w-full shadow-lg md:table-lg table-md">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Transaction Hash:</strong>
|
||||
</td>
|
||||
<td>{transaction.hash}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Block Number:</strong>
|
||||
</td>
|
||||
<td>{Number(transaction.blockNumber)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>From:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<Address address={transaction.from} format="long" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>To:</strong>
|
||||
</td>
|
||||
<td>
|
||||
{!receipt?.contractAddress ? (
|
||||
transaction.to && <Address address={transaction.to} format="long" />
|
||||
) : (
|
||||
<span>
|
||||
Contract Creation:
|
||||
<Address address={receipt.contractAddress} format="long" />
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Value:</strong>
|
||||
</td>
|
||||
<td>
|
||||
{formatEther(transaction.value)} {targetNetwork.nativeCurrency.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Function called:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div className="w-full md:max-w-[600px] lg:max-w-[800px] overflow-x-auto whitespace-nowrap">
|
||||
{functionCalled === "0x" ? (
|
||||
"This transaction did not call any function."
|
||||
) : (
|
||||
<>
|
||||
<span className="mr-2">{getFunctionDetails(transaction)}</span>
|
||||
<span className="badge badge-primary font-bold">{functionCalled}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Gas Price:</strong>
|
||||
</td>
|
||||
<td>{formatUnits(transaction.gasPrice || 0n, 9)} Gwei</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Data:</strong>
|
||||
</td>
|
||||
<td className="form-control">
|
||||
<textarea readOnly value={transaction.input} className="p-0 textarea-primary bg-inherit h-[150px]" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Logs:</strong>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{receipt?.logs?.map((log, i) => (
|
||||
<li key={i}>
|
||||
<strong>Log {i} topics:</strong> {JSON.stringify(log.topics, replacer, 2)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-2xl text-base-content">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
66
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
66
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
|
||||
import { ContractUI } from "~~/app/debug/_components/contract";
|
||||
import { ContractName } from "~~/utils/scaffold-eth/contract";
|
||||
import { getAllContracts } from "~~/utils/scaffold-eth/contractsData";
|
||||
|
||||
const selectedContractStorageKey = "scaffoldEth2.selectedContract";
|
||||
const contractsData = getAllContracts();
|
||||
const contractNames = Object.keys(contractsData) as ContractName[];
|
||||
|
||||
export function DebugContracts() {
|
||||
const [selectedContract, setSelectedContract] = useLocalStorage<ContractName>(
|
||||
selectedContractStorageKey,
|
||||
contractNames[0],
|
||||
{ initializeWithValue: false },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contractNames.includes(selectedContract)) {
|
||||
setSelectedContract(contractNames[0]);
|
||||
}
|
||||
}, [selectedContract, setSelectedContract]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6 lg:gap-y-8 py-8 lg:py-12 justify-center items-center">
|
||||
{contractNames.length === 0 ? (
|
||||
<p className="text-3xl mt-14">No contracts found!</p>
|
||||
) : (
|
||||
<>
|
||||
{contractNames.length > 1 && (
|
||||
<div className="flex flex-row gap-2 w-full max-w-7xl pb-1 px-6 lg:px-10 flex-wrap">
|
||||
{contractNames.map(contractName => (
|
||||
<button
|
||||
className={`btn btn-secondary btn-sm font-light hover:border-transparent ${
|
||||
contractName === selectedContract
|
||||
? "bg-base-300 hover:bg-base-300 no-animation"
|
||||
: "bg-base-100 hover:bg-secondary"
|
||||
}`}
|
||||
key={contractName}
|
||||
onClick={() => setSelectedContract(contractName)}
|
||||
>
|
||||
{contractName}
|
||||
{contractsData[contractName].external && (
|
||||
<span className="tooltip tooltip-top tooltip-accent" data-tip="External contract">
|
||||
<BarsArrowUpIcon className="h-4 w-4 cursor-pointer" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{contractNames.map(contractName => (
|
||||
<ContractUI
|
||||
key={contractName}
|
||||
contractName={contractName}
|
||||
className={contractName === selectedContract ? "" : "hidden"}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Tuple } from "./Tuple";
|
||||
import { TupleArray } from "./TupleArray";
|
||||
import { AbiParameter } from "abitype";
|
||||
import {
|
||||
AddressInput,
|
||||
Bytes32Input,
|
||||
BytesInput,
|
||||
InputBase,
|
||||
IntegerInput,
|
||||
IntegerVariant,
|
||||
} from "~~/components/scaffold-eth";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type ContractInputProps = {
|
||||
setForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
form: Record<string, any> | undefined;
|
||||
stateObjectKey: string;
|
||||
paramType: AbiParameter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic Input component to handle input's based on their function param type
|
||||
*/
|
||||
export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => {
|
||||
const inputProps = {
|
||||
name: stateObjectKey,
|
||||
value: form?.[stateObjectKey],
|
||||
placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type,
|
||||
onChange: (value: any) => {
|
||||
setForm(form => ({ ...form, [stateObjectKey]: value }));
|
||||
},
|
||||
};
|
||||
|
||||
const renderInput = () => {
|
||||
switch (paramType.type) {
|
||||
case "address":
|
||||
return <AddressInput {...inputProps} />;
|
||||
case "bytes32":
|
||||
return <Bytes32Input {...inputProps} />;
|
||||
case "bytes":
|
||||
return <BytesInput {...inputProps} />;
|
||||
case "string":
|
||||
return <InputBase {...inputProps} />;
|
||||
case "tuple":
|
||||
return (
|
||||
<Tuple
|
||||
setParentForm={setForm}
|
||||
parentForm={form}
|
||||
abiTupleParameter={paramType as AbiParameterTuple}
|
||||
parentStateObjectKey={stateObjectKey}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// Handling 'int' types and 'tuple[]' types
|
||||
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
|
||||
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
|
||||
} else if (paramType.type.startsWith("tuple[")) {
|
||||
return (
|
||||
<TupleArray
|
||||
setParentForm={setForm}
|
||||
parentForm={form}
|
||||
abiTupleParameter={paramType as AbiParameterTuple}
|
||||
parentStateObjectKey={stateObjectKey}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <InputBase {...inputProps} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 w-full">
|
||||
<div className="flex items-center ml-2">
|
||||
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
|
||||
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
|
||||
</div>
|
||||
{renderInput()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { ReadOnlyFunctionForm } from "~~/app/debug/_components/contract";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract<ContractName> }) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isQueryableWithParams =
|
||||
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0;
|
||||
return isQueryableWithParams;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No read methods</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
|
||||
<ReadOnlyFunctionForm
|
||||
abi={deployedContractData.abi as Abi}
|
||||
contractAddress={deployedContractData.address}
|
||||
abiFunction={fn}
|
||||
key={fn.name}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal file
104
packages/nextjs/app/debug/_components/contract/ContractUI.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
// @refresh reset
|
||||
import { useReducer } from "react";
|
||||
import { ContractReadMethods } from "./ContractReadMethods";
|
||||
import { ContractVariables } from "./ContractVariables";
|
||||
import { ContractWriteMethods } from "./ContractWriteMethods";
|
||||
import { Address, Balance } from "~~/components/scaffold-eth";
|
||||
import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { ContractName } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type ContractUIProps = {
|
||||
contractName: ContractName;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component to interface with deployed contracts.
|
||||
**/
|
||||
export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => {
|
||||
const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName);
|
||||
const networkColor = useNetworkColor();
|
||||
|
||||
if (deployedContractLoading) {
|
||||
return (
|
||||
<div className="mt-14">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!deployedContractData) {
|
||||
return (
|
||||
<p className="text-3xl mt-14">
|
||||
{`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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}`}>
|
||||
<div className="col-span-5 grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
<div className="col-span-1 flex flex-col">
|
||||
<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">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-bold">{contractName}</span>
|
||||
<Address address={deployedContractData.address} />
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="font-bold text-sm">Balance:</span>
|
||||
<Balance address={deployedContractData.address} className="px-0 h-1.5 min-h-[0.375rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{targetNetwork && (
|
||||
<p className="my-0 text-sm">
|
||||
<span className="font-bold">Network</span>:{" "}
|
||||
<span style={{ color: networkColor }}>{targetNetwork.name}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-base-300 rounded-3xl px-6 lg:px-8 py-4 shadow-lg shadow-base-300">
|
||||
<ContractVariables
|
||||
refreshDisplayVariables={refreshDisplayVariables}
|
||||
deployedContractData={deployedContractData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-2 flex flex-col gap-6">
|
||||
<div className="z-10">
|
||||
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
|
||||
<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">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<p className="my-0 text-sm">Read</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 divide-y divide-base-300">
|
||||
<ContractReadMethods deployedContractData={deployedContractData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10">
|
||||
<div className="bg-base-100 rounded-3xl shadow-md shadow-secondary border border-base-300 flex flex-col mt-10 relative">
|
||||
<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">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<p className="my-0 text-sm">Write</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 divide-y divide-base-300">
|
||||
<ContractWriteMethods
|
||||
deployedContractData={deployedContractData}
|
||||
onChange={triggerRefreshDisplayVariables}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { DisplayVariable } from "./DisplayVariable";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractVariables = ({
|
||||
refreshDisplayVariables,
|
||||
deployedContractData,
|
||||
}: {
|
||||
refreshDisplayVariables: boolean;
|
||||
deployedContractData: Contract<ContractName>;
|
||||
}) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isQueryableWithNoParams =
|
||||
(fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
|
||||
return isQueryableWithNoParams;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No contract variables</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }) => (
|
||||
<DisplayVariable
|
||||
abi={deployedContractData.abi as Abi}
|
||||
abiFunction={fn}
|
||||
contractAddress={deployedContractData.address}
|
||||
key={fn.name}
|
||||
refreshDisplayVariables={refreshDisplayVariables}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { WriteOnlyFunctionForm } from "~~/app/debug/_components/contract";
|
||||
import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
export const ContractWriteMethods = ({
|
||||
onChange,
|
||||
deployedContractData,
|
||||
}: {
|
||||
onChange: () => void;
|
||||
deployedContractData: Contract<ContractName>;
|
||||
}) => {
|
||||
if (!deployedContractData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const functionsToDisplay = (
|
||||
(deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]
|
||||
)
|
||||
.filter(fn => {
|
||||
const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure";
|
||||
return isWriteableFunction;
|
||||
})
|
||||
.map(fn => {
|
||||
return {
|
||||
fn,
|
||||
inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1));
|
||||
|
||||
if (!functionsToDisplay.length) {
|
||||
return <>No write methods</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{functionsToDisplay.map(({ fn, inheritedFrom }, idx) => (
|
||||
<WriteOnlyFunctionForm
|
||||
abi={deployedContractData.abi as Abi}
|
||||
key={`${fn.name}-${idx}}`}
|
||||
abiFunction={fn}
|
||||
onChange={onChange}
|
||||
contractAddress={deployedContractData.address}
|
||||
inheritedFrom={inheritedFrom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { displayTxResult } from "./utilsDisplay";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address } from "viem";
|
||||
import { useContractRead } from "wagmi";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
import { useAnimationConfig } from "~~/hooks/scaffold-eth";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
type DisplayVariableProps = {
|
||||
contractAddress: Address;
|
||||
abiFunction: AbiFunction;
|
||||
refreshDisplayVariables: boolean;
|
||||
inheritedFrom?: string;
|
||||
abi: Abi;
|
||||
};
|
||||
|
||||
export const DisplayVariable = ({
|
||||
contractAddress,
|
||||
abiFunction,
|
||||
refreshDisplayVariables,
|
||||
abi,
|
||||
inheritedFrom,
|
||||
}: DisplayVariableProps) => {
|
||||
const {
|
||||
data: result,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useContractRead({
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
onError: error => {
|
||||
notification.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { showAnimation } = useAnimationConfig(result);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [refetch, refreshDisplayVariables]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1 pb-2">
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium text-lg mb-0 break-all">{abiFunction.name}</h3>
|
||||
<button className="btn btn-ghost btn-xs" onClick={async () => await refetch()}>
|
||||
{isFetching ? (
|
||||
<span className="loading loading-spinner loading-xs"></span>
|
||||
) : (
|
||||
<ArrowPathIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</div>
|
||||
<div className="text-gray-500 font-medium flex flex-col items-start">
|
||||
<div>
|
||||
<div
|
||||
className={`break-all block transition bg-transparent ${
|
||||
showAnimation ? "bg-warning rounded-sm animate-pulse-fast" : ""
|
||||
}`}
|
||||
>
|
||||
{displayTxResult(result)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export const InheritanceTooltip = ({ inheritedFrom }: { inheritedFrom?: string }) => (
|
||||
<>
|
||||
{inheritedFrom && (
|
||||
<span
|
||||
className="tooltip tooltip-top tooltip-accent px-2 md:break-normal"
|
||||
data-tip={`Inherited from: ${inheritedFrom}`}
|
||||
>
|
||||
<InformationCircleIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address } from "viem";
|
||||
import { useContractRead } from "wagmi";
|
||||
import {
|
||||
ContractInput,
|
||||
displayTxResult,
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
transformAbiFunction,
|
||||
} from "~~/app/debug/_components/contract";
|
||||
import { getParsedError, notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
type ReadOnlyFunctionFormProps = {
|
||||
contractAddress: Address;
|
||||
abiFunction: AbiFunction;
|
||||
inheritedFrom?: string;
|
||||
abi: Abi;
|
||||
};
|
||||
|
||||
export const ReadOnlyFunctionForm = ({
|
||||
contractAddress,
|
||||
abiFunction,
|
||||
inheritedFrom,
|
||||
abi,
|
||||
}: ReadOnlyFunctionFormProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
|
||||
const [result, setResult] = useState<unknown>();
|
||||
|
||||
const { isFetching, refetch } = useContractRead({
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
args: getParsedContractFunctionArgs(form),
|
||||
enabled: false,
|
||||
onError: (error: any) => {
|
||||
const parsedErrror = getParsedError(error);
|
||||
notification.error(parsedErrror);
|
||||
},
|
||||
});
|
||||
|
||||
const transformedFunction = transformAbiFunction(abiFunction);
|
||||
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
return (
|
||||
<ContractInput
|
||||
key={key}
|
||||
setForm={updatedFormValue => {
|
||||
setResult(undefined);
|
||||
setForm(updatedFormValue);
|
||||
}}
|
||||
form={form}
|
||||
stateObjectKey={key}
|
||||
paramType={input}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-1">
|
||||
<p className="font-medium my-0 break-words">
|
||||
{abiFunction.name}
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</p>
|
||||
{inputElements}
|
||||
<div className="flex justify-between gap-2 flex-wrap">
|
||||
<div className="flex-grow w-4/5">
|
||||
{result !== null && result !== undefined && (
|
||||
<div className="bg-secondary rounded-3xl text-sm px-4 py-1.5 break-words">
|
||||
<p className="font-bold m-0 mb-1">Result:</p>
|
||||
<pre className="whitespace-pre-wrap break-words">{displayTxResult(result)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={async () => {
|
||||
const { data } = await refetch();
|
||||
setResult(data);
|
||||
}}
|
||||
disabled={isFetching}
|
||||
>
|
||||
{isFetching && <span className="loading loading-spinner loading-xs"></span>}
|
||||
Read 📡
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal file
44
packages/nextjs/app/debug/_components/contract/Tuple.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { ContractInput } from "./ContractInput";
|
||||
import { getFunctionInputKey, getInitalTupleFormState } from "./utilsContract";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type TupleProps = {
|
||||
abiTupleParameter: AbiParameterTuple;
|
||||
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
parentStateObjectKey: string;
|
||||
parentForm: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleFormState(abiTupleParameter));
|
||||
|
||||
useEffect(() => {
|
||||
const values = Object.values(form);
|
||||
const argsStruct: Record<string, any> = {};
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
|
||||
});
|
||||
|
||||
setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(form, replacer)]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
|
||||
<input type="checkbox" className="min-h-fit peer" />
|
||||
<div className="collapse-title p-0 min-h-fit peer-checked:mb-2 text-primary-content/50">
|
||||
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
|
||||
</div>
|
||||
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
|
||||
{abiTupleParameter?.components?.map((param, index) => {
|
||||
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
|
||||
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
139
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal file
139
packages/nextjs/app/debug/_components/contract/TupleArray.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { ContractInput } from "./ContractInput";
|
||||
import { getFunctionInputKey, getInitalTupleArrayFormState } from "./utilsContract";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
type TupleArrayProps = {
|
||||
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
|
||||
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
parentStateObjectKey: string;
|
||||
parentForm: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleArrayFormState(abiTupleParameter));
|
||||
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
|
||||
abiTupleParameter.components,
|
||||
]);
|
||||
|
||||
const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;
|
||||
|
||||
useEffect(() => {
|
||||
// Extract and group fields based on index prefix
|
||||
const groupedFields = Object.keys(form).reduce((acc, key) => {
|
||||
const [indexPrefix, ...restArray] = key.split("_");
|
||||
const componentName = restArray.join("_");
|
||||
if (!acc[indexPrefix]) {
|
||||
acc[indexPrefix] = {};
|
||||
}
|
||||
acc[indexPrefix][componentName] = form[key];
|
||||
return acc;
|
||||
}, {} as Record<string, Record<string, any>>);
|
||||
|
||||
let argsArray: Array<Record<string, any>> = [];
|
||||
|
||||
Object.keys(groupedFields).forEach(key => {
|
||||
const currentKeyValues = Object.values(groupedFields[key]);
|
||||
|
||||
const argsStruct: Record<string, any> = {};
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
|
||||
});
|
||||
|
||||
argsArray.push(argsStruct);
|
||||
});
|
||||
|
||||
if (depth > 1) {
|
||||
argsArray = argsArray.map(args => {
|
||||
return args[abiTupleParameter.components[0].name || "tuple"];
|
||||
});
|
||||
}
|
||||
|
||||
setParentForm(parentForm => {
|
||||
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(form, replacer)]);
|
||||
|
||||
const addInput = () => {
|
||||
setAdditionalInputs(previousValue => {
|
||||
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];
|
||||
|
||||
// Add the new inputs to the form
|
||||
setForm(form => {
|
||||
const newForm = { ...form };
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
|
||||
component,
|
||||
componentIndex,
|
||||
);
|
||||
newForm[key] = "";
|
||||
});
|
||||
return newForm;
|
||||
});
|
||||
|
||||
return newAdditionalInputs;
|
||||
});
|
||||
};
|
||||
|
||||
const removeInput = () => {
|
||||
// Remove the last inputs from the form
|
||||
setForm(form => {
|
||||
const newForm = { ...form };
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
|
||||
component,
|
||||
componentIndex,
|
||||
);
|
||||
delete newForm[key];
|
||||
});
|
||||
return newForm;
|
||||
});
|
||||
setAdditionalInputs(inputs => inputs.slice(0, -1));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="collapse collapse-arrow bg-base-200 pl-4 py-1.5 border-2 border-secondary">
|
||||
<input type="checkbox" className="min-h-fit peer" />
|
||||
<div className="collapse-title p-0 min-h-fit peer-checked:mb-1 text-primary-content/50">
|
||||
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
|
||||
</div>
|
||||
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
|
||||
{additionalInputs.map((additionalInput, additionalIndex) => (
|
||||
<div key={additionalIndex} className="space-y-1">
|
||||
<span className="badge bg-base-300 badge-sm">
|
||||
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
{additionalInput.map((param, index) => {
|
||||
const key = getFunctionInputKey(
|
||||
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
|
||||
param,
|
||||
index,
|
||||
);
|
||||
return (
|
||||
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex space-x-2">
|
||||
<button className="btn btn-sm btn-secondary" onClick={addInput}>
|
||||
+
|
||||
</button>
|
||||
{additionalInputs.length > 0 && (
|
||||
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
|
||||
-
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal file
48
packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { TransactionReceipt } from "viem";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { displayTxResult } from "~~/app/debug/_components/contract";
|
||||
|
||||
export const TxReceipt = (
|
||||
txResult: string | number | bigint | Record<string, any> | TransactionReceipt | undefined,
|
||||
) => {
|
||||
const [txResultCopied, setTxResultCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0">
|
||||
<div className="mt-1 pl-2">
|
||||
{txResultCopied ? (
|
||||
<CheckCircleIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<CopyToClipboard
|
||||
text={displayTxResult(txResult) as string}
|
||||
onCopy={() => {
|
||||
setTxResultCopied(true);
|
||||
setTimeout(() => {
|
||||
setTxResultCopied(false);
|
||||
}, 800);
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicateIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-wrap collapse collapse-arrow">
|
||||
<input type="checkbox" className="min-h-0 peer" />
|
||||
<div className="collapse-title text-sm min-h-0 py-1.5 pl-1">
|
||||
<strong>Transaction Receipt</strong>
|
||||
</div>
|
||||
<div className="collapse-content overflow-auto bg-secondary rounded-t-none rounded-3xl">
|
||||
<pre className="text-xs pt-4">{displayTxResult(txResult)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { InheritanceTooltip } from "./InheritanceTooltip";
|
||||
import { Abi, AbiFunction } from "abitype";
|
||||
import { Address, TransactionReceipt } from "viem";
|
||||
import { useContractWrite, useNetwork, useWaitForTransaction } from "wagmi";
|
||||
import {
|
||||
ContractInput,
|
||||
TxReceipt,
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
transformAbiFunction,
|
||||
} from "~~/app/debug/_components/contract";
|
||||
import { IntegerInput } from "~~/components/scaffold-eth";
|
||||
import { useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
|
||||
type WriteOnlyFunctionFormProps = {
|
||||
abi: Abi;
|
||||
abiFunction: AbiFunction;
|
||||
onChange: () => void;
|
||||
contractAddress: Address;
|
||||
inheritedFrom?: string;
|
||||
};
|
||||
|
||||
export const WriteOnlyFunctionForm = ({
|
||||
abi,
|
||||
abiFunction,
|
||||
onChange,
|
||||
contractAddress,
|
||||
inheritedFrom,
|
||||
}: WriteOnlyFunctionFormProps) => {
|
||||
const [form, setForm] = useState<Record<string, any>>(() => getInitialFormState(abiFunction));
|
||||
const [txValue, setTxValue] = useState<string | bigint>("");
|
||||
const { chain } = useNetwork();
|
||||
const writeTxn = useTransactor();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const writeDisabled = !chain || chain?.id !== targetNetwork.id;
|
||||
|
||||
const {
|
||||
data: result,
|
||||
isLoading,
|
||||
writeAsync,
|
||||
} = useContractWrite({
|
||||
address: contractAddress,
|
||||
functionName: abiFunction.name,
|
||||
abi: abi,
|
||||
args: getParsedContractFunctionArgs(form),
|
||||
});
|
||||
|
||||
const handleWrite = async () => {
|
||||
if (writeAsync) {
|
||||
try {
|
||||
const makeWriteWithParams = () => writeAsync({ value: BigInt(txValue) });
|
||||
await writeTxn(makeWriteWithParams);
|
||||
onChange();
|
||||
} catch (e: any) {
|
||||
console.error("⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [displayedTxResult, setDisplayedTxResult] = useState<TransactionReceipt>();
|
||||
const { data: txResult } = useWaitForTransaction({
|
||||
hash: result?.hash,
|
||||
});
|
||||
useEffect(() => {
|
||||
setDisplayedTxResult(txResult);
|
||||
}, [txResult]);
|
||||
|
||||
// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
|
||||
const transformedFunction = transformAbiFunction(abiFunction);
|
||||
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
return (
|
||||
<ContractInput
|
||||
key={key}
|
||||
setForm={updatedFormValue => {
|
||||
setDisplayedTxResult(undefined);
|
||||
setForm(updatedFormValue);
|
||||
}}
|
||||
form={form}
|
||||
stateObjectKey={key}
|
||||
paramType={input}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable";
|
||||
|
||||
return (
|
||||
<div className="py-5 space-y-3 first:pt-0 last:pb-1">
|
||||
<div className={`flex gap-3 ${zeroInputs ? "flex-row justify-between items-center" : "flex-col"}`}>
|
||||
<p className="font-medium my-0 break-words">
|
||||
{abiFunction.name}
|
||||
<InheritanceTooltip inheritedFrom={inheritedFrom} />
|
||||
</p>
|
||||
{inputs}
|
||||
{abiFunction.stateMutability === "payable" ? (
|
||||
<div className="flex flex-col gap-1.5 w-full">
|
||||
<div className="flex items-center ml-2">
|
||||
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
|
||||
<span className="block text-xs font-extralight leading-none">wei</span>
|
||||
</div>
|
||||
<IntegerInput
|
||||
value={txValue}
|
||||
onChange={updatedTxValue => {
|
||||
setDisplayedTxResult(undefined);
|
||||
setTxValue(updatedTxValue);
|
||||
}}
|
||||
placeholder="value (wei)"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex justify-between gap-2">
|
||||
{!zeroInputs && (
|
||||
<div className="flex-grow basis-0">
|
||||
{displayedTxResult ? <TxReceipt txResult={displayedTxResult} /> : null}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex ${
|
||||
writeDisabled &&
|
||||
"tooltip before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
|
||||
}`}
|
||||
data-tip={`${writeDisabled && "Wallet not connected or in the wrong network"}`}
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm" disabled={writeDisabled || isLoading} onClick={handleWrite}>
|
||||
{isLoading && <span className="loading loading-spinner loading-xs"></span>}
|
||||
Send 💸
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{zeroInputs && txResult ? (
|
||||
<div className="flex-grow basis-0">
|
||||
<TxReceipt txResult={txResult} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal file
8
packages/nextjs/app/debug/_components/contract/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from "./ContractInput";
|
||||
export * from "./ContractUI";
|
||||
export * from "./DisplayVariable";
|
||||
export * from "./ReadOnlyFunctionForm";
|
||||
export * from "./TxReceipt";
|
||||
export * from "./utilsContract";
|
||||
export * from "./utilsDisplay";
|
||||
export * from "./WriteOnlyFunctionForm";
|
||||
149
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal file
149
packages/nextjs/app/debug/_components/contract/utilsContract.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { AbiFunction, AbiParameter } from "abitype";
|
||||
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* Generates a key based on function metadata
|
||||
*/
|
||||
const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => {
|
||||
const name = input?.name || `input_${inputIndex}_`;
|
||||
return functionName + "_" + name + "_" + input.internalType + "_" + input.type;
|
||||
};
|
||||
|
||||
const isJsonString = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Recursive function to deeply parse JSON strings, correctly handling nested arrays and encoded JSON strings
|
||||
const deepParseValues = (value: any): any => {
|
||||
if (typeof value === "string") {
|
||||
if (isJsonString(value)) {
|
||||
const parsed = JSON.parse(value);
|
||||
return deepParseValues(parsed);
|
||||
} else {
|
||||
// It's a string but not a JSON string, return as is
|
||||
return value;
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
// If it's an array, recursively parse each element
|
||||
return value.map(element => deepParseValues(element));
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
// If it's an object, recursively parse each value
|
||||
return Object.entries(value).reduce((acc: any, [key, val]) => {
|
||||
acc[key] = deepParseValues(val);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Handle boolean values represented as strings
|
||||
if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") {
|
||||
return true;
|
||||
} else if (value === "false" || value === "0" || value === "0x0" || value === "0x00" || value === "0x0000") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* parses form input with array support
|
||||
*/
|
||||
const getParsedContractFunctionArgs = (form: Record<string, any>) => {
|
||||
return Object.keys(form).map(key => {
|
||||
const valueOfArg = form[key];
|
||||
|
||||
// Attempt to deeply parse JSON strings
|
||||
return deepParseValues(valueOfArg);
|
||||
});
|
||||
};
|
||||
|
||||
const getInitialFormState = (abiFunction: AbiFunction) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (!abiFunction.inputs) return initialForm;
|
||||
abiFunction.inputs.forEach((input, inputIndex) => {
|
||||
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const getInitalTupleFormState = (abiTupleParameter: AbiParameterTuple) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (abiTupleParameter.components.length === 0) return initialForm;
|
||||
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", component, componentIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const getInitalTupleArrayFormState = (abiTupleParameter: AbiParameterTuple) => {
|
||||
const initialForm: Record<string, any> = {};
|
||||
if (abiTupleParameter.components.length === 0) return initialForm;
|
||||
abiTupleParameter.components.forEach((component, componentIndex) => {
|
||||
const key = getFunctionInputKey("0_" + abiTupleParameter.name || "tuple", component, componentIndex);
|
||||
initialForm[key] = "";
|
||||
});
|
||||
return initialForm;
|
||||
};
|
||||
|
||||
const adjustInput = (input: AbiParameterTuple): AbiParameter => {
|
||||
if (input.type.startsWith("tuple[")) {
|
||||
const depth = (input.type.match(/\[\]/g) || []).length;
|
||||
return {
|
||||
...input,
|
||||
components: transformComponents(input.components, depth, {
|
||||
internalType: input.internalType || "struct",
|
||||
name: input.name,
|
||||
}),
|
||||
};
|
||||
} else if (input.components) {
|
||||
return {
|
||||
...input,
|
||||
components: input.components.map(value => adjustInput(value as AbiParameterTuple)),
|
||||
};
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
const transformComponents = (
|
||||
components: readonly AbiParameter[],
|
||||
depth: number,
|
||||
parentComponentData: { internalType?: string; name?: string },
|
||||
): AbiParameter[] => {
|
||||
// Base case: if depth is 1 or no components, return the original components
|
||||
if (depth === 1 || !components) {
|
||||
return [...components];
|
||||
}
|
||||
|
||||
// Recursive case: wrap components in an additional tuple layer
|
||||
const wrappedComponents: AbiParameter = {
|
||||
internalType: `${parentComponentData.internalType || "struct"}`.replace(/\[\]/g, "") + "[]".repeat(depth - 1),
|
||||
name: `${parentComponentData.name || "tuple"}`,
|
||||
type: `tuple${"[]".repeat(depth - 1)}`,
|
||||
components: transformComponents(components, depth - 1, parentComponentData),
|
||||
};
|
||||
|
||||
return [wrappedComponents];
|
||||
};
|
||||
|
||||
const transformAbiFunction = (abiFunction: AbiFunction): AbiFunction => {
|
||||
return {
|
||||
...abiFunction,
|
||||
inputs: abiFunction.inputs.map(value => adjustInput(value as AbiParameterTuple)),
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
getFunctionInputKey,
|
||||
getInitialFormState,
|
||||
getParsedContractFunctionArgs,
|
||||
getInitalTupleFormState,
|
||||
getInitalTupleArrayFormState,
|
||||
transformAbiFunction,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ReactElement } from "react";
|
||||
import { TransactionBase, TransactionReceipt, formatEther, isAddress } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
type DisplayContent =
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| Record<string, any>
|
||||
| TransactionBase
|
||||
| TransactionReceipt
|
||||
| undefined
|
||||
| unknown;
|
||||
|
||||
export const displayTxResult = (
|
||||
displayContent: DisplayContent | DisplayContent[],
|
||||
asText = false,
|
||||
): string | ReactElement | number => {
|
||||
if (displayContent == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof displayContent === "bigint") {
|
||||
try {
|
||||
const asNumber = Number(displayContent);
|
||||
if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) {
|
||||
return asNumber;
|
||||
} else {
|
||||
return "Ξ" + formatEther(displayContent);
|
||||
}
|
||||
} catch (e) {
|
||||
return "Ξ" + formatEther(displayContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof displayContent === "string" && isAddress(displayContent)) {
|
||||
return asText ? displayContent : <Address address={displayContent} />;
|
||||
}
|
||||
|
||||
if (Array.isArray(displayContent)) {
|
||||
const mostReadable = (v: DisplayContent) =>
|
||||
["number", "boolean"].includes(typeof v) ? v : displayTxResultAsText(v);
|
||||
const displayable = JSON.stringify(displayContent.map(mostReadable), replacer);
|
||||
|
||||
return asText ? (
|
||||
displayable
|
||||
) : (
|
||||
<span style={{ overflowWrap: "break-word", width: "100%" }}>{displayable.replaceAll(",", ",\n")}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.stringify(displayContent, replacer, 2);
|
||||
};
|
||||
|
||||
const displayTxResultAsText = (displayContent: DisplayContent) => displayTxResult(displayContent, true);
|
||||
28
packages/nextjs/app/debug/page.tsx
Normal file
28
packages/nextjs/app/debug/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DebugContracts } from "./_components/DebugContracts";
|
||||
import type { NextPage } from "next";
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Debug Contracts",
|
||||
description: "Debug your deployed 🏗 Scaffold-ETH 2 contracts in an easy way",
|
||||
});
|
||||
|
||||
const Debug: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<DebugContracts />
|
||||
<div className="text-center mt-8 bg-secondary p-10">
|
||||
<h1 className="text-4xl my-0">Debug Contracts</h1>
|
||||
<p className="text-neutral">
|
||||
You can debug & interact with your deployed contracts here.
|
||||
<br /> Check{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold [word-spacing:-0.5rem] px-1">
|
||||
packages / nextjs / app / debug / page.tsx
|
||||
</code>{" "}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Debug;
|
||||
61
packages/nextjs/app/layout.tsx
Normal file
61
packages/nextjs/app/layout.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import "@rainbow-me/rainbowkit/styles.css";
|
||||
import { Metadata } from "next";
|
||||
import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
|
||||
import { ThemeProvider } from "~~/components/ThemeProvider";
|
||||
import "~~/styles/globals.css";
|
||||
|
||||
const baseUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: `http://localhost:${process.env.PORT || 3000}`;
|
||||
const imageUrl = `${baseUrl}/thumbnail.jpg`;
|
||||
|
||||
const title = "Scaffold-ETH 2 App";
|
||||
const titleTemplate = "%s | Scaffold-ETH 2";
|
||||
const description = "Built with 🏗 Scaffold-ETH 2";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(baseUrl),
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description,
|
||||
openGraph: {
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: imageUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
images: [imageUrl],
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description,
|
||||
},
|
||||
icons: {
|
||||
icon: [{ url: "/favicon.png", sizes: "32x32", type: "image/png" }],
|
||||
},
|
||||
};
|
||||
|
||||
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<body>
|
||||
<ThemeProvider enableSystem>
|
||||
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScaffoldEthApp;
|
||||
71
packages/nextjs/app/page.tsx
Normal file
71
packages/nextjs/app/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { NextPage } from "next";
|
||||
import { useAccount } from "wagmi";
|
||||
import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { address: connectedAddress } = useAccount();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center flex-col flex-grow pt-10">
|
||||
<div className="px-5">
|
||||
<h1 className="text-center">
|
||||
<span className="block text-2xl mb-2">Welcome to</span>
|
||||
<span className="block text-4xl font-bold">Scaffold-ETH 2</span>
|
||||
</h1>
|
||||
<div className="flex justify-center items-center space-x-2">
|
||||
<p className="my-2 font-medium">Connected Address:</p>
|
||||
<Address address={connectedAddress} />
|
||||
</div>
|
||||
<p className="text-center text-lg">
|
||||
Get started by editing{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block">
|
||||
packages/nextjs/app/page.tsx
|
||||
</code>
|
||||
</p>
|
||||
<p className="text-center text-lg">
|
||||
Edit your smart contract{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block">
|
||||
YourContract.sol
|
||||
</code>{" "}
|
||||
in{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold max-w-full break-words break-all inline-block">
|
||||
packages/hardhat/contracts
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow bg-base-300 w-full mt-16 px-8 py-12">
|
||||
<div className="flex justify-center items-center gap-12 flex-col sm:flex-row">
|
||||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
|
||||
<BugAntIcon className="h-8 w-8 fill-secondary" />
|
||||
<p>
|
||||
Tinker with your smart contract using the{" "}
|
||||
<Link href="/debug" passHref className="link">
|
||||
Debug Contracts
|
||||
</Link>{" "}
|
||||
tab.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center max-w-xs rounded-3xl">
|
||||
<MagnifyingGlassIcon className="h-8 w-8 fill-secondary" />
|
||||
<p>
|
||||
Explore your local transactions with the{" "}
|
||||
<Link href="/blockexplorer" passHref className="link">
|
||||
Block Explorer
|
||||
</Link>{" "}
|
||||
tab.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
80
packages/nextjs/components/Footer.tsx
Normal file
80
packages/nextjs/components/Footer.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { HeartIcon } from "@heroicons/react/24/outline";
|
||||
import { SwitchTheme } from "~~/components/SwitchTheme";
|
||||
import { BuidlGuidlLogo } from "~~/components/assets/BuidlGuidlLogo";
|
||||
import { Faucet } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
/**
|
||||
* Site footer
|
||||
*/
|
||||
export const Footer = () => {
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const isLocalNetwork = targetNetwork.id === hardhat.id;
|
||||
|
||||
return (
|
||||
<div className="min-h-0 py-5 px-1 mb-11 lg:mb-0">
|
||||
<div>
|
||||
<div className="fixed flex justify-between items-center w-full z-10 p-4 bottom-0 left-0 pointer-events-none">
|
||||
<div className="flex flex-col md:flex-row gap-2 pointer-events-auto">
|
||||
{nativeCurrencyPrice > 0 && (
|
||||
<div>
|
||||
<div className="btn btn-primary btn-sm font-normal gap-1 cursor-auto">
|
||||
<CurrencyDollarIcon className="h-4 w-4" />
|
||||
<span>{nativeCurrencyPrice}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLocalNetwork && (
|
||||
<>
|
||||
<Faucet />
|
||||
<Link href="/blockexplorer" passHref className="btn btn-primary btn-sm font-normal gap-1">
|
||||
<MagnifyingGlassIcon className="h-4 w-4" />
|
||||
<span>Block Explorer</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SwitchTheme className={`pointer-events-auto ${isLocalNetwork ? "self-end md:self-auto" : ""}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<ul className="menu menu-horizontal w-full">
|
||||
<div className="flex justify-center items-center gap-2 text-sm w-full">
|
||||
<div className="text-center">
|
||||
<a href="https://github.com/scaffold-eth/se-2" target="_blank" rel="noreferrer" className="link">
|
||||
Fork me
|
||||
</a>
|
||||
</div>
|
||||
<span>·</span>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<p className="m-0 text-center">
|
||||
Built with <HeartIcon className="inline-block h-4 w-4" /> at
|
||||
</p>
|
||||
<a
|
||||
className="flex justify-center items-center gap-1"
|
||||
href="https://buidlguidl.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<BuidlGuidlLogo className="w-3 h-5 pb-1" />
|
||||
<span className="link">BuidlGuidl</span>
|
||||
</a>
|
||||
</div>
|
||||
<span>·</span>
|
||||
<div className="text-center">
|
||||
<a href="https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA" target="_blank" rel="noreferrer" className="link">
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
110
packages/nextjs/components/Header.tsx
Normal file
110
packages/nextjs/components/Header.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
|
||||
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
|
||||
import { useOutsideClick } from "~~/hooks/scaffold-eth";
|
||||
|
||||
type HeaderMenuLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const menuLinks: HeaderMenuLink[] = [
|
||||
{
|
||||
label: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
label: "Debug Contracts",
|
||||
href: "/debug",
|
||||
icon: <BugAntIcon className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
export const HeaderMenuLinks = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuLinks.map(({ label, href, icon }) => {
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
className={`${
|
||||
isActive ? "bg-secondary shadow-md" : ""
|
||||
} 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`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Site header
|
||||
*/
|
||||
export const Header = () => {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const burgerMenuRef = useRef<HTMLDivElement>(null);
|
||||
useOutsideClick(
|
||||
burgerMenuRef,
|
||||
useCallback(() => setIsDrawerOpen(false), []),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="sticky lg:static top-0 navbar bg-base-100 min-h-0 flex-shrink-0 justify-between z-20 shadow-md shadow-secondary px-0 sm:px-2">
|
||||
<div className="navbar-start w-auto lg:w-1/2">
|
||||
<div className="lg:hidden dropdown" ref={burgerMenuRef}>
|
||||
<label
|
||||
tabIndex={0}
|
||||
className={`ml-1 btn btn-ghost ${isDrawerOpen ? "hover:bg-secondary" : "hover:bg-transparent"}`}
|
||||
onClick={() => {
|
||||
setIsDrawerOpen(prevIsOpenState => !prevIsOpenState);
|
||||
}}
|
||||
>
|
||||
<Bars3Icon className="h-1/2" />
|
||||
</label>
|
||||
{isDrawerOpen && (
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52"
|
||||
onClick={() => {
|
||||
setIsDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
<HeaderMenuLinks />
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<Link href="/" passHref className="hidden lg:flex items-center gap-2 ml-4 mr-6 shrink-0">
|
||||
<div className="flex relative w-10 h-10">
|
||||
<Image alt="SE2 logo" className="cursor-pointer" fill src="/logo.svg" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold leading-tight">Scaffold-ETH</span>
|
||||
<span className="text-xs">Ethereum dev stack</span>
|
||||
</div>
|
||||
</Link>
|
||||
<ul className="hidden lg:flex lg:flex-nowrap menu menu-horizontal px-1 gap-2">
|
||||
<HeaderMenuLinks />
|
||||
</ul>
|
||||
</div>
|
||||
<div className="navbar-end flex-grow mr-4">
|
||||
<RainbowKitCustomConnectButton />
|
||||
<FaucetButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
60
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal file
60
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { WagmiConfig } from "wagmi";
|
||||
import { Footer } from "~~/components/Footer";
|
||||
import { Header } from "~~/components/Header";
|
||||
import { BlockieAvatar } from "~~/components/scaffold-eth";
|
||||
import { ProgressBar } from "~~/components/scaffold-eth/ProgressBar";
|
||||
import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
|
||||
import { appChains } from "~~/services/web3/wagmiConnectors";
|
||||
|
||||
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
|
||||
const price = useNativeCurrencyPrice();
|
||||
const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);
|
||||
|
||||
useEffect(() => {
|
||||
if (price > 0) {
|
||||
setNativeCurrencyPrice(price);
|
||||
}
|
||||
}, [setNativeCurrencyPrice, price]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="relative flex flex-col flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScaffoldEthAppWithProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WagmiConfig config={wagmiConfig}>
|
||||
<ProgressBar />
|
||||
<RainbowKitProvider
|
||||
chains={appChains.chains}
|
||||
avatar={BlockieAvatar}
|
||||
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
|
||||
>
|
||||
<ScaffoldEthApp>{children}</ScaffoldEthApp>
|
||||
</RainbowKitProvider>
|
||||
</WagmiConfig>
|
||||
);
|
||||
};
|
||||
44
packages/nextjs/components/SwitchTheme.tsx
Normal file
44
packages/nextjs/components/SwitchTheme.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const SwitchTheme = ({ className }: { className?: string }) => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isDarkMode) {
|
||||
setTheme("light");
|
||||
return;
|
||||
}
|
||||
setTheme("dark");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex space-x-2 h-8 items-center justify-center text-sm ${className}`}>
|
||||
<input
|
||||
id="theme-toggle"
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary bg-primary hover:bg-primary border-primary"
|
||||
onChange={handleToggle}
|
||||
checked={isDarkMode}
|
||||
/>
|
||||
{
|
||||
<label htmlFor="theme-toggle" className={`swap swap-rotate ${!isDarkMode ? "swap-active" : ""}`}>
|
||||
<SunIcon className="swap-on h-5 w-5" />
|
||||
<MoonIcon className="swap-off h-5 w-5" />
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
};
|
||||
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const BuidlGuidlLogo = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="53"
|
||||
height="72"
|
||||
viewBox="0 0 53 72"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
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"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
136
packages/nextjs/components/scaffold-eth/Address.tsx
Normal file
136
packages/nextjs/components/scaffold-eth/Address.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { Address as AddressType, getAddress, isAddress } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useEnsAvatar, useEnsName } from "wagmi";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { BlockieAvatar } from "~~/components/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
|
||||
|
||||
type AddressProps = {
|
||||
address?: AddressType;
|
||||
disableAddressLink?: boolean;
|
||||
format?: "short" | "long";
|
||||
size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl";
|
||||
};
|
||||
|
||||
const blockieSizeMap = {
|
||||
xs: 6,
|
||||
sm: 7,
|
||||
base: 8,
|
||||
lg: 9,
|
||||
xl: 10,
|
||||
"2xl": 12,
|
||||
"3xl": 15,
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays an address (or ENS) with a Blockie image and option to copy address.
|
||||
*/
|
||||
export const Address = ({ address, disableAddressLink, format, size = "base" }: AddressProps) => {
|
||||
const [ens, setEns] = useState<string | null>();
|
||||
const [ensAvatar, setEnsAvatar] = useState<string | null>();
|
||||
const [addressCopied, setAddressCopied] = useState(false);
|
||||
const checkSumAddress = address ? getAddress(address) : undefined;
|
||||
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const { data: fetchedEns } = useEnsName({
|
||||
address: checkSumAddress,
|
||||
enabled: isAddress(checkSumAddress ?? ""),
|
||||
chainId: 1,
|
||||
});
|
||||
const { data: fetchedEnsAvatar } = useEnsAvatar({
|
||||
name: fetchedEns,
|
||||
enabled: Boolean(fetchedEns),
|
||||
chainId: 1,
|
||||
cacheTime: 30_000,
|
||||
});
|
||||
|
||||
// We need to apply this pattern to avoid Hydration errors.
|
||||
useEffect(() => {
|
||||
setEns(fetchedEns);
|
||||
}, [fetchedEns]);
|
||||
|
||||
useEffect(() => {
|
||||
setEnsAvatar(fetchedEnsAvatar);
|
||||
}, [fetchedEnsAvatar]);
|
||||
|
||||
// Skeleton UI
|
||||
if (!checkSumAddress) {
|
||||
return (
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="rounded-md bg-slate-300 h-6 w-6"></div>
|
||||
<div className="flex items-center space-y-6">
|
||||
<div className="h-2 w-28 bg-slate-300 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAddress(checkSumAddress)) {
|
||||
return <span className="text-error">Wrong address</span>;
|
||||
}
|
||||
|
||||
const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, checkSumAddress);
|
||||
let displayAddress = checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4);
|
||||
|
||||
if (ens) {
|
||||
displayAddress = ens;
|
||||
} else if (format === "long") {
|
||||
displayAddress = checkSumAddress;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<BlockieAvatar
|
||||
address={checkSumAddress}
|
||||
ensImage={ensAvatar}
|
||||
size={(blockieSizeMap[size] * 24) / blockieSizeMap["base"]}
|
||||
/>
|
||||
</div>
|
||||
{disableAddressLink ? (
|
||||
<span className={`ml-1.5 text-${size} font-normal`}>{displayAddress}</span>
|
||||
) : targetNetwork.id === hardhat.id ? (
|
||||
<span className={`ml-1.5 text-${size} font-normal`}>
|
||||
<Link href={blockExplorerAddressLink}>{displayAddress}</Link>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
className={`ml-1.5 text-${size} font-normal`}
|
||||
target="_blank"
|
||||
href={blockExplorerAddressLink}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{displayAddress}
|
||||
</a>
|
||||
)}
|
||||
{addressCopied ? (
|
||||
<CheckCircleIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<CopyToClipboard
|
||||
text={checkSumAddress}
|
||||
onCopy={() => {
|
||||
setAddressCopied(true);
|
||||
setTimeout(() => {
|
||||
setAddressCopied(false);
|
||||
}, 800);
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicateIcon
|
||||
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
67
packages/nextjs/components/scaffold-eth/Balance.tsx
Normal file
67
packages/nextjs/components/scaffold-eth/Balance.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Address } from "viem";
|
||||
import { useAccountBalance } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
|
||||
type BalanceProps = {
|
||||
address?: Address;
|
||||
className?: string;
|
||||
usdMode?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display (ETH & USD) balance of an ETH address.
|
||||
*/
|
||||
export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const { balance, price, isError, isLoading } = useAccountBalance(address);
|
||||
const [displayUsdMode, setDisplayUsdMode] = useState(price > 0 ? Boolean(usdMode) : false);
|
||||
|
||||
const toggleBalanceMode = () => {
|
||||
if (price > 0) {
|
||||
setDisplayUsdMode(prevMode => !prevMode);
|
||||
}
|
||||
};
|
||||
|
||||
if (!address || isLoading || balance === null) {
|
||||
return (
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="rounded-md bg-slate-300 h-6 w-6"></div>
|
||||
<div className="flex items-center space-y-6">
|
||||
<div className="h-2 w-28 bg-slate-300 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className={`border-2 border-gray-400 rounded-md px-2 flex flex-col items-center max-w-fit cursor-pointer`}>
|
||||
<div className="text-warning">Error</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-sm btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`}
|
||||
onClick={toggleBalanceMode}
|
||||
>
|
||||
<div className="w-full flex items-center justify-center">
|
||||
{displayUsdMode ? (
|
||||
<>
|
||||
<span className="text-[0.8em] font-bold mr-1">$</span>
|
||||
<span>{(balance * price).toFixed(2)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{balance?.toFixed(4)}</span>
|
||||
<span className="text-[0.8em] font-bold ml-1">{targetNetwork.nativeCurrency.symbol}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { AvatarComponent } from "@rainbow-me/rainbowkit";
|
||||
import { blo } from "blo";
|
||||
|
||||
// Custom Avatar for RainbowKit
|
||||
export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
|
||||
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={ensImage || blo(address as `0x${string}`)}
|
||||
width={size}
|
||||
height={size}
|
||||
alt={`${address} avatar`}
|
||||
/>
|
||||
);
|
||||
130
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal file
130
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Address as AddressType, createWalletClient, http, parseEther } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useNetwork } from "wagmi";
|
||||
import { BanknotesIcon } from "@heroicons/react/24/outline";
|
||||
import { Address, AddressInput, Balance, EtherInput } from "~~/components/scaffold-eth";
|
||||
import { useTransactor } from "~~/hooks/scaffold-eth";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
// Account index to use from generated hardhat accounts.
|
||||
const FAUCET_ACCOUNT_INDEX = 0;
|
||||
|
||||
const localWalletClient = createWalletClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Faucet modal which lets you send ETH to any address.
|
||||
*/
|
||||
export const Faucet = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputAddress, setInputAddress] = useState<AddressType>();
|
||||
const [faucetAddress, setFaucetAddress] = useState<AddressType>();
|
||||
const [sendValue, setSendValue] = useState("");
|
||||
|
||||
const { chain: ConnectedChain } = useNetwork();
|
||||
|
||||
const faucetTxn = useTransactor(localWalletClient);
|
||||
|
||||
useEffect(() => {
|
||||
const getFaucetAddress = async () => {
|
||||
try {
|
||||
const accounts = await localWalletClient.getAddresses();
|
||||
setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]);
|
||||
} catch (error) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">Cannot connect to local provider</p>
|
||||
<p className="m-0">
|
||||
- Did you forget to run <code className="italic bg-base-300 text-base font-bold">yarn chain</code> ?
|
||||
</p>
|
||||
<p className="mt-1 break-normal">
|
||||
- Or you can change <code className="italic bg-base-300 text-base font-bold">targetNetwork</code> in{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold">scaffold.config.ts</code>
|
||||
</p>
|
||||
</>,
|
||||
);
|
||||
console.error("⚡️ ~ file: Faucet.tsx:getFaucetAddress ~ error", error);
|
||||
}
|
||||
};
|
||||
getFaucetAddress();
|
||||
}, []);
|
||||
|
||||
const sendETH = async () => {
|
||||
if (!faucetAddress) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await faucetTxn({
|
||||
to: inputAddress,
|
||||
value: parseEther(sendValue as `${number}`),
|
||||
account: faucetAddress,
|
||||
chain: hardhat,
|
||||
});
|
||||
setLoading(false);
|
||||
setInputAddress(undefined);
|
||||
setSendValue("");
|
||||
} catch (error) {
|
||||
console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render only on local chain
|
||||
if (ConnectedChain?.id !== hardhat.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="faucet-modal" className="btn btn-primary btn-sm font-normal gap-1">
|
||||
<BanknotesIcon className="h-4 w-4" />
|
||||
<span>Faucet</span>
|
||||
</label>
|
||||
<input type="checkbox" id="faucet-modal" className="modal-toggle" />
|
||||
<label htmlFor="faucet-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative">
|
||||
{/* dummy input to capture event onclick on modal box */}
|
||||
<input className="h-0 w-0 absolute top-0 left-0" />
|
||||
<h3 className="text-xl font-bold mb-3">Local Faucet</h3>
|
||||
<label htmlFor="faucet-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
|
||||
✕
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex space-x-4">
|
||||
<div>
|
||||
<span className="text-sm font-bold">From:</span>
|
||||
<Address address={faucetAddress} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-bold pl-3">Available:</span>
|
||||
<Balance address={faucetAddress} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<AddressInput
|
||||
placeholder="Destination Address"
|
||||
value={inputAddress ?? ""}
|
||||
onChange={value => setInputAddress(value as AddressType)}
|
||||
/>
|
||||
<EtherInput placeholder="Amount to send" value={sendValue} onChange={value => setSendValue(value)} />
|
||||
<button className="h-10 btn btn-primary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
|
||||
{!loading ? (
|
||||
<BanknotesIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
<span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
71
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal file
71
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createWalletClient, http, parseEther } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useAccount, useNetwork } from "wagmi";
|
||||
import { BanknotesIcon } from "@heroicons/react/24/outline";
|
||||
import { useAccountBalance, useTransactor } from "~~/hooks/scaffold-eth";
|
||||
|
||||
// Number of ETH faucet sends to an address
|
||||
const NUM_OF_ETH = "1";
|
||||
const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
||||
|
||||
const localWalletClient = createWalletClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
/**
|
||||
* FaucetButton button which lets you grab eth.
|
||||
*/
|
||||
export const FaucetButton = () => {
|
||||
const { address } = useAccount();
|
||||
const { balance } = useAccountBalance(address);
|
||||
|
||||
const { chain: ConnectedChain } = useNetwork();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const faucetTxn = useTransactor(localWalletClient);
|
||||
|
||||
const sendETH = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await faucetTxn({
|
||||
chain: hardhat,
|
||||
account: FAUCET_ADDRESS,
|
||||
to: address,
|
||||
value: parseEther(NUM_OF_ETH),
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render only on local chain
|
||||
if (ConnectedChain?.id !== hardhat.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
balance
|
||||
? "ml-1"
|
||||
: "ml-1 tooltip tooltip-bottom tooltip-secondary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:right-0"
|
||||
}
|
||||
data-tip="Grab funds from faucet"
|
||||
>
|
||||
<button className="btn btn-secondary btn-sm px-2 rounded-full" onClick={sendETH} disabled={loading}>
|
||||
{!loading ? (
|
||||
<BanknotesIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<span className="loading loading-spinner loading-xs"></span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
117
packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Normal file
117
packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { blo } from "blo";
|
||||
import { useDebounceValue } from "usehooks-ts";
|
||||
import { Address, isAddress } from "viem";
|
||||
import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi";
|
||||
import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth";
|
||||
|
||||
/**
|
||||
* Address input with ENS name resolution
|
||||
*/
|
||||
export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps<Address | string>) => {
|
||||
// Debounce the input to keep clean RPC calls when resolving ENS names
|
||||
// If the input is an address, we don't need to debounce it
|
||||
const [_debouncedValue] = useDebounceValue(value, 500);
|
||||
const debouncedValue = isAddress(value) ? value : _debouncedValue;
|
||||
const isDebouncedValueLive = debouncedValue === value;
|
||||
|
||||
// If the user changes the input after an ENS name is already resolved, we want to remove the stale result
|
||||
const settledValue = isDebouncedValueLive ? debouncedValue : undefined;
|
||||
|
||||
const {
|
||||
data: ensAddress,
|
||||
isLoading: isEnsAddressLoading,
|
||||
isError: isEnsAddressError,
|
||||
isSuccess: isEnsAddressSuccess,
|
||||
} = useEnsAddress({
|
||||
name: settledValue,
|
||||
enabled: isDebouncedValueLive && isENS(debouncedValue),
|
||||
chainId: 1,
|
||||
cacheTime: 30_000,
|
||||
});
|
||||
|
||||
const [enteredEnsName, setEnteredEnsName] = useState<string>();
|
||||
const {
|
||||
data: ensName,
|
||||
isLoading: isEnsNameLoading,
|
||||
isError: isEnsNameError,
|
||||
isSuccess: isEnsNameSuccess,
|
||||
} = useEnsName({
|
||||
address: settledValue as Address,
|
||||
enabled: isAddress(debouncedValue),
|
||||
chainId: 1,
|
||||
cacheTime: 30_000,
|
||||
});
|
||||
|
||||
const { data: ensAvatar, isLoading: isEnsAvtarLoading } = useEnsAvatar({
|
||||
name: ensName,
|
||||
enabled: Boolean(ensName),
|
||||
chainId: 1,
|
||||
cacheTime: 30_000,
|
||||
});
|
||||
|
||||
// ens => address
|
||||
useEffect(() => {
|
||||
if (!ensAddress) return;
|
||||
|
||||
// ENS resolved successfully
|
||||
setEnteredEnsName(debouncedValue);
|
||||
onChange(ensAddress);
|
||||
}, [ensAddress, onChange, debouncedValue]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: Address) => {
|
||||
setEnteredEnsName(undefined);
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const reFocus =
|
||||
isEnsAddressError ||
|
||||
isEnsNameError ||
|
||||
isEnsNameSuccess ||
|
||||
isEnsAddressSuccess ||
|
||||
ensName === null ||
|
||||
ensAddress === null;
|
||||
|
||||
return (
|
||||
<InputBase<Address>
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
error={ensAddress === null}
|
||||
value={value as Address}
|
||||
onChange={handleChange}
|
||||
disabled={isEnsAddressLoading || isEnsNameLoading || disabled}
|
||||
reFocus={reFocus}
|
||||
prefix={
|
||||
ensName ? (
|
||||
<div className="flex bg-base-300 rounded-l-full items-center">
|
||||
{isEnsAvtarLoading && <div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>}
|
||||
{ensAvatar ? (
|
||||
<span className="w-[35px]">
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
<img className="w-full rounded-full" src={ensAvatar} alt={`${ensAddress} avatar`} />
|
||||
}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-accent px-2">{enteredEnsName ?? ensName}</span>
|
||||
</div>
|
||||
) : (
|
||||
(isEnsNameLoading || isEnsAddressLoading) && (
|
||||
<div className="flex bg-base-300 rounded-l-full items-center gap-2 pr-2">
|
||||
<div className="skeleton bg-base-200 w-[35px] h-[35px] rounded-full shrink-0"></div>
|
||||
<div className="skeleton bg-base-200 h-3 w-20"></div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
suffix={
|
||||
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
value && <img alt="" className="!rounded-full" src={blo(value as `0x${string}`)} width="35" height="35" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useCallback } from "react";
|
||||
import { hexToString, isHex, stringToHex } from "viem";
|
||||
import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
|
||||
|
||||
export const Bytes32Input = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
|
||||
const convertStringToBytes32 = useCallback(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 }));
|
||||
}, [onChange, value]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
<div
|
||||
className="self-center cursor-pointer text-xl font-semibold px-4 text-accent"
|
||||
onClick={convertStringToBytes32}
|
||||
>
|
||||
#
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
27
packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx
Normal file
27
packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { bytesToString, isHex, toBytes, toHex } from "viem";
|
||||
import { CommonInputProps, InputBase } from "~~/components/scaffold-eth";
|
||||
|
||||
export const BytesInput = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => {
|
||||
const convertStringToBytes = useCallback(() => {
|
||||
onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value)));
|
||||
}, [onChange, value]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
<div
|
||||
className="self-center cursor-pointer text-xl font-semibold px-4 text-accent"
|
||||
onClick={convertStringToBytes}
|
||||
>
|
||||
#
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
134
packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx
Normal file
134
packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { CommonInputProps, InputBase, SIGNED_NUMBER_REGEX } from "~~/components/scaffold-eth";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
const MAX_DECIMALS_USD = 2;
|
||||
|
||||
function etherValueToDisplayValue(usdMode: boolean, etherValue: string, nativeCurrencyPrice: number) {
|
||||
if (usdMode && nativeCurrencyPrice) {
|
||||
const parsedEthValue = parseFloat(etherValue);
|
||||
if (Number.isNaN(parsedEthValue)) {
|
||||
return etherValue;
|
||||
} else {
|
||||
// We need to round the value rather than use toFixed,
|
||||
// since otherwise a user would not be able to modify the decimal value
|
||||
return (
|
||||
Math.round(parsedEthValue * nativeCurrencyPrice * 10 ** MAX_DECIMALS_USD) /
|
||||
10 ** MAX_DECIMALS_USD
|
||||
).toString();
|
||||
}
|
||||
} else {
|
||||
return etherValue;
|
||||
}
|
||||
}
|
||||
|
||||
function displayValueToEtherValue(usdMode: boolean, displayValue: string, nativeCurrencyPrice: number) {
|
||||
if (usdMode && nativeCurrencyPrice) {
|
||||
const parsedDisplayValue = parseFloat(displayValue);
|
||||
if (Number.isNaN(parsedDisplayValue)) {
|
||||
// Invalid number.
|
||||
return displayValue;
|
||||
} else {
|
||||
// Compute the ETH value if a valid number.
|
||||
return (parsedDisplayValue / nativeCurrencyPrice).toString();
|
||||
}
|
||||
} else {
|
||||
return displayValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for ETH amount with USD conversion.
|
||||
*
|
||||
* onChange will always be called with the value in ETH
|
||||
*/
|
||||
export const EtherInput = ({
|
||||
value,
|
||||
name,
|
||||
placeholder,
|
||||
onChange,
|
||||
disabled,
|
||||
usdMode,
|
||||
}: CommonInputProps & { usdMode?: boolean }) => {
|
||||
const [transitoryDisplayValue, setTransitoryDisplayValue] = useState<string>();
|
||||
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice);
|
||||
const [internalUsdMode, setInternalUSDMode] = useState(nativeCurrencyPrice > 0 ? Boolean(usdMode) : false);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalUSDMode(nativeCurrencyPrice > 0 ? Boolean(usdMode) : false);
|
||||
}, [usdMode, nativeCurrencyPrice]);
|
||||
|
||||
// The displayValue is derived from the ether value that is controlled outside of the component
|
||||
// In usdMode, it is converted to its usd value, in regular mode it is unaltered
|
||||
const displayValue = useMemo(() => {
|
||||
const newDisplayValue = etherValueToDisplayValue(internalUsdMode, value, nativeCurrencyPrice);
|
||||
if (transitoryDisplayValue && parseFloat(newDisplayValue) === parseFloat(transitoryDisplayValue)) {
|
||||
return transitoryDisplayValue;
|
||||
}
|
||||
// Clear any transitory display values that might be set
|
||||
setTransitoryDisplayValue(undefined);
|
||||
return newDisplayValue;
|
||||
}, [nativeCurrencyPrice, transitoryDisplayValue, internalUsdMode, value]);
|
||||
|
||||
const handleChangeNumber = (newValue: string) => {
|
||||
if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Following condition is a fix to prevent usdMode from experiencing different display values
|
||||
// than what the user entered. This can happen due to floating point rounding errors that are introduced in the back and forth conversion
|
||||
if (internalUsdMode) {
|
||||
const decimals = newValue.split(".")[1];
|
||||
if (decimals && decimals.length > MAX_DECIMALS_USD) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Since the display value is a derived state (calculated from the ether value), usdMode would not allow introducing a decimal point.
|
||||
// This condition handles a transitory state for a display value with a trailing decimal sign
|
||||
if (newValue.endsWith(".") || newValue.endsWith(".0")) {
|
||||
setTransitoryDisplayValue(newValue);
|
||||
} else {
|
||||
setTransitoryDisplayValue(undefined);
|
||||
}
|
||||
|
||||
const newEthValue = displayValueToEtherValue(internalUsdMode, newValue, nativeCurrencyPrice);
|
||||
onChange(newEthValue);
|
||||
};
|
||||
|
||||
const toggleMode = () => {
|
||||
if (nativeCurrencyPrice > 0) {
|
||||
setInternalUSDMode(!internalUsdMode);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={displayValue}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChangeNumber}
|
||||
disabled={disabled}
|
||||
prefix={<span className="pl-4 -mr-2 text-accent self-center">{internalUsdMode ? "$" : "Ξ"}</span>}
|
||||
suffix={
|
||||
<div
|
||||
className={`${
|
||||
nativeCurrencyPrice > 0
|
||||
? ""
|
||||
: "tooltip tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
|
||||
}`}
|
||||
data-tip="Unable to fetch price"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary h-[2.2rem] min-h-[2.2rem]"
|
||||
onClick={toggleMode}
|
||||
disabled={!internalUsdMode && !nativeCurrencyPrice}
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
packages/nextjs/components/scaffold-eth/Input/InputBase.tsx
Normal file
66
packages/nextjs/components/scaffold-eth/Input/InputBase.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ChangeEvent, FocusEvent, ReactNode, useCallback, useEffect, useRef } from "react";
|
||||
import { CommonInputProps } from "~~/components/scaffold-eth";
|
||||
|
||||
type InputBaseProps<T> = CommonInputProps<T> & {
|
||||
error?: boolean;
|
||||
prefix?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
reFocus?: boolean;
|
||||
};
|
||||
|
||||
export const InputBase = <T extends { toString: () => string } | undefined = string>({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
disabled,
|
||||
prefix,
|
||||
suffix,
|
||||
reFocus,
|
||||
}: InputBaseProps<T>) => {
|
||||
const inputReft = useRef<HTMLInputElement>(null);
|
||||
|
||||
let modifier = "";
|
||||
if (error) {
|
||||
modifier = "border-error";
|
||||
} else if (disabled) {
|
||||
modifier = "border-disabled bg-base-300";
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value as unknown as T);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
// Runs only when reFocus prop is passed, useful for setting the cursor
|
||||
// at the end of the input. Example AddressInput
|
||||
const onFocus = (e: FocusEvent<HTMLInputElement, Element>) => {
|
||||
if (reFocus !== undefined) {
|
||||
e.currentTarget.setSelectionRange(e.currentTarget.value.length, e.currentTarget.value.length);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (reFocus !== undefined && reFocus === true) inputReft.current?.focus();
|
||||
}, [reFocus]);
|
||||
|
||||
return (
|
||||
<div className={`flex border-2 border-base-300 bg-base-200 rounded-full text-accent ${modifier}`}>
|
||||
{prefix}
|
||||
<input
|
||||
className="input input-ghost focus-within:border-transparent focus:outline-none focus:bg-transparent focus:text-gray-400 h-[2.2rem] min-h-[2.2rem] px-4 border w-full font-medium placeholder:text-accent/50 text-gray-400"
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value?.toString()}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
ref={inputReft}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{suffix}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth";
|
||||
|
||||
type IntegerInputProps = CommonInputProps<string | bigint> & {
|
||||
variant?: IntegerVariant;
|
||||
disableMultiplyBy1e18?: boolean;
|
||||
};
|
||||
|
||||
export const IntegerInput = ({
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
placeholder,
|
||||
disabled,
|
||||
variant = IntegerVariant.UINT256,
|
||||
disableMultiplyBy1e18 = false,
|
||||
}: IntegerInputProps) => {
|
||||
const [inputError, setInputError] = useState(false);
|
||||
const multiplyBy1e18 = useCallback(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
return onChange(value * 10n ** 18n);
|
||||
}
|
||||
return onChange(BigInt(Math.round(Number(value) * 10 ** 18)));
|
||||
}, [onChange, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidInteger(variant, value, false)) {
|
||||
setInputError(false);
|
||||
} else {
|
||||
setInputError(true);
|
||||
}
|
||||
}, [value, variant]);
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
error={inputError}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
suffix={
|
||||
!inputError &&
|
||||
!disableMultiplyBy1e18 && (
|
||||
<div
|
||||
className="space-x-4 flex tooltip tooltip-top tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
|
||||
data-tip="Multiply by 10^18 (wei)"
|
||||
>
|
||||
<button
|
||||
className={`${disabled ? "cursor-not-allowed" : "cursor-pointer"} font-semibold px-4 text-accent`}
|
||||
onClick={multiplyBy1e18}
|
||||
disabled={disabled}
|
||||
>
|
||||
∗
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
9
packages/nextjs/components/scaffold-eth/Input/index.ts
Normal file
9
packages/nextjs/components/scaffold-eth/Input/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
export * from "./AddressInput";
|
||||
export * from "./Bytes32Input";
|
||||
export * from "./BytesInput";
|
||||
export * from "./EtherInput";
|
||||
export * from "./InputBase";
|
||||
export * from "./IntegerInput";
|
||||
export * from "./utils";
|
||||
111
packages/nextjs/components/scaffold-eth/Input/utils.ts
Normal file
111
packages/nextjs/components/scaffold-eth/Input/utils.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
export type CommonInputProps<T = string> = {
|
||||
value: T;
|
||||
onChange: (newValue: T) => void;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export enum IntegerVariant {
|
||||
UINT8 = "uint8",
|
||||
UINT16 = "uint16",
|
||||
UINT24 = "uint24",
|
||||
UINT32 = "uint32",
|
||||
UINT40 = "uint40",
|
||||
UINT48 = "uint48",
|
||||
UINT56 = "uint56",
|
||||
UINT64 = "uint64",
|
||||
UINT72 = "uint72",
|
||||
UINT80 = "uint80",
|
||||
UINT88 = "uint88",
|
||||
UINT96 = "uint96",
|
||||
UINT104 = "uint104",
|
||||
UINT112 = "uint112",
|
||||
UINT120 = "uint120",
|
||||
UINT128 = "uint128",
|
||||
UINT136 = "uint136",
|
||||
UINT144 = "uint144",
|
||||
UINT152 = "uint152",
|
||||
UINT160 = "uint160",
|
||||
UINT168 = "uint168",
|
||||
UINT176 = "uint176",
|
||||
UINT184 = "uint184",
|
||||
UINT192 = "uint192",
|
||||
UINT200 = "uint200",
|
||||
UINT208 = "uint208",
|
||||
UINT216 = "uint216",
|
||||
UINT224 = "uint224",
|
||||
UINT232 = "uint232",
|
||||
UINT240 = "uint240",
|
||||
UINT248 = "uint248",
|
||||
UINT256 = "uint256",
|
||||
INT8 = "int8",
|
||||
INT16 = "int16",
|
||||
INT24 = "int24",
|
||||
INT32 = "int32",
|
||||
INT40 = "int40",
|
||||
INT48 = "int48",
|
||||
INT56 = "int56",
|
||||
INT64 = "int64",
|
||||
INT72 = "int72",
|
||||
INT80 = "int80",
|
||||
INT88 = "int88",
|
||||
INT96 = "int96",
|
||||
INT104 = "int104",
|
||||
INT112 = "int112",
|
||||
INT120 = "int120",
|
||||
INT128 = "int128",
|
||||
INT136 = "int136",
|
||||
INT144 = "int144",
|
||||
INT152 = "int152",
|
||||
INT160 = "int160",
|
||||
INT168 = "int168",
|
||||
INT176 = "int176",
|
||||
INT184 = "int184",
|
||||
INT192 = "int192",
|
||||
INT200 = "int200",
|
||||
INT208 = "int208",
|
||||
INT216 = "int216",
|
||||
INT224 = "int224",
|
||||
INT232 = "int232",
|
||||
INT240 = "int240",
|
||||
INT248 = "int248",
|
||||
INT256 = "int256",
|
||||
}
|
||||
|
||||
export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/;
|
||||
export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/;
|
||||
|
||||
export const isValidInteger = (dataType: IntegerVariant, value: bigint | string, strict = true) => {
|
||||
const isSigned = dataType.startsWith("i");
|
||||
const bitcount = Number(dataType.substring(isSigned ? 3 : 4));
|
||||
|
||||
let valueAsBigInt;
|
||||
try {
|
||||
valueAsBigInt = BigInt(value);
|
||||
} catch (e) {}
|
||||
if (typeof valueAsBigInt !== "bigint") {
|
||||
if (strict) {
|
||||
return false;
|
||||
}
|
||||
if (!value || typeof value !== "string") {
|
||||
return true;
|
||||
}
|
||||
return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value);
|
||||
} else if (!isSigned && valueAsBigInt < 0) {
|
||||
return false;
|
||||
}
|
||||
const hexString = valueAsBigInt.toString(16);
|
||||
const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? "";
|
||||
if (
|
||||
significantHexDigits.length * 4 > bitcount ||
|
||||
(isSigned && significantHexDigits.length * 4 === bitcount && parseInt(significantHexDigits.slice(-1)?.[0], 16) < 8)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Treat any dot-separated string as a potential ENS name
|
||||
const ensRegex = /.+\..+/;
|
||||
export const isENS = (address = "") => ensRegex.test(address);
|
||||
72
packages/nextjs/components/scaffold-eth/ProgressBar.tsx
Normal file
72
packages/nextjs/components/scaffold-eth/ProgressBar.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import NProgress from "nprogress";
|
||||
|
||||
type PushStateInput = [data: any, unused: string, url?: string | URL | null | undefined];
|
||||
|
||||
export function ProgressBar() {
|
||||
const height = "3px";
|
||||
const color = "#2299dd";
|
||||
|
||||
const styles = (
|
||||
<style>
|
||||
{`
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
#nprogress .bar {
|
||||
background: ${color};
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${typeof height === `string` ? height : `${height}px`};
|
||||
}
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px ${color}, 0 0 5px ${color};
|
||||
opacity: 1.0;
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
const handleAnchorClick = (event: MouseEvent) => {
|
||||
const targetUrl = (event.currentTarget as HTMLAnchorElement).href;
|
||||
const currentUrl = location.href;
|
||||
if (targetUrl !== currentUrl) {
|
||||
NProgress.start();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMutation: MutationCallback = () => {
|
||||
const anchorElements = document.querySelectorAll("a");
|
||||
anchorElements.forEach(anchor => anchor.addEventListener("click", handleAnchorClick));
|
||||
};
|
||||
|
||||
const mutationObserver = new MutationObserver(handleMutation);
|
||||
mutationObserver.observe(document, { childList: true, subtree: true });
|
||||
|
||||
window.history.pushState = new Proxy(window.history.pushState, {
|
||||
apply: (target, thisArg, argArray: PushStateInput) => {
|
||||
NProgress.done();
|
||||
return target.apply(thisArg, argArray);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return styles;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { NetworkOptions } from "./NetworkOptions";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import { getAddress } from "viem";
|
||||
import { Address, useDisconnect } from "wagmi";
|
||||
import {
|
||||
ArrowLeftOnRectangleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowsRightLeftIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
DocumentDuplicateIcon,
|
||||
QrCodeIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlockieAvatar, isENS } from "~~/components/scaffold-eth";
|
||||
import { useOutsideClick } from "~~/hooks/scaffold-eth";
|
||||
import { getTargetNetworks } from "~~/utils/scaffold-eth";
|
||||
|
||||
const allowedNetworks = getTargetNetworks();
|
||||
|
||||
type AddressInfoDropdownProps = {
|
||||
address: Address;
|
||||
blockExplorerAddressLink: string | undefined;
|
||||
displayName: string;
|
||||
ensAvatar?: string;
|
||||
};
|
||||
|
||||
export const AddressInfoDropdown = ({
|
||||
address,
|
||||
ensAvatar,
|
||||
displayName,
|
||||
blockExplorerAddressLink,
|
||||
}: AddressInfoDropdownProps) => {
|
||||
const { disconnect } = useDisconnect();
|
||||
const checkSumAddress = getAddress(address);
|
||||
|
||||
const [addressCopied, setAddressCopied] = useState(false);
|
||||
|
||||
const [selectingNetwork, setSelectingNetwork] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDetailsElement>(null);
|
||||
const closeDropdown = () => {
|
||||
setSelectingNetwork(false);
|
||||
dropdownRef.current?.removeAttribute("open");
|
||||
};
|
||||
useOutsideClick(dropdownRef, closeDropdown);
|
||||
|
||||
return (
|
||||
<>
|
||||
<details ref={dropdownRef} className="dropdown dropdown-end leading-3">
|
||||
<summary tabIndex={0} className="btn btn-secondary btn-sm pl-0 pr-2 shadow-md dropdown-toggle gap-0 !h-auto">
|
||||
<BlockieAvatar address={checkSumAddress} size={30} ensImage={ensAvatar} />
|
||||
<span className="ml-2 mr-1">
|
||||
{isENS(displayName) ? displayName : checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4)}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
</summary>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content menu z-[2] p-2 mt-2 shadow-center shadow-accent bg-base-200 rounded-box gap-1"
|
||||
>
|
||||
<NetworkOptions hidden={!selectingNetwork} />
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
{addressCopied ? (
|
||||
<div className="btn-sm !rounded-xl flex gap-3 py-3">
|
||||
<CheckCircleIcon
|
||||
className="text-xl font-normal h-6 w-4 cursor-pointer ml-2 sm:ml-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className=" whitespace-nowrap">Copy address</span>
|
||||
</div>
|
||||
) : (
|
||||
<CopyToClipboard
|
||||
text={checkSumAddress}
|
||||
onCopy={() => {
|
||||
setAddressCopied(true);
|
||||
setTimeout(() => {
|
||||
setAddressCopied(false);
|
||||
}, 800);
|
||||
}}
|
||||
>
|
||||
<div className="btn-sm !rounded-xl flex gap-3 py-3">
|
||||
<DocumentDuplicateIcon
|
||||
className="text-xl font-normal h-6 w-4 cursor-pointer ml-2 sm:ml-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className=" whitespace-nowrap">Copy address</span>
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</li>
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<label htmlFor="qrcode-modal" className="btn-sm !rounded-xl flex gap-3 py-3">
|
||||
<QrCodeIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span className="whitespace-nowrap">View QR Code</span>
|
||||
</label>
|
||||
</li>
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button className="menu-item btn-sm !rounded-xl flex gap-3 py-3" type="button">
|
||||
<ArrowTopRightOnSquareIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<a
|
||||
target="_blank"
|
||||
href={blockExplorerAddressLink}
|
||||
rel="noopener noreferrer"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
View on Block Explorer
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{allowedNetworks.length > 1 ? (
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button
|
||||
className="btn-sm !rounded-xl flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectingNetwork(true);
|
||||
}}
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Switch Network</span>
|
||||
</button>
|
||||
</li>
|
||||
) : null}
|
||||
<li className={selectingNetwork ? "hidden" : ""}>
|
||||
<button
|
||||
className="menu-item text-error btn-sm !rounded-xl flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Disconnect</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { Address as AddressType } from "viem";
|
||||
import { Address } from "~~/components/scaffold-eth";
|
||||
|
||||
type AddressQRCodeModalProps = {
|
||||
address: AddressType;
|
||||
modalId: string;
|
||||
};
|
||||
|
||||
export const AddressQRCodeModal = ({ address, modalId }: AddressQRCodeModalProps) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input type="checkbox" id={`${modalId}`} className="modal-toggle" />
|
||||
<label htmlFor={`${modalId}`} className="modal cursor-pointer">
|
||||
<label className="modal-box relative">
|
||||
{/* dummy input to capture event onclick on modal box */}
|
||||
<input className="h-0 w-0 absolute top-0 left-0" />
|
||||
<label htmlFor={`${modalId}`} className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
|
||||
✕
|
||||
</label>
|
||||
<div className="space-y-3 py-6">
|
||||
<div className="flex space-x-4 flex-col items-center gap-6">
|
||||
<QRCodeSVG value={address} size={256} />
|
||||
<Address address={address} format="long" disableAddressLink />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useNetwork, useSwitchNetwork } from "wagmi";
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { getNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { getTargetNetworks } from "~~/utils/scaffold-eth";
|
||||
|
||||
const allowedNetworks = getTargetNetworks();
|
||||
|
||||
type NetworkOptionsProps = {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export const NetworkOptions = ({ hidden = false }: NetworkOptionsProps) => {
|
||||
const { switchNetwork } = useSwitchNetwork();
|
||||
const { chain } = useNetwork();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowedNetworks
|
||||
.filter(allowedNetwork => allowedNetwork.id !== chain?.id)
|
||||
.map(allowedNetwork => (
|
||||
<li key={allowedNetwork.id} className={hidden ? "hidden" : ""}>
|
||||
<button
|
||||
className="menu-item btn-sm !rounded-xl flex gap-3 py-3 whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
switchNetwork?.(allowedNetwork.id);
|
||||
}}
|
||||
>
|
||||
<ArrowsRightLeftIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>
|
||||
Switch to{" "}
|
||||
<span
|
||||
style={{
|
||||
color: getNetworkColor(allowedNetwork, isDarkMode),
|
||||
}}
|
||||
>
|
||||
{allowedNetwork.name}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NetworkOptions } from "./NetworkOptions";
|
||||
import { useDisconnect } from "wagmi";
|
||||
import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const WrongNetworkDropdown = () => {
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-end mr-2">
|
||||
<label tabIndex={0} className="btn btn-error btn-sm dropdown-toggle gap-1">
|
||||
<span>Wrong network</span>
|
||||
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content menu p-2 mt-1 shadow-center shadow-accent bg-base-200 rounded-box gap-1"
|
||||
>
|
||||
<NetworkOptions />
|
||||
<li>
|
||||
<button
|
||||
className="menu-item text-error btn-sm !rounded-xl flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>Disconnect</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
// @refresh reset
|
||||
import { Balance } from "../Balance";
|
||||
import { AddressInfoDropdown } from "./AddressInfoDropdown";
|
||||
import { AddressQRCodeModal } from "./AddressQRCodeModal";
|
||||
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
|
||||
import { ConnectButton } from "@rainbow-me/rainbowkit";
|
||||
import { Address } from "viem";
|
||||
import { useAutoConnect, useNetworkColor } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
|
||||
|
||||
/**
|
||||
* Custom Wagmi Connect Button (watch balance + custom design)
|
||||
*/
|
||||
export const RainbowKitCustomConnectButton = () => {
|
||||
useAutoConnect();
|
||||
const networkColor = useNetworkColor();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
return (
|
||||
<ConnectButton.Custom>
|
||||
{({ account, chain, openConnectModal, mounted }) => {
|
||||
const connected = mounted && account && chain;
|
||||
const blockExplorerAddressLink = account
|
||||
? getBlockExplorerAddressLink(targetNetwork, account.address)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(() => {
|
||||
if (!connected) {
|
||||
return (
|
||||
<button className="btn btn-primary btn-sm" onClick={openConnectModal} type="button">
|
||||
Connect Wallet
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (chain.unsupported || chain.id !== targetNetwork.id) {
|
||||
return <WrongNetworkDropdown />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center mr-1">
|
||||
<Balance address={account.address as Address} className="min-h-0 h-auto" />
|
||||
<span className="text-xs" style={{ color: networkColor }}>
|
||||
{chain.name}
|
||||
</span>
|
||||
</div>
|
||||
<AddressInfoDropdown
|
||||
address={account.address as Address}
|
||||
displayName={account.displayName}
|
||||
ensAvatar={account.ensAvatar}
|
||||
blockExplorerAddressLink={blockExplorerAddressLink}
|
||||
/>
|
||||
<AddressQRCodeModal address={account.address as Address} modalId="qrcode-modal" />
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ConnectButton.Custom>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
7
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./Address";
|
||||
export * from "./Balance";
|
||||
export * from "./BlockieAvatar";
|
||||
export * from "./Faucet";
|
||||
export * from "./FaucetButton";
|
||||
export * from "./Input";
|
||||
export * from "./RainbowKitCustomConnectButton";
|
||||
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
const deployedContracts = {} as const;
|
||||
|
||||
export default deployedContracts satisfies GenericContractsDeclaration;
|
||||
16
packages/nextjs/contracts/externalContracts.ts
Normal file
16
packages/nextjs/contracts/externalContracts.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const externalContracts = {
|
||||
* 1: {
|
||||
* DAI: {
|
||||
* address: "0x...",
|
||||
* abi: [...],
|
||||
* },
|
||||
* },
|
||||
* } as const;
|
||||
*/
|
||||
const externalContracts = {} as const;
|
||||
|
||||
export default externalContracts satisfies GenericContractsDeclaration;
|
||||
16
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
16
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from "./useAccountBalance";
|
||||
export * from "./useAnimationConfig";
|
||||
export * from "./useBurnerWallet";
|
||||
export * from "./useDeployedContractInfo";
|
||||
export * from "./useNativeCurrencyPrice";
|
||||
export * from "./useNetworkColor";
|
||||
export * from "./useOutsideClick";
|
||||
export * from "./useScaffoldContract";
|
||||
export * from "./useScaffoldContractRead";
|
||||
export * from "./useScaffoldContractWrite";
|
||||
export * from "./useScaffoldEventSubscriber";
|
||||
export * from "./useScaffoldEventHistory";
|
||||
export * from "./useTransactor";
|
||||
export * from "./useFetchBlocks";
|
||||
export * from "./useContractLogs";
|
||||
export * from "./useAutoConnect";
|
||||
36
packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts
Normal file
36
packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTargetNetwork } from "./useTargetNetwork";
|
||||
import { Address } from "viem";
|
||||
import { useBalance } from "wagmi";
|
||||
import { useGlobalState } from "~~/services/store/store";
|
||||
|
||||
export function useAccountBalance(address?: Address) {
|
||||
const [isEthBalance, setIsEthBalance] = useState(true);
|
||||
const [balance, setBalance] = useState<number | null>(null);
|
||||
const price = useGlobalState(state => state.nativeCurrencyPrice);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const {
|
||||
data: fetchedBalanceData,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useBalance({
|
||||
address,
|
||||
watch: true,
|
||||
chainId: targetNetwork.id,
|
||||
});
|
||||
|
||||
const onToggleBalance = useCallback(() => {
|
||||
if (price > 0) {
|
||||
setIsEthBalance(!isEthBalance);
|
||||
}
|
||||
}, [isEthBalance, price]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedBalanceData?.formatted) {
|
||||
setBalance(Number(fetchedBalanceData.formatted));
|
||||
}
|
||||
}, [fetchedBalanceData, targetNetwork]);
|
||||
|
||||
return { balance, price, isError, isLoading, onToggleBalance, isEthBalance };
|
||||
}
|
||||
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
20
packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ANIMATION_TIME = 2000;
|
||||
|
||||
export function useAnimationConfig(data: any) {
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [prevData, setPrevData] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (prevData !== undefined && prevData !== data) {
|
||||
setShowAnimation(true);
|
||||
setTimeout(() => setShowAnimation(false), ANIMATION_TIME);
|
||||
}
|
||||
setPrevData(data);
|
||||
}, [data, prevData]);
|
||||
|
||||
return {
|
||||
showAnimation,
|
||||
};
|
||||
}
|
||||
82
packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts
Normal file
82
packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffectOnce, useLocalStorage, useReadLocalStorage } from "usehooks-ts";
|
||||
import { Chain, hardhat } from "viem/chains";
|
||||
import { Connector, useAccount, useConnect } from "wagmi";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
import { burnerWalletId } from "~~/services/web3/wagmi-burner/BurnerConnector";
|
||||
import { getTargetNetworks } from "~~/utils/scaffold-eth";
|
||||
|
||||
const SCAFFOLD_WALLET_STORAGE_KEY = "scaffoldEth2.wallet";
|
||||
const WAGMI_WALLET_STORAGE_KEY = "wagmi.wallet";
|
||||
|
||||
// ID of the SAFE connector instance
|
||||
const SAFE_ID = "safe";
|
||||
|
||||
/**
|
||||
* This function will get the initial wallet connector (if any), the app will connect to
|
||||
* @param initialNetwork
|
||||
* @param previousWalletId
|
||||
* @param connectors
|
||||
* @returns
|
||||
*/
|
||||
const getInitialConnector = (
|
||||
initialNetwork: Chain,
|
||||
previousWalletId: string,
|
||||
connectors: Connector[],
|
||||
): { connector: Connector | undefined; chainId?: number } | undefined => {
|
||||
// Look for the SAFE connector instance and connect to it instantly if loaded in SAFE frame
|
||||
const safeConnectorInstance = connectors.find(connector => connector.id === SAFE_ID && connector.ready);
|
||||
|
||||
if (safeConnectorInstance) {
|
||||
return { connector: safeConnectorInstance };
|
||||
}
|
||||
|
||||
const allowBurner = scaffoldConfig.onlyLocalBurnerWallet ? initialNetwork.id === hardhat.id : true;
|
||||
|
||||
if (!previousWalletId) {
|
||||
// The user was not connected to a wallet
|
||||
if (allowBurner && scaffoldConfig.walletAutoConnect) {
|
||||
const connector = connectors.find(f => f.id === burnerWalletId);
|
||||
return { connector, chainId: initialNetwork.id };
|
||||
}
|
||||
} else {
|
||||
// the user was connected to wallet
|
||||
if (scaffoldConfig.walletAutoConnect) {
|
||||
if (previousWalletId === burnerWalletId && !allowBurner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connector = connectors.find(f => f.id === previousWalletId);
|
||||
return { connector };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically connect to a wallet/connector based on config and prior wallet
|
||||
*/
|
||||
export const useAutoConnect = (): void => {
|
||||
const wagmiWalletValue = useReadLocalStorage<string>(WAGMI_WALLET_STORAGE_KEY);
|
||||
const [walletId, setWalletId] = useLocalStorage<string>(SCAFFOLD_WALLET_STORAGE_KEY, wagmiWalletValue ?? "", {
|
||||
initializeWithValue: false,
|
||||
});
|
||||
const connectState = useConnect();
|
||||
useAccount({
|
||||
onConnect({ connector }) {
|
||||
setWalletId(connector?.id ?? "");
|
||||
},
|
||||
onDisconnect() {
|
||||
window.localStorage.setItem(WAGMI_WALLET_STORAGE_KEY, JSON.stringify(""));
|
||||
setWalletId("");
|
||||
},
|
||||
});
|
||||
|
||||
useEffectOnce(() => {
|
||||
const initialConnector = getInitialConnector(getTargetNetworks()[0], walletId, connectState.connectors);
|
||||
|
||||
if (initialConnector?.connector) {
|
||||
connectState.connect({ connector: initialConnector.connector, chainId: initialConnector.chainId });
|
||||
}
|
||||
});
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user