diff --git a/app/rewards/page.tsx b/app/rewards/page.tsx index 92a5952..d896510 100644 --- a/app/rewards/page.tsx +++ b/app/rewards/page.tsx @@ -5,9 +5,10 @@ import { useAppKitAccount } from "@reown/appkit/react"; import { useAppKit } from "@reown/appkit/react"; import { useRewardToken } from "@/lib/hooks/useRewardToken"; import { useERC20Token } from "@/lib/hooks/useERC20Token"; +import { useProjectNFT } from "@/lib/hooks/useProjectNFT"; import { formatDistanceToNow } from "date-fns"; import { zhCN } from "date-fns/locale"; -import { Coins, History, Wallet, ChevronRight, Gift, Clock, ExternalLink, Plus, Info } from "lucide-react"; +import { Coins, History, Wallet, ChevronRight, Gift, Clock, ExternalLink, Plus, Info, Award, CheckCircle, XCircle, Medal } from "lucide-react"; import { sepolia } from "wagmi/chains"; export default function RewardsPage() { @@ -17,21 +18,31 @@ export default function RewardsPage() { availableRewards, lastClaimTime, claimRewards, - isLoading, - isSuccess, - errorMessage, + isLoading: isRewardLoading, + isSuccess: isRewardSuccess, + errorMessage: rewardErrorMessage, isSepoliaNetwork } = useRewardToken(); const { balance, } = useERC20Token(); + const { + projectsStatus, + mintProjectNFT, + isLoading: isNFTLoading, + isMintSuccess, + errorMessage: nftErrorMessage, + } = useProjectNFT(); const [showSuccessMessage, setShowSuccessMessage] = useState(false); const [showNetworkInfo, setShowNetworkInfo] = useState(false); const [switchError, setSwitchError] = useState(null); + const [showNFTSuccess, setShowNFTSuccess] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + const [activeTab, setActiveTab] = useState<'rewards' | 'nfts'>('rewards'); // 处理成功领取后的提示 useEffect(() => { - if (isSuccess) { + if (isRewardSuccess) { setShowSuccessMessage(true); const timer = setTimeout(() => { setShowSuccessMessage(false); @@ -39,7 +50,30 @@ export default function RewardsPage() { return () => clearTimeout(timer); } - }, [isSuccess]); + }, [isRewardSuccess]); + + // 处理NFT铸造成功提示 + useEffect(() => { + if (isMintSuccess) { + setShowNFTSuccess(true); + const timer = setTimeout(() => { + setShowNFTSuccess(false); + }, 3000); + + return () => clearTimeout(timer); + } + }, [isMintSuccess]); + + // 处理错误信息 + useEffect(() => { + if (rewardErrorMessage) { + setErrorMsg(rewardErrorMessage); + } else if (nftErrorMessage) { + setErrorMsg(nftErrorMessage); + } else { + setErrorMsg(null); + } + }, [rewardErrorMessage, nftErrorMessage]); // 格式化上次领取时间 const formattedLastClaimTime = lastClaimTime ? @@ -94,7 +128,7 @@ export default function RewardsPage() {

连接您的钱包

- 请先连接您的钱包以查看和领取奖励代币 + 请先连接您的钱包以查看和领取奖励代币以及项目成就NFT

+ + + ))} + + )} + +
+

关于成就NFT

+

+ 成就NFT是对您完成社区学习项目的永久记录和认证。每个NFT代表您在特定项目中的参与和成就, + 并可作为您学习历程的证明。项目得分达到通过标准即可领取对应的专属NFT。 +

+
+ + ); + + // 渲染内容区域标签 + const renderTabs = () => ( +
+
+ + + +
+
+ ); // 渲染合适的内容 const renderContent = () => { @@ -243,7 +423,12 @@ export default function RewardsPage() { } else if (!isSepoliaNetwork) { return renderNetworkSwitchPrompt(); } else { - return renderRewardsCard(); + return ( + <> + {renderTabs()} + {activeTab === 'rewards' ? renderRewardsCard() : renderNFTCards()} + + ); } }; @@ -256,7 +441,7 @@ export default function RewardsPage() { CFC 奖励中心

- 参与社区活动和贡献代码,获取 CFC 代币奖励。这些代币可用于社区治理和解锁特殊功能。 + 参与社区活动和贡献代码,获取 CFC 代币奖励和专属成就NFT。这些奖励证明了您的技能和贡献。

