Initial commit

This commit is contained in:
Yash Goyal
2024-03-22 20:42:31 +05:30
committed by GitHub
commit 3ac3b294a8
148 changed files with 23070 additions and 0 deletions

58
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged --verbose

21
.lintstagedrc.js Normal file
View 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],
};

File diff suppressed because one or more lines are too long

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
View 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
View 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
View 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
View 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.
![Debug Contracts tab](https://github.com/scaffold-eth/scaffold-eth-2/assets/55535804/b237af0c-5027-4849-a5c1-2e31495cccb1)
## 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
View 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"
}
}

View 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=

View File

@@ -0,0 +1,8 @@
# folders
artifacts
cache
contracts
node_modules/
typechain-types
# files
**/*.json

View 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
View 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

View 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"
}
}
]
}

View 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 {}
}

View 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"];

View 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;

View 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;

View 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"
}
}

View 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;
});

View 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;
});

View 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);
});
});
});

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

View 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=

View File

@@ -0,0 +1,11 @@
# folders
.next
node_modules/
# files
**/*.less
**/*.css
**/*.scss
**/*.json
**/*.png
**/*.svg
**/generated/**/*

View 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
View 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
View File

@@ -0,0 +1 @@
strict-peer-dependencies = false

View File

@@ -0,0 +1,8 @@
{
"arrowParens": "avoid",
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "all",
"importOrder": ["^react$", "^next/(.*)$", "<THIRD_PARTY_MODULES>", "^@heroicons/(.*)$", "^~~/(.*)$"],
"importOrderSortSpecifiers": true
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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} />}
</>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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";

View 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;

View 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;

View 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;

View 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;

View 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>
);
}

View File

@@ -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>
);
};

View File

@@ -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}
/>
))}
</>
);
};

View 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>
);
};

View File

@@ -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}
/>
))}
</>
);
};

View File

@@ -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}
/>
))}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
)}
</>
);

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View 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";

View 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,
};

View File

@@ -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);

View 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;

View 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;

View 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;

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>;
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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`}
/>
);

View 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>
);
};

View 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>
);
};

View 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" />
}
/>
);
};

View File

@@ -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>
}
/>
);
};

View 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>
}
/>
);
};

View 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>
}
/>
);
};

View 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>
);
};

View File

@@ -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>
)
}
/>
);
};

View 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";

View 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);

View 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;
}

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
))}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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";

View 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;

View 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;

View 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";

View 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 };
}

View 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,
};
}

View 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