Initial commit with 🏗️ create-eth @ 2.0.4

This commit is contained in:
han
2026-01-21 11:18:02 +07:00
commit 2acc4d2eae
145 changed files with 27715 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
---
description:
globs:
alwaysApply: true
---
This codebase contains Scaffold-ETH 2 (SE-2), everything you need to build dApps on Ethereum. Its tech stack is NextJS, RainbowKit, Wagmi and Typescript. Supports Hardhat and Foundry.
It's a yarn monorepo that contains following packages:
- Hardhat (`packages/hardhat`): The solidity framework to write, test and deploy EVM Smart Contracts.
- NextJS (`packages/nextjs`): The UI framework extended with utilities to make interacting with Smart Contracts easy (using Next.js App Router, not Pages Router).
The usual dev flow is:
- Start SE-2 locally:
- `yarn chain`: Starts a local blockchain network
- `yarn deploy`: Deploys SE-2 default contract
- `yarn start`: Starts the frontend
- Write a Smart Contract (modify the deployment script in `packages/hardhat/deploy` if needed)
- Deploy it locally (`yarn deploy`)
- Go to the `http://locahost:3000/debug` page to interact with your contract with a nice UI
- Iterate until you get the functionality you want in your contract
- Write tests for the contract in `packages/hardhat/test`
- Create your custom UI using all the SE-2 components, hooks, and utilities.
- Deploy your Smart Contrac to a live network
- Deploy your UI (`yarn vercel` or `yarn ipfs`)
- You can tweak which network the frontend is pointing (and some other configurations) in `scaffold.config.ts`
## Smart Contract UI interactions guidelines
SE-2 provides a set of hooks that facilitates contract interactions from the UI. It reads the contract data from `deployedContracts.ts` and `externalContracts.ts`, located in `packages/nextjs/contracts`.
### Reading data from a contract
Use the `useScaffoldReadContract` (`packages/nextjs/hooks/scaffold-eth/useScaffoldReadContract.ts`) hook.
Example:
```typescript
const { data: someData } = useScaffoldReadContract({
contractName: "YourContract",
functionName: "functionName",
args: [arg1, arg2], // optional
});
```
### Writing data to a contract
Use the `useScaffoldWriteContract` (`packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts`) hook.
1. Initilize the hook with just the contract name
2. Call the `writeContractAsync` function.
Example:
```typescript
const { writeContractAsync: writeYourContractAsync } = useScaffoldWriteContract(
{ contractName: "YourContract" }
);
// Usage (this will send a write transaction to the contract)
await writeContractAsync({
functionName: "functionName",
args: [arg1, arg2], // optional
value: parseEther("0.1"), // optional, for payable functions
});
```
Never use any other patterns for contract interaction. The hooks are:
- useScaffoldReadContract (for reading)
- useScaffoldWriteContract (for writing)
### Reading events from a contract
Use the `useScaffoldEventHistory` (`packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts`) hook.
Example:
```typescript
const {
data: events,
isLoading,
error,
} = useScaffoldEventHistory({
contractName: "YourContract",
eventName: "GreetingChange",
watch: true, // optional, if true, the hook will watch for new events
});
```
The `data` property consists of an array of events and can be displayed as:
```jsx
<div>
{events?.map((event) => (
<div key={event.logIndex}>
<p>{event.args.greetingSetter}</p>
<p>{event.args.newGreeting}</p>
<p>{event.args.premium}</p>
<p>{event.args.value}</p>
</div>
))}
</div>
```
### Other Hooks
SE-2 also provides other hooks to interact with blockchain data: `useScaffoldWatchContractEvent`, `useScaffoldEventHistory`, `useDeployedContractInfo`, `useScaffoldContract`, `useTransactor`. They live under `packages/nextjs/hooks/scaffold-eth`.
## Display Components guidelines
With the `@scaffold-ui/components` library, SE-2 provides a set of pre-built React components for common Ethereum use cases:
- `Address`: Always use this when displaying an ETH address
- `AddressInput`: Always use this when users need to input an ETH address
- `Balance`: Display the ETH/USDC balance of a given address
- `EtherInput`: An extended number input with ETH/USD conversion.
For fully customizable components, you can use the hooks from the `@scaffold-ui/hooks` library to get the data you need.
Find the relevant information from the documentation and the codebase. Think step by step before answering the question.

1
.cursorignore Normal file
View File

@@ -0,0 +1 @@
*

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 hardhat lint
run: yarn hardhat:lint --max-warnings=0
- name: Run nextjs lint
run: yarn next:lint --max-warnings=0
- name: Check typings on nextjs
run: yarn next:check-types

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# dependencies
node_modules
# yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# eslint
.eslintcache
# misc
.DS_Store
# IDE
.vscode
.idea
# cli
dist

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
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

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

13
.yarnrc.yml Normal file
View File

@@ -0,0 +1,13 @@
enableColors: true
nmHoistingLimits: workspaces
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
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.

321
README.md Normal file
View File

