Initial commit with 🏗️ create-eth @ 2.0.4
This commit is contained in:
114
.cursor/rules/scaffold-eth.mdc
Normal file
114
.cursor/rules/scaffold-eth.mdc
Normal 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
1
.cursorignore
Normal file
@@ -0,0 +1 @@
|
||||
*
|
||||
43
.github/workflows/lint.yaml
vendored
Normal file
43
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node: [lts/*]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup node env
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run hardhat node, deploy contracts (& generate contracts typescript output)
|
||||
run: yarn chain & yarn deploy
|
||||
|
||||
- name: Run 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
23
.gitignore
vendored
Normal 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
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
yarn lint-staged --verbose
|
||||
21
.lintstagedrc.js
Normal file
21
.lintstagedrc.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require("path");
|
||||
|
||||
const buildNextEslintCommand = (filenames) =>
|
||||
`yarn next:lint --fix --file ${filenames
|
||||
.map((f) => path.relative(path.join("packages", "nextjs"), f))
|
||||
.join(" --file ")}`;
|
||||
|
||||
const checkTypesNextCommand = () => "yarn next:check-types";
|
||||
|
||||
const buildHardhatEslintCommand = (filenames) =>
|
||||
`yarn hardhat:lint-staged --fix ${filenames
|
||||
.map((f) => path.relative(path.join("packages", "hardhat"), f))
|
||||
.join(" ")}`;
|
||||
|
||||
module.exports = {
|
||||
"packages/nextjs/**/*.{ts,tsx}": [
|
||||
buildNextEslintCommand,
|
||||
checkTypesNextCommand,
|
||||
],
|
||||
"packages/hardhat/**/*.{ts,tsx}": [buildHardhatEslintCommand],
|
||||
};
|
||||
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
13
.yarnrc.yml
Normal file
13
.yarnrc.yml
Normal 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
86
CONTRIBUTING.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Welcome to Scaffold-ETH 2 Contributing Guide
|
||||
|
||||
Thank you for investing your time in contributing to Scaffold-ETH 2!
|
||||
|
||||
This guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved.
|
||||
|
||||
## About the Project
|
||||
|
||||
Scaffold-ETH 2 is a minimal and forkable repo providing builders with a starter kit to build decentralized applications on Ethereum.
|
||||
|
||||
Read the [README](README.md) to get an overview of the project.
|
||||
|
||||
### Vision
|
||||
|
||||
The goal of Scaffold-ETH 2 is to provide the primary building blocks for a decentralized application.
|
||||
|
||||
The repo can be forked to include integrations and more features, but we want to keep the master branch simple and minimal.
|
||||
|
||||
### Project Status
|
||||
|
||||
The project is under active development.
|
||||
|
||||
You can view the open Issues, follow the development process and contribute to the project.
|
||||
|
||||
## Getting started
|
||||
|
||||
You can contribute to this repo in many ways:
|
||||
|
||||
- Solve open issues
|
||||
- Report bugs or feature requests
|
||||
- Improve the documentation
|
||||
|
||||
Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions:
|
||||
|
||||
- Search for existing Issues and PRs before creating your own.
|
||||
- Contributions should only fix/add the functionality in the issue OR address style issues, not both.
|
||||
- If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error.
|
||||
- Please use the same formatting in the code repository. You can configure your IDE to do it by using the prettier / linting config files included in each package.
|
||||
- If applicable, please edit the README.md file to reflect the changes.
|
||||
|
||||
### Issues
|
||||
|
||||
Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created.
|
||||
|
||||
#### Solve an issue
|
||||
|
||||
Scan through our [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues) to find one that interests you.
|
||||
|
||||
If a contributor is working on the issue, they will be assigned to the individual. If you find an issue to work on, you are welcome to assign it to yourself and open a PR with a fix for it.
|
||||
|
||||
#### Create a new issue
|
||||
|
||||
If a related issue doesn't exist, you can open a new issue.
|
||||
|
||||
Some tips to follow when you are creating an issue:
|
||||
|
||||
- Provide as much context as possible. Over-communicate to give the most details to the reader.
|
||||
- Include the steps to reproduce the issue or the reason for adding the feature.
|
||||
- Screenshots, videos etc., are highly appreciated.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
#### Pull Request Process
|
||||
|
||||
We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
|
||||
|
||||
1. Fork the repo
|
||||
2. Clone the project
|
||||
3. Create a new branch with a descriptive name
|
||||
4. Commit your changes to the new branch
|
||||
5. Push changes to your fork
|
||||
6. Open a PR in our repository and tag one of the maintainers to review your PR
|
||||
|
||||
Here are some tips for a high-quality pull request:
|
||||
|
||||
- Create a title for the PR that accurately defines the work done.
|
||||
- Structure the description neatly to make it easy to consume by the readers. For example, you can include bullet points and screenshots instead of having one large paragraph.
|
||||
- Add the link to the issue if applicable.
|
||||
- Have a good commit message that summarises the work done.
|
||||
|
||||
Once you submit your PR:
|
||||
|
||||
- We may ask questions, request additional information or ask for changes to be made before a PR can be merged. Please note that these are to make the PR clear for everyone involved and aims to create a frictionless interaction process.
|
||||
- As you update your PR and apply changes, mark each conversation resolved.
|
||||
|
||||
Once the PR is approved, we'll "squash-and-merge" to keep the git commit history clean.
|
||||
21
LICENCE
Normal file
21
LICENCE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 BuidlGuidl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
538
README.md
Normal file
538
README.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# 🏗 Scaffold-ETH 2
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://docs.scaffoldeth.io">Documentation</a> |
|
||||
<a href="https://scaffoldeth.io">Website</a>
|
||||
</h4>
|
||||
|
||||
🧪 An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts.
|
||||
|
||||
⚙️ Built using NextJS, RainbowKit, Hardhat, Wagmi, Viem, and Typescript.
|
||||
|
||||
- ✅ **Contract Hot Reload**: Your frontend auto-adapts to your smart contract as you edit it.
|
||||
- 🪝 **[Custom hooks](https://docs.scaffoldeth.io/hooks/)**: Collection of React hooks wrapper around [wagmi](https://wagmi.sh/) to simplify interactions with smart contracts with typescript autocompletion.
|
||||
- 🧱 [**Components**](https://docs.scaffoldeth.io/components/): Collection of common web3 components to quickly build your frontend.
|
||||
- 🔥 **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet.
|
||||
- 🔐 **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network.
|
||||
|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
Before you begin, you need to install the following tools:
|
||||
|
||||
- [Node (>= 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: 📣 Crowdfunding App
|
||||
|
||||

|
||||
|
||||
🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. If the users cooperate, the money is collected in a second smart contract. If they defect, the worst that can happen is everyone gets their money back. The users only have to trust the code, not each other.
|
||||
|
||||
🌟 The final deliverable is deploying a Dapp that lets users send ether to a contract and then fund the cause if the conditions are met, then `yarn vercel` your app to a public webserver. Submit the url on [SpeedrunEthereum.com](https://speedrunethereum.com)!
|
||||
|
||||
> 💬 Meet other builders working on this challenge and get help in the [challenge Telegram](https://t.me/joinchat/E6r91UFt4oMJlt01)!
|
||||
|
||||
---
|
||||
|
||||
## 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` whenever you want to deploy new contracts to the frontend. If you haven't made any contract changes, you can run `yarn deploy --reset` for a completely fresh deploy.
|
||||
|
||||
🔏 Now you are ready to edit your smart contract `CrowdFund.sol` in `packages/hardhat/contracts`
|
||||
|
||||
---
|
||||
|
||||
|
||||
⚗️ At this point you will need to know basic Solidity syntax. If not, you can pick it up quickly by tinkering with concepts from [📑 Solidity By Example](https://solidity-by-example.org/) using [🏗️ Scaffold-ETH-2](https://scaffoldeth.io). (In particular: global units, primitive data types, mappings, sending ether, and payable functions.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
⚠️ 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
|
||||
|
||||
---
|
||||
|
||||
## 🧑🚀 Your Mission
|
||||
|
||||
🏦 Build a `CrowdFund.sol` contract that collects **ETH** from numerous addresses using a payable `contribute()` function and keeps track of `balances`. After some `deadline` if it has at least some `threshold` of ETH, it sends it to a `FundingRecipient` contract (This is a stand-in for any potential use case a group of people would want to fund together). It then triggers the `complete()` action, sending the full balance. If not enough **ETH** is collected, allow users to `withdraw()`.
|
||||
|
||||
🔢 Each step is laid out in the following checkpoints. Try to complete them without hints but if you are struggling then you can get clearer context by pressing the "🔎 Hint" in each checkpoint.
|
||||
|
||||
👀 Also, you should try to keep your contract organized by the standard you will see in the contract. Keeping errors, events, functions, etc. sorted under their own sections helps to maintain contract readability.
|
||||
## Checkpoint 1: 🤝 Contributing 💵
|
||||
|
||||
>Let's start by implementing a state variable that we will need in the function logic.
|
||||
|
||||
⚖️ You'll need to track individual `balances` using a mapping. This way we will know who gave what in the event that the funding effort fails to raise enough and everyone needs to be refunded. Add it under the existing fundingRecipient variable.
|
||||
|
||||
```solidity
|
||||
mapping(address => uint256) public balances;
|
||||
```
|
||||
|
||||
> Next let's add an event. Events are useful for outside services that are watching the chain for certain things to occur. In our case, the front end is going to use this event to know when a contribution takes place.
|
||||
|
||||
📣 Add an event to the contract called `Contribution` that receives the address of the contributor and the amount they contributed.
|
||||
|
||||
```solidity
|
||||
event Contribution(address, uint256);
|
||||
```
|
||||
|
||||
>📝 Note: If you use named arguments in your event (e.g. `event Contribution(address indexed contributor, uint256 amount)`), you'll need to update `/packages/nextjs/app/contributions/page.tsx` to reference event parameters by their names instead of numeric indices.
|
||||
|
||||
### Implementing the `contribute()` function
|
||||
|
||||
> 👩💻 Now focus on writing your `contribute()` function. The payable method already exists but is empty. Go fill it with logic!
|
||||
|
||||
The goal of this function is to allow anyone to contribute to the pool of funds. To do this effectively it will need to do the following:
|
||||
- Update the `balances` mapping
|
||||
- Emit the `Contribute` event
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
You can set mappings like you would access a Javascript array.
|
||||
For a mapping like this `mapping(address => uint256) public map` and `address addr = 0x1234...5678` you would access is like this: `map[addr]`.
|
||||
|
||||
You need to use the address for the sender of the transaction and you will need to know how much value was sent. Is there an easy way to access these details about the transaction `msg`? 🤔
|
||||
|
||||
Go check https://solidity-by-example.org/ if you need help on the syntax.
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
function contribute() public payable {
|
||||
balances[msg.sender] += msg.value;
|
||||
emit Contribution(msg.sender, msg.value);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
### Try it out
|
||||
|
||||
👩💻 Now redeploy (`yarn deploy`) and go test your function using the `Debug Contracts` tab in the front end.
|
||||
|
||||

|
||||
|
||||
> 💸 Need more funds from the faucet? Click on _"Grab funds from faucet"_, or use the Faucet feature at the bottom left of the page to get as much as you need!
|
||||
|
||||

|
||||
|
||||
> ✏ Need to troubleshoot your code? `hardhat/console.sol` is already imported in your contract so you can call `console.log()` right in your Solidity code. The output will appear in your `yarn chain` terminal.
|
||||
|
||||
---
|
||||
|
||||
### 🥅 Goals
|
||||
|
||||
- [ ] Do you see the balance of the `CrowdFund` contract go up when you `contribute()`?
|
||||
- [ ] Is your `balance` correctly tracked?
|
||||
- [ ] Do you see the events in the `Contributions` tab?
|
||||
|
||||

|
||||
|
||||
### Testing your progress
|
||||
|
||||
🔍 Run the following command to check if you implemented the function correctly.
|
||||
|
||||
```shell
|
||||
yarn test --grep "Checkpoint1"
|
||||
```
|
||||
|
||||
✅ Did the tests pass? You can dig into any errors by viewing the tests at `packages/hardhat/test/CrowdFund.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint 2: 📤 Withdrawing Funds
|
||||
|
||||
> Let's implement the `withdraw` function. First lets set up an important state variable and some errors we may need.
|
||||
|
||||
🔘 / ⚪ Create a bool to track whether the contract is `openToWithdraw` in the case that the funding fails to fill enough before the deadline and everyone needs to be refunded.
|
||||
|
||||
```solidity
|
||||
bool public openToWithdraw; // Solidity variables default to an empty/false state
|
||||
```
|
||||
|
||||
🚫 Next let's add the following custom errors to the `/// Errors` section of the contract.
|
||||
|
||||
```solidity
|
||||
error NotOpenToWithdraw();
|
||||
error WithdrawTransferFailed(address to, uint256 amount);
|
||||
```
|
||||
|
||||
> ❓Did you know that custom errors are more gas efficient than using revert string errors?
|
||||
>
|
||||
> ❌ `require(condition, "Condition Not Met")`
|
||||
>
|
||||
> ✔️ `if (!condition) { revert ConditionNotMet(); }`
|
||||
|
||||
### Implementing the `withdraw()` function
|
||||
|
||||
> 🛠️ Now you can implement the logic inside the `withdraw` function.
|
||||
|
||||
This function will need to do the following:
|
||||
- Check that `openToWithdraw` is true. Throw `NotOpenToWithdraw` if not.
|
||||
- Send the correct amount to the user who is withdrawing. Throw `WithdrawTransferFailed` if it does not succeed.
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
You need to send the user's balance (`balances[msg.sender]`) back to their address.
|
||||
The important thing is that you only send the correct amount to the user AND they can only do it when `openToWithdraw` is true.
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
function withdraw() public {
|
||||
if (!openToWithdraw) revert NotOpenToWithdraw();
|
||||
|
||||
uint256 balance = balances[msg.sender];
|
||||
balances[msg.sender] = 0;
|
||||
|
||||
(bool success,) = msg.sender.call{value: balance}("");
|
||||
if (!success) revert WithdrawTransferFailed(msg.sender, balance);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
### Try it out
|
||||
|
||||
⚙️ Go switch `openToWithdraw` to be true by default so we can test the function through the front end: `bool public openToWithdraw = true;`
|
||||
|
||||
👩💻 Now redeploy (`yarn deploy`) and go test your function using the `Crowdfund` or `Debug Contracts` tabs in the front end. You should be able to contribute and then withdraw the ether.
|
||||
|
||||
>‼️ Once you are content that it works as expected make sure you switch `openToWithdraw` back to false.
|
||||
|
||||
---
|
||||
|
||||
### 🥅 Goals
|
||||
|
||||
- [ ] Can you withdraw your ether after contributing?
|
||||
- [ ] What happens if you try to withdraw again after you have already withdrawn? Does this always fail?
|
||||
- [ ] What about with multiple users?
|
||||
### Testing your progress
|
||||
|
||||
🔍 Run the following command to check if you implemented the function correctly.
|
||||
|
||||
```shell
|
||||
yarn test --grep "Checkpoint2"
|
||||
```
|
||||
|
||||
✅ Did the tests pass? You can dig into any errors by viewing the tests at `packages/hardhat/test/CrowdFund.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint 3: 🔬 State Machine / Timing ⏱
|
||||
|
||||
### State Machine
|
||||
|
||||
> ⚙️ Think of your smart contract like a _state machine_. First, there is a **contribute** period. Then, if you have gathered a certain `threshold` worth of ETH, there is a **success** state. Or, we go into a **withdraw** state to let users withdraw their funds.
|
||||
|
||||
⌛ Let's go ahead and add a `deadline` variable and set it to the current `block.timestamp` plus 30 seconds. We will need this to know if time is up.
|
||||
|
||||
```solidity
|
||||
uint256 public deadline = block.timestamp + 30 seconds;
|
||||
```
|
||||
|
||||
📏 Also track a constant called `threshold` set to `1 ether`. This will be the threshold at which we will consider the cause to be funded. Below this threshold it will be the failure scenario where people withdraw their funds.
|
||||
|
||||
```solidity
|
||||
uint256 public constant threshold = 1 ether;
|
||||
```
|
||||
|
||||
🚫 Let's add another custom error that we can throw if this function gets called too early.
|
||||
|
||||
```solidity
|
||||
error TooEarly(uint256 deadline, uint256 currentTimestamp);
|
||||
```
|
||||
|
||||
### Implementing the `execute()` function
|
||||
|
||||
>🧠 Smart contracts can't execute automatically, you always need to have a transaction execute to change state. Because of this, you will need to have an `execute()` function that _anyone_ can call, just once, after the `deadline` has expired.
|
||||
|
||||
👩💻 Write your `execute()` function. It will need to do the following:
|
||||
- Make sure it can only be executed when the deadline has passed. If not then throw `TooEarly`
|
||||
- If the threshold is met then trigger the `fundingRecipient.complete` method while sending the locked funds
|
||||
- Otherwise set `openToWithdraw` to true so that people can get their funds back
|
||||
|
||||
> ‼️ Check the `FundingRecipient.sol` to see what function you will call but DO NOT edit the `FundingRecipient.sol` as it can slow the auto grading.
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
If the `address(this).balance` of the contract is over the `threshold` by the `deadline`, you will want to call: `fundingRecipient.complete{value: address(this).balance}()`
|
||||
|
||||
If the balance is less than the `threshold`, you want to set the `openForWithdraw` bool to `true` which will allow users to `withdraw()` their funds.
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
function execute() public {
|
||||
if (block.timestamp <= deadline) revert TooEarly(deadline, block.timestamp);
|
||||
|
||||
if (address(this).balance >= threshold) {
|
||||
fundingRecipient.complete{value: address(this).balance}();
|
||||
} else {
|
||||
openToWithdraw = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
### Timing
|
||||
|
||||
🏃You'll have 30 seconds after deploying until the deadline is reached, you can adjust this in the contract to make it longer if that helps you test.
|
||||
|
||||
> 👩💻 Go update the `timeLeft()` view function so that it returns how much time is left.
|
||||
|
||||
⚠️ Be careful! If `block.timestamp >= deadline` you want to `return 0;`
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
If the `deadline` is greater than `block.timestamp` then return the difference between the two. Otherwise return 0.
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
function timeLeft() public view returns (uint256) {
|
||||
return deadline > block.timestamp ? deadline - block.timestamp : 0;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
> 👩💻 You can call `yarn deploy --reset` any time you want a fresh contract, it will get redeployed even if there are no changes on it.
|
||||
> You may need it when you want to reload the _"Time Left"_ of your tests.
|
||||
|
||||
### Try it out
|
||||
|
||||
💪 Your `Crowdfund` tab should be almost done and working at this point. Test out all the functionality to see if the `Execute!` button works as expected for each case.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🥅 Goals
|
||||
|
||||
- [ ] Can you see `timeLeft` counting down in the `Crowdfund` tab?
|
||||
- [ ] If enough ETH is contributed by the deadline, does your `execute()` function correctly call `complete()` and contribute the ETH?
|
||||
- [ ] If the threshold isn't met by the deadline, are you able to `withdraw()` your funds?
|
||||
### Testing your progress
|
||||
|
||||
🔍 Run the following command to check if you implemented the functions correctly.
|
||||
|
||||
```shell
|
||||
yarn test --grep "Checkpoint3"
|
||||
```
|
||||
|
||||
✅ Did the tests pass? You can dig into any errors by viewing the tests at `packages/hardhat/test/CrowdFund.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint 4: 💵 Receive Function / UX 🙎
|
||||
|
||||
🎀 To improve the user experience, set your contract up so it accepts ETH sent to it and calls `contribute()`. You will use a special `receive()` function that is called by default when people send funds to a contract.
|
||||
|
||||
> Use the [receive()](https://docs.soliditylang.org/en/v0.8.9/contracts.html?highlight=receive#receive-ether-function) function in solidity to "catch" ETH sent to the contract *without a specific method indicated* and call `contribute()` to update `balances`.
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
Don't overthink it. This `receive` method will be called anytime somebody sends funds directly to your contract without any particular method specified. Just make sure the `contribute()` method is called when this happens so that their balance is updated.
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
receive() external payable {
|
||||
contribute();
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🥅 Goals
|
||||
|
||||
- [ ] If you send ETH directly to the contract address does it update your `balance` and the `balance` of the contract?
|
||||
|
||||
---
|
||||
|
||||
## ⚔️ Side Quests
|
||||
|
||||
- [ ] Can `execute()` get called more than once, and is that okay?
|
||||
- [ ] Can you contribute and withdraw freely after the `deadline`, and is that okay?
|
||||
|
||||
---
|
||||
|
||||
### 🐸 It's a trap!
|
||||
|
||||
- [ ] Make sure funds can't get trapped in the contract! **Try sending funds after you have executed! What happens?**
|
||||
- [ ] Update the [modifier](https://solidity-by-example.org/function-modifier/) called `notCompleted`. It should check that `FundingRecipient` is not completed yet. Use it to protect your `execute`, `contribute` and `withdraw` functions by throwing a new custom error if it has already been completed.
|
||||
|
||||
<details markdown='1'>
|
||||
<summary>🔎 Hint</summary>
|
||||
|
||||
You can access the funding recipient contract with the `fundingRecipient` variable. Then you just need to make sure that `.completed()` does not return `true`. If it does then you need to revert with an error; Your choice for what the error will be called. `AlreadyCompleted`? `RecipientAlreadyFunded`? Or your own idea for a good error name. You choose!
|
||||
|
||||
<details markdown='1'>
|
||||
|
||||
<summary>🎯 Solution</summary>
|
||||
|
||||
```solidity
|
||||
// Errors
|
||||
// ...Existing errors
|
||||
error AlreadyCompleted(); // Or whatever name you want
|
||||
|
||||
// Modifiers
|
||||
modifier notCompleted() {
|
||||
if (fundingRecipient.completed()) revert AlreadyCompleted();
|
||||
_;
|
||||
}
|
||||
|
||||
// Functions
|
||||
function contribute() public payable notCompleted {
|
||||
// ...Existing code
|
||||
}
|
||||
|
||||
function withdraw() public notCompleted {
|
||||
// ...Existing code
|
||||
}
|
||||
|
||||
function execute() public notCompleted {
|
||||
// ...Existing code
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
### ⚠️ Test it!
|
||||
|
||||
- Now is a good time to run `yarn test` to run the automated testing for everything you have done. It will test that you hit the core checkpoints. You are looking for all green checkmarks and passing tests!
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint 5: 💾 Deploy your contract! 🛰
|
||||
|
||||
📡 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/joinchat/E6r91UFt4oMJlt01). People are usually more than willing to share.
|
||||
|
||||
> 📝 If you plan on testing your challenge on the live network don't forget to set your `deadline` to a nice amount of time such as `block.timestamp + 2 hours`
|
||||
|
||||
🚀 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 6: 🚢 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/crowdfund 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 for going through **Speedrun Ethereum** but...
|
||||
|
||||
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 7: 📜 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
60
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
13
packages/hardhat/.env.example
Normal file
13
packages/hardhat/.env.example
Normal 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
30
packages/hardhat/.gitignore
vendored
Normal 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
|
||||
18
packages/hardhat/.prettierrc.json
Normal file
18
packages/hardhat/.prettierrc.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
61
packages/hardhat/contracts/CrowdFund.sol
Normal file
61
packages/hardhat/contracts/CrowdFund.sol
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.20; // Do not change the solidity version as it negatively impacts submission grading
|
||||
|
||||
import "hardhat/console.sol";
|
||||
import "./FundingRecipient.sol";
|
||||
|
||||
contract CrowdFund {
|
||||
/////////////////
|
||||
/// Errors //////
|
||||
/////////////////
|
||||
|
||||
// Errors go here...
|
||||
|
||||
//////////////////////
|
||||
/// State Variables //
|
||||
//////////////////////
|
||||
|
||||
FundingRecipient public fundingRecipient;
|
||||
|
||||
////////////////
|
||||
/// Events /////
|
||||
////////////////
|
||||
|
||||
// Events go here...
|
||||
|
||||
///////////////////
|
||||
/// Modifiers /////
|
||||
///////////////////
|
||||
|
||||
modifier notCompleted() {
|
||||
_;
|
||||
}
|
||||
|
||||
///////////////////
|
||||
/// Constructor ///
|
||||
///////////////////
|
||||
|
||||
constructor(address fundingRecipientAddress) {
|
||||
fundingRecipient = FundingRecipient(fundingRecipientAddress);
|
||||
}
|
||||
|
||||
///////////////////
|
||||
/// Functions /////
|
||||
///////////////////
|
||||
|
||||
function contribute() public payable {}
|
||||
|
||||
function withdraw() public {}
|
||||
|
||||
function execute() public {}
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
////////////////////////
|
||||
/// View Functions /////
|
||||
////////////////////////
|
||||
|
||||
function timeLeft() public view returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
10
packages/hardhat/contracts/FundingRecipient.sol
Normal file
10
packages/hardhat/contracts/FundingRecipient.sol
Normal file
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.20; // Do not change the solidity version as it negatively impacts submission grading
|
||||
|
||||
contract FundingRecipient {
|
||||
bool public completed;
|
||||
|
||||
function complete() public payable {
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
22
packages/hardhat/deploy/00_deploy_funding_recipient.ts
Normal file
22
packages/hardhat/deploy/00_deploy_funding_recipient.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "FundingRecipient" using the deployer account.
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployFundingRecipient: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy } = hre.deployments;
|
||||
|
||||
await deploy("FundingRecipient", {
|
||||
from: deployer,
|
||||
log: true,
|
||||
autoMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
export default deployFundingRecipient;
|
||||
|
||||
deployFundingRecipient.tags = ["FundingRecipient"];
|
||||
25
packages/hardhat/deploy/01_deploy_crowdfund.ts
Normal file
25
packages/hardhat/deploy/01_deploy_crowdfund.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
/**
|
||||
* Deploys a contract named "CrowdFund" using the deployer account.
|
||||
*
|
||||
* @param hre HardhatRuntimeEnvironment object.
|
||||
*/
|
||||
const deployCrowdFund: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const { deployer } = await hre.getNamedAccounts();
|
||||
const { deploy, get } = hre.deployments;
|
||||
|
||||
const fundingRecipient = await get("FundingRecipient");
|
||||
|
||||
await deploy("CrowdFund", {
|
||||
from: deployer,
|
||||
args: [fundingRecipient.address],
|
||||
log: true,
|
||||
autoMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
export default deployCrowdFund;
|
||||
|
||||
deployCrowdFund.tags = ["CrowdFund"];
|
||||
44
packages/hardhat/eslint.config.mjs
Normal file
44
packages/hardhat/eslint.config.mjs
Normal 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
156
packages/hardhat/hardhat.config.ts
Normal file
156
packages/hardhat/hardhat.config.ts
Normal 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;
|
||||
63
packages/hardhat/package.json
Normal file
63
packages/hardhat/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
58
packages/hardhat/scripts/generateAccount.ts
Normal file
58
packages/hardhat/scripts/generateAccount.ts
Normal 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;
|
||||
});
|
||||
136
packages/hardhat/scripts/generateTsAbis.ts
Normal file
136
packages/hardhat/scripts/generateTsAbis.ts
Normal 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;
|
||||
72
packages/hardhat/scripts/importAccount.ts
Normal file
72
packages/hardhat/scripts/importAccount.ts
Normal 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;
|
||||
});
|
||||
52
packages/hardhat/scripts/listAccount.ts
Normal file
52
packages/hardhat/scripts/listAccount.ts
Normal 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;
|
||||
});
|
||||
31
packages/hardhat/scripts/revealPK.ts
Normal file
31
packages/hardhat/scripts/revealPK.ts
Normal 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;
|
||||
});
|
||||
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal 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);
|
||||
2
packages/hardhat/test/.gitkeep
Normal file
2
packages/hardhat/test/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Write tests for your smart contract in this directory
|
||||
# Example: YourContract.ts
|
||||
368
packages/hardhat/test/CrowdFund.ts
Normal file
368
packages/hardhat/test/CrowdFund.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
//
|
||||
// This script executes when you run 'yarn test'
|
||||
//
|
||||
import { ethers, network } from "hardhat";
|
||||
import { expect } from "chai";
|
||||
import { FundingRecipient, CrowdFund } from "../typechain-types";
|
||||
|
||||
describe("🚩 Challenge: 📣 Crowdfunding App", function () {
|
||||
let fundingRecipient: FundingRecipient;
|
||||
let crowdFundContract: CrowdFund;
|
||||
|
||||
describe("CrowdFund", function () {
|
||||
const contractAddress = process.env.CONTRACT_ADDRESS;
|
||||
|
||||
let contractArtifact: string;
|
||||
if (contractAddress) {
|
||||
// For the autograder.
|
||||
contractArtifact = `contracts/download-${contractAddress}.sol:CrowdFund`;
|
||||
} else {
|
||||
contractArtifact = "contracts/CrowdFund.sol:CrowdFund";
|
||||
}
|
||||
|
||||
const deployContracts = async () => {
|
||||
const FundingRecipientFactory = await ethers.getContractFactory("FundingRecipient");
|
||||
fundingRecipient = (await FundingRecipientFactory.deploy()) as FundingRecipient;
|
||||
|
||||
const CrowdFundFactory = await ethers.getContractFactory(contractArtifact);
|
||||
crowdFundContract = (await CrowdFundFactory.deploy(await fundingRecipient.getAddress())) as CrowdFund;
|
||||
};
|
||||
|
||||
describe("Checkpoint 1: 🤝 Contributing 💵", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
const getContributionEventsFromReceipt = (receipt: any) => {
|
||||
// We avoid chai matchers like `.to.emit` so this works in minimal environments.
|
||||
const parsed = receipt.logs
|
||||
.map((log: any) => {
|
||||
try {
|
||||
return crowdFundContract.interface.parseLog(log);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
return parsed.filter((p: any) => p.name === "Contribution");
|
||||
};
|
||||
|
||||
it("Checkpoint1: balances should go up when you contribute()", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const startingBalance = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const contributeTx = await crowdFundContract.contribute({ value: amount });
|
||||
const receipt = await contributeTx.wait();
|
||||
expect(receipt?.status).to.equal(1);
|
||||
|
||||
const newBalance = await crowdFundContract.balances(owner.address);
|
||||
expect(newBalance).to.equal(startingBalance + amount);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should emit a Contribution event with contributor + amount", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const tx = await crowdFundContract.contribute({ value: amount });
|
||||
const receipt = await tx.wait();
|
||||
expect(receipt?.status).to.equal(1);
|
||||
|
||||
const contributionEvents = getContributionEventsFromReceipt(receipt);
|
||||
expect(contributionEvents.length).to.equal(1);
|
||||
|
||||
const evt = contributionEvents[0];
|
||||
expect(evt.args[0]).to.equal(owner.address);
|
||||
expect(evt.args[1]).to.equal(amount);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should accumulate multiple contributions from the same user", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const starting = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const a1 = ethers.parseEther("0.001");
|
||||
const a2 = ethers.parseEther("0.002");
|
||||
await (await crowdFundContract.contribute({ value: a1 })).wait();
|
||||
await (await crowdFundContract.contribute({ value: a2 })).wait();
|
||||
|
||||
const ending = await crowdFundContract.balances(owner.address);
|
||||
expect(ending).to.equal(starting + a1 + a2);
|
||||
});
|
||||
|
||||
it("Checkpoint1: should track balances independently per contributor", async function () {
|
||||
const [owner, secondAccount] = await ethers.getSigners();
|
||||
|
||||
const a1 = ethers.parseEther("0.001");
|
||||
const a2 = ethers.parseEther("0.002");
|
||||
|
||||
await (await crowdFundContract.connect(owner).contribute({ value: a1 })).wait();
|
||||
await (await crowdFundContract.connect(secondAccount).contribute({ value: a2 })).wait();
|
||||
|
||||
expect(await crowdFundContract.balances(owner.address)).to.equal(a1);
|
||||
expect(await crowdFundContract.balances(secondAccount.address)).to.equal(a2);
|
||||
});
|
||||
|
||||
it("Checkpoint1: contract ETH balance should increase when someone contributes", async function () {
|
||||
const startContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.contribute({ value: amount })).wait();
|
||||
|
||||
const endContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
expect(endContractBal).to.equal(startContractBal + amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 2: 📤 Withdrawing Funds", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
const setOpenToWithdrawTrue = async () => {
|
||||
// Checkpoint 2 doesn't include a setter for `openToWithdraw`, so we toggle it directly in storage.
|
||||
//
|
||||
// Don't worry if you don't understand the wizardry that happens here.
|
||||
//
|
||||
// Important: `openToWithdraw` might be:
|
||||
// - in its own storage slot (bool uses the least-significant byte of the slot), OR
|
||||
// - packed into an existing slot (e.g. declared right after an `address`, so it shares slot 0),
|
||||
// in which case flipping `0x...01` for the whole slot DOES NOT necessarily flip the bool.
|
||||
//
|
||||
// So we probe (slot, byteOffset) by mutating ONE byte at a time and checking which mutation makes
|
||||
// `openToWithdraw()` return true. This effectively answers: "which storage location impacts this variable?"
|
||||
const target = await crowdFundContract.getAddress();
|
||||
|
||||
// If it's already open, nothing to do.
|
||||
if (await crowdFundContract.openToWithdraw()) return;
|
||||
|
||||
const writeStorageAt = async (slot: bigint, value: string) => {
|
||||
const slotHex = ethers.zeroPadValue(ethers.toBeHex(slot), 32);
|
||||
await network.provider.send("hardhat_setStorageAt", [target, slotHex, value]);
|
||||
};
|
||||
|
||||
const hexToBytes32 = (hex: string) => {
|
||||
// Expect 0x-prefixed 32-byte hex from `getStorage`.
|
||||
const normalized = hex.startsWith("0x") ? hex.slice(2) : hex;
|
||||
const padded = normalized.padStart(64, "0");
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < 64; i += 2) out.push(parseInt(padded.slice(i, i + 2), 16));
|
||||
if (out.length !== 32) throw new Error("Expected 32 bytes");
|
||||
return out;
|
||||
};
|
||||
|
||||
const bytes32ToHex = (bytes: number[]) => {
|
||||
if (bytes.length !== 32) throw new Error("Expected 32 bytes");
|
||||
const hex = bytes.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
return "0x" + hex;
|
||||
};
|
||||
|
||||
// Probe a reasonable number of slots so re-ordering / adding variables doesn't break tests.
|
||||
// (Mapping `balances` lives at a slot too, but since we always revert unsuccessful writes,
|
||||
// it's safe to probe it.)
|
||||
for (let s = 0n; s <= 20n; s++) {
|
||||
const original = await ethers.provider.getStorage(target, s);
|
||||
const originalBytes = hexToBytes32(original);
|
||||
|
||||
try {
|
||||
// Try flipping each byte to 0x01 (leaving all other bytes unchanged).
|
||||
// Storage words are represented big-endian in hex, but since we probe ALL 32 bytes,
|
||||
// we don't need to reason about endianness/packing direction here.
|
||||
for (let byteIdx = 0; byteIdx < 32; byteIdx++) {
|
||||
const mutated = [...originalBytes];
|
||||
mutated[byteIdx] = 0x01;
|
||||
await writeStorageAt(s, bytes32ToHex(mutated));
|
||||
|
||||
const isOpen = await crowdFundContract.openToWithdraw();
|
||||
if (isOpen === true) {
|
||||
// Found the (slot, byteIdx) that impacts `openToWithdraw`.
|
||||
// We intentionally keep this storage mutation for the test.
|
||||
return;
|
||||
}
|
||||
|
||||
// Not the right byte → restore original before continuing.
|
||||
await writeStorageAt(s, original);
|
||||
}
|
||||
} catch {
|
||||
// If the function doesn't exist yet, nothing to do here.
|
||||
}
|
||||
// Ensure we leave storage exactly as we found it before moving to the next slot.
|
||||
await writeStorageAt(s, original);
|
||||
}
|
||||
|
||||
throw new Error("Could not locate `openToWithdraw` storage slot to toggle it for tests.");
|
||||
};
|
||||
|
||||
it("Checkpoint2: withdraw should revert with NotOpenToWithdraw() when withdrawals are not open", async function () {
|
||||
await expect(crowdFundContract.withdraw()).to.be.revertedWithCustomError(
|
||||
crowdFundContract,
|
||||
"NotOpenToWithdraw",
|
||||
);
|
||||
});
|
||||
|
||||
it("Checkpoint2: withdraw should send your full balance and zero-out your recorded balance", async function () {
|
||||
const [, contributor] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.connect(contributor).contribute({ value: amount })).wait();
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(amount);
|
||||
|
||||
// We modify storage directly to set openToWithdraw to true since we have not implemented the setter yet.
|
||||
await setOpenToWithdrawTrue();
|
||||
|
||||
const startingBalance = await ethers.provider.getBalance(contributor.address);
|
||||
const withdrawTx = await crowdFundContract.connect(contributor).withdraw();
|
||||
|
||||
const tx = await ethers.provider.getTransaction(withdrawTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
const receipt = await ethers.provider.getTransactionReceipt(withdrawTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
const endingBalance = await ethers.provider.getBalance(contributor.address);
|
||||
|
||||
expect(endingBalance).to.equal(startingBalance + amount - gasCost);
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(0n);
|
||||
});
|
||||
|
||||
it("Checkpoint2: withdrawing twice should not let you withdraw more than you contributed", async function () {
|
||||
const [, contributor] = await ethers.getSigners();
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
await (await crowdFundContract.connect(contributor).contribute({ value: amount })).wait();
|
||||
|
||||
// We modify storage directly to set openToWithdraw to true since we have not implemented the setter yet.
|
||||
await setOpenToWithdrawTrue();
|
||||
|
||||
await (await crowdFundContract.connect(contributor).withdraw()).wait();
|
||||
const balanceAfterFirst = await ethers.provider.getBalance(contributor.address);
|
||||
|
||||
// Second withdraw should refund 0; only gas should change wallet balance.
|
||||
const secondTx = await crowdFundContract.connect(contributor).withdraw();
|
||||
const tx = await ethers.provider.getTransaction(secondTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
const receipt = await ethers.provider.getTransactionReceipt(secondTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
|
||||
const balanceAfterSecond = await ethers.provider.getBalance(contributor.address);
|
||||
expect(balanceAfterSecond).to.equal(balanceAfterFirst - gasCost);
|
||||
expect(await crowdFundContract.balances(contributor.address)).to.equal(0n);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 3: 🔬 State Machine / Timing ⏱", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
it("Checkpoint3: execute() should revert with TooEarly() if called before the deadline", async function () {
|
||||
await expect(crowdFundContract.execute()).to.be.revertedWithCustomError(crowdFundContract, "TooEarly");
|
||||
});
|
||||
|
||||
it("Checkpoint3: timeLeft should decrease as time moves forward (until it hits 0)", async function () {
|
||||
const t1 = await crowdFundContract.timeLeft();
|
||||
expect(Number(t1)).to.be.greaterThan(0);
|
||||
|
||||
await network.provider.send("evm_increaseTime", [5]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
const t2 = await crowdFundContract.timeLeft();
|
||||
expect(Number(t2)).to.be.lessThan(Number(t1));
|
||||
expect(Number(t2)).to.be.greaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("Checkpoint3: if enough is contributed and time has passed, execute() should complete()", async function () {
|
||||
const timeLeft1 = await crowdFundContract.timeLeft();
|
||||
expect(
|
||||
Number(timeLeft1),
|
||||
"timeLeft not greater than 0. Did you implement the timeLeft() function correctly?",
|
||||
).to.greaterThan(0);
|
||||
|
||||
const amount = ethers.parseEther("1");
|
||||
await crowdFundContract.contribute({ value: amount });
|
||||
|
||||
await network.provider.send("evm_increaseTime", [72 * 3600]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
const timeLeft2 = await crowdFundContract.timeLeft();
|
||||
expect(
|
||||
Number(timeLeft2),
|
||||
"timeLeft not equal to 0. Did you implement the timeLeft() function correctly?",
|
||||
).to.equal(0);
|
||||
|
||||
const startRecipientBal = await ethers.provider.getBalance(await fundingRecipient.getAddress());
|
||||
const startContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
await crowdFundContract.execute();
|
||||
|
||||
const result = await fundingRecipient.completed();
|
||||
expect(result).to.equal(true);
|
||||
|
||||
const endRecipientBal = await ethers.provider.getBalance(await fundingRecipient.getAddress());
|
||||
const endContractBal = await ethers.provider.getBalance(await crowdFundContract.getAddress());
|
||||
|
||||
// Funds should have moved into the FundingRecipient via `complete{value: ...}()`
|
||||
expect(endRecipientBal).to.equal(startRecipientBal + startContractBal);
|
||||
expect(endContractBal).to.equal(0n);
|
||||
});
|
||||
|
||||
it("Checkpoint3: if not enough is contributed and time has passed, execute() should enable withdraw", async function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [owner, secondAccount] = await ethers.getSigners();
|
||||
|
||||
await crowdFundContract.connect(secondAccount).contribute({
|
||||
value: ethers.parseEther("0.001"),
|
||||
});
|
||||
|
||||
await network.provider.send("evm_increaseTime", [72 * 3600]);
|
||||
await network.provider.send("evm_mine");
|
||||
|
||||
await crowdFundContract.execute();
|
||||
|
||||
const result = await fundingRecipient.completed();
|
||||
expect(result).to.equal(false);
|
||||
|
||||
// If `openToWithdraw` is implemented (Checkpoint 2/3), it should now be true.
|
||||
// We use `as any` so earlier checkpoints can still compile this test file.
|
||||
if ((crowdFundContract as any).openToWithdraw) {
|
||||
const isOpen = await (crowdFundContract as any).openToWithdraw();
|
||||
expect(isOpen).to.equal(true);
|
||||
}
|
||||
|
||||
const startingBalance = await ethers.provider.getBalance(secondAccount.address);
|
||||
const withdrawTx = await crowdFundContract.connect(secondAccount).withdraw();
|
||||
|
||||
const tx = await ethers.provider.getTransaction(withdrawTx.hash);
|
||||
if (!tx) throw new Error("Cannot resolve transaction");
|
||||
|
||||
const receipt = await ethers.provider.getTransactionReceipt(withdrawTx.hash);
|
||||
if (!receipt) throw new Error("Cannot resolve receipt");
|
||||
|
||||
const gasCost = tx.gasPrice * receipt.gasUsed;
|
||||
const endingBalance = await ethers.provider.getBalance(secondAccount.address);
|
||||
|
||||
expect(endingBalance).to.equal(startingBalance + ethers.parseEther("0.001") - gasCost);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkpoint 4: 💵 Receive Function / UX 🙎", function () {
|
||||
beforeEach(async function () {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
it("Checkpoint4: sending ETH directly to the contract should behave like contribute()", async function () {
|
||||
const [owner] = await ethers.getSigners();
|
||||
const startingBalance = await crowdFundContract.balances(owner.address);
|
||||
|
||||
const amount = ethers.parseEther("0.001");
|
||||
const sendTx = await owner.sendTransaction({ to: await crowdFundContract.getAddress(), value: amount });
|
||||
await sendTx.wait();
|
||||
|
||||
const newBalance = await crowdFundContract.balances(owner.address);
|
||||
expect(newBalance).to.equal(startingBalance + amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
11
packages/hardhat/tsconfig.json
Normal file
11
packages/hardhat/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
14
packages/nextjs/.env.example
Normal file
14
packages/nextjs/.env.example
Normal 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
38
packages/nextjs/.gitignore
vendored
Normal 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
|
||||
9
packages/nextjs/.prettierrc.js
Normal file
9
packages/nextjs/.prettierrc.js
Normal 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")],
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Address } from "viem";
|
||||
import { useContractLogs } from "~~/hooks/scaffold-eth";
|
||||
import { replacer } from "~~/utils/scaffold-eth/common";
|
||||
|
||||
export const AddressLogsTab = ({ address }: { address: Address }) => {
|
||||
const contractLogs = useContractLogs(address);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{contractLogs.map((log, i) => (
|
||||
<div key={i}>
|
||||
<strong>Log:</strong> {JSON.stringify(log, replacer, 2)}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Address, createPublicClient, http, toHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: hardhat,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
export const AddressStorageTab = ({ address }: { address: Address }) => {
|
||||
const [storage, setStorage] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStorage = async () => {
|
||||
try {
|
||||
const storageData = [];
|
||||
let idx = 0;
|
||||
|
||||
while (true) {
|
||||
const storageAtPosition = await publicClient.getStorageAt({
|
||||
address: address,
|
||||
slot: toHex(idx),
|
||||
});
|
||||
|
||||
if (storageAtPosition === "0x" + "0".repeat(64)) break;
|
||||
|
||||
if (storageAtPosition) {
|
||||
storageData.push(storageAtPosition);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
setStorage(storageData);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch storage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStorage();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
{storage.length > 0 ? (
|
||||
<div className="mockup-code overflow-auto max-h-[500px]">
|
||||
<pre className="px-5 whitespace-pre-wrap break-words">
|
||||
{storage.map((data, i) => (
|
||||
<div key={i}>
|
||||
<strong>Storage Slot {i}:</strong> {data}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-lg">This contract does not have any variables.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
12
packages/nextjs/app/blockexplorer/_components/BackButton.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const BackButton = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<button className="btn btn-sm btn-primary" onClick={() => router.back()}>
|
||||
Back
|
||||
</button>
|
||||
);
|
||||
};
|
||||
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal file
102
packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
49
packages/nextjs/app/blockexplorer/_components/SearchBar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isAddress, isHex } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { usePublicClient } from "wagmi";
|
||||
|
||||
export const SearchBar = () => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const client = usePublicClient({ chainId: hardhat.id });
|
||||
|
||||
const handleSearch = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (isHex(searchInput)) {
|
||||
try {
|
||||
const tx = await client?.getTransaction({ hash: searchInput });
|
||||
if (tx) {
|
||||
router.push(`/blockexplorer/transaction/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAddress(searchInput)) {
|
||||
router.push(`/blockexplorer/address/${searchInput}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearch} className="flex items-center justify-end mb-5 space-x-3 mx-5">
|
||||
<input
|
||||
className="border-primary bg-base-100 text-base-content 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
7
packages/nextjs/app/blockexplorer/_components/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./SearchBar";
|
||||
export * from "./BackButton";
|
||||
export * from "./AddressCodeTab";
|
||||
export * from "./TransactionHash";
|
||||
export * from "./ContractTabs";
|
||||
export * from "./PaginationButton";
|
||||
export * from "./TransactionsTable";
|
||||
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal file
101
packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Normal 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;
|
||||
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
12
packages/nextjs/app/blockexplorer/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Block Explorer",
|
||||
description: "Block Explorer created with 🏗 Scaffold-ETH 2",
|
||||
});
|
||||
|
||||
const BlockExplorerLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default BlockExplorerLayout;
|
||||
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
83
packages/nextjs/app/blockexplorer/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { PaginationButton, SearchBar, TransactionsTable } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { hardhat } from "viem/chains";
|
||||
import { useFetchBlocks } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
import { notification } from "~~/utils/scaffold-eth";
|
||||
|
||||
const BlockExplorer: NextPage = () => {
|
||||
const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks();
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const [isLocalNetwork, setIsLocalNetwork] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id !== hardhat.id) {
|
||||
setIsLocalNetwork(false);
|
||||
}
|
||||
}, [targetNetwork.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetNetwork.id === hardhat.id && error) {
|
||||
setHasError(true);
|
||||
}
|
||||
}, [targetNetwork.id, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLocalNetwork) {
|
||||
notification.error(
|
||||
<>
|
||||
<p className="font-bold mt-0 mb-1">
|
||||
<code className="italic bg-base-300 text-base font-bold"> 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
63
packages/nextjs/app/contributions/page.tsx
Normal file
63
packages/nextjs/app/contributions/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { Address } from "@scaffold-ui/components";
|
||||
import type { NextPage } from "next";
|
||||
import { formatEther } from "viem";
|
||||
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
|
||||
|
||||
const ContributionsPage: NextPage = () => {
|
||||
const { data: contributionEvents, isLoading } = useScaffoldEventHistory({
|
||||
contractName: "CrowdFund",
|
||||
eventName: "Contribution",
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center flex-col flex-grow pt-10">
|
||||
<div className="px-5">
|
||||
<h1 className="text-center mb-3">
|
||||
<span className="block text-2xl font-bold">All Contributions</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto shadow-lg">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bg-primary">From</th>
|
||||
<th className="bg-primary">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!contributionEvents || contributionEvents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center">
|
||||
No events found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
contributionEvents.map((event, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Address address={event.args?.[0]} />
|
||||
</td>
|
||||
<td>{formatEther(event.args?.[1] || 0n)} ETH</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContributionsPage;
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { ETHToPrice } from "./EthToPrice";
|
||||
import { Address } from "@scaffold-ui/components";
|
||||
import { useWatchBalance } from "@scaffold-ui/hooks";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { formatEther, parseEther } from "viem";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
|
||||
export const ContributeContractInteraction = ({ address }: { address?: string }) => {
|
||||
const { address: connectedAddress } = useAccount();
|
||||
|
||||
const { data: crowdFundContract } = useDeployedContractInfo({ contractName: "CrowdFund" });
|
||||
const { data: fundingRecipientContract } = useDeployedContractInfo({ contractName: "FundingRecipient" });
|
||||
|
||||
const { data: crowdFundContractBalance } = useWatchBalance({ address: crowdFundContract?.address });
|
||||
const { data: fundingRecipientBalance } = useWatchBalance({ address: fundingRecipientContract?.address });
|
||||
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
|
||||
const { data: threshold } = useScaffoldReadContract({
|
||||
contractName: "CrowdFund",
|
||||
functionName: "threshold",
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const { data: timeLeft } = useScaffoldReadContract({
|
||||
contractName: "CrowdFund",
|
||||
functionName: "timeLeft",
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const { data: myContribution } = useScaffoldReadContract({
|
||||
contractName: "CrowdFund",
|
||||
functionName: "balances",
|
||||
args: [connectedAddress],
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const { data: isFundingCompleted } = useScaffoldReadContract({
|
||||
contractName: "FundingRecipient",
|
||||
functionName: "completed",
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const { writeContractAsync } = useScaffoldWriteContract({ contractName: "CrowdFund" });
|
||||
|
||||
return (
|
||||
<div className="flex items-center flex-col flex-grow w-full px-4 gap-12">
|
||||
{isFundingCompleted && (
|
||||
<div className="flex flex-col items-center gap-2 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-6 mt-12 w-full max-w-lg">
|
||||
<p className="block m-0 font-semibold">🎉 Crowdfunding contract triggered FundingRecipient 🎉</p>
|
||||
<div className="flex items-center">
|
||||
<ETHToPrice
|
||||
value={fundingRecipientBalance ? formatEther(fundingRecipientBalance.value) : undefined}
|
||||
className="text-[1rem]"
|
||||
/>
|
||||
<p className="block m-0 text-lg -ml-1">received</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex flex-col items-center space-y-8 bg-base-100 shadow-lg shadow-secondary border-8 border-secondary rounded-xl p-6 w-full max-w-lg ${
|
||||
!isFundingCompleted ? "mt-24" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col w-full items-center">
|
||||
<p className="block text-2xl mt-0 mb-2 font-semibold">CrowdFund Contract</p>
|
||||
<Address address={address} size="xl" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-around w-full">
|
||||
<div className="flex flex-col items-center justify-center w-1/2">
|
||||
<p className="block text-xl mt-0 mb-1 font-semibold">Time Left</p>
|
||||
<p className="m-0 p-0">{timeLeft ? `${humanizeDuration(Number(timeLeft) * 1000)}` : "DONE"}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center w-1/2">
|
||||
<p className="block text-xl mt-0 mb-1 font-semibold">You Contributed</p>
|
||||
<span>
|
||||
{myContribution ? formatEther(myContribution) : 0} {targetNetwork.nativeCurrency.symbol}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center shrink-0 w-full">
|
||||
<p className="block text-xl mt-0 mb-1 font-semibold">Total Contributed</p>
|
||||
<div className="flex space-x-2">
|
||||
<ETHToPrice value={crowdFundContractBalance ? formatEther(crowdFundContractBalance.value) : undefined} />
|
||||
<span>/</span>
|
||||
<ETHToPrice value={threshold ? formatEther(threshold) : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-5">
|
||||
<div className="flex space-x-7">
|
||||
<button
|
||||
className="btn btn-primary uppercase"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeContractAsync({ functionName: "execute" });
|
||||
} catch (err) {
|
||||
console.error("Error calling execute function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Execute
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-primary uppercase"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeContractAsync({ functionName: "withdraw" });
|
||||
} catch (err) {
|
||||
console.error("Error calling withdraw function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Withdraw
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-primary uppercase"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await writeContractAsync({ functionName: "contribute", value: parseEther("0.5") });
|
||||
} catch (err) {
|
||||
console.error("Error calling contribute function", err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
🤝 Contribute 0.5 ether!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
53
packages/nextjs/app/crowdfund/_components/EthToPrice.tsx
Normal file
53
packages/nextjs/app/crowdfund/_components/EthToPrice.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useFetchNativeCurrencyPrice } from "@scaffold-ui/hooks";
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
|
||||
|
||||
type TBalanceProps = {
|
||||
value?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display (ETH & USD) value for the input value provided.
|
||||
*/
|
||||
export const ETHToPrice = ({ value, className = "" }: TBalanceProps) => {
|
||||
const [isEthBalance, setIsEthBalance] = useState(true);
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const { price } = useFetchNativeCurrencyPrice();
|
||||
|
||||
const onToggleBalance = useCallback(() => {
|
||||
if (price > 0) {
|
||||
setIsEthBalance(!isEthBalance);
|
||||
}
|
||||
}, [isEthBalance, price]);
|
||||
|
||||
if (!value) {
|
||||
return (
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="flex items-center space-y-6">
|
||||
<div className="h-5 w-12 bg-slate-300 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-sm 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>{parseFloat(value).toFixed(4)}</span>
|
||||
<span className="text-xs font-bold ml-1">{targetNetwork.nativeCurrency.symbol}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xs font-bold mr-1">$</span>
|
||||
<span>{(parseFloat(value) * price).toFixed(2)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
2
packages/nextjs/app/crowdfund/_components/index.tsx
Normal file
2
packages/nextjs/app/crowdfund/_components/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./EthToPrice";
|
||||
export * from "./ContributeContractInteraction";
|
||||
12
packages/nextjs/app/crowdfund/page.tsx
Normal file
12
packages/nextjs/app/crowdfund/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ContributeContractInteraction } from "./_components";
|
||||
import type { NextPage } from "next";
|
||||
import { useDeployedContractInfo } from "~~/hooks/scaffold-eth";
|
||||
|
||||
const CrowdFundPage: NextPage = () => {
|
||||
const { data: crowdFundContract } = useDeployedContractInfo({ contractName: "CrowdFund" });
|
||||
return <ContributeContractInteraction key={crowdFundContract?.address} address={crowdFundContract?.address} />;
|
||||
};
|
||||
|
||||
export default CrowdFundPage;
|
||||
38
packages/nextjs/app/debug/_components/ContractUI.tsx
Normal file
38
packages/nextjs/app/debug/_components/ContractUI.tsx
Normal 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} />;
|
||||
};
|
||||
71
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal file
71
packages/nextjs/app/debug/_components/DebugContracts.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
packages/nextjs/app/debug/page.tsx
Normal file
28
packages/nextjs/app/debug/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DebugContracts } from "./_components/DebugContracts";
|
||||
import type { NextPage } from "next";
|
||||
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Debug Contracts",
|
||||
description: "Debug your deployed 🏗 Scaffold-ETH 2 contracts in an easy way",
|
||||
});
|
||||
|
||||
const Debug: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<DebugContracts />
|
||||
<div className="text-center mt-8 bg-secondary p-10">
|
||||
<h1 className="text-4xl my-0">Debug Contracts</h1>
|
||||
<p className="text-neutral">
|
||||
You can debug & interact with your deployed contracts here.
|
||||
<br /> Check{" "}
|
||||
<code className="italic bg-base-300 text-base font-bold [word-spacing:-0.5rem] px-1">
|
||||
packages / nextjs / app / debug / page.tsx
|
||||
</code>{" "}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Debug;
|
||||
31
packages/nextjs/app/layout.tsx
Normal file
31
packages/nextjs/app/layout.tsx
Normal 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: "Crowdfunding | 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;
|
||||
16
packages/nextjs/app/not-found.tsx
Normal file
16
packages/nextjs/app/not-found.tsx
Normal 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're looking for doesn't exist.</p>
|
||||
<Link href="/" className="btn btn-primary">
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
packages/nextjs/app/page.tsx
Normal file
101
packages/nextjs/app/page.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"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">(SpeedrunEthereum Challenge: Crowdfunding App 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: 📣 Crowdfunding App</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-center text-lg mt-8">
|
||||
🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an
|
||||
adversarial group of players can use to work together. In this challenge, you create a decentralized
|
||||
application where users can coordinate a group funding effort. If the users cooperate, the money is
|
||||
collected in a second smart contract. If they defect, the worst that can happen is everyone gets
|
||||
their money back. The users only have to trust the code, not each other.
|
||||
</p>
|
||||
<p className="text-center text-lg">
|
||||
🌟 The final deliverable is deploying a Dapp that lets users send ether to a contract and then fund
|
||||
the cause if the conditions are met, then deploy your app to a public webserver. Submit the url on{" "}
|
||||
<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;
|
||||
80
packages/nextjs/components/Footer.tsx
Normal file
80
packages/nextjs/components/Footer.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { 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>
|
||||
);
|
||||
};
|
||||
114
packages/nextjs/components/Header.tsx
Normal file
114
packages/nextjs/components/Header.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"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 { CircleStackIcon, InboxStackIcon } 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: "Crowdfund",
|
||||
href: "/crowdfund",
|
||||
icon: <CircleStackIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Contributions",
|
||||
href: "/contributions",
|
||||
icon: <InboxStackIcon 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">Crowdfunding App</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>
|
||||
);
|
||||
};
|
||||
58
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal file
58
packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
42
packages/nextjs/components/SwitchTheme.tsx
Normal file
42
packages/nextjs/components/SwitchTheme.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
9
packages/nextjs/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
};
|
||||
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
18
packages/nextjs/components/assets/BuidlGuidlLogo.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const BuidlGuidlLogo = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="53"
|
||||
height="72"
|
||||
viewBox="0 0 53 72"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M25.9 17.434v15.638h3.927v9.04h9.718v-9.04h6.745v18.08l-10.607 19.88-12.11-.182-12.11.183L.856 51.152v-18.08h6.713v9.04h9.75v-9.04h4.329V2.46a2.126 2.126 0 0 1 4.047-.914c1.074.412 2.157 1.5 3.276 2.626 1.33 1.337 2.711 2.726 4.193 3.095 1.496.373 2.605-.026 3.855-.475 1.31-.47 2.776-.997 5.005-.747 1.67.197 2.557 1.289 3.548 2.509 1.317 1.623 2.82 3.473 6.599 3.752l-.024.017c-2.42 1.709-5.726 4.043-10.86 3.587-1.605-.139-2.736-.656-3.82-1.153-1.546-.707-2.997-1.37-5.59-.832-2.809.563-4.227 1.892-5.306 2.903-.236.221-.456.427-.67.606Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
17
packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { AvatarComponent } from "@rainbow-me/rainbowkit";
|
||||
import { blo } from "blo";
|
||||
|
||||
// Custom Avatar for RainbowKit
|
||||
export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => (
|
||||
// Don't want to use nextJS Image here (and adding remote patterns for the URL)
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={ensImage || blo(address as `0x${string}`)}
|
||||
width={size}
|
||||
height={size}
|
||||
alt={`${address} avatar`}
|
||||
/>
|
||||
);
|
||||
140
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal file
140
packages/nextjs/components/scaffold-eth/Faucet.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
73
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal file
73
packages/nextjs/components/scaffold-eth/FaucetButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NetworkOptions } from "./NetworkOptions";
|
||||
import { useDisconnect } from "wagmi";
|
||||
import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export const WrongNetworkDropdown = () => {
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-end mr-2">
|
||||
<label tabIndex={0} className="btn btn-error btn-sm dropdown-toggle gap-1">
|
||||
<span>Wrong network</span>
|
||||
<ChevronDownIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content menu p-2 mt-1 shadow-center shadow-accent bg-base-200 rounded-box gap-1"
|
||||
>
|
||||
<NetworkOptions />
|
||||
<li>
|
||||
<button
|
||||
className="menu-item text-error btn-sm rounded-xl! flex gap-3 py-3"
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" />
|
||||
<span>Disconnect</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,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>
|
||||
);
|
||||
};
|
||||
4
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
4
packages/nextjs/components/scaffold-eth/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./BlockieAvatar";
|
||||
export * from "./Faucet";
|
||||
export * from "./FaucetButton";
|
||||
export * from "./RainbowKitCustomConnectButton";
|
||||
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
9
packages/nextjs/contracts/deployedContracts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
const deployedContracts = {} as const;
|
||||
|
||||
export default deployedContracts satisfies GenericContractsDeclaration;
|
||||
16
packages/nextjs/contracts/externalContracts.ts
Normal file
16
packages/nextjs/contracts/externalContracts.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const externalContracts = {
|
||||
* 1: {
|
||||
* DAI: {
|
||||
* address: "0x...",
|
||||
* abi: [...],
|
||||
* },
|
||||
* },
|
||||
* } as const;
|
||||
*/
|
||||
const externalContracts = {} as const;
|
||||
|
||||
export default externalContracts satisfies GenericContractsDeclaration;
|
||||
32
packages/nextjs/eslint.config.mjs
Normal file
32
packages/nextjs/eslint.config.mjs
Normal 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
14
packages/nextjs/hooks/scaffold-eth/index.ts
Normal file
14
packages/nextjs/hooks/scaffold-eth/index.ts
Normal 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";
|
||||
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal file
40
packages/nextjs/hooks/scaffold-eth/useContractLogs.ts
Normal 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;
|
||||
};
|
||||
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal file
19
packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
Normal 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 };
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal file
133
packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
22
packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts
Normal file
22
packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts
Normal 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);
|
||||
};
|
||||
23
packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts
Normal file
23
packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts
Normal 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]);
|
||||
};
|
||||
65
packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts
Normal file
65
packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
292
packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts
Normal file
292
packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
194
packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts
Normal file
194
packages/nextjs/hooks/scaffold-eth/useScaffoldWriteContract.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
19
packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts
Normal file
19
packages/nextjs/hooks/scaffold-eth/useSelectedNetwork.ts
Normal 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;
|
||||
}
|
||||
24
packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts
Normal file
24
packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts
Normal 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]);
|
||||
}
|
||||
115
packages/nextjs/hooks/scaffold-eth/useTransactor.tsx
Normal file
115
packages/nextjs/hooks/scaffold-eth/useTransactor.tsx
Normal 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
6
packages/nextjs/next-env.d.ts
vendored
Normal 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.
|
||||
29
packages/nextjs/next.config.ts
Normal file
29
packages/nextjs/next.config.ts
Normal 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;
|
||||
64
packages/nextjs/package.json
Normal file
64
packages/nextjs/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"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",
|
||||
"humanize-duration": "^3.28.0",
|
||||
"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/humanize-duration": "^3",
|
||||
"@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
Reference in New Issue
Block a user