diff --git a/members/Old000Driver/readme.md b/members/Old000Driver/readme.md
new file mode 100644
index 000000000..ae2f61888
--- /dev/null
+++ b/members/Old000Driver/readme.md
@@ -0,0 +1,15 @@
+# Web3 前端训练营报名
+
+## 个人信息
+
+* GitHub ID:Old000Driver
+* OpenBuild Username:792002710
+* ERC20 钱包地址:0x2282050684b096d36393e3d60F4bBa3f3d0cC661
+
+### 个人介绍
+
+`有4年前端开发经验,从事过网络安全行业和金融行业,目前已经学习并练习过 solidity、etherjs、nextjs、tailwind,以成为 web3 合格前端为目标继续补充学习`
+
+## 任务提交
+
+`建议所有的作业结果在个人的 GitHub 下单独建立仓库放置,在此 readme 里只当相关链接或简介描述即可`
diff --git a/members/Old000Driver/task1/.gitignore b/members/Old000Driver/task1/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/members/Old000Driver/task1/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/members/Old000Driver/task1/README.md b/members/Old000Driver/task1/README.md
new file mode 100644
index 000000000..74872fd4a
--- /dev/null
+++ b/members/Old000Driver/task1/README.md
@@ -0,0 +1,50 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default tseslint.config({
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
+- Optionally add `...tseslint.configs.stylisticTypeChecked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
+
+```js
+// eslint.config.js
+import react from 'eslint-plugin-react'
+
+export default tseslint.config({
+ // Set the react version
+ settings: { react: { version: '18.3' } },
+ plugins: {
+ // Add the react plugin
+ react,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended rules
+ ...react.configs.recommended.rules,
+ ...react.configs['jsx-runtime'].rules,
+ },
+})
+```
diff --git a/members/Old000Driver/task1/eslint.config.js b/members/Old000Driver/task1/eslint.config.js
new file mode 100644
index 000000000..092408a9f
--- /dev/null
+++ b/members/Old000Driver/task1/eslint.config.js
@@ -0,0 +1,28 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ },
+)
diff --git a/members/Old000Driver/task1/index.html b/members/Old000Driver/task1/index.html
new file mode 100644
index 000000000..480c218ca
--- /dev/null
+++ b/members/Old000Driver/task1/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/members/Old000Driver/task1/package.json b/members/Old000Driver/task1/package.json
new file mode 100644
index 000000000..3487f93ab
--- /dev/null
+++ b/members/Old000Driver/task1/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "openbuildtasks",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.17.0",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "globals": "^15.14.0",
+ "typescript": "~5.6.2",
+ "typescript-eslint": "^8.18.2",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/members/Old000Driver/task1/public/vite.svg b/members/Old000Driver/task1/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/members/Old000Driver/task1/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/members/Old000Driver/task1/src/App.tsx b/members/Old000Driver/task1/src/App.tsx
new file mode 100644
index 000000000..077368158
--- /dev/null
+++ b/members/Old000Driver/task1/src/App.tsx
@@ -0,0 +1,60 @@
+import { useState, useEffect } from "react";
+import Header from "./components/Header";
+import ToDoList from "./components/ToDoList";
+import AddToDo from "./components/AddToDo";
+
+export interface Todo {
+ id: number;
+ text: string;
+ completed: boolean;
+}
+
+function App() {
+ const [todos, setTodos] = useState(() => {
+ const savedTodos = localStorage.getItem("todos");
+ return savedTodos ? JSON.parse(savedTodos) : [];
+ });
+
+ useEffect(() => {
+ localStorage.setItem("todos", JSON.stringify(todos));
+ }, [todos]);
+
+ const addTodo = (text: string) => {
+ if (text.trim()) {
+ setTodos([
+ ...todos,
+ {
+ id: Date.now(),
+ text: text.trim(),
+ completed: false,
+ },
+ ]);
+ }
+ };
+
+ const deleteTodo = (id: number) => {
+ setTodos(todos.filter((todo) => todo.id !== id));
+ };
+
+ const toggleTodo = (id: number) => {
+ setTodos(
+ todos.map((todo) =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ )
+ );
+ };
+
+ return (
+
+ );
+}
+
+export default App;
diff --git a/members/Old000Driver/task1/src/assets/react.svg b/members/Old000Driver/task1/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/members/Old000Driver/task1/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/members/Old000Driver/task1/src/components/AddToDo.tsx b/members/Old000Driver/task1/src/components/AddToDo.tsx
new file mode 100644
index 000000000..cb0fb05c1
--- /dev/null
+++ b/members/Old000Driver/task1/src/components/AddToDo.tsx
@@ -0,0 +1,34 @@
+import React, { useState } from "react";
+
+interface AddToDoProps {
+ onAdd: (text: string) => void;
+}
+
+const AddToDo: React.FC = ({ onAdd }) => {
+ const [text, setText] = useState("");
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ onAdd(text);
+ setText("");
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default AddToDo;
diff --git a/members/Old000Driver/task1/src/components/Header.tsx b/members/Old000Driver/task1/src/components/Header.tsx
new file mode 100644
index 000000000..ec48b4e9b
--- /dev/null
+++ b/members/Old000Driver/task1/src/components/Header.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Header: React.FC = () => {
+ return (
+
+ )
+}
+
+export default Header
\ No newline at end of file
diff --git a/members/Old000Driver/task1/src/components/ToDoItem.tsx b/members/Old000Driver/task1/src/components/ToDoItem.tsx
new file mode 100644
index 000000000..9d5959c13
--- /dev/null
+++ b/members/Old000Driver/task1/src/components/ToDoItem.tsx
@@ -0,0 +1,36 @@
+import React from 'react'
+import { Todo } from '../App'
+
+interface ToDoItemProps {
+ todo: Todo
+ onDelete: (id: number) => void
+ onToggle: (id: number) => void
+}
+
+const ToDoItem: React.FC = ({ todo, onDelete, onToggle }) => {
+ return (
+
+
+
+
+
+ )
+}
+
+export default ToDoItem
\ No newline at end of file
diff --git a/members/Old000Driver/task1/src/components/ToDoList.tsx b/members/Old000Driver/task1/src/components/ToDoList.tsx
new file mode 100644
index 000000000..339bbddea
--- /dev/null
+++ b/members/Old000Driver/task1/src/components/ToDoList.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import ToDoItem from "./ToDoItem";
+import { Todo } from "../App";
+
+interface ToDoListProps {
+ todos: Todo[];
+ onDelete: (id: number) => void;
+ onToggle: (id: number) => void;
+}
+
+const ToDoList: React.FC = ({ todos, onDelete, onToggle }) => {
+ return (
+
+ {todos.map((todo) => (
+
+ ))}
+
+ );
+};
+
+export default ToDoList;
diff --git a/members/Old000Driver/task1/src/index.css b/members/Old000Driver/task1/src/index.css
new file mode 100644
index 000000000..9a51dc2af
--- /dev/null
+++ b/members/Old000Driver/task1/src/index.css
@@ -0,0 +1,177 @@
+/* 基础样式 */
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
+ Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* 容器样式 */
+.container {
+ min-height: 100vh;
+ background-color: white;
+}
+
+.content {
+ width: auto;
+ margin: 0 auto;
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin-top: 2rem;
+ justify-content: center;
+ align-items: center;
+}
+
+/* Header 样式 */
+.header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.header h1 {
+ font-size: 1.875rem;
+ font-weight: bold;
+ color: #111827;
+}
+
+/* AddToDo 样式 */
+.add-todo-form {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+ width: 400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.add-todo-input {
+ flex: 1;
+ padding: 0.5rem 1rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.5rem;
+ outline: none;
+}
+
+.add-todo-input:focus {
+ border-color: #3b82f6;
+}
+
+.add-button {
+ min-width: 80px;
+ padding: 0.5rem 1.5rem;
+ background-color: #0ea5e9;
+ color: white;
+ font-weight: 500;
+ border: none;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+}
+
+.add-button:hover {
+ background-color: #0284c7;
+}
+
+/* ToDoList 样式 */
+.todo-list {
+ width: 400px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.todo-list > * + * {
+ margin-top: 0.75rem;
+}
+
+.todo-container {
+ padding: 36px;
+ border: 2px solid black;
+ width: 400px;
+ border-radius: 25px;
+ box-shadow: #111827;
+}
+
+/* ToDoItem 样式 */
+.todo-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ background-color: #f9fafb;
+ border-radius: 0.5rem;
+}
+
+.todo-item.completed {
+ opacity: 0.6;
+}
+
+.todo-text {
+ cursor: pointer;
+ color: #111827;
+}
+
+.todo-text.completed {
+ text-decoration: line-through;
+}
+
+.delete-button {
+ min-width: 60px;
+ padding: 0.5rem 1rem;
+ background-color: #ef4444;
+ color: white;
+ font-weight: 500;
+ border: none;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+}
+
+.delete-button:hover {
+ background-color: #dc2626;
+}
+
+/* 添加复选框按钮样式 */
+.toggle-button {
+ width: 24px;
+ height: 24px;
+ border: 2px solid #d1d5db;
+ border-radius: 50%;
+ margin-right: 12px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background: white;
+ transition: all 0.2s ease-in-out;
+}
+
+.toggle-button.completed {
+ background: #10b981;
+ border-color: #10b981;
+}
+
+.toggle-button.completed::after {
+ content: "✓";
+ color: white;
+ font-size: 14px;
+}
+
+/* 修改 todo-item 样式 */
+.todo-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ background-color: #f9fafb;
+ border-radius: 0.5rem;
+}
+
+.todo-left {
+ display: flex;
+ align-items: center;
+ flex: 1;
+}
diff --git a/members/Old000Driver/task1/src/main.tsx b/members/Old000Driver/task1/src/main.tsx
new file mode 100644
index 000000000..bef5202a3
--- /dev/null
+++ b/members/Old000Driver/task1/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/members/Old000Driver/task1/src/vite-env.d.ts b/members/Old000Driver/task1/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/members/Old000Driver/task1/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/members/Old000Driver/task1/tsconfig.app.json b/members/Old000Driver/task1/tsconfig.app.json
new file mode 100644
index 000000000..358ca9ba9
--- /dev/null
+++ b/members/Old000Driver/task1/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/members/Old000Driver/task1/tsconfig.json b/members/Old000Driver/task1/tsconfig.json
new file mode 100644
index 000000000..1ffef600d
--- /dev/null
+++ b/members/Old000Driver/task1/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/members/Old000Driver/task1/tsconfig.node.json b/members/Old000Driver/task1/tsconfig.node.json
new file mode 100644
index 000000000..db0becc8b
--- /dev/null
+++ b/members/Old000Driver/task1/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/members/Old000Driver/task1/vite.config.ts b/members/Old000Driver/task1/vite.config.ts
new file mode 100644
index 000000000..0e43ae8de
--- /dev/null
+++ b/members/Old000Driver/task1/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/members/Old000Driver/task2/bind-wallet.png b/members/Old000Driver/task2/bind-wallet.png
new file mode 100644
index 000000000..b468ffdf2
Binary files /dev/null and b/members/Old000Driver/task2/bind-wallet.png differ
diff --git a/members/Old000Driver/task2/readme.md b/members/Old000Driver/task2/readme.md
new file mode 100644
index 000000000..7fa15420f
--- /dev/null
+++ b/members/Old000Driver/task2/readme.md
@@ -0,0 +1,68 @@
+# Task2 Blockchain Basic
+
+## [单选题] 如果你莫名奇妙收到了一个 NFT,那么
+
+- [ ] 天上掉米,我应该马上点开他的链接
+- [☑️] 这可能是在对我进行诈骗!
+
+## [单选题] 群里大哥给我发的网站,说能赚大米,我应该
+
+- [ ] 赶紧冲啊,待会米被人抢了
+- [☑️] 谨慎判断,不在不信任的网站链接钱包
+
+## [单选题] 下列说法正确的是
+
+- [☑️] 一个私钥对应一个地址
+- [ ] 一个私钥对应多个地址
+- [ ] 多个私钥对应一个地址
+- [ ] 多个私钥对应多个地址
+
+## [单选题] 下列哪个是以太坊虚拟机的简称
+
+- [ ] CLR
+- [☑️] EVM
+- [ ] JVM
+
+## [单选题] 以下哪个是以太坊上正确的地址格式?
+
+- [ ] 1A4BHoT2sXFuHsyL6bnTcD1m6AP9C5uyT1
+- [ ] TEEuMMSc6zPJD36gfjBAR2GmqT6Tu1Rcut
+- [ ] 0x997fd71a4cf5d214009619808176b947aec122890a7fcee02e78e329596c94ba
+- [☑️] 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+
+## [多选题] 有一天某个大哥说要按市场价的 80% 出油给你,有可能
+
+- [☑️] 他在洗米
+- [☑️] 他良心发现
+- [☑️] 要给我黒米
+- [☑️] 给我下套呢
+
+## [多选题] 以下哪些是以太坊的二层扩容方案?
+
+- [ ] Lightning Network(闪电网络)
+- [☑️] Optimsitic Rollup
+- [☑️] Zk Rollup
+
+## [简答题] 简述区块链的网络结构
+
+节点、区块、链、共识机制、网络协议、智能合约、去中心化应用、用户钱包、矿工
+
+## [简答题] 智能合约是什么,有何作用?
+
+智能合约由代码控制业务部署在区块链上,可以在链上提供安全、可靠的服务,不受人为干扰。
+
+## [简答题] 怎么理解大家常说的 `EVM` 这个词汇?
+
+EVM 是一个虚拟机,专门设计用于执行智能合约。它是以太坊网络中的计算引擎,负责处理和执行所有智能合约代码
+
+## [分析题] 你对去中心化的理解
+
+去中心化是民主的体现,虽然会导致效率低下,但是可以保证公平、公正,不受个人或组织的控制。区块链技术的应用,使得金融上去中心化成为可能。鉴于我们的国情可能难以大面积推广,但是在一些特定的领域,比如金融领域,去中心化的应用还是有很大的前景。
+
+## [分析题] 比较区块链与传统数据库,你的看法?
+
+传统数据由中心化的数据库管理,容易被篡改,而区块链是去中心化的,数据不可篡改。传统数据库的数据安全性不高,而区块链的数据安全性高。传统数据库的数据难以追溯,而区块链的数据可以追溯。传统数据库的数据不透明,而区块链的数据透明。
+
+## 操作题
+
+
diff --git a/members/Old000Driver/task3/.gitignore b/members/Old000Driver/task3/.gitignore
new file mode 100644
index 000000000..115ec00a4
--- /dev/null
+++ b/members/Old000Driver/task3/.gitignore
@@ -0,0 +1,21 @@
+node_modules
+.env
+
+# Hardhat files
+/cache
+/artifacts
+
+# TypeChain files
+/typechain
+/typechain-types
+
+# solidity-coverage files
+/coverage
+/coverage.json
+
+# Hardhat Ignition default folder for deployments against a local node
+ignition/deployments/chain-31337
+
+.env
+
+*.markdown
\ No newline at end of file
diff --git a/members/Old000Driver/task3/README.md b/members/Old000Driver/task3/README.md
new file mode 100644
index 000000000..d04248dab
--- /dev/null
+++ b/members/Old000Driver/task3/README.md
@@ -0,0 +1,13 @@
+# Sample Hardhat Project
+
+This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract.
+
+Try running some of the following tasks:
+
+```shell
+npx hardhat help
+npx hardhat test
+REPORT_GAS=true npx hardhat test
+npx hardhat node
+npx hardhat ignition deploy ./ignition/modules/Lock.js
+```
diff --git a/members/Old000Driver/task3/contracts/Lock.sol b/members/Old000Driver/task3/contracts/Lock.sol
new file mode 100644
index 000000000..2f385f70e
--- /dev/null
+++ b/members/Old000Driver/task3/contracts/Lock.sol
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.28;
+
+// Uncomment this line to use console.log
+// import "hardhat/console.sol";
+
+contract Lock {
+ uint public unlockTime;
+ address payable public owner;
+
+ event Withdrawal(uint amount, uint when);
+
+ constructor(uint _unlockTime) payable {
+ require(
+ block.timestamp < _unlockTime,
+ "Unlock time should be in the future"
+ );
+
+ unlockTime = _unlockTime;
+ owner = payable(msg.sender);
+ }
+
+ function withdraw() public {
+ // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
+ // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);
+
+ require(block.timestamp >= unlockTime, "You can't withdraw yet");
+ require(msg.sender == owner, "You aren't the owner");
+
+ emit Withdrawal(address(this).balance, block.timestamp);
+
+ owner.transfer(address(this).balance);
+ }
+}
diff --git a/members/Old000Driver/task3/contracts/MyNFT.sol b/members/Old000Driver/task3/contracts/MyNFT.sol
new file mode 100644
index 000000000..bf93b5108
--- /dev/null
+++ b/members/Old000Driver/task3/contracts/MyNFT.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
+
+contract MyNFT is ERC721URIStorage {
+ uint256 private _tokenIdCounter;
+
+ constructor() ERC721("MyNFT", "MNFT") {}
+
+ function mint(address to, string memory tokenURI) public returns (uint256) {
+ uint256 tokenId = _tokenIdCounter++;
+ _mint(to, tokenId);
+ _setTokenURI(tokenId, tokenURI);
+ return tokenId;
+ }
+}
\ No newline at end of file
diff --git a/members/Old000Driver/task3/contracts/MyToken.sol b/members/Old000Driver/task3/contracts/MyToken.sol
new file mode 100644
index 000000000..59e0738ba
--- /dev/null
+++ b/members/Old000Driver/task3/contracts/MyToken.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+contract MyToken is ERC20 {
+ constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
+ _mint(msg.sender, initialSupply);
+ }
+}
\ No newline at end of file
diff --git a/members/Old000Driver/task3/contracts/NFTMarket.sol b/members/Old000Driver/task3/contracts/NFTMarket.sol
new file mode 100644
index 000000000..3244cf18c
--- /dev/null
+++ b/members/Old000Driver/task3/contracts/NFTMarket.sol
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+contract NFTMarket {
+ struct Listing {
+ address seller;
+ uint256 price;
+ address erc20Token;
+ }
+
+ // 添加新的状态变量来跟踪所有上架的NFT
+ struct ListedItem {
+ address nftContract;
+ uint256 tokenId;
+ }
+
+ ListedItem[] public allListedItems;
+ // 用于快速查找ListedItem在数组中的位置
+ mapping(address => mapping(uint256 => uint256)) private listingIndex;
+
+ // 映射:NFT合约地址 => TokenID => 上架信息
+ mapping(address => mapping(uint256 => Listing)) public listings;
+
+ // 事件声明
+ event ItemListed(
+ address indexed seller,
+ address indexed nftContract,
+ uint256 indexed tokenId,
+ uint256 price,
+ address erc20Token
+ );
+
+ event ItemPurchased(
+ address indexed buyer,
+ address indexed nftContract,
+ uint256 indexed tokenId,
+ uint256 price,
+ address erc20Token
+ );
+
+ // 添加下架事件
+ event ItemDelisted(
+ address indexed seller,
+ address indexed nftContract,
+ uint256 indexed tokenId
+ );
+
+ // 上架NFT
+ function listNFT(
+ address nftContract,
+ uint256 tokenId,
+ uint256 price,
+ address erc20Token
+ ) external {
+ IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
+ listings[nftContract][tokenId] = Listing({
+ seller: msg.sender,
+ price: price,
+ erc20Token: erc20Token
+ });
+
+ // 添加到已上架NFT列表
+ listingIndex[nftContract][tokenId] = allListedItems.length;
+ allListedItems.push(ListedItem(nftContract, tokenId));
+
+ emit ItemListed(msg.sender, nftContract, tokenId, price, erc20Token);
+ }
+
+ function _removeListing(address nftContract, uint256 tokenId) private {
+ uint256 index = listingIndex[nftContract][tokenId];
+ uint256 lastIndex = allListedItems.length - 1;
+
+ if (index != lastIndex) {
+ // 将最后一个元素移动到要删除的位置
+ ListedItem memory lastItem = allListedItems[lastIndex];
+ allListedItems[index] = lastItem;
+ listingIndex[lastItem.nftContract][lastItem.tokenId] = index;
+ }
+
+ // 删除最后一个元素
+ allListedItems.pop();
+ delete listingIndex[nftContract][tokenId];
+ }
+
+ // 购买NFT
+ function buyNFT(address nftContract, uint256 tokenId) external {
+ Listing memory listing = listings[nftContract][tokenId];
+ require(listing.price > 0, "Item not listed");
+
+ // 转移ERC20代币
+ IERC20(listing.erc20Token).transferFrom(
+ msg.sender,
+ listing.seller,
+ listing.price
+ );
+
+ // 转移NFT
+ IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
+
+ // 清除上架信息
+ delete listings[nftContract][tokenId];
+
+ _removeListing(nftContract, tokenId);
+
+ emit ItemPurchased(
+ msg.sender,
+ nftContract,
+ tokenId,
+ listing.price,
+ listing.erc20Token
+ );
+ }
+
+ // 下架NFT
+ function delistNFT(address nftContract, uint256 tokenId) external {
+ Listing memory listing = listings[nftContract][tokenId];
+ require(listing.seller == msg.sender, "Not the seller");
+
+ // 将NFT归还给卖家
+ IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
+
+ // 清除上架信息
+ delete listings[nftContract][tokenId];
+
+ _removeListing(nftContract, tokenId);
+
+ emit ItemDelisted(msg.sender, nftContract, tokenId);
+ }
+
+ // 获取NFT上架信息
+ function getListing(
+ address nftContract,
+ uint256 tokenId
+ )
+ external
+ view
+ returns (address seller, uint256 price, address erc20Token)
+ {
+ Listing memory listing = listings[nftContract][tokenId];
+ return (listing.seller, listing.price, listing.erc20Token);
+ }
+
+ // 检查NFT是否已上架
+ function isListed(
+ address nftContract,
+ uint256 tokenId
+ ) external view returns (bool) {
+ return listings[nftContract][tokenId].price > 0;
+ }
+
+ // 获取所有上架的NFT信息
+ function getAllListedNFTs() external view returns (
+ ListedItem[] memory items,
+ address[] memory sellers,
+ uint256[] memory prices,
+ address[] memory erc20Tokens
+ ) {
+ uint256 length = allListedItems.length;
+ items = allListedItems;
+ sellers = new address[](length);
+ prices = new uint256[](length);
+ erc20Tokens = new address[](length);
+
+ for (uint256 i = 0; i < length; i++) {
+ Listing memory listing = listings[items[i].nftContract][items[i].tokenId];
+ sellers[i] = listing.seller;
+ prices[i] = listing.price;
+ erc20Tokens[i] = listing.erc20Token;
+ }
+
+ return (items, sellers, prices, erc20Tokens);
+ }
+
+ // 获取上架NFT的总数
+ function getListedNFTCount() external view returns (uint256) {
+ return allListedItems.length;
+ }
+}
diff --git a/members/Old000Driver/task3/hardhat.config.js b/members/Old000Driver/task3/hardhat.config.js
new file mode 100644
index 000000000..edf758be2
--- /dev/null
+++ b/members/Old000Driver/task3/hardhat.config.js
@@ -0,0 +1,42 @@
+require("@nomicfoundation/hardhat-toolbox");
+require("@nomicfoundation/hardhat-verify");
+require("dotenv").config();
+
+/** @type import('hardhat/config').HardhatUserConfig */
+module.exports = {
+ solidity: {
+ version: "0.8.28",
+ settings: {
+ optimizer: {
+ enabled: true,
+ runs: 200,
+ },
+ },
+ },
+ networks: {
+ hardhat: {
+ chainId: 31337,
+ },
+ sepolia: {
+ url: process.env.SEPOLIA_RPC_URL,
+ accounts: [process.env.PRIVATE_KEY],
+ timeout: 60000, // 增加超时时间到60秒
+ },
+ },
+ etherscan: {
+ apiKey: process.env.ETHERSCAN_API_KEY, // 简化配置
+ customChains: [], // 如果需要自定义链,可以在这里添加
+ },
+ sourcify: {
+ enabled: true, // 启用 Sourcify 验证
+ },
+ paths: {
+ sources: "./contracts",
+ tests: "./test",
+ cache: "./cache",
+ artifacts: "./artifacts",
+ },
+ mocha: {
+ timeout: 60000, // 增加测试超时时间
+ },
+};
diff --git a/members/Old000Driver/task3/ignition/modules/Lock.js b/members/Old000Driver/task3/ignition/modules/Lock.js
new file mode 100644
index 000000000..32c7cd413
--- /dev/null
+++ b/members/Old000Driver/task3/ignition/modules/Lock.js
@@ -0,0 +1,18 @@
+// This setup uses Hardhat Ignition to manage smart contract deployments.
+// Learn more about it at https://hardhat.org/ignition
+
+const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
+
+const JAN_1ST_2030 = 1893456000;
+const ONE_GWEI = 1_000_000_000n;
+
+module.exports = buildModule("LockModule", (m) => {
+ const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
+ const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);
+
+ const lock = m.contract("Lock", [unlockTime], {
+ value: lockedAmount,
+ });
+
+ return { lock };
+});
diff --git a/members/Old000Driver/task3/package.json b/members/Old000Driver/task3/package.json
new file mode 100644
index 000000000..df341dbed
--- /dev/null
+++ b/members/Old000Driver/task3/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "hardhat-project",
+ "devDependencies": {
+ "@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
+ "@nomicfoundation/hardhat-ethers": "^3.0.0",
+ "@nomicfoundation/hardhat-toolbox": "^5.0.0",
+ "@nomicfoundation/hardhat-verify": "^2.0.12",
+ "chai": "^4.3.7",
+ "ethers": "^6.13.5",
+ "hardhat": "^2.22.18"
+ },
+ "dependencies": {
+ "@openzeppelin/contracts": "^5.2.0",
+ "dotenv": "^16.4.7"
+ }
+}
diff --git a/members/Old000Driver/task3/scripts/deploy.js b/members/Old000Driver/task3/scripts/deploy.js
new file mode 100644
index 000000000..40de6bcb7
--- /dev/null
+++ b/members/Old000Driver/task3/scripts/deploy.js
@@ -0,0 +1,26 @@
+const hre = require("hardhat");
+
+async function main() {
+ // 部署 ERC20 代币
+ const MyToken = await hre.ethers.getContractFactory("MyToken");
+ const erc20 = await MyToken.deploy(ethers.parseEther("1000000")); // 初始发行 100 万代币
+ await erc20.waitForDeployment();
+ console.log("ERC20 deployed to:", await erc20.getAddress());
+
+ // 部署 NFT 合约
+ const MyNFT = await hre.ethers.getContractFactory("MyNFT");
+ const nft = await MyNFT.deploy();
+ await nft.waitForDeployment();
+ console.log("NFT deployed to:", await nft.getAddress());
+
+ // 部署市场合约
+ const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");
+ const market = await NFTMarket.deploy();
+ await market.waitForDeployment();
+ console.log("Market deployed to:", await market.getAddress());
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
\ No newline at end of file
diff --git a/members/Old000Driver/task3/scripts/deployMarket.js b/members/Old000Driver/task3/scripts/deployMarket.js
new file mode 100644
index 000000000..1955776d5
--- /dev/null
+++ b/members/Old000Driver/task3/scripts/deployMarket.js
@@ -0,0 +1,48 @@
+const hre = require("hardhat");
+
+async function main() {
+ try {
+ console.log("开始部署 NFTMarket 合约...");
+
+ // 部署市场合约
+ const NFTMarket = await hre.ethers.getContractFactory("NFTMarket");
+ const market = await NFTMarket.deploy();
+ await market.waitForDeployment();
+
+ const marketAddress = await market.getAddress();
+ console.log("Market deployed to:", marketAddress);
+
+ // 等待几个区块确认
+ console.log("等待区块确认...");
+ await market.deploymentTransaction().wait(5);
+
+ // 验证合约
+ if (network.name !== "hardhat" && network.name !== "localhost") {
+ console.log("开始验证合约...");
+ try {
+ await hre.run("verify:verify", {
+ address: marketAddress,
+ constructorArguments: [],
+ contract: "contracts/NFTMarket.sol:NFTMarket"
+ });
+ console.log("合约验证成功!");
+ } catch (error) {
+ if (error.message.includes("Already Verified")) {
+ console.log("合约已经验证过了");
+ } else {
+ console.error("合约验证失败:", error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("部署过程出错:", error);
+ process.exitCode = 1;
+ }
+}
+
+main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/members/Old000Driver/task3/test-env.js b/members/Old000Driver/task3/test-env.js
new file mode 100644
index 000000000..5ccd96d1d
--- /dev/null
+++ b/members/Old000Driver/task3/test-env.js
@@ -0,0 +1,2 @@
+require("dotenv").config();
+console.log("ETHERSCAN_API_KEY:", process.env.ETHERSCAN_API_KEY);
\ No newline at end of file
diff --git a/members/Old000Driver/task3/test/Lock.js b/members/Old000Driver/task3/test/Lock.js
new file mode 100644
index 000000000..f0e6ba1b2
--- /dev/null
+++ b/members/Old000Driver/task3/test/Lock.js
@@ -0,0 +1,126 @@
+const {
+ time,
+ loadFixture,
+} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
+const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
+const { expect } = require("chai");
+
+describe("Lock", function () {
+ // We define a fixture to reuse the same setup in every test.
+ // We use loadFixture to run this setup once, snapshot that state,
+ // and reset Hardhat Network to that snapshot in every test.
+ async function deployOneYearLockFixture() {
+ const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
+ const ONE_GWEI = 1_000_000_000;
+
+ const lockedAmount = ONE_GWEI;
+ const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;
+
+ // Contracts are deployed using the first signer/account by default
+ const [owner, otherAccount] = await ethers.getSigners();
+
+ const Lock = await ethers.getContractFactory("Lock");
+ const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
+
+ return { lock, unlockTime, lockedAmount, owner, otherAccount };
+ }
+
+ describe("Deployment", function () {
+ it("Should set the right unlockTime", async function () {
+ const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
+
+ expect(await lock.unlockTime()).to.equal(unlockTime);
+ });
+
+ it("Should set the right owner", async function () {
+ const { lock, owner } = await loadFixture(deployOneYearLockFixture);
+
+ expect(await lock.owner()).to.equal(owner.address);
+ });
+
+ it("Should receive and store the funds to lock", async function () {
+ const { lock, lockedAmount } = await loadFixture(
+ deployOneYearLockFixture
+ );
+
+ expect(await ethers.provider.getBalance(lock.target)).to.equal(
+ lockedAmount
+ );
+ });
+
+ it("Should fail if the unlockTime is not in the future", async function () {
+ // We don't use the fixture here because we want a different deployment
+ const latestTime = await time.latest();
+ const Lock = await ethers.getContractFactory("Lock");
+ await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
+ "Unlock time should be in the future"
+ );
+ });
+ });
+
+ describe("Withdrawals", function () {
+ describe("Validations", function () {
+ it("Should revert with the right error if called too soon", async function () {
+ const { lock } = await loadFixture(deployOneYearLockFixture);
+
+ await expect(lock.withdraw()).to.be.revertedWith(
+ "You can't withdraw yet"
+ );
+ });
+
+ it("Should revert with the right error if called from another account", async function () {
+ const { lock, unlockTime, otherAccount } = await loadFixture(
+ deployOneYearLockFixture
+ );
+
+ // We can increase the time in Hardhat Network
+ await time.increaseTo(unlockTime);
+
+ // We use lock.connect() to send a transaction from another account
+ await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
+ "You aren't the owner"
+ );
+ });
+
+ it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
+ const { lock, unlockTime } = await loadFixture(
+ deployOneYearLockFixture
+ );
+
+ // Transactions are sent using the first signer by default
+ await time.increaseTo(unlockTime);
+
+ await expect(lock.withdraw()).not.to.be.reverted;
+ });
+ });
+
+ describe("Events", function () {
+ it("Should emit an event on withdrawals", async function () {
+ const { lock, unlockTime, lockedAmount } = await loadFixture(
+ deployOneYearLockFixture
+ );
+
+ await time.increaseTo(unlockTime);
+
+ await expect(lock.withdraw())
+ .to.emit(lock, "Withdrawal")
+ .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg
+ });
+ });
+
+ describe("Transfers", function () {
+ it("Should transfer the funds to the owner", async function () {
+ const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
+ deployOneYearLockFixture
+ );
+
+ await time.increaseTo(unlockTime);
+
+ await expect(lock.withdraw()).to.changeEtherBalances(
+ [owner, lock],
+ [lockedAmount, -lockedAmount]
+ );
+ });
+ });
+ });
+});
diff --git a/members/Old000Driver/task3/test/Market.test.js b/members/Old000Driver/task3/test/Market.test.js
new file mode 100644
index 000000000..7342c127b
--- /dev/null
+++ b/members/Old000Driver/task3/test/Market.test.js
@@ -0,0 +1,237 @@
+const { expect } = require("chai");
+const { ethers } = require("hardhat");
+const { parseEther } = ethers;
+
+describe("NFTMarket", function () {
+ let nftMarket;
+ let myNFT;
+ let myToken;
+ let owner;
+ let buyer;
+ let seller;
+ const TOKEN_ID = 0;
+ const PRICE = parseEther("1.0");
+
+ beforeEach(async function () {
+ [owner, seller, buyer] = await ethers.getSigners();
+
+ // 部署 NFT 合约
+ const MyNFT = await ethers.getContractFactory("MyNFT");
+ myNFT = await MyNFT.deploy();
+ await myNFT.waitForDeployment();
+ const nftAddress = await myNFT.getAddress(); // 获取合约地址
+
+ // 部署 ERC20 合约
+ const MyToken = await ethers.getContractFactory("MyToken");
+ myToken = await MyToken.deploy(parseEther("10000"));
+ await myToken.waitForDeployment();
+ const tokenAddress = await myToken.getAddress(); // 获取合约地址
+
+ // 转移一些代币给买家
+ await myToken.transfer(buyer.address, parseEther("2.0"));
+
+ // 部署市场合约
+ const NFTMarket = await ethers.getContractFactory("NFTMarket");
+ nftMarket = await NFTMarket.deploy();
+ await nftMarket.waitForDeployment();
+ const marketAddress = await nftMarket.getAddress(); // 获取合约地址
+
+ // 铸造 NFT 给 seller
+ await myNFT.mint(seller.address, "ipfs://test");
+
+ // seller 授权市场合约
+ await myNFT.connect(seller).setApprovalForAll(marketAddress, true);
+
+ // buyer 授权 ERC20
+ await myToken.connect(buyer).approve(marketAddress, PRICE);
+ });
+
+ describe("上架商品", function () {
+ it("应该能够正确上架 NFT", async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ await nftMarket.connect(seller).listNFT(
+ nftAddress,
+ TOKEN_ID,
+ PRICE,
+ tokenAddress
+ );
+
+ const listing = await nftMarket.listings(nftAddress, TOKEN_ID);
+ expect(listing.seller).to.equal(seller.address);
+ expect(listing.price).to.equal(PRICE);
+ expect(listing.erc20Token).to.equal(tokenAddress);
+ });
+
+ it("非 NFT 拥有者不能上架", async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ await expect(
+ nftMarket.connect(buyer).listNFT(
+ nftAddress,
+ TOKEN_ID,
+ PRICE,
+ tokenAddress
+ )
+ ).to.be.reverted;
+ });
+ });
+
+ describe("购买商品", function () {
+ beforeEach(async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ await nftMarket.connect(seller).listNFT(
+ nftAddress,
+ TOKEN_ID,
+ PRICE,
+ tokenAddress
+ );
+ });
+
+ it("应该能够正确购买 NFT", async function () {
+ const nftAddress = await myNFT.getAddress();
+ await nftMarket.connect(buyer).buyNFT(nftAddress, TOKEN_ID);
+
+ expect(await myNFT.ownerOf(TOKEN_ID)).to.equal(buyer.address);
+
+ const listing = await nftMarket.listings(nftAddress, TOKEN_ID);
+ expect(listing.seller).to.equal(ethers.ZeroAddress);
+ });
+
+ it("没有足够代币时不能购买", async function () {
+ const nftAddress = await myNFT.getAddress();
+ const poorBuyer = await ethers.provider.getSigner(3);
+ await expect(
+ nftMarket.connect(poorBuyer).buyNFT(nftAddress, TOKEN_ID)
+ ).to.be.reverted;
+ });
+ });
+
+ describe("下架商品", function () {
+ beforeEach(async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ // 先上架一个NFT
+ await nftMarket.connect(seller).listNFT(
+ nftAddress,
+ TOKEN_ID,
+ PRICE,
+ tokenAddress
+ );
+ });
+
+ it("卖家应该能够下架NFT", async function () {
+ const nftAddress = await myNFT.getAddress();
+
+ await expect(nftMarket.connect(seller).delistNFT(nftAddress, TOKEN_ID))
+ .to.emit(nftMarket, "ItemDelisted")
+ .withArgs(seller.address, nftAddress, TOKEN_ID);
+
+ // 验证NFT已返还给卖家
+ expect(await myNFT.ownerOf(TOKEN_ID)).to.equal(seller.address);
+
+ // 验证listing已被删除
+ const listing = await nftMarket.listings(nftAddress, TOKEN_ID);
+ expect(listing.price).to.equal(0);
+ });
+
+ it("非卖家不能下架NFT", async function () {
+ const nftAddress = await myNFT.getAddress();
+ await expect(
+ nftMarket.connect(buyer).delistNFT(nftAddress, TOKEN_ID)
+ ).to.be.revertedWith("Not the seller");
+ });
+ });
+
+ describe("查询功能", function () {
+ const TOKEN_ID_2 = 1;
+ beforeEach(async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ // 铸造第二个NFT
+ await myNFT.mint(seller.address, "ipfs://test2");
+
+ // 上架两个NFT
+ await nftMarket.connect(seller).listNFT(
+ nftAddress,
+ TOKEN_ID,
+ PRICE,
+ tokenAddress
+ );
+ await nftMarket.connect(seller).listNFT(
+ nftAddress,
+ TOKEN_ID_2,
+ parseEther("2.0"),
+ tokenAddress
+ );
+ });
+
+ it("应该能正确获取上架信息", async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ const listing = await nftMarket.getListing(nftAddress, TOKEN_ID);
+ expect(listing.seller).to.equal(seller.address);
+ expect(listing.price).to.equal(PRICE);
+ expect(listing.erc20Token).to.equal(tokenAddress);
+ });
+
+ it("应该正确判断NFT是否上架", async function () {
+ const nftAddress = await myNFT.getAddress();
+
+ expect(await nftMarket.isListed(nftAddress, TOKEN_ID)).to.be.true;
+ expect(await nftMarket.isListed(nftAddress, 999)).to.be.false;
+ });
+
+ it("应该能正确获取所有上架的NFT信息", async function () {
+ const nftAddress = await myNFT.getAddress();
+ const tokenAddress = await myToken.getAddress();
+
+ const [items, sellers, prices, erc20Tokens] = await nftMarket.getAllListedNFTs();
+
+ // 验证返回的数组长度
+ expect(items.length).to.equal(2);
+ expect(sellers.length).to.equal(2);
+ expect(prices.length).to.equal(2);
+ expect(erc20Tokens.length).to.equal(2);
+
+ // 验证第一个NFT的信息
+ expect(items[0].nftContract).to.equal(nftAddress);
+ expect(items[0].tokenId).to.equal(TOKEN_ID);
+ expect(sellers[0]).to.equal(seller.address);
+ expect(prices[0]).to.equal(PRICE);
+ expect(erc20Tokens[0]).to.equal(tokenAddress);
+
+ // 验证第二个NFT的信息
+ expect(items[1].nftContract).to.equal(nftAddress);
+ expect(items[1].tokenId).to.equal(TOKEN_ID_2);
+ expect(sellers[1]).to.equal(seller.address);
+ expect(prices[1]).to.equal(parseEther("2.0"));
+ expect(erc20Tokens[1]).to.equal(tokenAddress);
+ });
+
+ it("应该能正确获取上架NFT的总数", async function () {
+ expect(await nftMarket.getListedNFTCount()).to.equal(2);
+
+ // 下架一个NFT后,总数应该减少
+ const nftAddress = await myNFT.getAddress();
+ await nftMarket.connect(seller).delistNFT(nftAddress, TOKEN_ID);
+ expect(await nftMarket.getListedNFTCount()).to.equal(1);
+ });
+
+ it("购买NFT后应该从列表中移除", async function () {
+ const nftAddress = await myNFT.getAddress();
+ await nftMarket.connect(buyer).buyNFT(nftAddress, TOKEN_ID);
+
+ const [items] = await nftMarket.getAllListedNFTs();
+ expect(items.length).to.equal(1);
+ expect(items[0].tokenId).to.equal(TOKEN_ID_2);
+ });
+ });
+});
diff --git a/members/Old000Driver/task3/test/MyNFT.test.js b/members/Old000Driver/task3/test/MyNFT.test.js
new file mode 100644
index 000000000..108cfef98
--- /dev/null
+++ b/members/Old000Driver/task3/test/MyNFT.test.js
@@ -0,0 +1,49 @@
+const { expect } = require("chai");
+const { ethers } = require("hardhat");
+
+describe("MyNFT", function () {
+ let myNFT;
+ let owner;
+ let addr1;
+ let addr2;
+
+ beforeEach(async function () {
+ [owner, addr1, addr2] = await ethers.getSigners();
+
+ const MyNFT = await ethers.getContractFactory("MyNFT");
+ myNFT = await MyNFT.deploy();
+ await myNFT.waitForDeployment();
+ });
+
+ describe("铸造", function () {
+ it("应该能够铸造 NFT", async function () {
+ await myNFT.mint(addr1.address, "ipfs://test");
+ expect(await myNFT.ownerOf(0)).to.equal(addr1.address);
+ });
+
+ it("应该正确递增 tokenId", async function () {
+ await myNFT.mint(addr1.address, "ipfs://test1");
+ await myNFT.mint(addr2.address, "ipfs://test2");
+
+ expect(await myNFT.ownerOf(0)).to.equal(addr1.address);
+ expect(await myNFT.ownerOf(1)).to.equal(addr2.address);
+ });
+ });
+
+ describe("转账", function () {
+ beforeEach(async function () {
+ await myNFT.mint(addr1.address, "ipfs://test");
+ });
+
+ it("持有者应该能够转移 NFT", async function () {
+ await myNFT.connect(addr1).transferFrom(addr1.address, addr2.address, 0);
+ expect(await myNFT.ownerOf(0)).to.equal(addr2.address);
+ });
+
+ it("非持有者不能转移 NFT", async function () {
+ await expect(
+ myNFT.connect(addr2).transferFrom(addr1.address, addr2.address, 0)
+ ).to.be.reverted;
+ });
+ });
+});
diff --git a/members/Old000Driver/task3/test/MyToken.test.js b/members/Old000Driver/task3/test/MyToken.test.js
new file mode 100644
index 000000000..a92d8d3f6
--- /dev/null
+++ b/members/Old000Driver/task3/test/MyToken.test.js
@@ -0,0 +1,58 @@
+const { expect } = require("chai");
+const { ethers } = require("hardhat");
+const { parseEther } = ethers;
+
+describe("MyToken", function () {
+ let myToken;
+ let owner;
+ let addr1;
+ let addr2;
+ const INITIAL_SUPPLY = parseEther("1000000");
+
+ beforeEach(async function () {
+ [owner, addr1, addr2] = await ethers.getSigners();
+
+ const MyToken = await ethers.getContractFactory("MyToken");
+ myToken = await MyToken.deploy(INITIAL_SUPPLY);
+ await myToken.waitForDeployment();
+ });
+
+ describe("基础功能", function () {
+ it("应该设置正确的初始供应量", async function () {
+ expect(await myToken.totalSupply()).to.equal(INITIAL_SUPPLY);
+ expect(await myToken.balanceOf(owner.address)).to.equal(INITIAL_SUPPLY);
+ });
+
+ it("应该能够转账代币", async function () {
+ const transferAmount = parseEther("100");
+ await myToken.transfer(addr1.address, transferAmount);
+
+ expect(await myToken.balanceOf(addr1.address)).to.equal(transferAmount);
+ expect(await myToken.balanceOf(owner.address)).to.equal(INITIAL_SUPPLY - transferAmount);
+ });
+ });
+
+ describe("授权和委托转账", function () {
+ const approveAmount = parseEther("100");
+
+ it("应该能够授权和使用授权", async function () {
+ await myToken.approve(addr1.address, approveAmount);
+ await myToken.connect(addr1).transferFrom(owner.address, addr2.address, approveAmount);
+
+ expect(await myToken.balanceOf(addr2.address)).to.equal(approveAmount);
+ expect(await myToken.allowance(owner.address, addr1.address)).to.equal(0);
+ });
+
+ it("不能超额使用授权额度", async function () {
+ await myToken.approve(addr1.address, approveAmount);
+
+ await expect(
+ myToken.connect(addr1).transferFrom(
+ owner.address,
+ addr2.address,
+ approveAmount + parseEther("1")
+ )
+ ).to.be.reverted;
+ });
+ });
+});
diff --git a/members/Old000Driver/task4/.gitignore b/members/Old000Driver/task4/.gitignore
new file mode 100644
index 000000000..5ef6a5207
--- /dev/null
+++ b/members/Old000Driver/task4/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/members/Old000Driver/task4/README.md b/members/Old000Driver/task4/README.md
new file mode 100644
index 000000000..e215bc4cc
--- /dev/null
+++ b/members/Old000Driver/task4/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/members/Old000Driver/task4/app/favicon.ico b/members/Old000Driver/task4/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/members/Old000Driver/task4/app/favicon.ico differ
diff --git a/members/Old000Driver/task4/app/globals.css b/members/Old000Driver/task4/app/globals.css
new file mode 100644
index 000000000..ac6844236
--- /dev/null
+++ b/members/Old000Driver/task4/app/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/members/Old000Driver/task4/app/layout.tsx b/members/Old000Driver/task4/app/layout.tsx
new file mode 100644
index 000000000..32a564174
--- /dev/null
+++ b/members/Old000Driver/task4/app/layout.tsx
@@ -0,0 +1,15 @@
+import './globals.css';
+import '@rainbow-me/rainbowkit/styles.css';
+import { Providers } from './providers';
+
+function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default RootLayout;
diff --git a/members/Old000Driver/task4/app/page.tsx b/members/Old000Driver/task4/app/page.tsx
new file mode 100644
index 000000000..2da137074
--- /dev/null
+++ b/members/Old000Driver/task4/app/page.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { useState } from "react";
+import { ListNFT } from "../components/list-nft";
+import { NFTGrid } from "../components/nft-grid";
+import { useAccount } from "wagmi";
+import { useEffect } from "react";
+import { ConnectButton } from "@rainbow-me/rainbowkit";
+import NFTMarketplace from "@/components/nft-marketplace";
+
+export default function Home() {
+ // const [isConnected, setIsConnected] = useState(false)
+
+ const { isConnected, address } = useAccount();
+
+ useEffect(() => {
+ if (isConnected) {
+ // 这里可以执行连接成功后的回调
+ console.log("Connected with address:", address);
+ }
+ }, [isConnected, address]);
+
+ return (
+
+
+
+
NFT Marketplace
+
+
+
+
+ {isConnected && (
+ <>
+ {/*
+
Listed NFTs
+
+ */}
+
+ >
+ )}
+ {!isConnected && (
+
+
+ Welcome to NFT Marketplace
+
+
+ Please connect your wallet to start trading NFTs.
+
+
+ )}
+
+
+ );
+}
diff --git a/members/Old000Driver/task4/app/providers.tsx b/members/Old000Driver/task4/app/providers.tsx
new file mode 100644
index 000000000..0bf437bf6
--- /dev/null
+++ b/members/Old000Driver/task4/app/providers.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { WagmiProvider } from "wagmi";
+import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
+import { useEffect, useState } from "react";
+
+import { config } from "../wagmi";
+
+const queryClient = new QueryClient();
+
+// 确保初始渲染匹配服务器端
+// if (!mounted) {
+// return {children}
;
+// }
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+ return (
+ <>
+ {mounted ? (
+
+
+ {children}
+
+
+ ) : null}
+ >
+ );
+}
diff --git a/members/Old000Driver/task4/components.json b/members/Old000Driver/task4/components.json
new file mode 100644
index 000000000..d9ef0ae53
--- /dev/null
+++ b/members/Old000Driver/task4/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/members/Old000Driver/task4/components/buy-nft.tsx b/members/Old000Driver/task4/components/buy-nft.tsx
new file mode 100644
index 000000000..10a8898cf
--- /dev/null
+++ b/members/Old000Driver/task4/components/buy-nft.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { NFTMarketABI } from "@/constants/abi";
+import { toast } from "sonner";
+import { ListingNFT } from "@/utils/fetchListings";
+
+interface BuyNFTProps {
+ nft: ListingNFT;
+ onBuyNFT: (tokenId: string) => void;
+}
+
+export function BuyNFT({ nft, onBuyNFT }: BuyNFTProps) {
+ const [open, setOpen] = useState(false);
+ const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
+ const { writeContractAsync } = useWriteContract();
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });
+
+ const handleBuy = async () => {
+ if (isConfirming) return;
+
+ try {
+ console.log("提交购买交易...");
+ const result = await writeContractAsync({
+ address: process.env.NEXT_PUBLIC_MARKET_CONTRACT_ADDRESS as `0x${string}`,
+ abi: NFTMarketABI,
+ functionName: "buyNFT",
+ args: [nft.nftContract, BigInt(nft.tokenId)],
+ });
+
+ console.log("writeContractAsync 返回值:", result);
+ if (typeof result === "string") {
+ setTxHash(result as `0x${string}`);
+ } else if (result.hash) {
+ setTxHash(result.hash);
+ } else {
+ throw new Error("无法获取交易哈希");
+ }
+
+ toast.success("交易已提交");
+ } catch (error) {
+ console.error("购买失败:", error);
+ toast.error(error instanceof Error ? error.message : "未知错误");
+ }
+ };
+
+ // 监听交易确认
+ useEffect(() => {
+ if (isSuccess && txHash) {
+ toast.success("购买成功");
+ onBuyNFT(nft.tokenId);
+ setTxHash(undefined);
+ setOpen(false); // 关闭对话框
+ }
+ }, [isSuccess, txHash, onBuyNFT, nft.tokenId]);
+
+ return (
+
+ );
+}
diff --git a/members/Old000Driver/task4/components/delist-nft.tsx b/members/Old000Driver/task4/components/delist-nft.tsx
new file mode 100644
index 000000000..671410576
--- /dev/null
+++ b/members/Old000Driver/task4/components/delist-nft.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { NFTMarketABI } from "@/constants/abi";
+import { toast } from "sonner";
+import { ListingNFT } from "@/utils/fetchListings";
+
+interface DelistNFTProps {
+ nft: ListingNFT;
+ onDelist: (tokenId: string) => void;
+}
+
+export function DelistNFT({ nft, onDelist }: DelistNFTProps) {
+ const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
+ const { writeContractAsync } = useWriteContract();
+ const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });
+
+ const handleDelist = async () => {
+ if (isConfirming) return;
+
+ try {
+ console.log("提交取消上架交易...");
+ const result = await writeContractAsync({
+ address: process.env.NEXT_PUBLIC_MARKET_CONTRACT_ADDRESS as `0x${string}`,
+ abi: NFTMarketABI,
+ functionName: "delistNFT",
+ args: [nft.nftContract, BigInt(nft.tokenId)],
+ });
+
+ console.log("writeContractAsync 返回值:", result);
+ if (typeof result === "string") {
+ setTxHash(result as `0x${string}`);
+ } else if (result.hash) {
+ setTxHash(result.hash);
+ } else {
+ throw new Error("无法获取交易哈希");
+ }
+
+ toast.success("交易已提交");
+ } catch (error) {
+ console.error("取消上架失败:", error);
+ toast.error(error instanceof Error ? error.message : "未知错误");
+ }
+ };
+
+ // 监听交易确认
+ useEffect(() => {
+ if (isSuccess && txHash) {
+ toast.success("取消上架成功");
+ onDelist(nft.tokenId);
+ setTxHash(undefined);
+ }
+ }, [isSuccess, txHash, onDelist, nft.tokenId]);
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/members/Old000Driver/task4/components/header.tsx b/members/Old000Driver/task4/components/header.tsx
new file mode 100644
index 000000000..e7bd66a99
--- /dev/null
+++ b/members/Old000Driver/task4/components/header.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import { useState } from "react"
+import { Input } from "@/components/ui/input"
+import { Button } from "@/components/ui/button"
+import { Search } from "lucide-react"
+
+export function Header() {
+ const [searchQuery, setSearchQuery] = useState("")
+
+ const handleSearch = () => {
+ // In a real application, you would implement the search functionality here
+ console.log("Searching for:", searchQuery)
+ }
+
+ return (
+
+ )
+}
+
diff --git a/members/Old000Driver/task4/components/list-nft.tsx b/members/Old000Driver/task4/components/list-nft.tsx
new file mode 100644
index 000000000..21fdca4a7
--- /dev/null
+++ b/members/Old000Driver/task4/components/list-nft.tsx
@@ -0,0 +1,134 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
+import { NFTMarketABI } from "@/constants/abi";
+import { parseEther } from "viem";
+import { toast } from "sonner";
+
+// MTK 代币地址
+const MTK_TOKEN_ADDRESS = process.env.NEXT_PUBLIC_MARKET_ERC20_ADDRESS;
+const NFT_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_NFT_CONTRACT_ADDRESS;
+
+interface ListNFTProps {
+ onSuccess: () => Promise;
+}
+
+export function ListNFT({ onSuccess }: ListNFTProps) {
+ const [open, setOpen] = useState(false);
+ const [tokenId, setTokenId] = useState("");
+ const [price, setPrice] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { data: hash, writeContract, isPending } = useWriteContract();
+
+ // 监听交易状态
+ const { isLoading: isConfirming, isSuccess: isConfirmed } =
+ useWaitForTransactionReceipt({
+ hash,
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+
+ try {
+ // 将价格转换为wei (18位精度)
+ const priceInWei = parseEther(price);
+
+ // 调用合约的 listNFT 函数
+ console.log(
+ "🔔",
+ NFT_CONTRACT_ADDRESS,
+ tokenId,
+ priceInWei,
+ MTK_TOKEN_ADDRESS
+ );
+ await writeContract({
+ address: process.env
+ .NEXT_PUBLIC_MARKET_CONTRACT_ADDRESS as `0x${string}`,
+ abi: NFTMarketABI,
+ functionName: "listNFT",
+ args: [
+ NFT_CONTRACT_ADDRESS,
+ tokenId,
+ priceInWei, // 使用转换后的价格
+ MTK_TOKEN_ADDRESS, // 使用 MTK 代币地址
+ ],
+ });
+
+ toast.success("NFT listing submitted");
+ // 重置表单
+ setOpen(false);
+ setTokenId("");
+ setPrice("");
+ } catch (error) {
+ console.error("Error listing NFT:", error);
+ toast.error("Failed to list NFT. Please check the price format.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 交易确认后的处理
+ useEffect(() => {
+ if (isConfirmed) {
+ toast.success("NFT listed successfully!");
+ onSuccess(); // 调用刷新函数
+ }
+ }, [isConfirmed, onSuccess]);
+
+ return (
+
+ );
+}
diff --git a/members/Old000Driver/task4/components/nft-card.tsx b/members/Old000Driver/task4/components/nft-card.tsx
new file mode 100644
index 000000000..4652fa181
--- /dev/null
+++ b/members/Old000Driver/task4/components/nft-card.tsx
@@ -0,0 +1,59 @@
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import Image from "next/image";
+import { BuyNFT } from "./buy-nft";
+import { ListingNFT } from "@/utils/fetchListings";
+import { DelistNFT } from "./delist-nft";
+import { useAccount } from "wagmi";
+
+interface NFTCardProps {
+ nft: ListingNFT;
+ onBuyNFT: (id: string) => void;
+ onDelist: (id: string) => void;
+}
+
+export function NFTCard({ nft, onBuyNFT, onDelist }: NFTCardProps) {
+ const { address } = useAccount();
+ const isOwner = address?.toLowerCase() === nft.seller.toLowerCase();
+
+ return (
+
+
+ {nft.name}
+
+
+
+
+
+ {nft.name}
+ Price: {nft.price} MTK
+
+ Contract: {nft.nftContract.slice(0, 6)}...{nft.nftContract.slice(-4)}
+
+ Token ID: {nft.tokenId}
+
+ Seller: {nft.seller.slice(0, 6)}...{nft.seller.slice(-4)}
+
+
+
+ {isOwner ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/members/Old000Driver/task4/components/nft-grid.tsx b/members/Old000Driver/task4/components/nft-grid.tsx
new file mode 100644
index 000000000..961f06f4b
--- /dev/null
+++ b/members/Old000Driver/task4/components/nft-grid.tsx
@@ -0,0 +1,73 @@
+"use client"
+
+import { useState } from "react"
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+
+interface NFT {
+ id: string
+ name: string
+ contractAddress: string
+ tokenId: string
+ price: string
+ seller: string
+}
+
+// This is mock data. In a real application, you would fetch this data from your backend or blockchain
+const mockNFTs: NFT[] = [
+ { id: "1", name: "Cool Cat #1", contractAddress: "0x123...abc", tokenId: "1", price: "100", seller: "0xabc...123" },
+ {
+ id: "2",
+ name: "Bored Ape #42",
+ contractAddress: "0x456...def",
+ tokenId: "42",
+ price: "200",
+ seller: "0xdef...456",
+ },
+ {
+ id: "3",
+ name: "Crypto Punk #888",
+ contractAddress: "0x789...ghi",
+ tokenId: "888",
+ price: "150",
+ seller: "0xghi...789",
+ },
+ { id: "4", name: "Doodle #007", contractAddress: "0xabc...123", tokenId: "7", price: "80", seller: "0x123...abc" },
+ { id: "5", name: "Azuki #555", contractAddress: "0xdef...456", tokenId: "555", price: "120", seller: "0x456...def" },
+ {
+ id: "6",
+ name: "World of Women #303",
+ contractAddress: "0xghi...789",
+ tokenId: "303",
+ price: "90",
+ seller: "0x789...ghi",
+ },
+]
+
+export function NFTGrid() {
+ const [nfts] = useState(mockNFTs)
+
+ return (
+
+ {nfts.map((nft) => (
+
+
+ {nft.name}
+
+
+ Contract: {nft.contractAddress}
+ Token ID: {nft.tokenId}
+ Price: {nft.price} ERC20
+ Seller: {nft.seller}
+
+
+
+
+
+ ))}
+
+ )
+}
+
diff --git a/members/Old000Driver/task4/components/nft-marketplace.tsx b/members/Old000Driver/task4/components/nft-marketplace.tsx
new file mode 100644
index 000000000..810a923be
--- /dev/null
+++ b/members/Old000Driver/task4/components/nft-marketplace.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { NFTCard } from "./nft-card";
+import { ListNFT } from "./list-nft";
+import { fetchAllListings, type ListingNFT } from "@/utils/fetchListings";
+
+export default function NFTMarketplace() {
+ const [nftList, setNftList] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const refreshNFTs = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const listings = await fetchAllListings();
+ setNftList(listings);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "获取NFT列表失败");
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refreshNFTs();
+ }, [refreshNFTs]);
+
+ const handleBuyNFT = useCallback(
+ async (tokenId: string) => {
+ console.log(`Buying NFT with tokenId: ${tokenId}`);
+ await refreshNFTs(); // 购买后刷新列表
+ },
+ [refreshNFTs]
+ );
+
+ const handleDelist = useCallback(async (tokenId: string) => {
+ console.log("Starting refresh after delist for tokenId:", tokenId);
+ try {
+ await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒确保交易完全确认
+ await refreshNFTs();
+ console.log("Refresh completed after delist");
+ } catch (error) {
+ console.error("Error refreshing after delist:", error);
+ toast.error("Failed to refresh NFT list");
+ }
+ }, [refreshNFTs]);
+
+ return (
+
+
+ {isLoading && 加载中...
}
+ {error && {error}
}
+
+ {nftList.map((nft: ListingNFT) => (
+
+ ))}
+
+ {nftList.length === 0 && (
+
+ No NFTs found. Try a different search or list a new NFT.
+
+ )}
+
+ );
+}
diff --git a/members/Old000Driver/task4/components/theme-provider.tsx b/members/Old000Driver/task4/components/theme-provider.tsx
new file mode 100644
index 000000000..55c2f6eb6
--- /dev/null
+++ b/members/Old000Driver/task4/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/members/Old000Driver/task4/components/ui/accordion.tsx b/members/Old000Driver/task4/components/ui/accordion.tsx
new file mode 100644
index 000000000..24c788c2c
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/members/Old000Driver/task4/components/ui/alert-dialog.tsx b/members/Old000Driver/task4/components/ui/alert-dialog.tsx
new file mode 100644
index 000000000..25e7b4744
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/members/Old000Driver/task4/components/ui/alert.tsx b/members/Old000Driver/task4/components/ui/alert.tsx
new file mode 100644
index 000000000..41fa7e056
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/members/Old000Driver/task4/components/ui/aspect-ratio.tsx b/members/Old000Driver/task4/components/ui/aspect-ratio.tsx
new file mode 100644
index 000000000..d6a5226f5
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/members/Old000Driver/task4/components/ui/avatar.tsx b/members/Old000Driver/task4/components/ui/avatar.tsx
new file mode 100644
index 000000000..51e507ba9
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/members/Old000Driver/task4/components/ui/badge.tsx b/members/Old000Driver/task4/components/ui/badge.tsx
new file mode 100644
index 000000000..f000e3ef5
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/members/Old000Driver/task4/components/ui/breadcrumb.tsx b/members/Old000Driver/task4/components/ui/breadcrumb.tsx
new file mode 100644
index 000000000..60e6c96f7
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/members/Old000Driver/task4/components/ui/button.tsx b/members/Old000Driver/task4/components/ui/button.tsx
new file mode 100644
index 000000000..36496a287
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/members/Old000Driver/task4/components/ui/calendar.tsx b/members/Old000Driver/task4/components/ui/calendar.tsx
new file mode 100644
index 000000000..61d2b451e
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/members/Old000Driver/task4/components/ui/card.tsx b/members/Old000Driver/task4/components/ui/card.tsx
new file mode 100644
index 000000000..f62edea57
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/members/Old000Driver/task4/components/ui/carousel.tsx b/members/Old000Driver/task4/components/ui/carousel.tsx
new file mode 100644
index 000000000..ec505d00d
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/members/Old000Driver/task4/components/ui/chart.tsx b/members/Old000Driver/task4/components/ui/chart.tsx
new file mode 100644
index 000000000..8620baa3b
--- /dev/null
+++ b/members/Old000Driver/task4/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+