@@ -0,0 +1,321 @@
# 🏗 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 (>= v20.18.3)](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)
# 🚩 Challenge: 🎲 Dice Game
![readme](https://raw.githubusercontent.com/scaffold-eth/se-2-challenges/challenge-dice-game/extension/packages/nextjs/public/hero.png)
> 🎰 Randomness is tricky on a public deterministic blockchain. The block hash is an easy to use, but very weak form of randomness. This challenge will give you an example of a contract using the block hash to create random numbers. This randomness is exploitable. Other, stronger forms of randomness include commit/reveal schemes, oracles, or VRF from Chainlink.
> 💬 Dice Game is a contract that allows users to roll the dice to try and win the prize. If players roll either a 0, 1, 2, 3, 4 or 5 they will win the current prize amount. The initial prize is 10% of the contract's balance, which starts out at .05 Eth.
> 🧤 Every time a player rolls the dice, they are required to send .002 Eth. 40 percent of this value is added to the current prize amount while the other 60 percent stays in the contract to fund future prizes. Once a prize is won, the new prize amount is set to 10% of the total balance of the DiceGame contract.
> 🧨 Your job is to attack the Dice Game contract! You will create a new contract that will predict the randomness ahead of time and only roll the dice when you're guaranteed to be a winner!
> 💬 Meet other builders working on this challenge and get help in the [Challenge telegram](https://t.me/+3StA0aBSArFjNjUx)!
---
## Checkpoint 0: 📦 Environment 📚
> Start your local network (a blockchain emulator in your computer):
```sh
yarn chain
```
> in a second terminal window, 🛰 deploy your contract (locally):
```sh
yarn deploy
```
> in a third terminal window, start your 📱 frontend:
```sh
yarn start
```
📱 Open http://localhost:3000 to see the app.
> 👩‍💻 Rerun `yarn deploy --reset` whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address.
---
⚠️ We have disabled AI in Cursor and VSCode and highly suggest that you do not enable it so you can focus on the challenge, do everything by yourself, and hence better understand and remember things. If you are using another IDE, please disable AI yourself.
🔧 If you are a vibe-coder and don't care about understanding the syntax of the code used and just want to understand the general takeaways, you can re-enable AI by:
- Cursor: remove `*` from `.cursorignore` file
- VSCode: set `chat.disableAIFeatures` to `false` in `.vscode/settings.json` file
---
## Checkpoint 1: 🎲 Dice Game
🔍 Inspect the code in the `DiceGame.sol` contract in `packages/hardhat/contracts`
🔒 You will not be changing any code in the `DiceGame.sol` contract in this challenge. You will write your own contract to predict the outcome, then only roll the dice when it is favourable.
💸 Grab some funds from the faucet and roll the dice a few times. Watch the balance of the DiceGame contract in the Debug tab. It increases on a failed roll and decreases by the prize amount on a successful roll.
![Faucet](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/e82e3100-20fb-4886-a6bf-4113c3729f53)
### 🥅 Goals
- [ ] Track the solidity code to find out how the DiceGame contract is generating random numbers.
- [ ] Is it possible to predict the random number for any given roll?
---
## Checkpoint 2: 🔑 Rigged Contract
Start by creating a `receive()` function in the `RiggedRoll.sol` contract to allow it to receive ETH. This will allow us to fund the `RiggedRoll` contract from the faucet, which is required for our contract to call `rollTheDice()`.
<details markdown='1'>
<summary>🔎 Hint</summary>
Your rigged contract needs to be able to receive ETH via a plain transfer (no calldata). In Solidity, thats what `receive()` is for.
<details markdown='1'>
<summary>🎯 Solution</summary>
```solidity
receive() external payable {}
```
</details>
</details>
Add these custom error to the error section in `RiggedRoll.sol`:
```solidity
error NotEnoughETH(uint256 required, uint256 available);
error NotWinningRoll(uint256 roll);
```
Next add a `riggedRoll()` function. This function should predict the randomness of a roll, and if the outcome will be a winner, call `rollTheDice()` on the `DiceGame` contract. Revert with `NotEnoughETH` if the contract doesn't hold the required .002 ETH balance. Revert with `NotWinningRoll` if the calculated roll is not a winning number.
🃏 Predict the outcome by generating your random numbers in the exact same way as the DiceGame contract.
> 📣 Reminder! Calling `rollTheDice()` will fail unless you send a message value of at least .002 Eth! [Here is one example of how to send value with a function call.](https://ethereum.stackexchange.com/questions/6665/call-contract-and-send-value-from-solidity)
🎲 Keep in mind the dice on the frontend is using hexadecimal characters but in the contract we can get by with integers. A,B,C,D,E,F = 10,11,12,13,14,15
❓ If you're struggling to get the exact same random number as the DiceGame contract, try adding some `console.log()` statements in both contracts to help you track the values. These messages will appear in the Hardhat node terminal.
<details markdown='1'>
<summary>🔎 Hint</summary>
To match `DiceGame.sol`, youll want to:
- Read the current `nonce` from the `DiceGame` contract
- Use `blockhash(block.number - 1)` as the previous block hash
- Hash exactly the same tuple: `(prevHash, diceGameAddress, nonce)`
- Convert to a roll with `% 16`
- Only call `rollTheDice()` when the roll will be a winner
- Revert with `NotWinningRoll` if not a winning number; This way you never pay for gas unless you are winning
- Revert with `NotEnoughETH` if your contract doesnt have at least `0.002 ether` to spend
<details markdown='1'>
<summary>🎯 Solution</summary>
```solidity
function riggedRoll() external {
uint256 required = 0.002 ether;
uint256 available = address(this).balance;
if (available < required) revert NotEnoughETH(required, available);
bytes32 prevHash = blockhash(block.number - 1);
uint256 nonce = diceGame.nonce();
bytes32 hash = keccak256(abi.encodePacked(prevHash, address(diceGame), nonce));
uint256 roll = uint256(hash) % 16;
// Only roll when we know well win (matches the provided tests)
if (roll > 5) revert NotWinningRoll(roll);
diceGame.rollTheDice{value: required}();
}
```
</details>
</details>
🚀 To deploy your RiggedRoll contract, uncomment the appropriate lines in the `01_deploy_riggedRoll.ts` file in `packages/hardhat/deploy` and run `yarn deploy --reset`
💸 You will need to send some funds to your RiggedRoll contract before doing your first roll, you can use the Faucet button at the bottom left of the page.
🐞 Go to the `Debug Contracts` tab and try the `riggedRoll` method inside the `RiggedRoll` contract.
### ⚔️ Side Quest
- [ ] Uncomment the code in `packages/nextjs/app/dice/page.tsx` to show a riggedRoll button and contract balance on the main UI tab. Now you can test your function without switching tabs.
- [ ] Does your riggedRoll function only call `rollTheDice()` when it's going to be a winning roll? What happens when it does call `rollTheDice()`?
![RiggedLosingRoll](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/b6c8d7b4-139b-4f54-a62c-a0c77b3692a3)
### Testing your progress
🔍 Run the following command to check if you implemented the rigged roll logic correctly:
```shell
yarn test --grep "Checkpoint2"
```
✅ Did the tests pass? You can dig into any errors by viewing the tests at `packages/hardhat/test/RiggedRoll.ts`.
---
## Checkpoint 3: 💵 Where's my money?!?
You have beaten the game, but where is your money? Since the RiggedRoll contract is the one calling `rollTheDice()`, that is where the prize money is being sent.
![RiggedRollAddress](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/e9b9d164-2fb1-416a-9c5e-198d15bca0c6)
First, add another custom error to `RiggedRoll.sol`:
```solidity
error InsufficientBalance(uint256 requested, uint256 available);
```
📥 Now create a `withdraw(address _addr, uint256 _amount)` function to allow you to send ETH from RiggedRoll to another address.
Make sure you lock the withdraw function so it can only be called by the owner. The `Ownable` contract is already inherited so you have access to the `onlyOwner` modifier.
![WithdrawOnlyOwner](https://github.com/scaffold-eth/se-2-challenges/assets/55535804/e8397b1e-a077-4009-b518-30a6d8deb6e7)
> ⚠️ But wait, I am not the owner! You will want to set your front end address as the owner in `01_deploy_riggedRoll.ts`. This will allow your front end address to call the withdraw function.
<details markdown='1'>
<summary>🔎 Hint</summary>
- You should be able to withdraw the full contract balance.
- Use `call` to transfer ETH.
- You should revert with `InsufficientBalance(requested, available)` if `_amount` is greater than `address(this).balance`.
<details markdown='1'>
<summary>🎯 Solution</summary>
```solidity
function withdraw(address _addr, uint256 _amount) external onlyOwner {
uint256 available = address(this).balance;
if (_amount > available) revert InsufficientBalance(_amount, available);
(bool success, ) = payable(_addr).call{value: _amount}("");
require(success, "Withdraw failed");
}
```
</details>
</details>
### 🥅 Goals
- [ ] Can you send value from the RiggedRoll contract to your front end address?
- [ ] Is anyone able to call the withdraw function or only the owner?
### Testing your progress
🔍 Run the following command to check if you implemented the withdraw function correctly:
```shell
yarn test --grep "Checkpoint3"
```
✅ Did the tests pass? You can dig into any errors by viewing the tests at `packages/hardhat/test/RiggedRoll.ts`.
## Checkpoint 4: 💾 Deploy your contracts! 🛰
📡 Edit the `defaultNetwork` in `hardhat.config.ts` to match the name of one of testnets from the `networks` object. We recommend to use `"sepolia"` or `"optimismSepolia"`
🔐 You will need to generate a **deployer address** using `yarn generate` This creates a mnemonic and saves it locally.
👩‍🚀 Use `yarn account` to view your deployer account balances.
⛽️ You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network. You can also request ETH by sending a message with your new deployer address and preferred network in the [challenge Telegram](https://t.me/+3StA0aBSArFjNjUx). People are usually more than willing to share.
🚀 Run `yarn deploy` to deploy your smart contract to a public network (selected in `hardhat.config.ts`)
> 💬 Hint: Instead of editing `hardhat.config.ts` you can just add a network flag to the deploy command like this: `yarn deploy --network sepolia` or `yarn deploy --network optimismSepolia`
---
## Checkpoint 5: 🚢 Ship your frontend! 🚁
✏️ Edit your frontend config in `packages/nextjs/scaffold.config.ts` to change the `targetNetwork` to `chains.sepolia` (or `chains.optimismSepolia` if you deployed to OP Sepolia)
💻 View your frontend at http://localhost:3000 and verify you see the correct network.
📡 When you are ready to ship the frontend app...
📦 Run `yarn vercel` to package up your frontend and deploy.
> You might need to log in to Vercel first by running `yarn vercel:login`. Once you log in (email, GitHub, etc), the default options should work.
> If you want to redeploy to the same production URL you can run `yarn vercel --prod`. If you omit the `--prod` flag it will deploy it to a preview/test URL.
> Follow the steps to deploy to Vercel. It'll give you a public URL.
> 🦊 Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default 🔥 `burner wallets` are only available on `hardhat` . You can enable them on every chain by setting `onlyLocalBurnerWallet: false` in your frontend config (`scaffold.config.ts` in `packages/nextjs/`)
#### Configuration of Third-Party Services for Production-Grade Apps.
By default, 🏗 Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services.
This is great to complete your **SpeedRunEthereum**.
For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at:
- 🔷`ALCHEMY_API_KEY` variable in `packages/hardhat/.env` and `packages/nextjs/.env.local`. You can create API keys from the [Alchemy dashboard](https://dashboard.alchemy.com/).
- 📃`ETHERSCAN_API_KEY` variable in `packages/hardhat/.env` with your generated API key. You can get your key [here](https://etherscan.io/myapikey).
> 💬 Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing.
---
## Checkpoint 6: 📜 Contract Verification
Run the `yarn verify --network your_network` command to verify your contracts on etherscan 🛰
👉 Search this address on [Sepolia Etherscan](https://sepolia.etherscan.io/) (or [Optimism Sepolia Etherscan](https://sepolia-optimism.etherscan.io/) if you deployed to OP Sepolia) to get the URL you submit to 🏃‍♀️[SpeedRunEthereum.com](https://speedrunethereum.com).
---
> 🏃 Head to your next challenge [here](https://speedrunethereum.com).
> 💬 Problems, questions, comments on the stack? Post them to the [🏗 scaffold-eth developers chat](https://t.me/joinchat/F7nCRK3kI93PoCOk)
## 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.

60
package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "se-2",
"version": "0.0.1",
"private": true,
"workspaces": {
"packages": [
"packages/*"
]
},
"scripts": {
"account": "yarn hardhat:account",
"account:generate": "yarn workspace @se-2/hardhat account:generate",
"account:import": "yarn workspace @se-2/hardhat account:import",
"account:reveal-pk": "yarn workspace @se-2/hardhat account:reveal-pk",
"chain": "yarn hardhat:chain",
"compile": "yarn hardhat:compile",
"deploy": "yarn hardhat:deploy",
"fork": "yarn hardhat:fork",
"format": "yarn next:format && yarn hardhat:format",
"generate": "yarn account:generate",
"hardhat:account": "yarn workspace @se-2/hardhat account",
"hardhat:chain": "yarn workspace @se-2/hardhat chain",
"hardhat:check-types": "yarn workspace @se-2/hardhat check-types",
"hardhat:clean": "yarn workspace @se-2/hardhat clean",
"hardhat:compile": "yarn workspace @se-2/hardhat compile",
"hardhat:deploy": "yarn workspace @se-2/hardhat deploy",
"hardhat:flatten": "yarn workspace @se-2/hardhat flatten",
"hardhat:fork": "yarn workspace @se-2/hardhat fork",
"hardhat:format": "yarn workspace @se-2/hardhat format",
"hardhat:generate": "yarn workspace @se-2/hardhat generate",
"hardhat:hardhat-verify": "yarn workspace @se-2/hardhat hardhat-verify",
"hardhat:lint": "yarn workspace @se-2/hardhat lint",
"hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged",
"hardhat:test": "yarn workspace @se-2/hardhat test",
"hardhat:verify": "yarn workspace @se-2/hardhat verify",
"postinstall": "husky install",
"ipfs": "yarn workspace @se-2/nextjs ipfs",
"lint": "yarn next:lint && yarn hardhat:lint",
"next:build": "yarn workspace @se-2/nextjs build",
"next:check-types": "yarn workspace @se-2/nextjs check-types",
"next:format": "yarn workspace @se-2/nextjs format",
"next:lint": "yarn workspace @se-2/nextjs lint",
"next:serve": "yarn workspace @se-2/nextjs serve",
"precommit": "lint-staged",
"start": "yarn workspace @se-2/nextjs dev",
"test": "yarn hardhat:test",
"vercel": "yarn workspace @se-2/nextjs vercel",
"vercel:login": "yarn workspace @se-2/nextjs vercel:login",
"vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo",
"verify": "yarn hardhat:verify"
},
"devDependencies": {
"husky": "~9.1.6",
"lint-staged": "~13.2.2"
},
"packageManager": "yarn@3.2.3",
"engines": {
"node": ">=20.18.3"
}
}

View File

@@ -0,0 +1,13 @@
# 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=
ETHERSCAN_V2_API_KEY=
# Don't fill this value manually, run yarn generate to generate a new account or yarn account:import to import an existing PK.
DEPLOYER_PRIVATE_KEY_ENCRYPTED=

30
packages/hardhat/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# dependencies
node_modules
# env files
.env
# coverage
coverage
coverage.json
# typechain
typechain
typechain-types
# hardhat files
cache
artifacts
# zkSync files
artifacts-zk
cache-zk
# deployments
deployments/localhost
# typescript
*.tsbuildinfo
# other
temp

View File

@@ -0,0 +1,18 @@
{
"plugins": ["prettier-plugin-solidity"],
"arrowParens": "avoid",
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "all",
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 120,
"tabWidth": 4,
"singleQuote": false,
"bracketSpacing": true
}
}
]
}

View File

@@ -0,0 +1,72 @@
pragma solidity >=0.8.0 <0.9.0; //Do not change the solidity version as it negatively impacts submission grading
//SPDX-License-Identifier: MIT
import "hardhat/console.sol";
contract DiceGame {
/////////////////
/// Errors //////
/////////////////
error NotEnoughEther();
//////////////////////
/// State Variables //
//////////////////////
uint256 public nonce = 0;
uint256 public prize = 0;
////////////////
/// Events /////
////////////////
event Roll(address indexed player, uint256 amount, uint256 roll);
event Winner(address winner, uint256 amount);
///////////////////
/// Constructor ///
///////////////////
constructor() payable {
resetPrize();
}
///////////////////
/// Functions /////
///////////////////
function resetPrize() private {
prize = ((address(this).balance * 10) / 100);
}
function rollTheDice() public payable {
if (msg.value < 0.002 ether) {
revert NotEnoughEther();
}
bytes32 prevHash = blockhash(block.number - 1);
bytes32 hash = keccak256(abi.encodePacked(prevHash, address(this), nonce));
uint256 roll = uint256(hash) % 16;
console.log("\t", " Dice Game Roll:", roll);
nonce++;
prize += ((msg.value * 40) / 100);
emit Roll(msg.sender, msg.value, roll);
if (roll > 5) {
return;
}
uint256 amount = prize;
(bool sent, ) = msg.sender.call{ value: amount }("");
require(sent, "Failed to send Ether");
resetPrize();
emit Winner(msg.sender, amount);
}
receive() external payable {}
}

View File

@@ -0,0 +1,34 @@
pragma solidity >=0.8.0 <0.9.0; //Do not change the solidity version as it negatively impacts submission grading
//SPDX-License-Identifier: MIT
import "hardhat/console.sol";
import "./DiceGame.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract RiggedRoll is Ownable {
/////////////////
/// Errors //////
/////////////////
// Errors go here...
//////////////////////
/// State Variables //
//////////////////////
DiceGame public diceGame;
///////////////////
/// Constructor ///
///////////////////
constructor(address payable diceGameAddress) Ownable(msg.sender) {
diceGame = DiceGame(diceGameAddress);
}
///////////////////
/// Functions /////
///////////////////
// Functions go here...
}

View File

@@ -0,0 +1,29 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers } from "hardhat/";
import { DiceGame } from "../typechain-types/";
const deployDiceGame: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
await deploy("DiceGame", {
from: deployer,
value: String(ethers.parseEther("0.05")),
log: true,
});
// Simple example on how get the deployed dice game contract
const diceGame: DiceGame = await ethers.getContract("DiceGame");
const diceGameAddress = await diceGame.getAddress();
console.log("Deployed Dice Game Contract Address", diceGameAddress);
const balance = await ethers.provider.getBalance(diceGameAddress);
console.log("Deployed Dice Game Contract Balance", ethers.formatEther(balance.toString()));
};
export default deployDiceGame;
deployDiceGame.tags = ["DiceGame"];

View File

@@ -0,0 +1,33 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers } from "hardhat/";
import { DiceGame, RiggedRoll } from "../typechain-types";
const deployRiggedRoll: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
const diceGame: DiceGame = await ethers.getContract("DiceGame");
const diceGameAddress = await diceGame.getAddress();
// Uncomment to deploy RiggedRoll contract
// await deploy("RiggedRoll", {
// from: deployer,
// log: true,
// args: [diceGameAddress],
// autoMine: true,
// });
// const riggedRoll: RiggedRoll = await ethers.getContract("RiggedRoll", deployer);
// Please replace the text "Your Address" with your own address.
// try {
// await riggedRoll.transferOwnership("Your Address");
// } catch (err) {
// console.log(err);
// }
};
export default deployRiggedRoll;
deployRiggedRoll.tags = ["RiggedRoll"];

View File

@@ -0,0 +1,44 @@
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import prettierPlugin from "eslint-plugin-prettier";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
export default defineConfig([
globalIgnores(["**/artifacts", "**/cache", "**/contracts", "**/node_modules/", "**/typechain-types", "**/*.json"]),
{
extends: compat.extends("plugin:@typescript-eslint/recommended", "prettier"),
plugins: {
prettier: prettierPlugin,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
},
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "off",
"prettier/prettier": [
"warn",
{
endOfLine: "auto",
},
],
},
},
]);

View File

@@ -0,0 +1,156 @@
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";
import { task } from "hardhat/config";
import generateTsAbis from "./scripts/generateTsAbis";
// 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 || "cR4WnXePioePZ5fFrnSiR";
// If not set, it uses the hardhat account 0 private key.
// You can generate a random account with `yarn generate` or `yarn account:import` to import your existing PK
const deployerPrivateKey =
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
// If not set, it uses our block explorers default API keys.
const etherscanApiKey = process.env.ETHERSCAN_V2_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.20",
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: {
mainnet: {
url: "https://mainnet.rpc.buidlguidl.com",
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],
},
polygonAmoy: {
url: `https://polygon-amoy.g.alchemy.com/v2/${providerApiKey}`,
accounts: [deployerPrivateKey],
},
polygonZkEvm: {
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${providerApiKey}`,
accounts: [deployerPrivateKey],
},
polygonZkEvmCardona: {
url: `https://polygonzkevm-cardona.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],
},
celo: {
url: "https://forno.celo.org",
accounts: [deployerPrivateKey],
},
celoSepolia: {
url: "https://forno.celo-sepolia.celo-testnet.org/",
accounts: [deployerPrivateKey],
},
// 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",
},
mining: {
auto: true,
interval: 1000,
},
},
},
// Configuration for harhdat-verify plugin
etherscan: {
apiKey: etherscanApiKey,
},
// Configuration for etherscan-verify from hardhat-deploy plugin
verify: {
etherscan: {
apiKey: etherscanApiKey,
},
},
sourcify: {
enabled: false,
},
};
// Extend the deploy task
task("deploy").setAction(async (args, hre, runSuper) => {
// Run the original deploy task
await runSuper(args);
// Force run the generateTsAbis script
await generateTsAbis(hre);
});
export default config;

View File