@@ -264,13 +449,13 @@ export default function RewardsPage() { {renderContent()} {/* 错误信息 */} - {errorMessage && ( + {errorMsg && (
- {errorMessage} + {errorMsg}
)} - {/* 成功信息 */} + {/* 成功信息 - 代币 */} {showSuccessMessage && (
@@ -282,6 +467,18 @@ export default function RewardsPage() {
)} + {/* 成功信息 - NFT */} + {showNFTSuccess && ( +
+
+ + + +
+ 您已成功铸造项目成就NFT! +
+ )} + {/* 奖励说明 */}

如何获取更多奖励

@@ -291,8 +488,8 @@ export default function RewardsPage() { 1
-

参与 Hackathon 活动

-

根据项目完成情况和评分,获得相应的代币奖励。

+

参与共学项目

+

完成社区共学项目任务,获得代币奖励和专属项目成就NFT。

  • diff --git a/contracts/CFCNFT.sol b/contracts/CFCNFT.sol new file mode 100644 index 0000000..ba727a1 --- /dev/null +++ b/contracts/CFCNFT.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract CoLearnNFT is ERC721URIStorage, Ownable { + uint256 private _nextTokenId; + + constructor() ERC721("CoLearnNFT", "CLNFT") Ownable(msg.sender) { + _nextTokenId = 1; // tokenId 从1开始 + } + + /// @notice 仅限管理员调用,铸造 NFT 并将 IPFS URI 上链 + /// @param to NFT 接收者地址 + /// @param tokenURI_ 存储在 IPFS 上的 metadata URI + /// @return tokenId 新生成的 NFT tokenId + function mintNFT(address to, string calldata tokenURI_) external returns (uint256) { + uint256 tokenId = _nextTokenId; + _nextTokenId++; + _mint(to, tokenId); + _setTokenURI(tokenId, tokenURI_); + return tokenId; + } + + /// @notice 批量铸造 NFT + /// @param recipients 接收者地址数组 + /// @param tokenURIs 存储在 IPFS 上的 metadata URI 数组 + /// @return tokenIds 新生成的 NFT tokenId 数组 + function batchMintNFT(address[] calldata recipients, string[] calldata tokenURIs) external returns (uint256[] memory) { + require(recipients.length == tokenURIs.length, "Mismatched input lengths"); + uint256[] memory tokenIds = new uint256[](recipients.length); + for (uint256 i = 0; i < recipients.length; i++) { + uint256 tokenId = _nextTokenId; + _nextTokenId++; + _mint(recipients[i], tokenId); + _setTokenURI(tokenId, tokenURIs[i]); + tokenIds[i] = tokenId; + } + return tokenIds; + } +} \ No newline at end of file diff --git a/contracts/CFCToken.sol b/contracts/CFCToken.sol new file mode 100644 index 0000000..61347f6 --- /dev/null +++ b/contracts/CFCToken.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract CFCToken is ERC20, Ownable { + constructor() ERC20("CrazyForCode Token", "CFC") Ownable(msg.sender) { } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyOwner { + _burn(from, amount); + } +} diff --git a/contracts/CFCoLearning.sol b/contracts/CFCoLearning.sol new file mode 100644 index 0000000..1211bb0 --- /dev/null +++ b/contracts/CFCoLearning.sol @@ -0,0 +1,761 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface ICoLearnNFT { + /// @notice 铸造单个 NFT(仅管理员) + /// @param to 接收者地址 + /// @param tokenURI_ NFT 的 IPFS metadata URI + /// @return tokenId 新铸造的 NFT 的 ID + function mintNFT(address to, string calldata tokenURI_) + external + returns (uint256 tokenId); + + /// @notice 批量铸造多个 NFT(仅管理员) + /// @param recipients 接收者地址数组 + /// @param tokenURIs IPFS metadata URI 数组 + /// @return tokenIds 新铸造的 NFT 的 ID 数组 + function batchMintNFT( + address[] calldata recipients, + string[] calldata tokenURIs + ) external returns (uint256[] memory tokenIds); + + /// @notice 返回当前所有者(来自 Ownable) + /// @return owner 合约当前拥有者地址 + function owner() external view returns (address); + + /// @notice 转移合约拥有权(来自 Ownable) + function transferOwnership(address newOwner) external; + + /// @notice 放弃合约拥有权(来自 Ownable) + function renounceOwnership() external; + + /// @notice 查看某个 token 的元数据 URI(来自 ERC721URIStorage) + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice NFT 所有者 + function ownerOf(uint256 tokenId) external view returns (address); + + /// @notice NFT 标准的名称和符号(来自 ERC721) + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function approve(address to, uint256 tokenId) external; + + function getApproved(uint256 tokenId) + external + view + returns (address operator); + + function setApprovalForAll(address operator, bool _approved) external; + + function isApprovedForAll(address owner, address operator) + external + view + returns (bool); +} + +/** + * @title ProjectNFTManager + * @dev 整合项目管理、奖励发放和NFT认证的合约 + * 管理用户完成项目的状态,分发CFC代币奖励,并铸造成就NFT + */ +contract ProjectNFTManager is Ownable { + using SafeERC20 for IERC20; + + // CFC代币合约 + IERC20 public cfcToken; + + // NFT合约接口 + ICoLearnNFT public nftContract; + + // 用户上次领取奖励的时间 + mapping(address => uint256) private _lastClaimTime; + + // 用户可获得的奖励数量 + mapping(address => uint256) private _availableRewards; + + // 用户可领取的NFT的tokenId + mapping(address => mapping(string => uint256)) private _availableNFTs; + + // 奖励锁定期 + uint256 public constant CLAIM_COOLDOWN = 1 minutes; + + // 项目状态枚举 + enum ProjectStatus { + ACTIVE, + COMPLETED, + ARCHIVED + } + + // 项目信息结构 + struct Project { + string id; // 项目标识符 + string name; // 项目名称 + string description; // 项目描述 + uint256 startTime; // 开始时间 + uint256 endTime; // 结束时间 + uint256 rewardAmount; // 完成项目奖励的代币数量 + ProjectStatus status; // 项目状态 + uint256 requiredTaskCount; // 需要完成的任务数量 + uint256 passingScore; // 通过所需的最低分数 (百分比,例如:70表示70%) + } + + // 用户完成项目的记录 + struct UserProjectStatus { + bool registered; // 是否注册参与 + uint256 completedTasks; // 已完成的任务数 + uint256 score; // 项目得分 + bool hasClaimedReward; // 是否已领取代币奖励 + bool hasClaimedNFT; // 是否已铸造NFT + } + + // 所有项目 + Project[] public projects; + + // 项目ID到项目索引的映射 + mapping(string => uint256) private projectIdToIndex; + + // 用户地址 => 项目ID => 用户项目状态 + mapping(address => mapping(string => UserProjectStatus)) + public userProjects; + + // 事件定义 + // 项目相关事件 + event ProjectCreated( + string indexed projectId, + string name, + uint256 startTime, + uint256 endTime + ); + event ProjectUpdated(string indexed projectId, ProjectStatus status); + event UserRegistered(address indexed user, string indexed projectId); + event TaskCompleted( + address indexed user, + string indexed projectId, + uint256 taskCount + ); + event ProjectScoreUpdated( + address indexed user, + string indexed projectId, + uint256 score + ); + + // 奖励相关事件 + event RewardAdded(address indexed user, uint256 amount); + event RewardClaimed(address indexed user, uint256 amount); + event ProjectRewardClaimed( + address indexed user, + string indexed projectId, + uint256 amount + ); + + // NFT相关事件 + event NFTClaimed( + address indexed user, + string indexed projectId, + uint256 tokenId + ); + event TokenContractUpdated(address indexed newTokenContract); + event NFTContractUpdated(address indexed newNFTContract); + + /** + * @dev 构造函数 + * @param tokenAddress CFC代币合约地址 + * @param nftAddress 成就NFT合约地址 + */ + constructor(address tokenAddress, address nftAddress) Ownable(msg.sender) { + require(tokenAddress != address(0), "Token address cannot be zero"); + cfcToken = IERC20(tokenAddress); + + if (nftAddress != address(0)) { + nftContract = ICoLearnNFT(nftAddress); + } + } + + /** + * @dev 更新代币合约地址 + * @param newTokenAddress 新的代币合约地址 + */ + function updateTokenContract(address newTokenAddress) external onlyOwner { + require(newTokenAddress != address(0), "Token address cannot be zero"); + cfcToken = IERC20(newTokenAddress); + emit TokenContractUpdated(newTokenAddress); + } + + /** + * @dev 更新NFT合约地址 + * @param newNFTAddress 新的NFT合约地址 + */ + function updateNFTContract(address newNFTAddress) external onlyOwner { + require(newNFTAddress != address(0), "NFT address cannot be zero"); + nftContract = ICoLearnNFT(newNFTAddress); + emit NFTContractUpdated(newNFTAddress); + } + + /** + * @dev 创建新项目 + * @param _id 项目ID + * @param _name 项目名称 + * @param _description 项目描述 + * @param _startTime 开始时间 + * @param _endTime 结束时间 + * @param _rewardAmount 奖励代币数量 + * @param _requiredTaskCount 所需完成的任务数量 + * @param _passingScore 及格分数(百分比) + */ + function createProject( + string calldata _id, + string calldata _name, + string calldata _description, + uint256 _startTime, + uint256 _endTime, + uint256 _rewardAmount, + uint256 _requiredTaskCount, + uint256 _passingScore + ) external onlyOwner { + require(bytes(_id).length > 0, "Item ID cannot be empty"); + require( + _startTime < _endTime, + "Start time must be earlier than end time" + ); + require(_passingScore <= 100, "The passing score cannot exceed 100"); + require(projectIdToIndex[_id] == 0, "Project ID already exists"); + + Project memory newProject = Project({ + id: _id, + name: _name, + description: _description, + startTime: _startTime, + endTime: _endTime, + rewardAmount: _rewardAmount, + status: ProjectStatus.ACTIVE, + requiredTaskCount: _requiredTaskCount, + passingScore: _passingScore + }); + + projects.push(newProject); + // 索引从1开始,避免与默认值0冲突 + projectIdToIndex[_id] = projects.length; + + emit ProjectCreated(_id, _name, _startTime, _endTime); + } + + /** + * @dev 更新项目状态 + * @param _projectId 项目ID + * @param _status 新状态 + */ + function updateProjectStatus( + string calldata _projectId, + ProjectStatus _status + ) external onlyOwner { + uint256 projectIndex = getProjectIndex(_projectId); + projects[projectIndex].status = _status; + + emit ProjectUpdated(_projectId, _status); + } + + /** + * @dev 用户注册参与项目 + * @param user 用户地址 + * @param _projectId 项目ID + */ + function registerForProject(address user, string calldata _projectId) + external + { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require( + project.status == ProjectStatus.ACTIVE, + "Project is not active" + ); + require( + block.timestamp >= project.startTime, + "Project not started yet" + ); + require(block.timestamp <= project.endTime, "project is closed"); + require( + !userProjects[user][_projectId].registered, + "This item is already registered" + ); + + userProjects[user][_projectId] = UserProjectStatus({ + registered: true, + completedTasks: 0, + score: 0, + hasClaimedReward: false, + hasClaimedNFT: false + }); + + emit UserRegistered(user, _projectId); + } + + /** + * @dev 更新用户完成的任务数量(由管理员调用) + * @param _user 用户地址 + * @param _projectId 项目ID + * @param _taskCount 已完成任务数 + */ + function updateCompletedTasks( + address _user, + string calldata _projectId, + uint256 _taskCount + ) external onlyOwner { + require( + userProjects[_user][_projectId].registered, + "User not registered for this item" + ); + + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require( + _taskCount <= project.requiredTaskCount, + "The number of completed tasks cannot exceed the requirements" + ); + + userProjects[_user][_projectId].completedTasks = _taskCount; + + // 更新得分 + uint256 score = (_taskCount * 100) / project.requiredTaskCount; + userProjects[_user][_projectId].score = score; + + emit TaskCompleted(_user, _projectId, _taskCount); + emit ProjectScoreUpdated(_user, _projectId, score); + } + + /** + * @dev 手动设置用户项目得分(由管理员调用) + * @param _user 用户地址 + * @param _projectId 项目ID + * @param _score 得分(0-100) + */ + function setProjectScore( + address _user, + string calldata _projectId, + uint256 _score + ) external onlyOwner { + require( + userProjects[_user][_projectId].registered, + "User not registered for this item" + ); + require(_score <= 100, "Scores cannot exceed 100"); + + userProjects[_user][_projectId].score = _score; + + emit ProjectScoreUpdated(_user, _projectId, _score); + } + + /** + * @dev 为指定用户添加奖励(不关联特定项目) + * @param user 用户地址 + * @param amount 奖励数量 + */ + function addReward(address user, uint256 amount) external onlyOwner { + require(user != address(0), "invalid address"); + require(amount > 0, "Quantity must be greater than 0"); + + _availableRewards[user] += amount; + + emit RewardAdded(user, amount); + } + + /** + * @dev 为指定用户添加NFT(不关联特定项目) + * @param _user 用户地址 + */ + function addNFT( + address _user, + string calldata _projectId, + uint256 _tokenURI + ) external onlyOwner { + require(_user != address(0), "invalid address"); + + _availableNFTs[_user][_projectId] = _tokenURI; + } + + /** + * @dev 批量为用户添加奖励 + * @param users 用户地址数组 + * @param amounts 奖励数量数组 + */ + function batchAddRewards( + address[] calldata users, + uint256[] calldata amounts + ) external onlyOwner { + require(users.length == amounts.length, "Array length mismatch"); + require(users.length > 0, "Array cannot be empty"); + + for (uint256 i = 0; i < users.length; i++) { + if (users[i] != address(0) && amounts[i] > 0) { + _availableRewards[users[i]] += amounts[i]; + emit RewardAdded(users[i], amounts[i]); + } + } + } + + /** + * @dev 验证用户是否有资格铸造NFT + * @param _user 用户地址 + * @param _projectId 项目ID + * @return 是否有资格 + */ + function canMintNFT(address _user, string calldata _projectId) + public + view + returns (bool) + { + if (!userProjects[_user][_projectId].registered) { + return false; + } + + if (userProjects[_user][_projectId].hasClaimedNFT) { + return false; + } + + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + // 检查用户是否已经达到通过标准 + return userProjects[_user][_projectId].score >= project.passingScore; + } + + // 为用户mint NFT + function mintNFT( + address _user, + string calldata _projectId, + string calldata _tokenURI + ) external onlyOwner returns (uint256) { + uint256 tokenId = nftContract.mintNFT(address(this), _tokenURI); + _availableNFTs[_user][_projectId] = tokenId; + + return tokenId; + } + + /** + * @dev 用户领取项目完成奖励 + * @param _projectId 项目ID + * @return 是否成功 + */ + function claimProjectReward(string calldata _projectId) + external + returns (bool) + { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require( + userProjects[msg.sender][_projectId].registered, + "This item is not registered" + ); + require( + userProjects[msg.sender][_projectId].score >= project.passingScore, + "Failure to achieve a passing grade" + ); + require( + !userProjects[msg.sender][_projectId].hasClaimedReward, + "Reward received" + ); + + userProjects[msg.sender][_projectId].hasClaimedReward = true; + + // 发放代币奖励 + bool success = _transferReward(msg.sender, project.rewardAmount); + require(success, "Token transfer failed"); + + emit ProjectRewardClaimed(msg.sender, _projectId, project.rewardAmount); + return true; + } + + /** + * @dev 用户铸造项目完成NFT + * @param _projectId 项目id + * @return 是否成功 + */ + function mintProjectNFT(string calldata _projectId) + external + returns (bool) + { + require(address(nftContract) != address(0), "NFT contract not set"); + + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require( + userProjects[msg.sender][_projectId].registered, + "This item is not registered" + ); + require( + userProjects[msg.sender][_projectId].score >= project.passingScore, + "Failure to achieve a passing grade" + ); + require( + !userProjects[msg.sender][_projectId].hasClaimedNFT, + "Cast NFT" + ); + + // 标记为已铸造NFT + userProjects[msg.sender][_projectId].hasClaimedNFT = true; + uint256 _tokenId = _availableNFTs[msg.sender][_projectId]; + require(_tokenId != 0, "Can not mint NFT"); + nftContract.safeTransferFrom(address(this), msg.sender, _tokenId); + + return true; + } + + /** + * @dev 用户领取非项目相关的通用奖励 + * @return 是否成功领取 + */ + function claimRewards() external returns (bool) { + uint256 amount = _availableRewards[msg.sender]; + require(amount > 0, "There are no rewards available."); + + // 检查冷却期 + if (_lastClaimTime[msg.sender] > 0) { + require( + block.timestamp >= _lastClaimTime[msg.sender] + CLAIM_COOLDOWN, + "The cooling-off period has not expired" + ); + } + + // 更新状态(先更新状态再转账,防止重入攻击) + _availableRewards[msg.sender] = 0; + _lastClaimTime[msg.sender] = block.timestamp; + + // 转移代币 + bool success = _transferReward(msg.sender, amount); + require(success, "Token transfer failed"); + + emit RewardClaimed(msg.sender, amount); + return true; + } + + /** + * @dev 内部函数,处理奖励转账逻辑 + * @param to 接收地址 + * @param amount 转账数量 + * @return 是否成功 + */ + function _transferReward(address to, uint256 amount) + internal + returns (bool) + { + // 尝试从合约余额中转账 + uint256 contractBalance = cfcToken.balanceOf(address(this)); + if (contractBalance >= amount) { + // 如果合约余额足够,直接转账 + cfcToken.safeTransfer(to, amount); + return true; + } else { + // 如果合约余额不足,尝试从owner转账 + try cfcToken.transferFrom(owner(), to, amount) { + return true; + } catch { + return false; + } + } + } + + /** + * @dev 存入代币到合约(用于后续奖励发放) + * @param amount 存入数量 + */ + function depositTokens(uint256 amount) external { + require(amount > 0, "Quantity must be greater than 0"); + cfcToken.safeTransferFrom(msg.sender, address(this), amount); + } + + /** + * @dev 查询用户可领取的非项目相关奖励 + * @param account 用户地址 + * @return 可领取的奖励数量 + */ + function availableRewards(address account) external view returns (uint256) { + return _availableRewards[account]; + } + + /** + * @dev 查询用户上次领取奖励的时间 + * @param account 用户地址 + * @return 上次领取时间(时间戳) + */ + function lastClaimTime(address account) external view returns (uint256) { + return _lastClaimTime[account]; + } + + /** + * @dev 检查用户当前是否可以领取通用奖励 + * @param account 用户地址 + * @return 是否可以领取 + */ + function canClaim(address account) external view returns (bool) { + if (_availableRewards[account] == 0) { + return false; + } + + if (_lastClaimTime[account] == 0) { + return true; + } + + return block.timestamp >= _lastClaimTime[account] + CLAIM_COOLDOWN; + } + + /** + * @dev 计算用户还需等待多长时间才能领取通用奖励 + * @param account 用户地址 + * @return 需要等待的时间(秒) + */ + function timeUntilNextClaim(address account) + external + view + returns (uint256) + { + if (_availableRewards[account] == 0 || _lastClaimTime[account] == 0) { + return 0; + } + + uint256 nextClaimTime = _lastClaimTime[account] + CLAIM_COOLDOWN; + if (block.timestamp >= nextClaimTime) { + return 0; + } + + return nextClaimTime - block.timestamp; + } + + /** + * @dev 获取用户项目状态 + * @param _user 用户地址 + * @param _projectId 项目ID + * @return registered 是否注册 + * @return completedTasks 已完成任务数 + * @return score 分数 + * @return hasClaimedReward 是否已领取奖励 + * @return hasClaimedNFT 是否已铸造NFT + */ + function getUserProjectStatus(address _user, string calldata _projectId) + external + view + returns ( + bool registered, + uint256 completedTasks, + uint256 score, + bool hasClaimedReward, + bool hasClaimedNFT + ) + { + UserProjectStatus memory status = userProjects[_user][_projectId]; + return ( + status.registered, + status.completedTasks, + status.score, + status.hasClaimedReward, + status.hasClaimedNFT + ); + } + + /** + * @dev 获取项目数量 + * @return 项目数量 + */ + function getProjectCount() external view returns (uint256) { + return projects.length; + } + + /** + * @dev 获取项目信息 + * @param _projectId 项目ID + * @return id 项目ID + * @return name 项目名称 + * @return description 项目描述 + * @return startTime 开始时间 + * @return endTime 结束时间 + * @return rewardAmount 奖励数量 + * @return status 项目状态 + * @return requiredTaskCount 所需任务数 + * @return passingScore 及格分数 + */ + function getProjectDetails(string calldata _projectId) + external + view + returns ( + string memory id, + string memory name, + string memory description, + uint256 startTime, + uint256 endTime, + uint256 rewardAmount, + ProjectStatus status, + uint256 requiredTaskCount, + uint256 passingScore + ) + { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + return ( + project.id, + project.name, + project.description, + project.startTime, + project.endTime, + project.rewardAmount, + project.status, + project.requiredTaskCount, + project.passingScore + ); + } + + /** + * @dev 检索项目索引 + * @param _projectId 项目ID + * @return 项目索引 + */ + function getProjectIndex(string calldata _projectId) + public + view + returns (uint256) + { + uint256 index = projectIdToIndex[_projectId]; + require(index > 0, "item does not exist"); + return index - 1; + } + + // 获取用户可以铸造的nft + + /** + * @dev 紧急取回合约中的代币(仅限合约拥有者) + * @param amount 取回数量 + */ + function emergencyWithdraw(uint256 amount) external onlyOwner { + require(amount > 0, "Quantity must be greater than 0"); + uint256 balance = cfcToken.balanceOf(address(this)); + require(amount <= balance, "not sufficient funds"); + + cfcToken.safeTransfer(owner(), amount); + } +} diff --git a/contracts/ProjectManager.sol b/contracts/ProjectManager.sol new file mode 100644 index 0000000..7d638d9 --- /dev/null +++ b/contracts/ProjectManager.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./CFCNFT.sol"; +import "./CFCToken.sol"; + +/** + * @title ProjectManager + * @dev 管理社区学习项目及用户完成状态 + * 验证用户铸造NFT的资格,并在用户完成项目时发放奖励 + */ +contract ProjectManager is Ownable { + // NFT合约地址 + CFCNFT public nftContract; + + // 代币合约地址 + CFCToken public tokenContract; + + // 项目状态 + enum ProjectStatus { ACTIVE, COMPLETED, ARCHIVED } + + // 项目信息结构 + struct Project { + string id; // 项目标识符 + string name; // 项目名称 + string description; // 项目描述 + uint256 startTime; // 开始时间 + uint256 endTime; // 结束时间 + uint256 rewardAmount; // 完成项目奖励的代币数量 + ProjectStatus status; // 项目状态 + uint256 requiredTaskCount; // 需要完成的任务数量 + uint256 passingScore; // 通过所需的最低分数 (百分比,例如:70表示70%) + } + + // 用户完成项目的记录 + struct UserProjectStatus { + bool registered; // 是否注册参与 + uint256 completedTasks; // 已完成的任务数 + uint256 score; // 项目得分 + bool hasClaimedReward; // 是否已领取代币奖励 + bool hasClaimedNFT; // 是否已铸造NFT + } + + // 所有项目 + Project[] public projects; + + // 项目ID到项目索引的映射 + mapping(string => uint256) private projectIdToIndex; + + // 用户地址 => 项目ID => 用户项目状态 + mapping(address => mapping(string => UserProjectStatus)) public userProjects; + + // 事件定义 + event ProjectCreated(string indexed projectId, string name, uint256 startTime, uint256 endTime); + event ProjectUpdated(string indexed projectId, ProjectStatus status); + event UserRegistered(address indexed user, string indexed projectId); + event TaskCompleted(address indexed user, string indexed projectId, uint256 taskCount); + event ProjectScoreUpdated(address indexed user, string indexed projectId, uint256 score); + event RewardClaimed(address indexed user, string indexed projectId, uint256 amount); + event NFTClaimed(address indexed user, string indexed projectId, uint256 tokenId); + + /** + * @dev 构造函数 + */ + constructor() Ownable(msg.sender) {} + + /** + * @dev 设置NFT合约地址 + * @param _nftContract NFT合约地址 + */ + function setNFTContract(address _nftContract) external onlyOwner { + nftContract = CFCNFT(_nftContract); + } + + /** + * @dev 设置代币合约地址 + * @param _tokenContract 代币合约地址 + */ + function setTokenContract(address _tokenContract) external onlyOwner { + tokenContract = CFCToken(_tokenContract); + } + + /** + * @dev 创建新项目 + * @param _id 项目ID + * @param _name 项目名称 + * @param _description 项目描述 + * @param _startTime 开始时间 + * @param _endTime 结束时间 + * @param _rewardAmount 奖励代币数量 + * @param _requiredTaskCount 所需完成的任务数量 + * @param _passingScore 及格分数(百分比) + */ + function createProject( + string calldata _id, + string calldata _name, + string calldata _description, + uint256 _startTime, + uint256 _endTime, + uint256 _rewardAmount, + uint256 _requiredTaskCount, + uint256 _passingScore + ) external onlyOwner { + require(bytes(_id).length > 0, "项目ID不能为空"); + require(_endTime > _startTime, "结束时间必须晚于开始时间"); + require(_passingScore <= 100, "及格分数不能超过100%"); + require(projectIdToIndex[_id] == 0, "项目ID已存在"); + + Project memory newProject = Project({ + id: _id, + name: _name, + description: _description, + startTime: _startTime, + endTime: _endTime, + rewardAmount: _rewardAmount, + status: ProjectStatus.ACTIVE, + requiredTaskCount: _requiredTaskCount, + passingScore: _passingScore + }); + + projects.push(newProject); + projectIdToIndex[_id] = projects.length; + + emit ProjectCreated(_id, _name, _startTime, _endTime); + } + + /** + * @dev 更新项目状态 + * @param _projectId 项目ID + * @param _status 新状态 + */ + function updateProjectStatus(string calldata _projectId, ProjectStatus _status) external onlyOwner { + uint256 projectIndex = getProjectIndex(_projectId); + projects[projectIndex].status = _status; + + emit ProjectUpdated(_projectId, _status); + } + + /** + * @dev 用户注册参与项目 + * @param _projectId 项目ID + */ + function registerForProject(string calldata _projectId) external { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require(project.status == ProjectStatus.ACTIVE, "项目不处于活跃状态"); + require(block.timestamp >= project.startTime, "项目尚未开始"); + require(block.timestamp <= project.endTime, "项目已结束"); + require(!userProjects[msg.sender][_projectId].registered, "已注册此项目"); + + userProjects[msg.sender][_projectId] = UserProjectStatus({ + registered: true, + completedTasks: 0, + score: 0, + hasClaimedReward: false, + hasClaimedNFT: false + }); + + emit UserRegistered(msg.sender, _projectId); + } + + /** + * @dev 更新用户完成的任务数量(由管理员调用) + * @param _user 用户地址 + * @param _projectId 项目ID + * @param _taskCount 已完成任务数 + */ + function updateCompletedTasks(address _user, string calldata _projectId, uint256 _taskCount) external onlyOwner { + require(userProjects[_user][_projectId].registered, "用户未注册此项目"); + + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require(_taskCount <= project.requiredTaskCount, "完成任务数不能超过要求"); + + userProjects[_user][_projectId].completedTasks = _taskCount; + + // 更新得分 + uint256 score = (_taskCount * 100) / project.requiredTaskCount; + userProjects[_user][_projectId].score = score; + + emit TaskCompleted(_user, _projectId, _taskCount); + emit ProjectScoreUpdated(_user, _projectId, score); + } + + /** + * @dev 手动设置用户项目得分(由管理员调用) + * @param _user 用户地址 + * @param _projectId 项目ID + * @param _score 得分(0-100) + */ + function setProjectScore(address _user, string calldata _projectId, uint256 _score) external onlyOwner { + require(userProjects[_user][_projectId].registered, "用户未注册此项目"); + require(_score <= 100, "分数不能超过100"); + + userProjects[_user][_projectId].score = _score; + + emit ProjectScoreUpdated(_user, _projectId, _score); + } + + /** + * @dev 验证用户是否有资格铸造NFT + * @param _user 用户地址 + * @param _projectId 项目ID + * @return 是否有资格 + */ + function canMintNFT(address _user, string calldata _projectId) external view returns (bool) { + if (!userProjects[_user][_projectId].registered) { + return false; + } + + if (userProjects[_user][_projectId].hasClaimedNFT) { + return false; + } + + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + // 检查用户是否已经达到通过标准 + return userProjects[_user][_projectId].score >= project.passingScore; + } + + /** + * @dev 用户领取项目完成奖励 + * @param _projectId 项目ID + */ + function claimProjectReward(string calldata _projectId) external { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require(userProjects[msg.sender][_projectId].registered, "未注册此项目"); + require(userProjects[msg.sender][_projectId].score >= project.passingScore, "未达到及格分数"); + require(!userProjects[msg.sender][_projectId].hasClaimedReward, "已领取奖励"); + require(address(tokenContract) != address(0), "代币合约未设置"); + + userProjects[msg.sender][_projectId].hasClaimedReward = true; + + // 发放代币奖励 + tokenContract.mint(msg.sender, project.rewardAmount); + + emit RewardClaimed(msg.sender, _projectId, project.rewardAmount); + } + + /** + * @dev 用户铸造项目完成NFT + * @param _projectId 项目ID + * @return tokenId 铸造的NFT ID + */ + function mintProjectNFT(string calldata _projectId) external returns (uint256) { + uint256 projectIndex = getProjectIndex(_projectId); + Project memory project = projects[projectIndex]; + + require(userProjects[msg.sender][_projectId].registered, "未注册此项目"); + require(userProjects[msg.sender][_projectId].score >= project.passingScore, "未达到及格分数"); + require(!userProjects[msg.sender][_projectId].hasClaimedNFT, "已铸造NFT"); + require(address(nftContract) != address(0), "NFT合约未设置"); + + userProjects[msg.sender][_projectId].hasClaimedNFT = true; + + // 铸造NFT + uint256 tokenId = nftContract.mintNFT(msg.sender, _projectId); + + emit NFTClaimed(msg.sender, _projectId, tokenId); + + return tokenId; + } + + /** + * @dev 获取用户项目状态 + * @param _user 用户地址 + * @param _projectId 项目ID + * @return registered 是否注册 + * @return completedTasks 已完成任务数 + * @return score 分数 + * @return hasClaimedReward 是否已领取奖励 + * @return hasClaimedNFT 是否已铸造NFT + */ + function getUserProjectStatus(address _user, string calldata _projectId) external view returns ( + bool registered, + uint256 completedTasks, + uint256 score, + bool hasClaimedReward, + bool hasClaimedNFT + ) { + UserProjectStatus memory status = userProjects[_user][_projectId]; + return ( + status.registered, + status.completedTasks, + status.score, + status.hasClaimedReward, + status.hasClaimedNFT + ); + } + + /** + * @dev 获取项目数量 + * @return 项目数量 + */ + function getProjectCount() external view returns (uint256) { + return projects.length; + } + + /** + * @dev 检索项目索引 + * @param _projectId 项目ID + * @return 项目索引 + */ + function getProjectIndex(string calldata _projectId) public view returns (uint256) { + uint256 index = projectIdToIndex[_projectId]; + require(index > 0, "项目不存在"); + return index - 1; + } +} \ No newline at end of file diff --git a/data/projects.ts b/data/projects.ts index 7047b6e..992d49e 100644 --- a/data/projects.ts +++ b/data/projects.ts @@ -26,6 +26,7 @@ export const ProjectInfos: ProjectInfo[] = [ export const ProjectDetails: ProjectDetail[] = [ { + index: "1", id: "The-Missing-Semester", title: "计算机教育中缺失的一课", description: @@ -316,6 +317,7 @@ export const ProjectDetails: ProjectDetail[] = [ ], }, { + index: "2", id: "FullStack-Compass", title: "全栈开发指北", description: diff --git a/lib/contracts/CFCToke.json b/lib/contracts/CFCNFT.json similarity index 71% rename from lib/contracts/CFCToke.json rename to lib/contracts/CFCNFT.json index a876a89..703f638 100644 --- a/lib/contracts/CFCToke.json +++ b/lib/contracts/CFCNFT.json @@ -4,73 +4,41 @@ "stateMutability": "nonpayable", "type": "constructor" }, - { - "inputs": [], - "name": "ECDSAInvalidSignature", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "length", - "type": "uint256" - } - ], - "name": "ECDSAInvalidSignatureLength", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "ECDSAInvalidSignatureS", - "type": "error" - }, { "inputs": [ { "internalType": "address", - "name": "spender", + "name": "sender", "type": "address" }, { "internalType": "uint256", - "name": "allowance", + "name": "tokenId", "type": "uint256" }, { - "internalType": "uint256", - "name": "needed", - "type": "uint256" + "internalType": "address", + "name": "owner", + "type": "address" } ], - "name": "ERC20InsufficientAllowance", + "name": "ERC721IncorrectOwner", "type": "error" }, { "inputs": [ { "internalType": "address", - "name": "sender", + "name": "operator", "type": "address" }, { "internalType": "uint256", - "name": "balance", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "needed", + "name": "tokenId", "type": "uint256" } ], - "name": "ERC20InsufficientBalance", + "name": "ERC721InsufficientApproval", "type": "error" }, { @@ -81,88 +49,62 @@ "type": "address" } ], - "name": "ERC20InvalidApprover", + "name": "ERC721InvalidApprover", "type": "error" }, { "inputs": [ { "internalType": "address", - "name": "receiver", + "name": "operator", "type": "address" } ], - "name": "ERC20InvalidReceiver", + "name": "ERC721InvalidOperator", "type": "error" }, { "inputs": [ { "internalType": "address", - "name": "sender", + "name": "owner", "type": "address" } ], - "name": "ERC20InvalidSender", + "name": "ERC721InvalidOwner", "type": "error" }, { "inputs": [ { "internalType": "address", - "name": "spender", + "name": "receiver", "type": "address" } ], - "name": "ERC20InvalidSpender", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - } - ], - "name": "ERC2612ExpiredSignature", + "name": "ERC721InvalidReceiver", "type": "error" }, { "inputs": [ { "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "owner", + "name": "sender", "type": "address" } ], - "name": "ERC2612InvalidSigner", + "name": "ERC721InvalidSender", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, { "internalType": "uint256", - "name": "currentNonce", + "name": "tokenId", "type": "uint256" } ], - "name": "InvalidAccountNonce", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidShortString", + "name": "ERC721NonexistentToken", "type": "error" }, { @@ -187,17 +129,6 @@ "name": "OwnableUnauthorizedAccount", "type": "error" }, - { - "inputs": [ - { - "internalType": "string", - "name": "str", - "type": "string" - } - ], - "name": "StringTooLong", - "type": "error" - }, { "anonymous": false, "inputs": [ @@ -210,144 +141,134 @@ { "indexed": true, "internalType": "address", - "name": "spender", + "name": "approved", "type": "address" }, { - "indexed": false, + "indexed": true, "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" } ], "name": "Approval", "type": "event" }, - { - "anonymous": false, - "inputs": [], - "name": "EIP712DomainChanged", - "type": "event" - }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", - "name": "previousOwner", + "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "newOwner", + "name": "operator", "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" } ], - "name": "OwnershipTransferred", + "name": "ApprovalForAll", "type": "event" }, { "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" + "indexed": false, + "internalType": "uint256", + "name": "_fromTokenId", + "type": "uint256" }, { "indexed": false, "internalType": "uint256", - "name": "value", + "name": "_toTokenId", "type": "uint256" } ], - "name": "Transfer", + "name": "BatchMetadataUpdate", "type": "event" }, { - "inputs": [], - "name": "DOMAIN_SEPARATOR", - "outputs": [ + "anonymous": false, + "inputs": [ { - "internalType": "bytes32", - "name": "", - "type": "bytes32" + "indexed": false, + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" } ], - "stateMutability": "view", - "type": "function" + "name": "MetadataUpdate", + "type": "event" }, { + "anonymous": false, "inputs": [ { + "indexed": true, "internalType": "address", - "name": "owner", + "name": "previousOwner", "type": "address" }, { + "indexed": true, "internalType": "address", - "name": "spender", + "name": "newOwner", "type": "address" } ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" + "name": "OwnershipTransferred", + "type": "event" }, { + "anonymous": false, "inputs": [ { + "indexed": true, "internalType": "address", - "name": "spender", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", "type": "address" }, { + "indexed": true, "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" } ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" + "name": "Transfer", + "type": "event" }, { "inputs": [ { "internalType": "address", - "name": "minter", + "name": "to", "type": "address" }, { "internalType": "uint256", - "name": "amount", + "name": "tokenId", "type": "uint256" } ], - "name": "approveMinter", + "name": "approve", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -356,7 +277,7 @@ "inputs": [ { "internalType": "address", - "name": "account", + "name": "owner", "type": "address" } ], @@ -374,85 +295,65 @@ { "inputs": [ { - "internalType": "uint256", - "name": "value", - "type": "uint256" + "internalType": "address[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "string[]", + "name": "tokenURIs", + "type": "string[]" + } + ], + "name": "batchMintNFT", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" } ], - "name": "burn", - "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, { "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" } ], - "name": "burnFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", + "name": "getApproved", "outputs": [ { - "internalType": "uint8", + "internalType": "address", "name": "", - "type": "uint8" + "type": "address" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "eip712Domain", - "outputs": [ - { - "internalType": "bytes1", - "name": "fields", - "type": "bytes1" - }, - { - "internalType": "string", - "name": "name", - "type": "string" - }, - { - "internalType": "string", - "name": "version", - "type": "string" - }, - { - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - }, + "inputs": [ { "internalType": "address", - "name": "verifyingContract", + "name": "owner", "type": "address" }, { - "internalType": "bytes32", - "name": "salt", - "type": "bytes32" - }, + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ { - "internalType": "uint256[]", - "name": "extensions", - "type": "uint256[]" + "internalType": "bool", + "name": "", + "type": "bool" } ], "stateMutability": "view", @@ -465,14 +366,20 @@ "name": "to", "type": "address" }, + { + "internalType": "string", + "name": "tokenURI_", + "type": "string" + } + ], + "name": "mintNFT", + "outputs": [ { "internalType": "uint256", - "name": "amount", + "name": "", "type": "uint256" } ], - "name": "mint", - "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -490,27 +397,27 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "owner", + "outputs": [ { "internalType": "address", - "name": "owner", + "name": "", "type": "address" } ], - "name": "nonces", - "outputs": [ + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "tokenId", "type": "uint256" } ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", + "name": "ownerOf", "outputs": [ { "internalType": "address", @@ -521,64 +428,96 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { "internalType": "address", - "name": "owner", + "name": "from", "type": "address" }, { "internalType": "address", - "name": "spender", + "name": "to", "type": "address" }, { "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" - }, + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" + "internalType": "address", + "name": "from", + "type": "address" }, { - "internalType": "uint8", - "name": "v", - "type": "uint8" + "internalType": "address", + "name": "to", + "type": "address" }, { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" }, { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "name": "permit", + "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [], - "name": "renounceOwnership", + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [], - "name": "symbol", + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", "outputs": [ { - "internalType": "string", + "internalType": "bool", "name": "", - "type": "string" + "type": "bool" } ], "stateMutability": "view", @@ -586,12 +525,12 @@ }, { "inputs": [], - "name": "totalSupply", + "name": "symbol", "outputs": [ { - "internalType": "uint256", + "internalType": "string", "name": "", - "type": "uint256" + "type": "string" } ], "stateMutability": "view", @@ -599,26 +538,21 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "to", - "type": "address" - }, { "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" } ], - "name": "transfer", + "name": "tokenURI", "outputs": [ { - "internalType": "bool", + "internalType": "string", "name": "", - "type": "bool" + "type": "string" } ], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -635,18 +569,12 @@ }, { "internalType": "uint256", - "name": "value", + "name": "tokenId", "type": "uint256" } ], "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, diff --git a/lib/contracts/CFCToken.json b/lib/contracts/CFCToken.json new file mode 100644 index 0000000..96c9afd --- /dev/null +++ b/lib/contracts/CFCToken.json @@ -0,0 +1,425 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/lib/contracts/RewardToken.json b/lib/contracts/RewardToken.json index ed2bb2c..ff409a1 100644 --- a/lib/contracts/RewardToken.json +++ b/lib/contracts/RewardToken.json @@ -1,4 +1,27 @@ [ + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_tokenURI", + "type": "uint256" + } + ], + "name": "addNFT", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -35,6 +58,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "claimProjectReward", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "claimRewards", @@ -48,6 +90,54 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "string", + "name": "_id", + "type": "string" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_rewardAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_requiredTaskCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_passingScore", + "type": "uint256" + } + ], + "name": "createProject", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -74,12 +164,65 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + }, + { + "internalType": "string", + "name": "_tokenURI", + "type": "string" + } + ], + "name": "mintNFT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "mintProjectNFT", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { "internalType": "address", "name": "tokenAddress", "type": "address" + }, + { + "internalType": "address", + "name": "nftAddress", + "type": "address" } ], "stateMutability": "nonpayable", @@ -118,6 +261,44 @@ "name": "SafeERC20FailedOperation", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "NFTClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newNFTContract", + "type": "address" + } + ], + "name": "NFTContractUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -138,11 +319,35 @@ "type": "event" }, { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + } + ], + "name": "ProjectCreated", + "type": "event" }, { "anonymous": false, @@ -153,6 +358,12 @@ "name": "user", "type": "address" }, + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + }, { "indexed": false, "internalType": "uint256", @@ -160,7 +371,7 @@ "type": "uint256" } ], - "name": "RewardAdded", + "name": "ProjectRewardClaimed", "type": "event" }, { @@ -172,14 +383,20 @@ "name": "user", "type": "address" }, + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + }, { "indexed": false, "internalType": "uint256", - "name": "amount", + "name": "score", "type": "uint256" } ], - "name": "RewardClaimed", + "name": "ProjectScoreUpdated", "type": "event" }, { @@ -187,111 +404,473 @@ "inputs": [ { "indexed": true, - "internalType": "address", - "name": "newTokenContract", - "type": "address" + "internalType": "string", + "name": "projectId", + "type": "string" + }, + { + "indexed": false, + "internalType": "enum ProjectNFTManager.ProjectStatus", + "name": "status", + "type": "uint8" } ], - "name": "TokenContractUpdated", + "name": "ProjectUpdated", "type": "event" }, { "inputs": [ { "internalType": "address", - "name": "newOwner", + "name": "user", "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" } ], - "name": "transferOwnership", + "name": "registerForProject", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "newTokenAddress", - "type": "address" - } - ], - "name": "updateTokenContract", + "inputs": [], + "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { + "anonymous": false, "inputs": [ { + "indexed": true, "internalType": "address", - "name": "account", + "name": "user", "type": "address" - } - ], - "name": "availableRewards", - "outputs": [ + }, { + "indexed": false, "internalType": "uint256", - "name": "", + "name": "amount", "type": "uint256" } ], - "stateMutability": "view", - "type": "function" + "name": "RewardAdded", + "type": "event" }, { + "anonymous": false, "inputs": [ { + "indexed": true, "internalType": "address", - "name": "account", + "name": "user", "type": "address" - } - ], - "name": "canClaim", - "outputs": [ + }, { - "internalType": "bool", - "name": "", - "type": "bool" + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], - "stateMutability": "view", - "type": "function" + "name": "RewardClaimed", + "type": "event" }, { - "inputs": [], - "name": "cfcToken", - "outputs": [ + "inputs": [ { - "internalType": "contract IERC20", - "name": "", + "internalType": "address", + "name": "_user", "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "CLAIM_COOLDOWN", - "outputs": [ + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + }, { "internalType": "uint256", - "name": "", + "name": "_score", "type": "uint256" } ], - "stateMutability": "view", + "name": "setProjectScore", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { + "anonymous": false, "inputs": [ { + "indexed": true, "internalType": "address", - "name": "account", + "name": "user", "type": "address" - } + }, + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "taskCount", + "type": "uint256" + } + ], + "name": "TaskCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newTokenContract", + "type": "address" + } + ], + "name": "TokenContractUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_taskCount", + "type": "uint256" + } + ], + "name": "updateCompletedTasks", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newNFTAddress", + "type": "address" + } + ], + "name": "updateNFTContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_projectId", + "type": "string" + }, + { + "internalType": "enum ProjectNFTManager.ProjectStatus", + "name": "_status", + "type": "uint8" + } + ], + "name": "updateProjectStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newTokenAddress", + "type": "address" + } + ], + "name": "updateTokenContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "projectId", + "type": "string" + } + ], + "name": "UserRegistered", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "availableRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "canClaim", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "canMintNFT", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cfcToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "CLAIM_COOLDOWN", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProjectCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "getProjectDetails", + "outputs": [ + { + "internalType": "string", + "name": "id", + "type": "string" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + }, + { + "internalType": "enum ProjectNFTManager.ProjectStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "requiredTaskCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "passingScore", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "getProjectIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "string", + "name": "_projectId", + "type": "string" + } + ], + "name": "getUserProjectStatus", + "outputs": [ + { + "internalType": "bool", + "name": "registered", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "completedTasks", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "hasClaimedReward", + "type": "bool" + }, + { + "internalType": "bool", + "name": "hasClaimedNFT", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } ], "name": "lastClaimTime", "outputs": [ @@ -304,6 +883,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "nftContract", + "outputs": [ + { + "internalType": "contract IERC721", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -317,6 +909,65 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "projects", + "outputs": [ + { + "internalType": "string", + "name": "id", + "type": "string" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + }, + { + "internalType": "enum ProjectNFTManager.ProjectStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "requiredTaskCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "passingScore", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -335,5 +986,49 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "userProjects", + "outputs": [ + { + "internalType": "bool", + "name": "registered", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "completedTasks", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "hasClaimedReward", + "type": "bool" + }, + { + "internalType": "bool", + "name": "hasClaimedNFT", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/lib/contracts/contract-config.ts b/lib/contracts/contract-config.ts index 106c46f..fc6fda1 100644 --- a/lib/contracts/contract-config.ts +++ b/lib/contracts/contract-config.ts @@ -7,10 +7,10 @@ interface ContractConfig { } export const REWARD_TOKEN_CONTRACT: ContractConfig = { - name: "CrazyForCode Token", + name: "CrazyForCode CoLearning", address: { [mainnet.id]: "0x1234567890123456789012345678901234567890", // 主网测试地址,实际部署时更换 - [sepolia.id]: "0x3e61503D6504F5f72Fc46a04229048e96d5F9089", // Sepolia测试网地址,实际部署时更换 + [sepolia.id]: "0xc56d0e9F2Ca03fc18e674369eB481aC12147b082", // Sepolia测试网地址,实际部署时更换 }, }; @@ -18,10 +18,18 @@ export const CFC_TOKEN_CONTRACT: ContractConfig = { name: "CrazyForCode Token", address: { [mainnet.id]: "0x1234567890123456789012345678901234567890", // 主网测试地址,实际部署时更换 - [sepolia.id]: "0x7c0de9efd6edf521d71bbd4085728b8771a3f675", // Sepolia测试网地址,实际部署时更换 + [sepolia.id]: "0xf9F75d2f80826fC18065F8B4d42667C5fA1BB556", // Sepolia测试网地址,实际部署时更换 }, }; +export const CFC_NFT_CONTRACT: ContractConfig = { + name: "CrazyForCode NFT", + address: { + [mainnet.id]: "0x1234567890123456789012345678901234567890", // 主网测试地址,实际部署时更换 + [sepolia.id]: "0x4C4dAfb8239A49ED4dE0eFc14eFda476AdA52ce9", // Sepolia测试网地址,实际部署时更换 + }, +}; + export function getContractAddress( contract: ContractConfig, chainId: number diff --git a/lib/hooks/useERC20Token.ts b/lib/hooks/useERC20Token.ts index 9c74002..3787d1b 100644 --- a/lib/hooks/useERC20Token.ts +++ b/lib/hooks/useERC20Token.ts @@ -3,7 +3,7 @@ import { formatUnits, parseUnits } from "viem"; import { useState, useEffect } from "react"; import { useAppKitAccount } from "@reown/appkit/react"; import { sepolia } from "wagmi/chains"; -import ERC20_ABI from "../contracts/CFCToke.json"; +import ERC20_ABI from "../contracts/CFCToken.json"; import { CFC_TOKEN_CONTRACT, getContractAddress } from "../contracts/contract-config"; diff --git a/lib/hooks/useProjectNFT.ts b/lib/hooks/useProjectNFT.ts new file mode 100644 index 0000000..ace9b72 --- /dev/null +++ b/lib/hooks/useProjectNFT.ts @@ -0,0 +1,220 @@ +import { useReadContract, useWriteContract, useWaitForTransactionReceipt, useChainId } from "wagmi"; +import { formatUnits } from "viem"; +import { useState, useEffect } from "react"; +import { useAppKitAccount } from "@reown/appkit/react"; +import { sepolia } from "wagmi/chains"; +import { ProjectDetails } from "@/data/projects"; +import NFT_ABI from "@/lib/contracts/CFCNFT.json"; +import PROJECT_MANAGER_ABI from "@/lib/contracts/RewardToken.json"; +import { CFC_NFT_CONTRACT, REWARD_TOKEN_CONTRACT } from "@/lib/contracts/contract-config"; + + +const NFT_ADDRESS = CFC_NFT_CONTRACT.address[sepolia.id]; +const PROJECT_MANAGER_ADDRESS = REWARD_TOKEN_CONTRACT.address[sepolia.id]; + +export interface ProjectNFTStatus { + projectId: string; + projectName: string; + registered: boolean; + completedTasks: number; + totalTasks: number | null; + score: number; + passingScore: number | null; + canMint: boolean; + hasMinted: boolean; + hasClaimedReward: boolean; +} + +/** + * 使用项目NFT的钩子函数 + * @returns 项目NFT相关函数和状态 + */ +export function useProjectNFT() { + const { address, isConnected } = useAppKitAccount(); + const chainId = useChainId(); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [projectsStatus, setProjectsStatus] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState(null); + const [projectToCheck, setProjectToCheck] = useState<{ id: string, name: string } | null>(null); + + // 检查网络是否是Sepolia + const isSepoliaNetwork = chainId === sepolia.id; + + // 读取合约数据 + const { data: projectStatus, refetch: refetchProjectStatus } = useReadContract({ + address: PROJECT_MANAGER_ADDRESS, + abi: PROJECT_MANAGER_ABI, + functionName: "getUserProjectStatus", + args: address && projectToCheck ? [address, projectToCheck.id] : undefined, + }); + + const { data: projectDetails, refetch: refetchProjectDetails } = useReadContract({ + address: PROJECT_MANAGER_ADDRESS, + abi: PROJECT_MANAGER_ABI, + functionName: "getProjectDetails", + args: projectToCheck ? [projectToCheck.id] : undefined, + }); + + const { data: canMint, refetch: refetchCanMint } = useReadContract({ + address: PROJECT_MANAGER_ADDRESS, + abi: PROJECT_MANAGER_ABI, + functionName: "canMintNFT", + args: address && projectToCheck ? [address, projectToCheck.id] : undefined, + }); + + // 处理项目数据 + const processProjectData = () => { + if (!projectToCheck || !projectStatus || !projectDetails) return null; + + try { + const [registered, completedTasks, score, hasClaimedReward, hasClaimedNFT] = projectStatus as [boolean, bigint, bigint, boolean, boolean]; + const [id, name, description, startTime, endTime, rewardAmount, status, requiredTaskCount, passingScore] = projectDetails as [ + string, string, string, bigint, bigint, bigint, number, bigint, bigint + ]; + + return { + projectId: projectToCheck.id, + projectName: name, + registered, + completedTasks: Number(completedTasks), + totalTasks: Number(requiredTaskCount), + score: Number(score), + passingScore: Number(passingScore), + canMint: Boolean(canMint), + hasMinted: hasClaimedNFT, + hasClaimedReward + }; + } catch (error) { + console.error("处理项目数据失败:", error); + return null; + } + }; + + // 当项目数据更新时处理结果 + useEffect(() => { + const projectData = processProjectData(); + if (projectData) { + setProjectsStatus(prev => { + // 检查是否已存在该项目 + const existingIndex = prev.findIndex(p => p.projectId === projectData.projectId); + if (existingIndex >= 0) { + // 如果存在,更新该项目 + const updated = [...prev]; + updated[existingIndex] = projectData; + return updated; + } else { + // 如果不存在,添加新项目 + return [...prev, projectData]; + } + }); + console.log(projectsStatus) + } + }, [projectStatus, projectDetails, canMint]); + + // 读取所有项目状态 + const fetchAllProjectsStatus = async () => { + if (!address || !isConnected || !isSepoliaNetwork) return; + + setIsLoading(true); + setErrorMessage(null); + setProjectsStatus([]); // 清空现有状态 + + try { + // 从projects数据中获取项目列表 + for (const project of ProjectDetails) { + // 设置当前检查的项目 + setProjectToCheck({ id: project.index, name: project.title }); + + // 触发合约数据刷新 + await Promise.all([ + refetchProjectStatus(), + refetchProjectDetails(), + refetchCanMint() + ]); + + // 等待一小段时间以确保数据更新 + await new Promise(resolve => setTimeout(resolve, 300)); + } + } catch (error) { + console.error("获取所有项目状态失败:", error); + setErrorMessage("获取项目状态失败,请稍后再试"); + } finally { + setIsLoading(false); + setProjectToCheck(null); + } + }; + + // 铸造NFT相关 + const { writeContract, data: mintHash, isPending: isMintPending } = useWriteContract(); + + // 等待交易完成 + const { isLoading: isMintConfirming, isSuccess: isMintSuccess } = useWaitForTransactionReceipt({ + hash: mintHash, + }); + + // 铸造NFT函数 + const mintProjectNFT = async (projectId: string) => { + if (!isConnected || !address) { + setErrorMessage("请先连接您的钱包"); + return; + } + + if (!isSepoliaNetwork) { + setErrorMessage("请切换到Sepolia测试网"); + return; + } + + try { + setIsLoading(true); + setErrorMessage(null); + setSelectedProjectId(projectId); + + // 调用项目管理合约铸造NFT + writeContract({ + address: PROJECT_MANAGER_ADDRESS, + abi: PROJECT_MANAGER_ABI, + functionName: "mintProjectNFT", + args: [projectId], + }); + } catch (error) { + console.error("铸造NFT失败:", error); + setErrorMessage("铸造NFT失败,请稍后再试"); + setIsLoading(false); + setSelectedProjectId(null); + } + }; + + // 监听交易状态 + useEffect(() => { + if (isMintSuccess && selectedProjectId) { + // 刷新数据 + fetchAllProjectsStatus(); + setSelectedProjectId(null); + setIsLoading(false); + } + }, [isMintSuccess, selectedProjectId]); + + // 首次加载时获取所有项目状态 + useEffect(() => { + if (isConnected && address && isSepoliaNetwork) { + fetchAllProjectsStatus(); + } + }, [isConnected, address, isSepoliaNetwork]); + + return { + // 项目状态 + projectsStatus, + selectedProjectId, + + // 操作函数 + mintProjectNFT, + refreshProjectsStatus: fetchAllProjectsStatus, + + // 状态 + isLoading: isLoading || isMintPending || isMintConfirming, + isMintSuccess, + errorMessage, + isSepoliaNetwork, + }; +} \ No newline at end of file diff --git a/lib/project.ts b/lib/project.ts index 11f477d..4910234 100644 --- a/lib/project.ts +++ b/lib/project.ts @@ -29,6 +29,7 @@ export interface ProjectInfo { } export interface ProjectDetail { + index: string; id: string; hero_image_url?: string; github_url?: string; diff --git a/metadata-examples/fullstack-compass.json b/metadata-examples/fullstack-compass.json new file mode 100644 index 0000000..1346ad7 --- /dev/null +++ b/metadata-examples/fullstack-compass.json @@ -0,0 +1,51 @@ +{ + "name": "FullStack Compass - 完成证明", + "description": "此NFT证明持有者已成功完成'全栈开发指北'共学项目,掌握了现代前端开发、SaaS架构和产品设计的核心技能,能够独立开发全栈应用。", + "image": "ipfs://bafybeigtr5ynrmo5i45whnoqdhoenczpaguhcaaxtfshudtnggehlmlucy", + "external_url": "https://github.com/CFCoLearning/FullStack-Compass/blob/main/Hoshino_FullStack_Compass.md", + "attributes": [ + { + "trait_type": "项目类型", + "value": "前端开发" + }, + { + "trait_type": "完成日期", + "value": "2025-02-23", + "display_type": "date" + }, + { + "trait_type": "难度", + "value": "高级" + }, + { + "trait_type": "技能", + "value": "TypeScript" + }, + { + "trait_type": "技能", + "value": "React" + }, + { + "trait_type": "技能", + "value": "NextJS" + }, + { + "trait_type": "技能", + "value": "TailwindCSS" + }, + { + "trait_type": "成就", + "value": "全栈开发者" + } + ], + "badge": { + "color": "#8B5CF6", + "icon": "🚀", + "title": "全栈工程师" + }, + "properties": { + "project_id": "FullStack-Compass", + "completion_score": 92, + "certificate_number": "CFC-2025-FC-0001" + } +} \ No newline at end of file diff --git a/metadata-examples/missing-semester.json b/metadata-examples/missing-semester.json new file mode 100644 index 0000000..475e5b7 --- /dev/null +++ b/metadata-examples/missing-semester.json @@ -0,0 +1,51 @@ +{ + "name": "The Missing Semester - 完成证明", + "description": "此NFT证明持有者已成功完成'计算机教育中缺失的一课'共学项目,掌握了包括命令行工具、版本控制、数据处理等计算机实用技能。", + "image": "ipfs://bafybeideuo72rovxug3niusxtb6s2zpsobllqrc2riffx7ic6pirl75goy", + "external_url": "https://github.com/CFCoLearning/The-Missing-Semester/blob/main/Hoshino_Epoch1.md", + "attributes": [ + { + "trait_type": "项目类型", + "value": "计算工具" + }, + { + "trait_type": "完成日期", + "value": "2025-01-26", + "display_type": "date" + }, + { + "trait_type": "难度", + "value": "中级" + }, + { + "trait_type": "技能", + "value": "命令行" + }, + { + "trait_type": "技能", + "value": "版本控制" + }, + { + "trait_type": "技能", + "value": "自动化" + }, + { + "trait_type": "技能", + "value": "现代工作流" + }, + { + "trait_type": "成就", + "value": "计算能力提升" + } + ], + "badge": { + "color": "#3B82F6", + "icon": "🔧", + "title": "工具达人" + }, + "properties": { + "project_id": "The-Missing-Semester", + "completion_score": 85, + "certificate_number": "CFC-2025-MS-0001" + } +} \ No newline at end of file diff --git a/scripts/deploy-integrated-contracts.js b/scripts/deploy-integrated-contracts.js new file mode 100644 index 0000000..b26d0e4 --- /dev/null +++ b/scripts/deploy-integrated-contracts.js @@ -0,0 +1,78 @@ +// 部署共学系统合约 + +const hre = require("hardhat"); + +async function main() { + console.log("开始部署共学系统合约..."); + + // 部署CFC代币合约 + console.log("1. 部署CFC代币合约..."); + const CFCToken = await hre.ethers.getContractFactory("CFCToken"); + const cfcToken = await CFCToken.deploy(); + await cfcToken.deployed(); + console.log(` ✅ CFC代币合约已部署到地址: ${cfcToken.address}`); + + // 部署NFT合约 + console.log("2. 部署NFT合约..."); + const CFCNFT = await hre.ethers.getContractFactory("CFCNFT"); + const cfcNFT = await CFCNFT.deploy(); + await cfcNFT.deployed(); + console.log(` ✅ NFT合约已部署到地址: ${cfcNFT.address}`); + + // 部署共学业务合约 + console.log("3. 部署共学业务合约..."); + const CFCoLearning = await hre.ethers.getContractFactory("CFCoLearning"); + const cfCoLearning = await CFCoLearning.deploy(cfcToken.address, cfcNFT.address); + await cfCoLearning.deployed(); + console.log(` ✅ 共学业务合约已部署到地址: ${cfCoLearning.address}`); + + // 设置合约之间的关联 + console.log("4. 配置合约关联..."); + + // 给共学业务合约授权铸造CFC代币的权限 + console.log(" 授予共学业务合约代币铸造权限..."); + const grantMinterRoleTx = await cfcToken.mint(cfCoLearning.address, 0); // 铸造0个代币,只是为了测试权限 + await grantMinterRoleTx.wait(); + console.log(" ✅ 已授予共学业务合约代币铸造权限"); + + // 给共学业务合约授权铸造NFT的权限 + console.log(" 授予共学业务合约NFT铸造权限..."); + // 注意:这里需要修改CFCNFT合约,添加一个设置铸造权限的方法 + // 由于当前CFCNFT合约只有owner可以铸造,所以需要将CFCNFT的所有权转移给CFCoLearning + const transferOwnershipTx = await cfcNFT.transferOwnership(cfCoLearning.address); + await transferOwnershipTx.wait(); + console.log(" ✅ 已转移NFT合约所有权给共学业务合约"); + + // 添加示例项目 + console.log("5. 添加示例项目..."); + + // 当前时间 + const now = Math.floor(Date.now() / 1000); + const oneWeek = 60 * 60 * 24 * 7; + + // 添加示例项目到共学业务合约 + const createProjectTx = await cfCoLearning.createProject( + "全栈开发学习路径", + cfCoLearning.address, // 项目创建者为合约本身 + now, + now + (oneWeek * 4), // 4周后结束 + 2, // 每7天允许2次缺勤 + hre.ethers.utils.parseEther("100"), // 100 CFC代币奖励 + "ipfs://QmSampleProjectTemplate/fullstack-project-template.json" // 项目NFT模板URI + ); + await createProjectTx.wait(); + console.log(" ✅ 已添加示例项目"); + + console.log("\n📝 部署摘要:"); + console.log(`CFC代币合约: ${cfcToken.address}`); + console.log(`NFT合约: ${cfcNFT.address}`); + console.log(`共学业务合约: ${cfCoLearning.address}`); + console.log("\n部署与设置完成! 🎉"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file