@@ -0,0 +1,63 @@
{
"name": "@se-2/hardhat",
"version": "0.0.1",
"scripts": {
"account": "hardhat run scripts/listAccount.ts",
"account:generate": "hardhat run scripts/generateAccount.ts",
"account:import": "hardhat run scripts/importAccount.ts",
"account:reveal-pk": "hardhat run scripts/revealPK.ts",
"chain": "hardhat node --network hardhat --no-deploy",
"check-types": "tsc --noEmit --incremental",
"clean": "hardhat clean",
"compile": "hardhat compile",
"deploy": "ts-node scripts/runHardhatDeployWithPK.ts",
"flatten": "hardhat flatten",
"fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy",
"format": "prettier --write './**/*.(ts|sol)'",
"generate": "yarn account:generate",
"hardhat-verify": "hardhat verify",
"lint": "eslint",
"lint-staged": "eslint",
"test": "REPORT_GAS=true hardhat test --network hardhat",
"verify": "hardhat etherscan-verify"
},
"dependencies": {
"@inquirer/password": "^4.0.2",
"@openzeppelin/contracts": "~5.0.2",
"@typechain/ethers-v6": "~0.5.1",
"dotenv": "~16.4.5",
"envfile": "~7.1.0",
"qrcode": "~1.5.4"
},
"devDependencies": {
"@ethersproject/abi": "~5.7.0",
"@ethersproject/providers": "~5.7.2",
"@nomicfoundation/hardhat-chai-matchers": "~2.0.7",
"@nomicfoundation/hardhat-ethers": "~3.0.8",
"@nomicfoundation/hardhat-network-helpers": "~1.0.11",
"@nomicfoundation/hardhat-verify": "~2.0.10",
"@typechain/ethers-v5": "~11.1.2",
"@typechain/hardhat": "~9.1.0",
"@types/eslint": "~9.6.1",
"@types/mocha": "~10.0.10",
"@types/prettier": "~3.0.0",
"@types/qrcode": "~1.5.5",
"@typescript-eslint/eslint-plugin": "~8.27.0",
"@typescript-eslint/parser": "~8.27.0",
"chai": "~4.5.0",
"eslint": "~9.23.0",
"eslint-config-prettier": "~10.1.1",
"eslint-plugin-prettier": "~5.2.4",
"ethers": "~6.13.2",
"hardhat": "~2.22.10",
"hardhat-deploy": "^1.0.4",
"hardhat-deploy-ethers": "~0.4.2",
"hardhat-gas-reporter": "~2.2.1",
"prettier": "^3.5.3",
"prettier-plugin-solidity": "~1.4.1",
"solidity-coverage": "~0.8.13",
"ts-node": "~10.9.1",
"typechain": "~8.3.2",
"typescript": "^5.8.2"
}
}

View File

@@ -0,0 +1,58 @@
import { ethers } from "ethers";
import { parse, stringify } from "envfile";
import * as fs from "fs";
import password from "@inquirer/password";
const envFilePath = "./.env";
const getValidatedPassword = async () => {
while (true) {
const pass = await password({ message: "Enter a password to encrypt your private key:" });
const confirmation = await password({ message: "Confirm password:" });
if (pass === confirmation) {
return pass;
}
console.log("❌ Passwords don't match. Please try again.");
}
};
const setNewEnvConfig = async (existingEnvConfig = {}) => {
console.log("👛 Generating new Wallet\n");
const randomWallet = ethers.Wallet.createRandom();
const pass = await getValidatedPassword();
const encryptedJson = await randomWallet.encrypt(pass);
const newEnvConfig = {
...existingEnvConfig,
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
};
// Store in .env
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
};
async function main() {
if (!fs.existsSync(envFilePath)) {
// No .env file yet.
await setNewEnvConfig();
return;
}
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
return;
}
await setNewEnvConfig(existingEnvConfig);
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,136 @@
/**
* 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>;
const chainDirectories = getDirectories(DEPLOYMENTS_DIR);
for (const chainName of chainDirectories) {
let chainId;
try {
chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
console.log(`No chainId file found for ${chainName}`);
continue;
}
const contracts = {} as Record<string, any>;
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
const { abi, address, metadata, receipt } = JSON.parse(
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
);
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: receipt?.blockNumber };
}
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`,
await 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;

View File

@@ -0,0 +1,72 @@
import { ethers } from "ethers";
import { parse, stringify } from "envfile";
import * as fs from "fs";
import password from "@inquirer/password";
const envFilePath = "./.env";
const getValidatedPassword = async () => {
while (true) {
const pass = await password({ message: "Enter a password to encrypt your private key:" });
const confirmation = await password({ message: "Confirm password:" });
if (pass === confirmation) {
return pass;
}
console.log("❌ Passwords don't match. Please try again.");
}
};
const getWalletFromPrivateKey = async () => {
while (true) {
const privateKey = await password({ message: "Paste your private key:" });
try {
const wallet = new ethers.Wallet(privateKey);
return wallet;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
console.log("❌ Invalid private key format. Please try again.");
}
}
};
const setNewEnvConfig = async (existingEnvConfig = {}) => {
console.log("👛 Importing Wallet\n");
const wallet = await getWalletFromPrivateKey();
const pass = await getValidatedPassword();
const encryptedJson = await wallet.encrypt(pass);
const newEnvConfig = {
...existingEnvConfig,
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
};
// Store in .env
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
console.log("🪄 Imported wallet address:", wallet.address, "\n");
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
};
async function main() {
if (!fs.existsSync(envFilePath)) {
// No .env file yet.
await setNewEnvConfig();
return;
}
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
return;
}
await setNewEnvConfig(existingEnvConfig);
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,52 @@
import * as dotenv from "dotenv";
dotenv.config();
import { ethers, Wallet } from "ethers";
import QRCode from "qrcode";
import { config } from "hardhat";
import password from "@inquirer/password";
async function main() {
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
if (!encryptedKey) {
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
return;
}
const pass = await password({ message: "Enter your password to decrypt the private key:" });
let wallet: Wallet;
try {
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
console.log("❌ Failed to decrypt private key. Wrong password?");
return;
}
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)));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
console.log("Can't connect to network", networkName);
}
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,31 @@
import * as dotenv from "dotenv";
dotenv.config();
import { Wallet } from "ethers";
import password from "@inquirer/password";
async function main() {
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
if (!encryptedKey) {
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
return;
}
console.log("👀 This will reveal your private key on the console.\n");
const pass = await password({ message: "Enter your password to decrypt the private key:" });
let wallet: Wallet;
try {
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
} catch {
console.log("❌ Failed to decrypt private key. Wrong password?");
return;
}
console.log("\n🔑 Private key:", wallet.privateKey);
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,58 @@
import * as dotenv from "dotenv";
dotenv.config();
import { Wallet } from "ethers";
import password from "@inquirer/password";
import { spawn } from "child_process";
import { config } from "hardhat";
/**
* Unencrypts the private key and runs the hardhat deploy command
*/
async function main() {
const networkIndex = process.argv.indexOf("--network");
const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
if (networkName === "localhost" || networkName === "hardhat") {
// Deploy command on the localhost network
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
stdio: "inherit",
env: process.env,
shell: process.platform === "win32",
});
hardhat.on("exit", code => {
process.exit(code || 0);
});
return;
}
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
if (!encryptedKey) {
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
return;
}
const pass = await password({ message: "Enter password to decrypt private key:" });
try {
const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
stdio: "inherit",
env: process.env,
shell: process.platform === "win32",
});
hardhat.on("exit", code => {
process.exit(code || 0);
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
console.error("Failed to decrypt private key. Wrong password?");
process.exit(1);
}
}
main().catch(console.error);

View File

@@ -0,0 +1,2 @@
# Write tests for your smart contract in this directory
# Example: YourContract.ts

View File

@@ -0,0 +1,165 @@
import hre from "hardhat";
import { expect } from "chai";
// import { parseEther } from "ethers";
import { DiceGame, RiggedRoll, RiggedRoll__factory } from "../typechain-types";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
const { ethers } = hre;
describe("🚩 Challenge: 🎲 Dice Game", function () {
let diceGame: DiceGame;
let riggedRoll: RiggedRoll;
let deployer: HardhatEthersSigner;
const { provider } = ethers;
const rollAmountString = "0.002";
const rollAmount = ethers.parseEther(rollAmountString);
async function deployContracts() {
[deployer] = await ethers.getSigners();
const DiceGame = await ethers.getContractFactory("DiceGame");
diceGame = await DiceGame.deploy();
const contractAddress = process.env.CONTRACT_ADDRESS;
let contractArtifact;
if (contractAddress) {
contractArtifact = `contracts/download-${contractAddress}.sol:RiggedRoll`;
} else {
contractArtifact = "contracts/RiggedRoll.sol:RiggedRoll";
}
const diceGameAddress = await diceGame.getAddress();
const RiggedRoll = (await ethers.getContractFactory(contractArtifact)) as RiggedRoll__factory;
riggedRoll = await RiggedRoll.deploy(diceGameAddress);
}
async function fundRiggedContract() {
const riggedRollAddress = await riggedRoll.getAddress();
return deployer.sendTransaction({
to: riggedRollAddress,
value: rollAmount,
});
}
async function getRoll(getRollLessThanFive: boolean) {
let expectedRoll;
while (true) {
const latestBlockNumber = await provider.getBlockNumber();
const block = await provider.getBlock(latestBlockNumber);
if (!block) {
return;
}
const prevHash = block.hash;
const nonce = await diceGame.nonce();
const diceGameAddress = await diceGame.getAddress();
const hash = ethers.solidityPackedKeccak256(
["bytes32", "address", "uint256"],
[prevHash, diceGameAddress, nonce],
);
const bigInt = BigInt(hash);
expectedRoll = bigInt % 16n;
if (expectedRoll < 5n == getRollLessThanFive) {
break;
}
const options = { value: rollAmount };
await diceGame.rollTheDice(options);
}
return expectedRoll;
}
describe("Checkpoint2: 🔑 Rigged Contract", function () {
beforeEach(async function () {
await deployContracts();
});
it("Checkpoint2: Should deploy contracts", async function () {
const diceGameAddress = await diceGame.getAddress();
expect(await riggedRoll.diceGame()).to.equal(diceGameAddress);
});
it(`Checkpoint2: Should revert if balance is less than ${rollAmountString} ethers`, async function () {
await expect(riggedRoll.riggedRoll())
.to.be.revertedWithCustomError(riggedRoll, "NotEnoughETH")
.withArgs(rollAmount, 0);
});
it("Checkpoint2: Should transfer sufficient eth to RiggedRoll", async function () {
console.log("\t", "💸 Funding RiggedRoll contract");
await fundRiggedContract();
const riggedRollAddress = await riggedRoll.getAddress();
const balance = await provider.getBalance(riggedRollAddress);
console.log("\t", "💲 RiggedRoll balance: ", ethers.formatEther(balance));
expect(balance).to.gte(rollAmount, `Error when expecting DiceGame contract to have >= ${rollAmount} eth`);
});
it("Checkpoint2: Should call diceGame.rollTheDice for a roll <= 5", async () => {
console.log("\t", "💸 Funding RiggedRoll contract");
await fundRiggedContract();
const getRollLessThanFive = true;
const expectedRoll = await getRoll(getRollLessThanFive);
console.log("\t", "🎲 Expect roll to be less than or equal to 5. Dice Game Roll:", Number(expectedRoll));
const tx = await riggedRoll.riggedRoll();
const riggedRollAddress = await riggedRoll.getAddress();
await expect(tx).to.emit(diceGame, "Roll").withArgs(riggedRollAddress, rollAmount, expectedRoll);
await expect(tx).to.emit(diceGame, "Winner");
});
it("Checkpoint2: Should not call diceGame.rollTheDice for a roll > 5", async () => {
console.log("\t", "💸 Funding RiggedRoll contract");
await fundRiggedContract();
const getRollLessThanFive = false;
const expectedRoll = await getRoll(getRollLessThanFive);
console.log("\t", "🎲 Expect roll to be greater than 5. Dice Game Roll:", Number(expectedRoll));
console.log("\t", "◀ Expect riggedRoll to be reverted");
await expect(riggedRoll.riggedRoll())
.to.be.revertedWithCustomError(riggedRoll, "NotWinningRoll")
.withArgs(expectedRoll);
});
});
describe("Checkpoint3: 💵 Where's my money?!?", function () {
beforeEach(async function () {
await deployContracts();
});
it("Checkpoint3: Should withdraw funds", async () => {
console.log("\t", "💸 Funding RiggedRoll contract");
await fundRiggedContract();
const deployerPrevBalance = await provider.getBalance(deployer.address);
console.log("\t", "💲 Current RiggedRoll balance: ", ethers.formatEther(deployerPrevBalance));
const riggedRollAddress = await riggedRoll.getAddress();
const riggedRollBalance = await provider.getBalance(riggedRollAddress);
await riggedRoll.withdraw(deployer.address, riggedRollBalance);
const deployerCurrentBalance = await provider.getBalance(deployer.address);
console.log("\t", "💲 New RiggedRoll balance: ", ethers.formatEther(deployerCurrentBalance));
expect(
deployerPrevBalance < deployerCurrentBalance,
"Error when expecting RiggedRoll balance to increase when calling withdraw",
).to.true;
});
it("Checkpoint3: Should revert withdraw when amount exceeds contract balance", async () => {
console.log("\t", "💸 Funding RiggedRoll contract");
await fundRiggedContract();
const riggedRollAddress = await riggedRoll.getAddress();
const riggedRollBalance = await provider.getBalance(riggedRollAddress);
const tooMuch = riggedRollBalance + rollAmount;
await expect(riggedRoll.withdraw(deployer.address, tooMuch))
.to.be.revertedWithCustomError(riggedRoll, "InsufficientBalance")
.withArgs(tooMuch, riggedRollBalance);
});
});
});

View File

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

View File

@@ -0,0 +1,14 @@
# 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=

38
packages/nextjs/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# 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/
.vercel
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# typescript
*.tsbuildinfo
ipfs-upload.config.json

View File

@@ -0,0 +1,9 @@
module.exports = {
arrowParens: "avoid",
printWidth: 120,
tabWidth: 2,
trailingComma: "all",
importOrder: ["^react$", "^next/(.*)$", "<THIRD_PARTY_MODULES>", "^@heroicons/(.*)$", "^~~/(.*)$"],
importOrderSortSpecifiers: true,
plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")],
};

View File

@@ -0,0 +1,27 @@
type AddressCodeTabProps = {
bytecode: string;
assembly: string;
};
export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => {
const formattedAssembly = Array.from(assembly.matchAll(/\w+( 0x[a-fA-F0-9]+)?/g))
.map(it => it[0])
.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,48 @@
"use client";
import { BackButton } from "./BackButton";
import { ContractTabs } from "./ContractTabs";
import { Address, Balance } from "@scaffold-ui/components";
import { Address as AddressType } from "viem";
import { hardhat } from "viem/chains";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
export const AddressComponent = ({
address,
contractData,
}: {
address: AddressType;
contractData: { bytecode: string; assembly: string } | null;
}) => {
const { targetNetwork } = useTargetNetwork();
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"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${address}` : undefined
}
/>
<div className="flex gap-1 items-center">
<span className="font-bold text-sm">Balance:</span>
<Balance address={address} />
</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,102 @@
"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 { Address, createPublicClient, http } from "viem";
import { hardhat } from "viem/chains";
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
type AddressCodeTabProps = {
bytecode: string;
assembly: string;
};
type PageProps = {
address: Address;
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 role="tablist" className="tabs tabs-lift">
<button
role="tab"
className={`tab ${activeTab === "transactions" ? "tab-active" : ""}`}
onClick={() => setActiveTab("transactions")}
>
Transactions
</button>
<button
role="tab"
className={`tab ${activeTab === "code" ? "tab-active" : ""}`}
onClick={() => setActiveTab("code")}
>
Code
</button>
<button
role="tab"
className={`tab ${activeTab === "storage" ? "tab-active" : ""}`}
onClick={() => setActiveTab("storage")}
>
Storage
</button>
<button
role="tab"
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 ? "btn-disabled cursor-default" : "btn-primary";
const nextButtonClass = isNextButtonDisabled ? "btn-disabled cursor-default" : "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 placeholder:text-base-content/50 p-2 mr-2 w-full md:w-1/2 lg:w-1/3 rounded-md shadow-md focus:outline-hidden focus:ring-2 focus:ring-accent"
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,28 @@
import Link from "next/link";
import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
export const TransactionHash = ({ hash }: { hash: string }) => {
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
useCopyToClipboard();
return (
<div className="flex items-center">
<Link href={`/blockexplorer/transaction/${hash}`}>
{hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
</Link>
{isAddressCopiedToClipboard ? (
<CheckCircleIcon
className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
aria-hidden="true"
/>
) : (
<DocumentDuplicateIcon
className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
aria-hidden="true"
onClick={() => copyAddressToClipboard(hash)}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,97 @@
import { TransactionHash } from "./TransactionHash";
import { Address } from "@scaffold-ui/components";
import { formatEther } from "viem";
import { hardhat } from "viem/chains";
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/12 md:py-4">{timeMined}</td>
<td className="w-2/12 md:py-4">
<Address
address={tx.from}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${tx.from}` : undefined
}
/>
</td>
<td className="w-2/12 md:py-4">
{!receipt?.contractAddress ? (
tx.to && (
<Address
address={tx.to}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${tx.to}` : undefined
}
/>
)
) : (
<div className="relative">
<Address
address={receipt.contractAddress}
size="sm"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id
? `/blockexplorer/address/${receipt.contractAddress}`
: undefined
}
/>
<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,101 @@
import fs from "fs";
import path from "path";
import { Address } from "viem";
import { hardhat } from "viem/chains";
import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
import deployedContracts from "~~/contracts/deployedContracts";
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
type PageProps = {
params: Promise<{ address: Address }>;
};
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: Address) => {
const contracts = deployedContracts as GenericContractsDeclaration | null;
const chainId = hardhat.id;
if (!contracts || !contracts[chainId] || Object.keys(contracts[chainId]).length === 0) {
return null;
}
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[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 };
};
export function generateStaticParams() {
// An workaround to enable static exports in Next.js, generating single dummy page.
return [{ address: "0x0000000000000000000000000000000000000000" }];
}
const AddressPage = async (props: PageProps) => {
const params = await props.params;
const address = params?.address as Address;
if (isZeroAddress(address)) return null;
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"> targetNetwork </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,23 @@
import TransactionComp from "../_components/TransactionComp";
import type { NextPage } from "next";
import { Hash } from "viem";
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
type PageProps = {
params: Promise<{ txHash?: Hash }>;
};
export function generateStaticParams() {
// An workaround to enable static exports in Next.js, generating single dummy page.
return [{ txHash: "0x0000000000000000000000000000000000000000" }];
}
const TransactionPage: NextPage<PageProps> = async (props: PageProps) => {
const params = await props.params;
const txHash = params?.txHash as Hash;
if (isZeroAddress(txHash)) return null;
return <TransactionComp txHash={txHash} />;
};
export default TransactionPage;

View File

@@ -0,0 +1,177 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Address } from "@scaffold-ui/components";
import { Hash, Transaction, TransactionReceipt, formatEther, formatUnits } from "viem";
import { hardhat } from "viem/chains";
import { usePublicClient } from "wagmi";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { decodeTransactionData, getFunctionDetails } from "~~/utils/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
const TransactionComp = ({ txHash }: { txHash: Hash }) => {
const client = usePublicClient({ chainId: hardhat.id });
const router = useRouter();
const [transaction, setTransaction] = useState<Transaction>();
const [receipt, setReceipt] = useState<TransactionReceipt>();
const [functionCalled, setFunctionCalled] = useState<string>();
const { targetNetwork } = useTargetNetwork();
useEffect(() => {
if (txHash && client) {
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"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${transaction.from}` : undefined
}
/>
</td>
</tr>
<tr>
<td>
<strong>To:</strong>
</td>
<td>
{!receipt?.contractAddress ? (
transaction.to && (
<Address
address={transaction.to}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${transaction.to}` : undefined
}
/>
)
) : (
<span>
Contract Creation:
<Address
address={receipt.contractAddress}
format="long"
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id
? `/blockexplorer/address/${receipt.contractAddress}`
: undefined
}
/>
</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 w-full 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 TransactionComp;

View File

@@ -0,0 +1,38 @@
"use client";
// @refresh reset
import { Contract } from "@scaffold-ui/debug-contracts";
import { useDeployedContractInfo } 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 }: ContractUIProps) => {
const { targetNetwork } = useTargetNetwork();
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({ contractName });
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 <Contract contractName={contractName as string} contract={deployedContractData} chainId={targetNetwork.id} />;
};

View File

@@ -0,0 +1,71 @@
"use client";
import { useEffect, useMemo } from "react";
import { ContractUI } from "./ContractUI";
import "@scaffold-ui/debug-contracts/styles.css";
import { useSessionStorage } from "usehooks-ts";
import { BarsArrowUpIcon } from "@heroicons/react/20/solid";
import { ContractName, GenericContract } from "~~/utils/scaffold-eth/contract";
import { useAllContracts } from "~~/utils/scaffold-eth/contractsData";
const selectedContractStorageKey = "scaffoldEth2.selectedContract";
export function DebugContracts() {
const contractsData = useAllContracts();
const contractNames = useMemo(
() =>
Object.keys(contractsData).sort((a, b) => {
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
}) as ContractName[],
[contractsData],
);
const [selectedContract, setSelectedContract] = useSessionStorage<ContractName>(
selectedContractStorageKey,
contractNames[0],
{ initializeWithValue: false },
);
useEffect(() => {
if (!contractNames.includes(selectedContract)) {
setSelectedContract(contractNames[0]);
}
}, [contractNames, 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] as GenericContract)?.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 =>
contractName === selectedContract && <ContractUI key={contractName} contractName={contractName} />,
)}
</>
)}
</div>
);
}

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,68 @@
import { useEffect, useState } from "react";
import { useFetchNativeCurrencyPrice } from "@scaffold-ui/hooks";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
type TAmountProps = {
amount?: number;
className?: string;
isLoading?: boolean;
showUsdPrice?: boolean;
disableToggle?: boolean;
};
/**
* Display (ETH & USD) balance of an ETH address.
*/
export const Amount = ({
isLoading,
showUsdPrice = false,
amount = 0,
className = "",
disableToggle = false,
}: TAmountProps) => {
const { targetNetwork: configuredNetwork } = useTargetNetwork();
const { price } = useFetchNativeCurrencyPrice();
const [isEthBalance, setEthBalance] = useState<boolean>(!showUsdPrice);
useEffect(() => {
setEthBalance(!showUsdPrice);
}, [showUsdPrice]);
if (isLoading) {
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>
);
}
const onToggleBalance = () => {
if (!disableToggle) {
setEthBalance(!isEthBalance);
}
};
return (
<button
className={`btn btn-sm px-0 btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`}
onClick={onToggleBalance}
>
<div className="w-full flex items-center justify-center">
{isEthBalance ? (
<>
<span>{amount?.toFixed(4)}</span>
<span className="font-bold ml-1">{configuredNetwork.nativeCurrency.symbol}</span>
</>
) : (
<>
<span className="font-bold mr-1">$</span>
<span>{(amount * price).toFixed(2)}</span>
</>
)}
</div>
</button>
);
};

View File

@@ -0,0 +1,48 @@
import React from "react";
import { Address } from "@scaffold-ui/components";
import { Address as AddressType } from "viem";
export type Roll = {
address: AddressType;
amount: number;
roll: string;
};
export type RollEventsProps = {
rolls: Roll[];
};
export const RollEvents = ({ rolls }: RollEventsProps) => {
return (
<div className="mx-10">
<div className="flex w-auto justify-center h-10">
<p className="flex justify-center text-lg font-bold">Roll Events</p>
</div>
<table className="mt-4 p-2 bg-base-100 table table-zebra shadow-lg w-full overflow-hidden">
<thead className="text-accent text-lg">
<tr>
<th className="bg-primary text-lg" colSpan={3}>
<span>Address</span>
</th>
<th className="bg-primary text-lg">
<span>Roll</span>
</th>
</tr>
</thead>
<tbody>
{rolls.map(({ address, roll }, i) => (
<tr key={i}>
<td colSpan={3} className="py-3.5">
<Address address={address} size="lg" />
</td>
<td className="col-span-1 text-lg">
<span> {roll} </span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,65 @@
import React, { useState } from "react";
import { Amount } from "./Amount";
import { Address } from "@scaffold-ui/components";
import { Address as AddressType, formatEther } from "viem";
export type Winner = {
address: AddressType;
amount: bigint;
};
export type WinnerEventsProps = {
winners: Winner[];
};
export const WinnerEvents = ({ winners }: WinnerEventsProps) => {
const [showUsdPrice, setShowUsdPrice] = useState(true);
return (
<div className="mx-10">
<div className="flex w-auto justify-center h-10">
<p className="flex justify-center text-lg font-bold">Winner Events</p>
</div>
<table className="mt-4 p-2 bg-base-100 table table-zebra shadow-lg w-full overflow-hidden">
<thead className="text-accent text-lg">
<tr>
<th className="bg-primary" colSpan={3}>
Address
</th>
<th
className="bg-primary"
colSpan={2}
onClick={() => {
setShowUsdPrice(!showUsdPrice);
}}
>
Won
</th>
</tr>
</thead>
<tbody>
{winners.map(({ address, amount }, i) => (
<tr key={i}>
<td colSpan={3}>
<Address address={address} size="lg" />
</td>
<td
colSpan={2}
onClick={() => {
setShowUsdPrice(!showUsdPrice);
}}
>
<Amount
showUsdPrice={showUsdPrice}
amount={Number(formatEther(amount))}
disableToggle
className="text-lg"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./Amount";
export * from "./RollEvents";
export * from "./WinnerEvents";

View File

@@ -0,0 +1,193 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Address } from "@scaffold-ui/components";
import { useWatchBalance } from "@scaffold-ui/hooks";
import type { NextPage } from "next";
import { Address as AddressType, formatEther, parseEther } from "viem";
import { Amount, Roll, RollEvents, Winner, WinnerEvents } from "~~/app/dice/_components";
import {
useScaffoldContract,
useScaffoldEventHistory,
useScaffoldReadContract,
useScaffoldWriteContract,
} from "~~/hooks/scaffold-eth";
const ROLL_ETH_VALUE = "0.002";
const MAX_TABLE_ROWS = 10;
const DiceGame: NextPage = () => {
const [rolls, setRolls] = useState<Roll[]>([]);
const [winners, setWinners] = useState<Winner[]>([]);
const videoRef = useRef<HTMLVideoElement>(null);
const [rolled, setRolled] = useState(false);
const [isRolling, setIsRolling] = useState(false);
const { data: riggedRollContract } = useScaffoldContract({ contractName: "RiggedRoll" });
const { data: riggedRollBalance } = useWatchBalance({
address: riggedRollContract?.address,
});
const { data: prize } = useScaffoldReadContract({ contractName: "DiceGame", functionName: "prize" });
const { data: rollsHistoryData, isLoading: rollsHistoryLoading } = useScaffoldEventHistory({
contractName: "DiceGame",
eventName: "Roll",
watch: true,
});
useEffect(() => {
if (
!rollsHistoryLoading &&
Boolean(rollsHistoryData?.length) &&
(rollsHistoryData?.length as number) > rolls.length
) {
setIsRolling(false);
setRolls(
rollsHistoryData?.map(({ args }) => ({
address: args.player as AddressType,
amount: Number(args.amount),
roll: (args.roll as bigint).toString(16).toUpperCase(),
})) || [],
);
}
}, [rolls, rollsHistoryData, rollsHistoryLoading]);
const { data: winnerHistoryData, isLoading: winnerHistoryLoading } = useScaffoldEventHistory({
contractName: "DiceGame",
eventName: "Winner",
watch: true,
});
useEffect(() => {
if (
!winnerHistoryLoading &&
Boolean(winnerHistoryData?.length) &&
(winnerHistoryData?.length as number) > winners.length
) {
setIsRolling(false);
setWinners(
winnerHistoryData?.map(({ args }) => ({
address: args.winner as AddressType,
amount: args.amount as bigint,
})) || [],
);
}
}, [winnerHistoryData, winnerHistoryLoading, winners.length]);
const { writeContractAsync: writeDiceGameAsync, isError: rollTheDiceError } = useScaffoldWriteContract({
contractName: "DiceGame",
});
const { writeContractAsync: writeRiggedRollAsync, isError: riggedRollError } = useScaffoldWriteContract({
contractName: "RiggedRoll",
});
const immediateStopRolling = useCallback(() => {
setIsRolling(false);
setRolled(false);
}, []);
useEffect(() => {
if (rollTheDiceError || riggedRollError) {
immediateStopRolling();
}
}, [immediateStopRolling, riggedRollError, rollTheDiceError]);
useEffect(() => {
if (videoRef.current && !isRolling) {
// show last frame
videoRef.current.currentTime = 9999;
}
}, [isRolling]);
return (
<div className="py-10 px-10">
<div className="grid grid-cols-3 max-lg:grid-cols-1">
<div className="max-lg:row-start-2">
<RollEvents rolls={rolls.slice(0, MAX_TABLE_ROWS)} />
</div>
<div className="flex flex-col items-center pt-4 max-lg:row-start-1">
<div className="flex w-full justify-center">
<span className="text-xl"> Roll a 0, 1, 2, 3, 4 or 5 to win the prize! </span>
</div>
<div className="flex items-center mt-1">
<span className="text-lg mr-2">Prize:</span>
<Amount amount={prize ? Number(formatEther(prize)) : 0} showUsdPrice className="text-lg" />
</div>
<button
onClick={async () => {
if (!rolled) {
setRolled(true);
}
setIsRolling(true);
try {
await writeDiceGameAsync({ functionName: "rollTheDice", value: parseEther(ROLL_ETH_VALUE) });
} catch (err) {
console.error("Error calling rollTheDice function", err);
immediateStopRolling();
}
}}
disabled={isRolling}
className="mt-2 btn btn-secondary btn-xl normal-case font-xl text-lg"
>
Roll the dice!
</button>
<div className="mt-4 pt-2 flex flex-col items-center w-full justify-center border-t-4 border-primary">
<span className="text-2xl">Rigged Roll</span>
<div className="flex mt-2 items-center">
<span className="mr-2 text-lg">Address:</span>{" "}
<Address size="lg" address={riggedRollContract?.address} />{" "}
</div>
<div className="flex mt-1 items-center">
<span className="text-lg mr-2">Balance:</span>
<Amount amount={Number(riggedRollBalance?.formatted || 0)} showUsdPrice className="text-lg" />
</div>
</div>
{/* <button
onClick={async () => {
if (!rolled) {
setRolled(true);
}
setIsRolling(true);
try {
await writeRiggedRollAsync({ functionName: "riggedRoll" });
} catch (err) {
console.error("Error calling riggedRoll function", err);
immediateStopRolling();
}
}}
disabled={isRolling}
className="mt-2 btn btn-secondary btn-xl normal-case font-xl text-lg"
>
Rigged Roll!
</button> */}
<div className="flex mt-8">
{rolled ? (
isRolling ? (
<video key="rolling" width={300} height={300} loop src="/rolls/Spin.webm" autoPlay />
) : (
<video key="rolled" width={300} height={300} src={`/rolls/${rolls[0]?.roll || "0"}.webm`} autoPlay />
)
) : (
<video ref={videoRef} key="last" width={300} height={300} src={`/rolls/${rolls[0]?.roll || "0"}.webm`} />
)}
</div>
</div>
<div className="max-lg:row-start-3">
<WinnerEvents winners={winners.slice(0, MAX_TABLE_ROWS)} />
</div>
</div>
</div>
);
};
export default DiceGame;

View File

@@ -0,0 +1,31 @@
import { Space_Grotesk } from "next/font/google";
import "@rainbow-me/rainbowkit/styles.css";
import "@scaffold-ui/components/styles.css";
import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders";
import { ThemeProvider } from "~~/components/ThemeProvider";
import "~~/styles/globals.css";
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
});
export const metadata = getMetadata({
title: "Dice Game | Speedrun Ethereum",
description: "Built with 🏗 Scaffold-ETH 2",
});
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
<html suppressHydrationWarning className={`${spaceGrotesk.variable} font-space-grotesk`}>
<body>
<ThemeProvider enableSystem>
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
</ThemeProvider>
</body>
</html>
);
};
export default ScaffoldEthApp;

View File

@@ -0,0 +1,16 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="flex items-center h-full flex-1 justify-center bg-base-200">
<div className="text-center">
<h1 className="text-6xl font-bold m-0 mb-1">404</h1>
<h2 className="text-2xl font-semibold m-0">Page Not Found</h2>
<p className="text-base-content/70 m-0 mb-4">The page you&apos;re looking for doesn&apos;t exist.</p>
<Link href="/" className="btn btn-primary">
Go Home
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { Address } from "@scaffold-ui/components";
import type { NextPage } from "next";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
const Home: NextPage = () => {
const { address: connectedAddress } = useAccount();
const { targetNetwork } = useTargetNetwork();
return (
<>
<div className="flex items-center flex-col 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>
<span className="block text-xl font-bold">(Speedrun Ethereum Challenge: Dice game extension)</span>
</h1>
<div className="flex justify-center items-center space-x-2 flex-col">
<p className="my-2 font-medium">Connected Address:</p>
<Address
address={connectedAddress}
chain={targetNetwork}
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${connectedAddress}` : undefined
}
/>
</div>
<div className="flex items-center flex-col flex-grow pt-10">
<div className="px-5">
<h1 className="text-center mb-6">
<span className="block text-2xl mb-2">Speedrun Ethereum</span>
<span className="block text-4xl font-bold">Challenge: 🎲 Dice Game</span>
</h1>
<div className="flex flex-col items-center justify-center">
<Image
src="/hero.png"
width="727"
height="231"
alt="challenge banner"
className="rounded-xl border-4 border-primary"
/>
<div className="max-w-3xl">
<p className="text-lg mt-10">
🎰 Randomness is tricky on a public deterministic blockchain. The block hash is an easy to use, but
very weak form of randomness. This challenge will give you an example of a contract using the block
hash to create random numbers. This randomness is exploitable. Other, stronger forms of randomness
include commit/reveal schemes, oracles, or VRF from Chainlink.
</p>
<p className="text-lg mt-2">
🧤 Every time a player rolls the dice, they are required to send .002 Eth. 40 percent of this value
is added to the current prize amount while the other 60 percent stays in the contract to fund future
prizes. Once a prize is won, the new prize amount is set to 10% of the total balance of the DiceGame
contract.
</p>
<p className="text-lg mt-2">
🧨 Your job is to attack the Dice Game contract! You will create a new contract that will predict
the randomness ahead of time and only roll the dice when youre guaranteed to be a winner!
</p>
<p className="text-lg mt-2">
💬 Meet other builders working on this challenge and get help in the{" "}
<a href="https://t.me/+3StA0aBSArFjNjUx" target="_blank" rel="noreferrer" className="underline">
Telegram Group
</a>
</p>
<p className="text-center text-lg">
<a href="https://speedrunethereum.com/" target="_blank" rel="noreferrer" className="underline">
SpeedrunEthereum.com
</a>
!
</p>
</div>
</div>
</div>
</div>
</div>
<div className="grow bg-base-300 w-full mt-16 px-8 py-12">
<div className="flex justify-center items-center gap-12 flex-col md: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 { useFetchNativeCurrencyPrice } from "@scaffold-ui/hooks";
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";
/**
* Site footer
*/
export const Footer = () => {
const { targetNetwork } = useTargetNetwork();
const isLocalNetwork = targetNetwork.id === hardhat.id;
const { price: nativeCurrencyPrice } = useFetchNativeCurrencyPrice();
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.toFixed(2)}</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,109 @@
"use client";
import React, { useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { hardhat } from "viem/chains";
import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
import { CubeIcon } from "@heroicons/react/24/outline";
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
import { useOutsideClick, useTargetNetwork } from "~~/hooks/scaffold-eth";
type HeaderMenuLink = {
label: string;
href: string;
icon?: React.ReactNode;
};
export const menuLinks: HeaderMenuLink[] = [
{
label: "Home",
href: "/",
},
{
label: "Dice Game",
href: "/dice",
icon: <CubeIcon className="h-4 w-4" />,
},
{
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 { targetNetwork } = useTargetNetwork();
const isLocalNetwork = targetNetwork.id === hardhat.id;
const burgerMenuRef = useRef<HTMLDetailsElement>(null);
useOutsideClick(burgerMenuRef, () => {
burgerMenuRef?.current?.removeAttribute("open");
});
return (
<div className="sticky lg:static top-0 navbar bg-base-100 min-h-0 shrink-0 justify-between z-20 shadow-md shadow-secondary px-0 sm:px-2">
<div className="navbar-start w-auto lg:w-1/2">
<details className="dropdown" ref={burgerMenuRef}>
<summary className="ml-1 btn btn-ghost lg:hidden hover:bg-transparent">
<Bars3Icon className="h-1/2" />
</summary>
<ul
className="menu menu-compact dropdown-content mt-3 p-2 shadow-sm bg-base-100 rounded-box w-52"
onClick={() => {
burgerMenuRef?.current?.removeAttribute("open");
}}
>
<HeaderMenuLinks />
</ul>
</details>
<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">SRE Challenges</span>
<span className="text-xs">Dice Game</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 grow mr-4">
<RainbowKitCustomConnectButton />
{isLocalNetwork && <FaucetButton />}
</div>
</div>
);
};

View File

@@ -0,0 +1,58 @@
"use client";
import { useEffect, useState } from "react";
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppProgressBar as ProgressBar } from "next-nprogress-bar";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { WagmiProvider } from "wagmi";
import { Footer } from "~~/components/Footer";
import { Header } from "~~/components/Header";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
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 queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
export const ScaffoldEthAppWithProviders = ({ children }: { children: React.ReactNode }) => {
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ProgressBar height="3px" color="#2299dd" />
<ScaffoldEthApp>{children}</ScaffoldEthApp>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};

View File

@@ -0,0 +1,42 @@
"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 bg-secondary toggle-primary hover:bg-accent transition-all"
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,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,140 @@
"use client";
import { useEffect, useState } from "react";
import { Address, AddressInput, Balance, EtherInput } from "@scaffold-ui/components";
import { Address as AddressType, createWalletClient, http, parseEther } from "viem";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BanknotesIcon } from "@heroicons/react/24/outline";
import { useTargetNetwork, 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 { targetNetwork } = useTargetNetwork();
const { chain: ConnectedChain } = useAccount();
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 || !inputAddress) {
return;
}
try {
setLoading(true);
await faucetTxn({
to: inputAddress,
value: parseEther(sendValue as `${number}`),
account: faucetAddress,
});
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}
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${faucetAddress}` : undefined
}
/>
</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"
onValueChange={({ valueInEth }) => setSendValue(valueInEth)}
style={{ width: "100%" }}
/>
<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,73 @@
"use client";
import { useState } from "react";
import { useWatchBalance } from "@scaffold-ui/hooks";
import { createWalletClient, http, parseEther } from "viem";
import { hardhat } from "viem/chains";
import { useAccount } from "wagmi";
import { BanknotesIcon } from "@heroicons/react/24/outline";
import { 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, chain: ConnectedChain } = useAccount();
const { data: balance } = useWatchBalance({ address, chain: hardhat });
const [loading, setLoading] = useState(false);
const faucetTxn = useTransactor(localWalletClient);
const sendETH = async () => {
if (!address) return;
try {
setLoading(true);
await faucetTxn({
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;
}
const isBalanceZero = balance && balance.value === 0n;
return (
<div
className={
!isBalanceZero
? "ml-1"
: "ml-1 tooltip tooltip-bottom tooltip-primary tooltip-open font-bold before:left-auto before:transform-none before:content-[attr(data-tip)] before:-translate-x-2/5"
}
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,137 @@
import { useRef, useState } from "react";
import { NetworkOptions } from "./NetworkOptions";
import { getAddress } from "viem";
import { Address } from "viem";
import { useAccount, useDisconnect } from "wagmi";
import {
ArrowLeftOnRectangleIcon,
ArrowTopRightOnSquareIcon,
ArrowsRightLeftIcon,
CheckCircleIcon,
ChevronDownIcon,
DocumentDuplicateIcon,
EyeIcon,
QrCodeIcon,
} from "@heroicons/react/24/outline";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { useCopyToClipboard, useOutsideClick } from "~~/hooks/scaffold-eth";
import { getTargetNetworks } from "~~/utils/scaffold-eth";
import { isENS } from "~~/utils/scaffold-eth/common";
const BURNER_WALLET_ID = "burnerWallet";
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 { connector } = useAccount();
const checkSumAddress = getAddress(address);
const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
useCopyToClipboard();
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 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 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" : ""}>
<div
className="h-8 btn-sm rounded-xl! flex gap-3 py-3 cursor-pointer"
onClick={() => copyAddressToClipboard(checkSumAddress)}
>
{isAddressCopiedToClipboard ? (
<>
<CheckCircleIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
<span className="whitespace-nowrap">Copied!</span>
</>
) : (
<>
<DocumentDuplicateIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
<span className="whitespace-nowrap">Copy address</span>
</>
)}
</div>
</li>
<li className={selectingNetwork ? "hidden" : ""}>
<label htmlFor="qrcode-modal" className="h-8 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="h-8 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="h-8 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}
{connector?.id === BURNER_WALLET_ID ? (
<li>
<label htmlFor="reveal-burner-pk-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3 text-error">
<EyeIcon className="h-6 w-4 ml-2 sm:ml-0" />
<span>Reveal Private Key</span>
</label>
</li>
) : null}
<li className={selectingNetwork ? "hidden" : ""}>
<button
className="menu-item text-error h-8 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,44 @@
import { Address } from "@scaffold-ui/components";
import { QRCodeSVG } from "qrcode.react";
import { Address as AddressType } from "viem";
import { hardhat } from "viem/chains";
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
type AddressQRCodeModalProps = {
address: AddressType;
modalId: string;
};
export const AddressQRCodeModal = ({ address, modalId }: AddressQRCodeModalProps) => {
const { targetNetwork } = useTargetNetwork();
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 flex-col items-center gap-6">
<QRCodeSVG value={address} size={256} />
<Address
address={address}
format="long"
disableAddressLink
onlyEnsOrAddress
blockExplorerAddressLink={
targetNetwork.id === hardhat.id ? `/blockexplorer/address/${address}` : undefined
}
/>
</div>
</div>
</label>
</label>
</div>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { useTheme } from "next-themes";
import { useAccount, useSwitchChain } 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 { switchChain } = useSwitchChain();
const { chain } = useAccount();
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={() => {
switchChain?.({ chainId: 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,59 @@
import { useRef } from "react";
import { rainbowkitBurnerWallet } from "burner-connector";
import { ShieldExclamationIcon } from "@heroicons/react/24/outline";
import { useCopyToClipboard } from "~~/hooks/scaffold-eth";
import { getParsedError, notification } from "~~/utils/scaffold-eth";
const BURNER_WALLET_PK_KEY = "burnerWallet.pk";
export const RevealBurnerPKModal = () => {
const { copyToClipboard, isCopiedToClipboard } = useCopyToClipboard();
const modalCheckboxRef = useRef<HTMLInputElement>(null);
const handleCopyPK = async () => {
try {
const storage = rainbowkitBurnerWallet.useSessionStorage ? sessionStorage : localStorage;
const burnerPK = storage?.getItem(BURNER_WALLET_PK_KEY);
if (!burnerPK) throw new Error("Burner wallet private key not found");
await copyToClipboard(burnerPK);
notification.success("Burner wallet private key copied to clipboard");
} catch (e) {
const parsedError = getParsedError(e);
notification.error(parsedError);
if (modalCheckboxRef.current) modalCheckboxRef.current.checked = false;
}
};
return (
<>
<div>
<input type="checkbox" id="reveal-burner-pk-modal" className="modal-toggle" ref={modalCheckboxRef} />
<label htmlFor="reveal-burner-pk-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" />
<label htmlFor="reveal-burner-pk-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div>
<p className="text-lg font-semibold m-0 p-0">Copy Burner Wallet Private Key</p>
<div role="alert" className="alert alert-warning mt-4">
<ShieldExclamationIcon className="h-6 w-6" />
<span className="font-semibold">
Burner wallets are intended for local development only and are not safe for storing real funds.
</span>
</div>
<p>
Your Private Key provides <strong>full access</strong> to your entire wallet and funds. This is
currently stored <strong>temporarily</strong> in your browser.
</p>
<button className="btn btn-outline btn-error" onClick={handleCopyPK} disabled={isCopiedToClipboard}>
Copy Private Key To Clipboard
</button>
</div>
</label>
</label>
</div>
</>
);
};

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,76 @@
"use client";
// @refresh reset
import { AddressInfoDropdown } from "./AddressInfoDropdown";
import { AddressQRCodeModal } from "./AddressQRCodeModal";
import { RevealBurnerPKModal } from "./RevealBurnerPKModal";
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { Balance } from "@scaffold-ui/components";
import { Address } from "viem";
import { 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 = () => {
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-2">
<Balance
address={account.address as Address}
style={{
minHeight: "0",
height: "auto",
fontSize: "0.8em",
}}
/>
<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" />
<RevealBurnerPKModal />
</>
);
})()}
</>
);
}}
</ConnectButton.Custom>
);
};

View File

@@ -0,0 +1,4 @@
export * from "./BlockieAvatar";
export * from "./Faucet";
export * from "./FaucetButton";
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,32 @@
import { FlatCompat } from "@eslint/eslintrc";
import prettierPlugin from "eslint-plugin-prettier";
import { defineConfig } from "eslint/config";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
export default defineConfig([
{
plugins: {
prettier: prettierPlugin,
},
extends: compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prettier/prettier": [
"warn",
{
endOfLine: "auto",
},
],
},
},
]);

View File

@@ -0,0 +1,14 @@
export * from "./useContractLogs";
export * from "./useCopyToClipboard";
export * from "./useDeployedContractInfo";
export * from "./useFetchBlocks";
export * from "./useNetworkColor";
export * from "./useOutsideClick";
export * from "./useScaffoldContract";
export * from "./useScaffoldEventHistory";
export * from "./useScaffoldReadContract";
export * from "./useScaffoldWatchContractEvent";
export * from "./useScaffoldWriteContract";
export * from "./useTargetNetwork";
export * from "./useTransactor";
export * from "./useSelectedNetwork";

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { useTargetNetwork } from "./useTargetNetwork";
import { Address, Log } from "viem";
import { usePublicClient } from "wagmi";
export const useContractLogs = (address: Address) => {
const [logs, setLogs] = useState<Log[]>([]);
const { targetNetwork } = useTargetNetwork();
const client = usePublicClient({ chainId: targetNetwork.id });
useEffect(() => {
const fetchLogs = async () => {
if (!client) return console.error("Client not found");
try {
const existingLogs = await client.getLogs({
address: address,
fromBlock: 0n,
toBlock: "latest",
});
setLogs(existingLogs);
} catch (error) {
console.error("Failed to fetch logs:", error);
}
};
fetchLogs();
return client?.watchBlockNumber({
onBlockNumber: async (_blockNumber, prevBlockNumber) => {
const newLogs = await client.getLogs({
address: address,
fromBlock: prevBlockNumber,
toBlock: "latest",
});
setLogs(prevLogs => [...prevLogs, ...newLogs]);
},
});
}, [address, client]);
return logs;
};

View File

@@ -0,0 +1,19 @@
import { useState } from "react";
export const useCopyToClipboard = () => {
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setIsCopiedToClipboard(true);
setTimeout(() => {
setIsCopiedToClipboard(false);
}, 800);
} catch (err) {
console.error("Failed to copy text:", err);
}
};
return { copyToClipboard, isCopiedToClipboard };
};

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useIsMounted } from "usehooks-ts";
import { usePublicClient } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import {
Contract,
ContractCodeStatus,
ContractName,
UseDeployedContractConfig,
contracts,
} from "~~/utils/scaffold-eth/contract";
type DeployedContractData<TContractName extends ContractName> = {
data: Contract<TContractName> | undefined;
isLoading: boolean;
};
/**
* Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts
* and externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
*/
export function useDeployedContractInfo<TContractName extends ContractName>(
config: UseDeployedContractConfig<TContractName>,
): DeployedContractData<TContractName>;
/**
* @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
*/
export function useDeployedContractInfo<TContractName extends ContractName>(
contractName: TContractName,
): DeployedContractData<TContractName>;
export function useDeployedContractInfo<TContractName extends ContractName>(
configOrName: UseDeployedContractConfig<TContractName> | TContractName,
): DeployedContractData<TContractName> {
const isMounted = useIsMounted();
const finalConfig: UseDeployedContractConfig<TContractName> =
typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);
useEffect(() => {
if (typeof configOrName === "string") {
console.warn(
"Using `useDeployedContractInfo` with a string parameter is deprecated. Please use the object parameter version instead.",
);
}
}, [configOrName]);
const { contractName, chainId } = finalConfig;
const selectedNetwork = useSelectedNetwork(chainId);
const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract<TContractName>;
const [status, setStatus] = useState<ContractCodeStatus>(ContractCodeStatus.LOADING);
const publicClient = usePublicClient({ chainId: selectedNetwork.id });
useEffect(() => {
const checkContractDeployment = async () => {
try {
if (!isMounted() || !publicClient) return;
if (!deployedContract) {
setStatus(ContractCodeStatus.NOT_FOUND);
return;
}
const code = await publicClient.getBytecode({
address: deployedContract.address,
});
// If contract code is `0x` => no contract deployed on that address
if (code === "0x") {
setStatus(ContractCodeStatus.NOT_FOUND);
return;
}
setStatus(ContractCodeStatus.DEPLOYED);
} catch (e) {
console.error(e);
setStatus(ContractCodeStatus.NOT_FOUND);
}
};
checkContractDeployment();
}, [isMounted, contractName, deployedContract, publicClient]);
return {
data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
isLoading: status === ContractCodeStatus.LOADING,
};
}

View File

@@ -0,0 +1,133 @@
import { useCallback, useEffect, useState } from "react";
import {
Block,
Hash,
Transaction,
TransactionReceipt,
createTestClient,
publicActions,
walletActions,
webSocket,
} from "viem";
import { hardhat } from "viem/chains";
import { decodeTransactionData } from "~~/utils/scaffold-eth";
const BLOCKS_PER_PAGE = 20;
export const testClient = createTestClient({
chain: hardhat,
mode: "hardhat",
transport: webSocket("ws://127.0.0.1:8545"),
})
.extend(publicActions)
.extend(walletActions);
export const useFetchBlocks = () => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [transactionReceipts, setTransactionReceipts] = useState<{
[key: string]: TransactionReceipt;
}>({});
const [currentPage, setCurrentPage] = useState(0);
const [totalBlocks, setTotalBlocks] = useState(0n);
const [error, setError] = useState<Error | null>(null);
const fetchBlocks = useCallback(async () => {
setError(null);
try {
const blockNumber = await testClient.getBlockNumber();
setTotalBlocks(blockNumber);
const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE);
const blockNumbersToFetch = Array.from(
{ length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) },
(_, i) => startingBlock - BigInt(i),
);
const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => {
try {
return testClient.getBlock({ blockNumber, includeTransactions: true });
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
throw err;
}
});
const fetchedBlocks = await Promise.all(blocksWithTransactions);
fetchedBlocks.forEach(block => {
block.transactions.forEach(tx => decodeTransactionData(tx as Transaction));
});
const txReceipts = await Promise.all(
fetchedBlocks.flatMap(block =>
block.transactions.map(async tx => {
try {
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
return { [(tx as Transaction).hash]: receipt };
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
throw err;
}
}),
),
);
setBlocks(fetchedBlocks);
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) }));
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
}
}, [currentPage]);
useEffect(() => {
fetchBlocks();
}, [fetchBlocks]);
useEffect(() => {
const handleNewBlock = async (newBlock: any) => {
try {
if (currentPage === 0) {
if (newBlock.transactions.length > 0) {
const transactionsDetails = await Promise.all(
newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })),
);
newBlock.transactions = transactionsDetails;
}
newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction));
const receipts = await Promise.all(
newBlock.transactions.map(async (tx: Transaction) => {
try {
const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash });
return { [(tx as Transaction).hash]: receipt };
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred fetching receipt."));
throw err;
}
}),
);
setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]);
setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) }));
}
if (newBlock.number) {
setTotalBlocks(newBlock.number);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred."));
}
};
return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true });
}, [currentPage]);
return {
blocks,
transactionReceipts,
currentPage,
totalBlocks,
setCurrentPage,
error,
};
};

View File

@@ -0,0 +1,22 @@
import { useTheme } from "next-themes";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { AllowedChainIds, ChainWithAttributes } from "~~/utils/scaffold-eth";
export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"];
export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolean) {
const colorConfig = network.color ?? DEFAULT_NETWORK_COLOR;
return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig;
}
/**
* Gets the color of the target network
*/
export const useNetworkColor = (chainId?: AllowedChainIds) => {
const { resolvedTheme } = useTheme();
const chain = useSelectedNetwork(chainId);
const isDarkMode = resolvedTheme === "dark";
return getNetworkColor(chain, isDarkMode);
};

View File

@@ -0,0 +1,23 @@
import React, { useEffect } from "react";
/**
* Handles clicks outside of passed ref element
* @param ref - react ref of the element
* @param callback - callback function to call when clicked outside
*/
export const useOutsideClick = (ref: React.RefObject<HTMLElement | null>, callback: { (): void }) => {
useEffect(() => {
function handleOutsideClick(event: MouseEvent) {
if (!(event.target instanceof Element)) {
return;
}
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
}
document.addEventListener("click", handleOutsideClick);
return () => document.removeEventListener("click", handleOutsideClick);
}, [ref, callback]);
};

View File

@@ -0,0 +1,65 @@
import { Account, Address, Chain, Client, Transport, getContract } from "viem";
import { usePublicClient } from "wagmi";
import { GetWalletClientReturnType } from "wagmi/actions";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { Contract, ContractName } from "~~/utils/scaffold-eth/contract";
/**
* Gets a viem instance of the contract present in deployedContracts.ts or externalContracts.ts corresponding to
* targetNetworks configured in scaffold.config.ts. Optional walletClient can be passed for doing write transactions.
* @param config - The config settings for the hook
* @param config.contractName - deployed contract name
* @param config.walletClient - optional walletClient from wagmi useWalletClient hook can be passed for doing write transactions
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
*/
export const useScaffoldContract = <
TContractName extends ContractName,
TWalletClient extends Exclude<GetWalletClientReturnType, null> | undefined,
>({
contractName,
walletClient,
chainId,
}: {
contractName: TContractName;
walletClient?: TWalletClient | null;
chainId?: AllowedChainIds;
}) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork?.id as AllowedChainIds,
});
const publicClient = usePublicClient({ chainId: selectedNetwork?.id });
let contract = undefined;
if (deployedContractData && publicClient) {
contract = getContract<
Transport,
Address,
Contract<TContractName>["abi"],
TWalletClient extends Exclude<GetWalletClientReturnType, null>
? {
public: Client<Transport, Chain>;
wallet: TWalletClient;
}
: { public: Client<Transport, Chain> },
Chain,
Account
>({
address: deployedContractData.address,
abi: deployedContractData.abi as Contract<TContractName>["abi"],
client: {
public: publicClient,
wallet: walletClient ? walletClient : undefined,
} as any,
});
}
return {
data: contract,
isLoading: deployedContractLoading,
};
};

View File

@@ -0,0 +1,292 @@
import { useEffect, useState } from "react";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype";
import { BlockNumber, GetLogsParameters } from "viem";
import { hardhat } from "viem/chains";
import { Config, UsePublicClientReturnType, useBlockNumber, usePublicClient } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { replacer } from "~~/utils/scaffold-eth/common";
import {
ContractAbi,
ContractName,
UseScaffoldEventHistoryConfig,
UseScaffoldEventHistoryData,
} from "~~/utils/scaffold-eth/contract";
const getEvents = async (
getLogsParams: GetLogsParameters<AbiEvent | undefined, AbiEvent[] | undefined, boolean, BlockNumber, BlockNumber>,
publicClient?: UsePublicClientReturnType<Config, number>,
Options?: {
blockData?: boolean;
transactionData?: boolean;
receiptData?: boolean;
},
) => {
const logs = await publicClient?.getLogs({
address: getLogsParams.address,
fromBlock: getLogsParams.fromBlock,
toBlock: getLogsParams.toBlock,
args: getLogsParams.args,
event: getLogsParams.event,
});
if (!logs) return undefined;
const finalEvents = await Promise.all(
logs.map(async log => {
return {
...log,
blockData:
Options?.blockData && log.blockHash ? await publicClient?.getBlock({ blockHash: log.blockHash }) : null,
transactionData:
Options?.transactionData && log.transactionHash
? await publicClient?.getTransaction({ hash: log.transactionHash })
: null,
receiptData:
Options?.receiptData && log.transactionHash
? await publicClient?.getTransactionReceipt({ hash: log.transactionHash })
: null,
};
}),
);
return finalEvents;
};
/**
* @deprecated **Recommended only for local (hardhat/anvil) chains and development.**
* It uses getLogs which can overload RPC endpoints (especially on L2s with short block times).
* For production, use an indexer such as ponder.sh or similar to query contract events efficiently.
*
* Reads events from a deployed contract.
* @param config - The config settings
* @param config.contractName - deployed contract name
* @param config.eventName - name of the event to listen for
* @param config.fromBlock - optional block number to start reading events from (defaults to `deployedOnBlock` in deployedContracts.ts if set for contract, otherwise defaults to 0)
* @param config.toBlock - optional block number to stop reading events at (if not provided, reads until current block)
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param config.filters - filters to be applied to the event (parameterName: value)
* @param config.blockData - if set to true it will return the block data for each event (default: false)
* @param config.transactionData - if set to true it will return the transaction data for each event (default: false)
* @param config.receiptData - if set to true it will return the receipt data for each event (default: false)
* @param config.watch - if set to true, the events will be updated every pollingInterval milliseconds set at scaffoldConfig (default: false)
* @param config.enabled - set this to false to disable the hook from running (default: true)
* @param config.blocksBatchSize - optional batch size for fetching events. If specified, each batch will contain at most this many blocks (default: 500)
*/
export const useScaffoldEventHistory = <
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
TBlockData extends boolean = false,
TTransactionData extends boolean = false,
TReceiptData extends boolean = false,
>({
contractName,
eventName,
fromBlock,
toBlock,
chainId,
filters,
blockData,
transactionData,
receiptData,
watch,
enabled = true,
blocksBatchSize = 500,
}: UseScaffoldEventHistoryConfig<TContractName, TEventName, TBlockData, TTransactionData, TReceiptData>) => {
const selectedNetwork = useSelectedNetwork(chainId);
// Runtime warning for non-local chains
useEffect(() => {
if (selectedNetwork.id !== hardhat.id) {
console.log(
"⚠️ useScaffoldEventHistory is not optimized for production use. It can overload RPC endpoints (especially on L2s)",
);
}
}, [selectedNetwork.id]);
const publicClient = usePublicClient({
chainId: selectedNetwork.id,
});
const [liveEvents, setLiveEvents] = useState<any[]>([]);
const [lastFetchedBlock, setLastFetchedBlock] = useState<bigint | null>(null);
const [isPollingActive, setIsPollingActive] = useState(false);
const { data: blockNumber } = useBlockNumber({ watch: watch, chainId: selectedNetwork.id });
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const event =
deployedContractData &&
((deployedContractData.abi as Abi).find(part => part.type === "event" && part.name === eventName) as AbiEvent);
const isContractAddressAndClientReady = Boolean(deployedContractData?.address) && Boolean(publicClient);
const fromBlockValue =
fromBlock !== undefined
? fromBlock
: BigInt(
deployedContractData && "deployedOnBlock" in deployedContractData
? deployedContractData.deployedOnBlock || 0
: 0,
);
const query = useInfiniteQuery({
queryKey: [
"eventHistory",
{
contractName,
address: deployedContractData?.address,
eventName,
fromBlock: fromBlockValue?.toString(),
toBlock: toBlock?.toString(),
chainId: selectedNetwork.id,
filters: JSON.stringify(filters, replacer),
blocksBatchSize: blocksBatchSize.toString(),
},
],
queryFn: async ({ pageParam }) => {
if (!isContractAddressAndClientReady) return undefined;
// Calculate the toBlock for this batch
let batchToBlock = toBlock;
const batchEndBlock = pageParam + BigInt(blocksBatchSize) - 1n;
const maxBlock = toBlock || (blockNumber ? BigInt(blockNumber) : undefined);
if (maxBlock) {
batchToBlock = batchEndBlock < maxBlock ? batchEndBlock : maxBlock;
}
const data = await getEvents(
{
address: deployedContractData?.address,
event,
fromBlock: pageParam,
toBlock: batchToBlock,
args: filters,
},
publicClient,
{ blockData, transactionData, receiptData },
);
setLastFetchedBlock(batchToBlock || blockNumber || 0n);
return data;
},
enabled: enabled && isContractAddressAndClientReady && !isPollingActive, // Disable when polling starts
initialPageParam: fromBlockValue,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (!blockNumber || fromBlockValue >= blockNumber) return undefined;
const nextBlock = lastPageParam + BigInt(blocksBatchSize);
// Don't go beyond the specified toBlock or current block
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
if (nextBlock > maxBlock) return undefined;
return nextBlock;
},
select: data => {
const events = data.pages.flat() as unknown as UseScaffoldEventHistoryData<
TContractName,
TEventName,
TBlockData,
TTransactionData,
TReceiptData
>;
return {
pages: events?.reverse(),
pageParams: data.pageParams,
};
},
});
// Check if we're caught up and should start polling
const shouldStartPolling = () => {
if (!watch || !blockNumber || isPollingActive) return false;
return !query.hasNextPage && query.status === "success";
};
// Poll for new events when watch mode is enabled
useQuery({
queryKey: ["liveEvents", contractName, eventName, blockNumber?.toString(), lastFetchedBlock?.toString()],
enabled: Boolean(
watch && enabled && isContractAddressAndClientReady && blockNumber && (shouldStartPolling() || isPollingActive),
),
queryFn: async () => {
if (!isContractAddressAndClientReady || !blockNumber) return null;
if (!isPollingActive && shouldStartPolling()) {
setIsPollingActive(true);
}
const maxBlock = toBlock && toBlock < blockNumber ? toBlock : blockNumber;
const startBlock = lastFetchedBlock || maxBlock;
// Only fetch if there are new blocks to check
if (startBlock >= maxBlock) return null;
const newEvents = await getEvents(
{
address: deployedContractData?.address,
event,
fromBlock: startBlock + 1n,
toBlock: maxBlock,
args: filters,
},
publicClient,
{ blockData, transactionData, receiptData },
);
if (newEvents && newEvents.length > 0) {
setLiveEvents(prev => [...newEvents, ...prev]);
}
setLastFetchedBlock(maxBlock);
return newEvents;
},
refetchInterval: false,
});
// Manual trigger to fetch next page when previous page completes (only when not polling)
useEffect(() => {
if (
!isPollingActive &&
query.status === "success" &&
query.hasNextPage &&
!query.isFetchingNextPage &&
!query.error
) {
query.fetchNextPage();
}
}, [query, isPollingActive]);
// Combine historical data from infinite query with live events from watch hook
const historicalEvents = query.data?.pages || [];
const allEvents = [...liveEvents, ...historicalEvents] as typeof historicalEvents;
// remove duplicates
const seenEvents = new Set<string>();
const combinedEvents = allEvents.filter(event => {
const eventKey = `${event?.transactionHash}-${event?.logIndex}-${event?.blockHash}`;
if (seenEvents.has(eventKey)) {
return false;
}
seenEvents.add(eventKey);
return true;
}) as typeof historicalEvents;
return {
data: combinedEvents,
status: query.status,
error: query.error,
isLoading: query.isLoading,
isFetchingNewEvent: query.isFetchingNextPage,
refetch: query.refetch,
};
};

View File

@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { QueryObserverResult, RefetchOptions, useQueryClient } from "@tanstack/react-query";
import type { ExtractAbiFunctionNames } from "abitype";
import { ReadContractErrorType } from "viem";
import { useBlockNumber, useReadContract } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import {
AbiFunctionReturnType,
ContractAbi,
ContractName,
UseScaffoldReadConfig,
} from "~~/utils/scaffold-eth/contract";
/**
* Wrapper around wagmi's useContractRead hook which automatically loads (by name) the contract ABI and address from
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
* @param config - The config settings, including extra wagmi configuration
* @param config.contractName - deployed contract name
* @param config.functionName - name of the function to be called
* @param config.args - args to be passed to the function call
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
*/
export const useScaffoldReadContract = <
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "pure" | "view">,
>({
contractName,
functionName,
args,
chainId,
...readConfig
}: UseScaffoldReadConfig<TContractName, TFunctionName>) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContract } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const { query: queryOptions, watch, ...readContractConfig } = readConfig;
// set watch to true by default
const defaultWatch = watch ?? true;
const readContractHookRes = useReadContract({
chainId: selectedNetwork.id,
functionName,
address: deployedContract?.address,
abi: deployedContract?.abi,
args,
...(readContractConfig as any),
query: {
enabled: !Array.isArray(args) || !args.some(arg => arg === undefined),
...queryOptions,
},
}) as Omit<ReturnType<typeof useReadContract>, "data" | "refetch"> & {
data: AbiFunctionReturnType<ContractAbi, TFunctionName> | undefined;
refetch: (
options?: RefetchOptions | undefined,
) => Promise<QueryObserverResult<AbiFunctionReturnType<ContractAbi, TFunctionName>, ReadContractErrorType>>;
};
const queryClient = useQueryClient();
const { data: blockNumber } = useBlockNumber({
watch: defaultWatch,
chainId: selectedNetwork.id,
query: {
enabled: defaultWatch,
},
});
useEffect(() => {
if (defaultWatch) {
queryClient.invalidateQueries({ queryKey: readContractHookRes.queryKey });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blockNumber]);
return readContractHookRes;
};

View File

@@ -0,0 +1,40 @@
import { Abi, ExtractAbiEventNames } from "abitype";
import { Log } from "viem";
import { useWatchContractEvent } from "wagmi";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract";
/**
* Wrapper around wagmi's useEventSubscriber hook which automatically loads (by name) the contract ABI and
* address from the contracts present in deployedContracts.ts & externalContracts.ts
* @param config - The config settings
* @param config.contractName - deployed contract name
* @param config.eventName - name of the event to listen for
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param config.onLogs - the callback that receives events.
*/
export const useScaffoldWatchContractEvent = <
TContractName extends ContractName,
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
>({
contractName,
eventName,
chainId,
onLogs,
}: UseScaffoldEventConfig<TContractName, TEventName>) => {
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
return useWatchContractEvent({
address: deployedContractData?.address,
abi: deployedContractData?.abi as Abi,
chainId: selectedNetwork.id,
onLogs: (logs: Log[]) => onLogs(logs as Parameters<typeof onLogs>[0]),
eventName,
});
};

View File

@@ -0,0 +1,194 @@
import { useEffect, useState } from "react";
import { MutateOptions } from "@tanstack/react-query";
import { Abi, ExtractAbiFunctionNames } from "abitype";
import { Config, UseWriteContractParameters, useAccount, useConfig, useWriteContract } from "wagmi";
import { WriteContractErrorType, WriteContractReturnType } from "wagmi/actions";
import { WriteContractVariables } from "wagmi/query";
import { useSelectedNetwork } from "~~/hooks/scaffold-eth";
import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth";
import { AllowedChainIds, notification } from "~~/utils/scaffold-eth";
import {
ContractAbi,
ContractName,
ScaffoldWriteContractOptions,
ScaffoldWriteContractVariables,
UseScaffoldWriteConfig,
simulateContractWriteAndNotifyError,
} from "~~/utils/scaffold-eth/contract";
type ScaffoldWriteContractReturnType<TContractName extends ContractName> = Omit<
ReturnType<typeof useWriteContract>,
"writeContract" | "writeContractAsync"
> & {
isMining: boolean;
writeContractAsync: <
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: ScaffoldWriteContractOptions,
) => Promise<WriteContractReturnType | undefined>;
writeContract: <TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
) => void;
};
export function useScaffoldWriteContract<TContractName extends ContractName>(
config: UseScaffoldWriteConfig<TContractName>,
): ScaffoldWriteContractReturnType<TContractName>;
/**
* @deprecated Use object parameter version instead: useScaffoldWriteContract({ contractName: "YourContract" })
*/
export function useScaffoldWriteContract<TContractName extends ContractName>(
contractName: TContractName,
writeContractParams?: UseWriteContractParameters,
): ScaffoldWriteContractReturnType<TContractName>;
/**
* Wrapper around wagmi's useWriteContract hook which automatically loads (by name) the contract ABI and address from
* the contracts present in deployedContracts.ts & externalContracts.ts corresponding to targetNetworks configured in scaffold.config.ts
* @param contractName - name of the contract to be written to
* @param config.chainId - optional chainId that is configured with the scaffold project to make use for multi-chain interactions.
* @param writeContractParams - wagmi's useWriteContract parameters
*/
export function useScaffoldWriteContract<TContractName extends ContractName>(
configOrName: UseScaffoldWriteConfig<TContractName> | TContractName,
writeContractParams?: UseWriteContractParameters,
): ScaffoldWriteContractReturnType<TContractName> {
const finalConfig =
typeof configOrName === "string"
? { contractName: configOrName, writeContractParams, chainId: undefined }
: (configOrName as UseScaffoldWriteConfig<TContractName>);
const { contractName, chainId, writeContractParams: finalWriteContractParams } = finalConfig;
const wagmiConfig = useConfig();
useEffect(() => {
if (typeof configOrName === "string") {
console.warn(
"Using `useScaffoldWriteContract` with a string parameter is deprecated. Please use the object parameter version instead.",
);
}
}, [configOrName]);
const { chain: accountChain } = useAccount();
const writeTx = useTransactor();
const [isMining, setIsMining] = useState(false);
const wagmiContractWrite = useWriteContract(finalWriteContractParams);
const selectedNetwork = useSelectedNetwork(chainId);
const { data: deployedContractData } = useDeployedContractInfo({
contractName,
chainId: selectedNetwork.id as AllowedChainIds,
});
const sendContractWriteAsyncTx = async <
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: ScaffoldWriteContractOptions,
) => {
if (!deployedContractData) {
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
return;
}
if (!accountChain?.id) {
notification.error("Please connect your wallet");
return;
}
if (accountChain?.id !== selectedNetwork.id) {
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
return;
}
try {
setIsMining(true);
const { blockConfirmations, onBlockConfirmation, ...mutateOptions } = options || {};
const writeContractObject = {
abi: deployedContractData.abi as Abi,
address: deployedContractData.address,
...variables,
} as WriteContractVariables<Abi, string, any[], Config, number>;
if (!finalConfig?.disableSimulate) {
await simulateContractWriteAndNotifyError({
wagmiConfig,
writeContractParams: writeContractObject,
chainId: selectedNetwork.id as AllowedChainIds,
});
}
const makeWriteWithParams = () =>
wagmiContractWrite.writeContractAsync(
writeContractObject,
mutateOptions as
| MutateOptions<
WriteContractReturnType,
WriteContractErrorType,
WriteContractVariables<Abi, string, any[], Config, number>,
unknown
>
| undefined,
);
const writeTxResult = await writeTx(makeWriteWithParams, { blockConfirmations, onBlockConfirmation });
return writeTxResult;
} catch (e: any) {
throw e;
} finally {
setIsMining(false);
}
};
const sendContractWriteTx = <
TContractName extends ContractName,
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, "nonpayable" | "payable">,
>(
variables: ScaffoldWriteContractVariables<TContractName, TFunctionName>,
options?: Omit<ScaffoldWriteContractOptions, "onBlockConfirmation" | "blockConfirmations">,
) => {
if (!deployedContractData) {
notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?");
return;
}
if (!accountChain?.id) {
notification.error("Please connect your wallet");
return;
}
if (accountChain?.id !== selectedNetwork.id) {
notification.error(`Wallet is connected to the wrong network. Please switch to ${selectedNetwork.name}`);
return;
}
wagmiContractWrite.writeContract(
{
abi: deployedContractData.abi as Abi,
address: deployedContractData.address,
...variables,
} as WriteContractVariables<Abi, string, any[], Config, number>,
options as
| MutateOptions<
WriteContractReturnType,
WriteContractErrorType,
WriteContractVariables<Abi, string, any[], Config, number>,
unknown
>
| undefined,
);
};
return {
...wagmiContractWrite,
isMining,
// Overwrite wagmi's writeContactAsync
writeContractAsync: sendContractWriteAsyncTx,
// Overwrite wagmi's writeContract
writeContract: sendContractWriteTx,
};
}

View File

@@ -0,0 +1,19 @@
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
import { AllowedChainIds } from "~~/utils/scaffold-eth";
import { ChainWithAttributes, NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth/networks";
/**
* Given a chainId, retrives the network object from `scaffold.config`,
* if not found default to network set by `useTargetNetwork` hook
*/
export function useSelectedNetwork(chainId?: AllowedChainIds): ChainWithAttributes {
const globalTargetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
const targetNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chainId);
if (targetNetwork) {
return { ...targetNetwork, ...NETWORKS_EXTRA_DATA[targetNetwork.id] };
}
return globalTargetNetwork;
}

View File

@@ -0,0 +1,24 @@
import { useEffect, useMemo } from "react";
import { useAccount } from "wagmi";
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
import { ChainWithAttributes } from "~~/utils/scaffold-eth";
import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth";
/**
* Retrieves the connected wallet's network from scaffold.config or defaults to the 0th network in the list if the wallet is not connected.
*/
export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } {
const { chain } = useAccount();
const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork);
const setTargetNetwork = useGlobalState(({ setTargetNetwork }) => setTargetNetwork);
useEffect(() => {
const newSelectedNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chain?.id);
if (newSelectedNetwork && newSelectedNetwork.id !== targetNetwork.id) {
setTargetNetwork({ ...newSelectedNetwork, ...NETWORKS_EXTRA_DATA[newSelectedNetwork.id] });
}
}, [chain?.id, setTargetNetwork, targetNetwork.id]);
return useMemo(() => ({ targetNetwork }), [targetNetwork]);
}

View File

@@ -0,0 +1,115 @@
import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem";
import { Config, useWalletClient } from "wagmi";
import { getPublicClient } from "wagmi/actions";
import { SendTransactionMutate } from "wagmi/query";
import scaffoldConfig from "~~/scaffold.config";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";
import { AllowedChainIds, getBlockExplorerTxLink, notification } from "~~/utils/scaffold-eth";
import { TransactorFuncOptions, getParsedErrorWithAllAbis } from "~~/utils/scaffold-eth/contract";
type TransactionFunc = (
tx: (() => Promise<Hash>) | Parameters<SendTransactionMutate<Config, undefined>>[0],
options?: TransactorFuncOptions,
) => Promise<Hash | undefined>;
/**
* Custom notification content for TXs.
*/
const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => {
return (
<div className={`flex flex-col ml-1 cursor-default`}>
<p className="my-0">{message}</p>
{blockExplorerLink && blockExplorerLink.length > 0 ? (
<a href={blockExplorerLink} target="_blank" rel="noreferrer" className="block link">
check out transaction
</a>
) : null}
</div>
);
};
/**
* Runs Transaction passed in to returned function showing UI feedback.
* @param _walletClient - Optional wallet client to use. If not provided, will use the one from useWalletClient.
* @returns function that takes in transaction function as callback, shows UI feedback for transaction and returns a promise of the transaction hash
*/
export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => {
let walletClient = _walletClient;
const { data } = useWalletClient();
if (walletClient === undefined && data) {
walletClient = data;
}
const result: TransactionFunc = async (tx, options) => {
if (!walletClient) {
notification.error("Cannot access account");
console.error("⚡️ ~ file: useTransactor.tsx ~ error");
return;
}
let notificationId = null;
let transactionHash: Hash | undefined = undefined;
let transactionReceipt: TransactionReceipt | undefined;
let blockExplorerTxURL = "";
let chainId: number = scaffoldConfig.targetNetworks[0].id;
try {
chainId = await walletClient.getChainId();
// Get full transaction from public client
const publicClient = getPublicClient(wagmiConfig);
notificationId = notification.loading(<TxnNotification message="Awaiting for user confirmation" />);
if (typeof tx === "function") {
// Tx is already prepared by the caller
const result = await tx();
transactionHash = result;
} else if (tx != null) {
transactionHash = await walletClient.sendTransaction(tx as SendTransactionParameters);
} else {
throw new Error("Incorrect transaction passed to transactor");
}
notification.remove(notificationId);
blockExplorerTxURL = chainId ? getBlockExplorerTxLink(chainId, transactionHash) : "";
notificationId = notification.loading(
<TxnNotification message="Waiting for transaction to complete." blockExplorerLink={blockExplorerTxURL} />,
);
transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: transactionHash,
confirmations: options?.blockConfirmations,
});
notification.remove(notificationId);
if (transactionReceipt.status === "reverted") throw new Error("Transaction reverted");
notification.success(
<TxnNotification message="Transaction completed successfully!" blockExplorerLink={blockExplorerTxURL} />,
{
icon: "🎉",
},
);
if (options?.onBlockConfirmation) options.onBlockConfirmation(transactionReceipt);
} catch (error: any) {
if (notificationId) {
notification.remove(notificationId);
}
console.error("⚡️ ~ file: useTransactor.ts ~ error", error);
const message = getParsedErrorWithAllAbis(error, chainId as AllowedChainIds);
// if receipt was reverted, show notification with block explorer link and return error
if (transactionReceipt?.status === "reverted") {
notification.error(<TxnNotification message={message} blockExplorerLink={blockExplorerTxURL} />);
throw error;
}
notification.error(message);
throw error;
}
return transactionHash;
};
return result;
};

6
packages/nextjs/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,29 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
devIndicators: false,
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
webpack: config => {
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push("pino-pretty", "lokijs", "encoding");
return config;
},
};
const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
if (isIpfs) {
nextConfig.output = "export";
nextConfig.trailingSlash = true;
nextConfig.images = {
unoptimized: true,
};
}
module.exports = nextConfig;

View File

@@ -0,0 +1,62 @@
{
"name": "@se-2/nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"check-types": "tsc --noEmit --incremental",
"dev": "next dev",
"format": "prettier --write . '!(node_modules|.next|contracts)/**/*'",
"ipfs": "NEXT_PUBLIC_IPFS_BUILD=true yarn build && yarn bgipfs upload config init -u https://upload.bgipfs.com && CID=$(yarn bgipfs upload out | grep -o 'CID: [^ ]*' | cut -d' ' -f2) && [ ! -z \"$CID\" ] && echo '🚀 Upload complete! Your site is now available at: https://community.bgipfs.com/ipfs/'$CID || echo '❌ Upload failed'",
"lint": "next lint",
"serve": "next start",
"start": "next dev",
"vercel": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env VERCEL_TELEMETRY_DISABLED=1",
"vercel:login": "vercel login",
"vercel:yolo": "vercel --build-env YARN_ENABLE_IMMUTABLE_INSTALLS=false --build-env ENABLE_EXPERIMENTAL_COREPACK=1 --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --build-env VERCEL_TELEMETRY_DISABLED=1"
},
"dependencies": {
"@heroicons/react": "~2.1.5",
"@rainbow-me/rainbowkit": "2.2.9",
"@react-native-async-storage/async-storage": "~2.2.0",
"@scaffold-ui/components": "^0.1.7",
"@scaffold-ui/debug-contracts": "^0.1.6",
"@scaffold-ui/hooks": "^0.1.5",
"@tanstack/react-query": "~5.59.15",
"blo": "~1.2.0",
"burner-connector": "0.0.20",
"daisyui": "5.0.9",
"kubo-rpc-client": "~5.0.2",
"next": "~15.2.8",
"next-nprogress-bar": "~2.3.13",
"next-themes": "~0.3.0",
"qrcode.react": "~4.0.1",
"react": "~19.2.3",
"react-dom": "~19.2.3",
"react-hot-toast": "~2.4.0",
"usehooks-ts": "~3.1.0",
"viem": "2.39.0",
"wagmi": "2.19.5",
"zustand": "~5.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "4.0.15",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"@types/node": "~18.19.50",
"@types/react": "~19.0.7",
"abitype": "1.0.6",
"autoprefixer": "~10.4.20",
"bgipfs": "~0.0.12",
"eslint": "~9.23.0",
"eslint-config-next": "~15.2.3",
"eslint-config-prettier": "~10.1.1",
"eslint-plugin-prettier": "~5.2.4",
"postcss": "~8.4.45",
"prettier": "~3.5.3",
"tailwindcss": "4.1.3",
"type-fest": "~4.26.1",
"typescript": "~5.8.2",
"vercel": "~39.1.3"
},
"packageManager": "yarn@3.2.3"
}

Some files were not shown because too many files have changed in this diff Show More