From bc3a53f033098e4e8dcf7147ea8d142c88784573 Mon Sep 17 00:00:00 2001 From: Duc Minh Date: Fri, 28 Mar 2025 11:30:36 +0700 Subject: [PATCH 01/14] Register for OpenGuild Lost Tribes Challenges --- challenge-1-vesting/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/challenge-1-vesting/README.md b/challenge-1-vesting/README.md index 9cc7a2c..f5a0d0c 100644 --- a/challenge-1-vesting/README.md +++ b/challenge-1-vesting/README.md @@ -8,9 +8,9 @@ OpenGuild Labs makes the repository to introduce OpenHack workshop participants Add your information to the below list to officially participate in the workshop challenge (This is the first mission of the whole workshop) -| Emoji | Name | Github Username | Occupations | -| ----- | ---- | ------------------------------------- | ----------- | -| 🎅 | Ippo | [NTP-996](https://github.com/NTP-996) | DevRel | +| Emoji | Name | Github Username | Occupations | +| ----- | --------- | ------------------------------------------- | ------------ | +| ❄️ | Đức Minh | [DxcMint868](https://github.com/DxcMint868) | Unemployed | ## 💻 Local development environment setup From 1c17223baa83605485f0b89e5f73f6d768f53caa Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Fri, 28 Mar 2025 16:59:02 +0700 Subject: [PATCH 02/14] feat(challenge-1): implement token vesting contract --- .../contracts/TokenVesting.sol | 120 ++++++++++++++++-- 1 file changed, 107 insertions(+), 13 deletions(-) diff --git a/challenge-1-vesting/contracts/TokenVesting.sol b/challenge-1-vesting/contracts/TokenVesting.sol index 43d4c3a..74e4c62 100644 --- a/challenge-1-vesting/contracts/TokenVesting.sol +++ b/challenge-1-vesting/contracts/TokenVesting.sol @@ -29,19 +29,27 @@ import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { - struct VestingSchedule { // TODO: Define the vesting schedule struct + struct VestingSchedule { + uint256 totalAmount; + uint256 startTime; + uint256 cliffDuration; + uint256 vestDuration; + uint256 amountClaimed; + bool revoked; } // Token being vested // TODO: Add state variables - + address public token; // Mapping from beneficiary to vesting schedule // TODO: Add state variables + mapping(address => VestingSchedule) public vestingSchedules; // Whitelist of beneficiaries // TODO: Add state variables + mapping(address => bool) public whitelist; // Events event VestingScheduleCreated(address indexed beneficiary, uint256 amount); @@ -50,9 +58,9 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { event BeneficiaryWhitelisted(address indexed beneficiary); event BeneficiaryRemovedFromWhitelist(address indexed beneficiary); - constructor(address tokenAddress) { - // TODO: Initialize the contract - + constructor(address _tokenAddress) { + // TODO: Initialize the contract + token = _tokenAddress; } // Modifier to check if beneficiary is whitelisted @@ -76,25 +84,111 @@ contract TokenVesting is Ownable(msg.sender), Pausable, ReentrancyGuard { address beneficiary, uint256 amount, uint256 cliffDuration, - uint256 vestingDuration, + uint256 vestDuration, uint256 startTime - ) external onlyOwner onlyWhitelisted(beneficiary) whenNotPaused { + ) + external + onlyOwner + onlyWhitelisted(beneficiary) + whenNotPaused + nonReentrant + { // TODO: Implement vesting schedule creation + require( + startTime > block.timestamp, + "Vesting schedule must start in the future" + ); + require(amount > 0, "What are you even trying to vest?"); + + require( + vestDuration > 0, + "Vest duration must be greater than 0, or this contract will miserably fail, bro" + ); + + VestingSchedule memory schedule = VestingSchedule( + amount, + startTime, + cliffDuration, + vestDuration, + 0, + false + ); + + vestingSchedules[beneficiary] = schedule; + + IERC20(token).transferFrom(owner(), address(this), amount); + + emit VestingScheduleCreated(beneficiary, amount); } function calculateVestedAmount( address beneficiary ) public view returns (uint256) { - // TODO: Implement vested amount calculation + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + require(schedule.totalAmount > 0, "Vesting schedule not found"); + + uint256 currTimestamp = block.timestamp; + uint256 cliffEndTimestamp = schedule.startTime + schedule.cliffDuration; + + // If current time is before the cliff, return 0 + if (currTimestamp <= cliffEndTimestamp) { + return 0; + } + + // If vesting is fully completed, return total vested amount + if (currTimestamp >= schedule.startTime + schedule.vestDuration) { + return schedule.totalAmount; + } + + // Calculate vested amount + uint256 vestedTime = currTimestamp - schedule.startTime; + uint256 vestedAmount = (schedule.totalAmount * vestedTime) / + schedule.vestDuration; + + return vestedAmount; } - function claimVestedTokens() external nonReentrant whenNotPaused { - // TODO: Implement token claiming + function claimVestedTokens() external whenNotPaused nonReentrant { + // TODO: Implement token claiming + address beneficiary = _msgSender(); + + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + require(!schedule.revoked, "Vesting schedule revoked"); + + uint256 vestedAmount = calculateVestedAmount(beneficiary); + uint256 claimableAmount = vestedAmount - schedule.amountClaimed; + if (claimableAmount == 0) { + revert("No tokens to claim"); + } + + vestingSchedules[beneficiary].amountClaimed = vestedAmount; + emit TokensClaimed(beneficiary, claimableAmount); + + IERC20(token).transfer(beneficiary, claimableAmount); } - function revokeVesting(address beneficiary) external onlyOwner { + function revokeVesting( + address beneficiary + ) external onlyOwner nonReentrant { // TODO: Implement vesting revocation - + VestingSchedule memory schedule = vestingSchedules[beneficiary]; + require(!schedule.revoked, "Already revoked"); + + schedule.revoked = true; + vestingSchedules[beneficiary] = schedule; + emit VestingRevoked(beneficiary); + + uint256 vestedAmount = calculateVestedAmount(beneficiary); + uint256 unclaimedAmount = vestedAmount - schedule.amountClaimed; + if (unclaimedAmount > 0) { + IERC20(token).transfer(beneficiary, unclaimedAmount); + } + if (schedule.totalAmount - vestedAmount > 0) { + IERC20(token).transfer( + owner(), + schedule.totalAmount - vestedAmount + ); + } } function pause() external onlyOwner { @@ -145,4 +239,4 @@ Solution template (key points to implement): - Calculate and transfer unvested tokens back - Mark schedule as revoked - Emit event -*/ \ No newline at end of file +*/ From 57cae1b957d375dfc3a94c99ddb8162321484734 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Fri, 28 Mar 2025 20:26:56 +0700 Subject: [PATCH 03/14] chore: add 'dotenv' as dev dependency --- challenge-2-yield-farm/package-lock.json | 14 ++++++++++++++ challenge-2-yield-farm/package.json | 1 + 2 files changed, 15 insertions(+) diff --git a/challenge-2-yield-farm/package-lock.json b/challenge-2-yield-farm/package-lock.json index 4cbdfb0..72d0088 100644 --- a/challenge-2-yield-farm/package-lock.json +++ b/challenge-2-yield-farm/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "dotenv": "^16.4.7", "hardhat": "^2.22.17" } }, @@ -3383,6 +3384,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", diff --git a/challenge-2-yield-farm/package.json b/challenge-2-yield-farm/package.json index 5366ac8..910b509 100644 --- a/challenge-2-yield-farm/package.json +++ b/challenge-2-yield-farm/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "dotenv": "^16.4.7", "hardhat": "^2.22.17" }, "dependencies": { From cc34ec43a35a55a0808a9fd802ab8c2392d7d4bf Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Fri, 28 Mar 2025 21:55:49 +0700 Subject: [PATCH 04/14] feat(challenge-2): implement yield farming contract --- challenge-2-yield-farm/contracts/yeild.sol | 115 +++++++++++++++++++-- 1 file changed, 108 insertions(+), 7 deletions(-) diff --git a/challenge-2-yield-farm/contracts/yeild.sol b/challenge-2-yield-farm/contracts/yeild.sol index 421496a..84fb02a 100644 --- a/challenge-2-yield-farm/contracts/yeild.sol +++ b/challenge-2-yield-farm/contracts/yeild.sol @@ -71,10 +71,14 @@ contract YieldFarm is ReentrancyGuard, Ownable { uint256 _rewardRate ) Ownable(msg.sender) { // TODO: Initialize contract state + lpToken = IERC20(_lpToken); + rewardToken = IERC20(_rewardToken); + rewardRate = _rewardRate; + lastUpdateTime = block.timestamp; } function updateReward(address _user) internal { - rewardPerTokenStored = rewardPerToken(); + rewardPerTokenStored = rewardPerToken(); // get the current reward per token lastUpdateTime = block.timestamp; if (_user != address(0)) { @@ -90,6 +94,15 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Calculate rewards since last update // 2. Apply boost multiplier // 3. Return total pending rewards + if (totalStaked == 0) { + return 0; + } + + uint256 timeDelta = block.timestamp - lastUpdateTime; + return + rewardPerTokenStored + + (timeDelta * rewardRate * 1e18) / + totalStaked; } function earned(address _user) public view returns (uint256) { @@ -98,6 +111,14 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Calculate rewards since last update // 2. Apply boost multiplier // 3. Return total pending rewards + UserInfo memory user = userInfo[_user]; + uint256 boostMultiplier = calculateBoostMultiplier(_user); + uint256 newRewardPerToken = rewardPerToken(); + uint256 newReward = (user.amount * newRewardPerToken) / + 1e18 - + user.rewardDebt; + + return (newReward * boostMultiplier) / 100; } /** @@ -105,12 +126,32 @@ contract YieldFarm is ReentrancyGuard, Ownable { * @param _amount Amount of LP tokens to stake */ function stake(uint256 _amount) external nonReentrant { - // TODO: Implement staking logic - // Requirements: - // 1. Update rewards - // 2. Transfer LP tokens from user - // 3. Update user info and total staked amount - // 4. Emit Staked event + require(_amount > 0, "Cannot stake 0"); + + address userAddress = msg.sender; + UserInfo storage user = userInfo[userAddress]; + + // Update user's staking info + if (user.amount == 0) { + // First time staking, set start time for boost calculation + user.startTime = block.timestamp; + } + + uint256 claimableRewardTokens = earned(userAddress); + if (claimableRewardTokens > 0) { + rewardToken.transfer(userAddress, claimableRewardTokens); + } + + // Transfer LP tokens from user to contract + lpToken.transferFrom(userAddress, address(this), _amount); + + // Update user's staked amount and total staked in contract + updateReward(userAddress); + + user.amount += _amount; + totalStaked += _amount; + + emit Staked(userAddress, _amount); } /** @@ -124,6 +165,30 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 2. Transfer LP tokens to user // 3. Update user info and total staked amount // 4. Emit Withdrawn event + + address userAddr = msg.sender; + UserInfo storage user = userInfo[userAddr]; + + require(user.amount >= _amount, "Insufficient balance"); + + uint256 claimableRewardTokens = earned(userAddr); + if (claimableRewardTokens > 0) { + user.pendingRewards = 0; + rewardToken.transfer(userAddr, claimableRewardTokens); + } + + updateReward(userAddr); + + totalStaked -= _amount; + user.amount -= _amount; + + if (user.amount == 0) { + user.startTime = 0; + } + + emit Withdrawn(userAddr, _amount); + + lpToken.transfer(userAddr, _amount); } /** @@ -136,6 +201,14 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 2. Transfer rewards to user // 3. Update user reward debt // 4. Emit RewardsClaimed event + address userAddr = msg.sender; + + uint256 claimableRewardTokens = earned(userAddr); + if (claimableRewardTokens > 0) { + updateReward(userAddr); + rewardToken.transfer(userAddr, claimableRewardTokens); + emit RewardsClaimed(userAddr, claimableRewardTokens); + } } /** @@ -147,6 +220,18 @@ contract YieldFarm is ReentrancyGuard, Ownable { // 1. Transfer all LP tokens back to user // 2. Reset user info // 3. Emit EmergencyWithdrawn event + address userAddr = msg.sender; + UserInfo storage user = userInfo[userAddr]; + + lpToken.transfer(userAddr, user.amount); + + totalStaked -= user.amount; + user.amount = 0; + user.startTime = 0; + user.rewardDebt = 0; + user.pendingRewards = 0; + + emit EmergencyWithdrawn(userAddr, user.amount); } /** @@ -161,6 +246,17 @@ contract YieldFarm is ReentrancyGuard, Ownable { // Requirements: // 1. Calculate staking duration // 2. Return appropriate multiplier based on duration thresholds + UserInfo memory user = userInfo[_user]; + uint256 stakedDuration = block.timestamp - user.startTime; + if (stakedDuration > BOOST_THRESHOLD_3) { + return 200; + } else if (stakedDuration > BOOST_THRESHOLD_2) { + return 150; + } else if (stakedDuration > BOOST_THRESHOLD_1) { + return 125; + } else { + return 100; + } } /** @@ -172,6 +268,11 @@ contract YieldFarm is ReentrancyGuard, Ownable { // Requirements: // 1. Update rewards before changing rate // 2. Set new reward rate + // Update rewards before changing rate + updateReward(address(0)); + + // Set new reward rate + rewardRate = _newRate; } /** From 9c5d48e93a2ea41e6f8a4aa1f36e8b59b37ad2e6 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:44:21 +0700 Subject: [PATCH 05/14] feat(challenge-3): misc refactor, add sepolia support for testing --- challenge-3-frontend/app/providers.tsx | 76 ++++++++------------------ 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/challenge-3-frontend/app/providers.tsx b/challenge-3-frontend/app/providers.tsx index 91243fe..f375659 100644 --- a/challenge-3-frontend/app/providers.tsx +++ b/challenge-3-frontend/app/providers.tsx @@ -1,94 +1,68 @@ -'use client'; +"use client"; -import * as React from 'react'; +import * as React from "react"; import { RainbowKitProvider, getDefaultWallets, getDefaultConfig, -} from '@rainbow-me/rainbowkit'; +} from "@rainbow-me/rainbowkit"; import { phantomWallet, trustWallet, ledgerWallet, -} from '@rainbow-me/rainbowkit/wallets'; -import { - manta, - moonbaseAlpha, - moonbeam -} from 'wagmi/chains'; -import { defineChain } from 'viem'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { WagmiProvider, http, createConfig } from 'wagmi'; -import { Provider as JotaiProvider } from 'jotai'; -// import according to docs +} from "@rainbow-me/rainbowkit/wallets"; +import { sepolia, manta, moonbaseAlpha, moonbeam } from "wagmi/chains"; +import { defineChain } from "viem"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WagmiProvider, http } from "wagmi"; +import { Provider as JotaiProvider } from "jotai"; export const westendAssetHub = defineChain({ id: 420420421, name: "Westend AssetHub", nativeCurrency: { decimals: 18, - name: 'Westend', - symbol: 'WND', + name: "Westend", + symbol: "WND", }, rpcUrls: { default: { - http: ['https://westend-asset-hub-eth-rpc.polkadot.io'], - webSocket: ['wss://westend-asset-hub-eth-rpc.polkadot.io'], + http: ["https://westend-asset-hub-eth-rpc.polkadot.io"], + webSocket: ["wss://westend-asset-hub-eth-rpc.polkadot.io"], }, }, blockExplorers: { - default: { name: 'Explorer', url: 'https://assethub-westend.subscan.io' }, + default: { name: "Explorer", url: "https://assethub-westend.subscan.io" }, }, contracts: { multicall3: { - address: '0x5545dec97cb957e83d3e6a1e82fabfacf9764cf1', + address: "0x5545dec97cb957e83d3e6a1e82fabfacf9764cf1", blockCreated: 10174702, }, }, -}) - -export const localConfig = createConfig({ - chains: [ - westendAssetHub, - manta, - moonbaseAlpha, - moonbeam, - ], - transports: { - [westendAssetHub.id]: http(), - [manta.id]: http(), - [moonbaseAlpha.id]: http(), - [moonbeam.id]: http(), - }, - ssr: true, }); const { wallets } = getDefaultWallets(); -// initialize and destructure wallets object -const config = getDefaultConfig({ - appName: "DOTUI", // Name your app - projectId: "ddf8cf3ee0013535c3760d4c79c9c8b9", // Enter your WalletConnect Project ID here +const localConfig = getDefaultConfig({ + appName: "pkVester", // Name your app + projectId: "ed53c978c176ff8e0e1c463760d1bd75", // Enter your WalletConnect Project ID here wallets: [ ...wallets, { - groupName: 'Other', + groupName: "Other", wallets: [phantomWallet, trustWallet, ledgerWallet], }, ], - chains: [ - westendAssetHub, - moonbeam, - moonbaseAlpha, - manta - ], + chains: [westendAssetHub, moonbeam, moonbaseAlpha, manta, sepolia], transports: { [westendAssetHub.id]: http(), [moonbeam.id]: http(), [moonbaseAlpha.id]: http(), [manta.id]: http(), + [sepolia.id]: http(), }, - ssr: true, // Because it is Nextjs's App router, you need to declare ssr as true + ssr: true, }); const queryClient = new QueryClient(); @@ -96,11 +70,9 @@ const queryClient = new QueryClient(); export function Providers({ children }: { children: React.ReactNode }) { return ( - + - - {children} - + {children} From 437b80f18172016af0b765ba40a9cdbfecf8c010 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:44:52 +0700 Subject: [PATCH 06/14] chore(challenge-3): add Radix UI dropdown, icons, and progress components --- challenge-3-frontend/package-lock.json | 441 +++++++++++++++++++++++++ challenge-3-frontend/package.json | 3 + 2 files changed, 444 insertions(+) diff --git a/challenge-3-frontend/package-lock.json b/challenge-3-frontend/package-lock.json index dec4c63..be75dff 100644 --- a/challenge-3-frontend/package-lock.json +++ b/challenge-3-frontend/package-lock.json @@ -10,7 +10,10 @@ "dependencies": { "@hookform/resolvers": "^3.9.1", "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", @@ -1274,6 +1277,76 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", @@ -1314,6 +1387,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -1355,6 +1437,300 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", @@ -1458,6 +1834,71 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", diff --git a/challenge-3-frontend/package.json b/challenge-3-frontend/package.json index f4b780e..407d534 100644 --- a/challenge-3-frontend/package.json +++ b/challenge-3-frontend/package.json @@ -11,7 +11,10 @@ "dependencies": { "@hookform/resolvers": "^3.9.1", "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", From d3e1f9b5a4cebc5dba169ea57de89561b2feb265 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:45:52 +0700 Subject: [PATCH 07/14] chore(challenge-3): add Token and TokenVesting abi --- challenge-3-frontend/abis/Token.json | 353 +++++++++++++++++ challenge-3-frontend/abis/TokenVesting.json | 415 ++++++++++++++++++++ 2 files changed, 768 insertions(+) create mode 100644 challenge-3-frontend/abis/Token.json create mode 100644 challenge-3-frontend/abis/TokenVesting.json diff --git a/challenge-3-frontend/abis/Token.json b/challenge-3-frontend/abis/Token.json new file mode 100644 index 0000000..dbf2ae6 --- /dev/null +++ b/challenge-3-frontend/abis/Token.json @@ -0,0 +1,353 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "MockERC20", + "sourceName": "contracts/token.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "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" + }, + { + "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": "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": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "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": "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" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b5060405162000a6c38038062000a6c833981016040819052620000349162000123565b818160036200004483826200021c565b5060046200005382826200021c565b5050505050620002e8565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200008657600080fd5b81516001600160401b0380821115620000a357620000a36200005e565b604051601f8301601f19908116603f01168101908282118183101715620000ce57620000ce6200005e565b81604052838152602092508683858801011115620000eb57600080fd5b600091505b838210156200010f5785820183015181830184015290820190620000f0565b600093810190920192909252949350505050565b600080604083850312156200013757600080fd5b82516001600160401b03808211156200014f57600080fd5b6200015d8683870162000074565b935060208501519150808211156200017457600080fd5b50620001838582860162000074565b9150509250929050565b600181811c90821680620001a257607f821691505b602082108103620001c357634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200021757600081815260208120601f850160051c81016020861015620001f25750805b601f850160051c820191505b818110156200021357828155600101620001fe565b5050505b505050565b81516001600160401b038111156200023857620002386200005e565b62000250816200024984546200018d565b84620001c9565b602080601f8311600181146200028857600084156200026f5750858301515b600019600386901b1c1916600185901b17855562000213565b600085815260208120601f198616915b82811015620002b95788860151825594840194600190910190840162000298565b5085821015620002d85787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b61077480620002f86000396000f3fe608060405234801561001057600080fd5b506004361061008e5760003560e01c806306fdde0314610093578063095ea7b3146100b157806318160ddd146100d457806323b872dd146100e6578063313ce567146100f957806340c10f191461010857806370a082311461011d57806395d89b4114610146578063a9059cbb1461014e578063dd62ed3e14610161575b600080fd5b61009b610174565b6040516100a89190610589565b60405180910390f35b6100c46100bf3660046105f3565b610206565b60405190151581526020016100a8565b6002545b6040519081526020016100a8565b6100c46100f436600461061d565b610220565b604051601281526020016100a8565b61011b6101163660046105f3565b610244565b005b6100d861012b366004610659565b6001600160a01b031660009081526020819052604090205490565b61009b610252565b6100c461015c3660046105f3565b610261565b6100d861016f36600461067b565b61026f565b606060038054610183906106ae565b80601f01602080910402602001604051908101604052809291908181526020018280546101af906106ae565b80156101fc5780601f106101d1576101008083540402835291602001916101fc565b820191906000526020600020905b8154815290600101906020018083116101df57829003601f168201915b5050505050905090565b60003361021481858561029a565b60019150505b92915050565b60003361022e8582856102ac565b610239858585610308565b506001949350505050565b61024e8282610367565b5050565b606060048054610183906106ae565b600033610214818585610308565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6102a7838383600161039d565b505050565b60006102b8848461026f565b9050600019811461030257818110156102f357828183604051637dc7a0d960e11b81526004016102ea939291906106e8565b60405180910390fd5b6103028484848403600061039d565b50505050565b6001600160a01b038316610332576000604051634b637e8f60e11b81526004016102ea9190610709565b6001600160a01b03821661035c57600060405163ec442f0560e01b81526004016102ea9190610709565b6102a7838383610472565b6001600160a01b03821661039157600060405163ec442f0560e01b81526004016102ea9190610709565b61024e60008383610472565b6001600160a01b0384166103c757600060405163e602df0560e01b81526004016102ea9190610709565b6001600160a01b0383166103f1576000604051634a1406b160e11b81526004016102ea9190610709565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561030257826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161046491815260200190565b60405180910390a350505050565b6001600160a01b03831661049d578060026000828254610492919061071d565b909155506104fc9050565b6001600160a01b038316600090815260208190526040902054818110156104dd5783818360405163391434e360e21b81526004016102ea939291906106e8565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661051857600280548290039055610537565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161057c91815260200190565b60405180910390a3505050565b600060208083528351808285015260005b818110156105b65785810183015185820160400152820161059a565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b03811681146105ee57600080fd5b919050565b6000806040838503121561060657600080fd5b61060f836105d7565b946020939093013593505050565b60008060006060848603121561063257600080fd5b61063b846105d7565b9250610649602085016105d7565b9150604084013590509250925092565b60006020828403121561066b57600080fd5b610674826105d7565b9392505050565b6000806040838503121561068e57600080fd5b610697836105d7565b91506106a5602084016105d7565b90509250929050565b600181811c908216806106c257607f821691505b6020821081036106e257634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561021a57634e487b7160e01b600052601160045260246000fdfea26469706673582212209347d21ac20032ee3f25505032ec818f7c691d846517784428650571f95574d964736f6c63430008140033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061008e5760003560e01c806306fdde0314610093578063095ea7b3146100b157806318160ddd146100d457806323b872dd146100e6578063313ce567146100f957806340c10f191461010857806370a082311461011d57806395d89b4114610146578063a9059cbb1461014e578063dd62ed3e14610161575b600080fd5b61009b610174565b6040516100a89190610589565b60405180910390f35b6100c46100bf3660046105f3565b610206565b60405190151581526020016100a8565b6002545b6040519081526020016100a8565b6100c46100f436600461061d565b610220565b604051601281526020016100a8565b61011b6101163660046105f3565b610244565b005b6100d861012b366004610659565b6001600160a01b031660009081526020819052604090205490565b61009b610252565b6100c461015c3660046105f3565b610261565b6100d861016f36600461067b565b61026f565b606060038054610183906106ae565b80601f01602080910402602001604051908101604052809291908181526020018280546101af906106ae565b80156101fc5780601f106101d1576101008083540402835291602001916101fc565b820191906000526020600020905b8154815290600101906020018083116101df57829003601f168201915b5050505050905090565b60003361021481858561029a565b60019150505b92915050565b60003361022e8582856102ac565b610239858585610308565b506001949350505050565b61024e8282610367565b5050565b606060048054610183906106ae565b600033610214818585610308565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6102a7838383600161039d565b505050565b60006102b8848461026f565b9050600019811461030257818110156102f357828183604051637dc7a0d960e11b81526004016102ea939291906106e8565b60405180910390fd5b6103028484848403600061039d565b50505050565b6001600160a01b038316610332576000604051634b637e8f60e11b81526004016102ea9190610709565b6001600160a01b03821661035c57600060405163ec442f0560e01b81526004016102ea9190610709565b6102a7838383610472565b6001600160a01b03821661039157600060405163ec442f0560e01b81526004016102ea9190610709565b61024e60008383610472565b6001600160a01b0384166103c757600060405163e602df0560e01b81526004016102ea9190610709565b6001600160a01b0383166103f1576000604051634a1406b160e11b81526004016102ea9190610709565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561030257826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161046491815260200190565b60405180910390a350505050565b6001600160a01b03831661049d578060026000828254610492919061071d565b909155506104fc9050565b6001600160a01b038316600090815260208190526040902054818110156104dd5783818360405163391434e360e21b81526004016102ea939291906106e8565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b03821661051857600280548290039055610537565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161057c91815260200190565b60405180910390a3505050565b600060208083528351808285015260005b818110156105b65785810183015185820160400152820161059a565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b03811681146105ee57600080fd5b919050565b6000806040838503121561060657600080fd5b61060f836105d7565b946020939093013593505050565b60008060006060848603121561063257600080fd5b61063b846105d7565b9250610649602085016105d7565b9150604084013590509250925092565b60006020828403121561066b57600080fd5b610674826105d7565b9392505050565b6000806040838503121561068e57600080fd5b610697836105d7565b91506106a5602084016105d7565b90509250929050565b600181811c908216806106c257607f821691505b6020821081036106e257634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561021a57634e487b7160e01b600052601160045260246000fdfea26469706673582212209347d21ac20032ee3f25505032ec818f7c691d846517784428650571f95574d964736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/challenge-3-frontend/abis/TokenVesting.json b/challenge-3-frontend/abis/TokenVesting.json new file mode 100644 index 0000000..e4cea15 --- /dev/null +++ b/challenge-3-frontend/abis/TokenVesting.json @@ -0,0 +1,415 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TokenVesting", + "sourceName": "contracts/TokenVesting.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "BeneficiaryRemovedFromWhitelist", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "BeneficiaryWhitelisted", + "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": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "VestingRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "VestingScheduleCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "addToWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "calculateVestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimVestedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cliffDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vestDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + } + ], + "name": "createVestingSchedule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "removeFromWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "revokeVesting", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "vestingSchedules", + "outputs": [ + { + "internalType": "uint256", + "name": "totalAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cliffDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vestDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountClaimed", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "revoked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b5060405161112638038061112683398101604081905261002f916100e5565b338061005557604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b61005e81610095565b506000805460ff60a01b1916905560018055600280546001600160a01b0319166001600160a01b0392909216919091179055610115565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000602082840312156100f757600080fd5b81516001600160a01b038116811461010e57600080fd5b9392505050565b611002806101246000396000f3fe608060405234801561001057600080fd5b50600436106100c55760003560e01c80631bf0b08b146100ca5780633b0da260146100df5780633f4ba83a146100f25780635c975abb146100fa578063715018a6146101175780638456cb591461011f5780638ab1d681146101275780638da5cb5b1461013a5780639b19251a1461014f578063e43252d714610172578063e74f3fbb14610185578063f2fde38b1461018d578063fc0c546a146101a0578063fdb20ccb146101b3578063ffa06b2a1461022b575b600080fd5b6100dd6100d8366004610e9e565b61024c565b005b6100dd6100ed366004610ee0565b6105c3565b6100dd61084f565b610102610861565b60405190151581526020015b60405180910390f35b6100dd610871565b6100dd610883565b6100dd610135366004610ee0565b610893565b6101426108e4565b60405161010e9190610f02565b61010261015d366004610ee0565b60046020526000908152604090205460ff1681565b6100dd610180366004610ee0565b6108f3565b6100dd61098f565b6100dd61019b366004610ee0565b610b82565b600254610142906001600160a01b031681565b6101fc6101c1366004610ee0565b600360208190526000918252604090912080546001820154600283015493830154600484015460059094015492949193919290919060ff1686565b6040805196875260208701959095529385019290925260608401526080830152151560a082015260c00161010e565b61023e610239366004610ee0565b610bbd565b60405190815260200161010e565b610254610cf9565b6001600160a01b038516600090815260046020526040902054859060ff166102c15760405162461bcd60e51b815260206004820152601b60248201527a10995b99599a58da585c9e481b9bdd081dda1a5d195b1a5cdd1959602a1b60448201526064015b60405180910390fd5b6102c9610d2b565b6102d1610d51565b4282116103325760405162461bcd60e51b815260206004820152602960248201527f56657374696e67207363686564756c65206d75737420737461727420696e207460448201526868652066757475726560b81b60648201526084016102b8565b6000851161038c5760405162461bcd60e51b815260206004820152602160248201527f576861742061726520796f75206576656e20747279696e6720746f20766573746044820152603f60f81b60648201526084016102b8565b6000831161041a5760405162461bcd60e51b815260206004820152604f60248201527f56657374206475726174696f6e206d757374206265206772656174657220746860448201527f616e20302c206f72207468697320636f6e74726163742077696c6c206d69736560648201526e7261626c79206661696c2c2062726f60881b608482015260a4016102b8565b60006040518060c00160405280878152602001848152602001868152602001858152602001600081526020016000151581525090508060036000896001600160a01b03166001600160a01b03168152602001908152602001600020600082015181600001556020820151816001015560408201518160020155606082015181600301556080820151816004015560a08201518160050160006101000a81548160ff021916908315150217905550905050600260009054906101000a90046001600160a01b03166001600160a01b03166323b872dd6104f66108e4565b6040516001600160e01b031960e084901b1681526001600160a01b039091166004820152306024820152604481018990526064016020604051808303816000875af1158015610549573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061056d9190610f16565b50866001600160a01b03167f969705509595726740fe60cc30769bbd53c883efff4d8e70108a82817e0392a9876040516105a991815260200190565b60405180910390a2506105bb60018055565b505050505050565b6105cb610cf9565b6105d3610d51565b6001600160a01b038116600090815260036020818152604092839020835160c081018552815481526001820154928101929092526002810154938201939093529082015460608201526004820154608082015260059091015460ff1615801560a08301526106755760405162461bcd60e51b815260206004820152600f60248201526e105b1c9958591e481c995d9bdad959608a1b60448201526064016102b8565b600160a082018181526001600160a01b03841660008181526003602081815260408084208851815591880151968201969096558587015160028201556060870151918101919091556080860151600482015592516005909301805460ff19169315159390931790925591517f68d870ac0aff3819234e8a1fc8f357b40d75212f2dc8594b97690fa205b3bab29190a2600061070f83610bbd565b905060008260800151826107239190610f4e565b905080156107a25760025460405163a9059cbb60e01b81526001600160a01b039091169063a9059cbb9061075d9087908590600401610f67565b6020604051808303816000875af115801561077c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a09190610f16565b505b82516000906107b2908490610f4e565b1115610840576002546001600160a01b031663a9059cbb6107d16108e4565b85516107de908690610f4e565b6040518363ffffffff1660e01b81526004016107fb929190610f67565b6020604051808303816000875af115801561081a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061083e9190610f16565b505b50505061084c60018055565b50565b610857610cf9565b61085f610d7b565b565b600054600160a01b900460ff1690565b610879610cf9565b61085f6000610dca565b61088b610cf9565b61085f610e1a565b61089b610cf9565b6001600160a01b038116600081815260046020526040808220805460ff19169055517f1ecef1b5180dc14b16608c5c5ec1fa28998e2f94e460b91c1b50bdfb643cc1389190a250565b6000546001600160a01b031690565b6108fb610cf9565b6001600160a01b0381166109435760405162461bcd60e51b815260206004820152600f60248201526e496e76616c6964206164647265737360881b60448201526064016102b8565b6001600160a01b038116600081815260046020526040808220805460ff19166001179055517f07e751107375f503d05dfa76b5038ce1c5b7d46e9e45768f913ac333780394399190a250565b610997610d2b565b61099f610d51565b33600081815260036020818152604092839020835160c081018552815481526001820154928101929092526002810154938201939093529082015460608201526004820154608082015260059091015460ff1615801560a0830152610a415760405162461bcd60e51b815260206004820152601860248201527715995cdd1a5b99c81cd8da19591d5b19481c995d9bdad95960421b60448201526064016102b8565b6000610a4c83610bbd565b90506000826080015182610a609190610f4e565b905080600003610aa75760405162461bcd60e51b81526020600482015260126024820152714e6f20746f6b656e7320746f20636c61696d60701b60448201526064016102b8565b6001600160a01b03841660008181526003602052604090819020600401849055517f896e034966eaaf1adc54acc0f257056febbd300c9e47182cf761982cf1f5e43090610af79084815260200190565b60405180910390a260025460405163a9059cbb60e01b81526001600160a01b039091169063a9059cbb90610b319087908590600401610f67565b6020604051808303816000875af1158015610b50573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b749190610f16565b505050505061085f60018055565b610b8a610cf9565b6001600160a01b038116610bb4576000604051631e4fbdf760e01b81526004016102b89190610f02565b61084c81610dca565b6001600160a01b0381166000908152600360208181526040808420815160c08101835281548082526001830154948201949094526002820154928101929092529283015460608201526004830154608082015260059092015460ff16151560a0830152610c695760405162461bcd60e51b815260206004820152601a60248201527915995cdd1a5b99c81cd8da19591d5b19481b9bdd08199bdd5b9960321b60448201526064016102b8565b604081015160208201514291600091610c829190610f80565b9050808211610c9657506000949350505050565b82606001518360200151610caa9190610f80565b8210610cb95750505192915050565b6000836020015183610ccb9190610f4e565b905060008460600151828660000151610ce49190610f93565b610cee9190610faa565b979650505050505050565b33610d026108e4565b6001600160a01b03161461085f573360405163118cdaa760e01b81526004016102b89190610f02565b610d33610861565b1561085f5760405163d93c066560e01b815260040160405180910390fd5b600260015403610d7457604051633ee5aeb560e01b815260040160405180910390fd5b6002600155565b610d83610e5d565b6000805460ff60a01b191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b604051610dc09190610f02565b60405180910390a1565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b610e22610d2b565b6000805460ff60a01b1916600160a01b1790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258610db33390565b610e65610861565b61085f57604051638dfc202b60e01b815260040160405180910390fd5b80356001600160a01b0381168114610e9957600080fd5b919050565b600080600080600060a08688031215610eb657600080fd5b610ebf86610e82565b97602087013597506040870135966060810135965060800135945092505050565b600060208284031215610ef257600080fd5b610efb82610e82565b9392505050565b6001600160a01b0391909116815260200190565b600060208284031215610f2857600080fd5b81518015158114610efb57600080fd5b634e487b7160e01b600052601160045260246000fd5b81810381811115610f6157610f61610f38565b92915050565b6001600160a01b03929092168252602082015260400190565b80820180821115610f6157610f61610f38565b8082028115828204841417610f6157610f61610f38565b600082610fc757634e487b7160e01b600052601260045260246000fd5b50049056fea2646970667358221220c959251d2b14022f03b3227478d3f07bbba6712558cbf46497703f39879c40a464736f6c63430008140033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100c55760003560e01c80631bf0b08b146100ca5780633b0da260146100df5780633f4ba83a146100f25780635c975abb146100fa578063715018a6146101175780638456cb591461011f5780638ab1d681146101275780638da5cb5b1461013a5780639b19251a1461014f578063e43252d714610172578063e74f3fbb14610185578063f2fde38b1461018d578063fc0c546a146101a0578063fdb20ccb146101b3578063ffa06b2a1461022b575b600080fd5b6100dd6100d8366004610e9e565b61024c565b005b6100dd6100ed366004610ee0565b6105c3565b6100dd61084f565b610102610861565b60405190151581526020015b60405180910390f35b6100dd610871565b6100dd610883565b6100dd610135366004610ee0565b610893565b6101426108e4565b60405161010e9190610f02565b61010261015d366004610ee0565b60046020526000908152604090205460ff1681565b6100dd610180366004610ee0565b6108f3565b6100dd61098f565b6100dd61019b366004610ee0565b610b82565b600254610142906001600160a01b031681565b6101fc6101c1366004610ee0565b600360208190526000918252604090912080546001820154600283015493830154600484015460059094015492949193919290919060ff1686565b6040805196875260208701959095529385019290925260608401526080830152151560a082015260c00161010e565b61023e610239366004610ee0565b610bbd565b60405190815260200161010e565b610254610cf9565b6001600160a01b038516600090815260046020526040902054859060ff166102c15760405162461bcd60e51b815260206004820152601b60248201527a10995b99599a58da585c9e481b9bdd081dda1a5d195b1a5cdd1959602a1b60448201526064015b60405180910390fd5b6102c9610d2b565b6102d1610d51565b4282116103325760405162461bcd60e51b815260206004820152602960248201527f56657374696e67207363686564756c65206d75737420737461727420696e207460448201526868652066757475726560b81b60648201526084016102b8565b6000851161038c5760405162461bcd60e51b815260206004820152602160248201527f576861742061726520796f75206576656e20747279696e6720746f20766573746044820152603f60f81b60648201526084016102b8565b6000831161041a5760405162461bcd60e51b815260206004820152604f60248201527f56657374206475726174696f6e206d757374206265206772656174657220746860448201527f616e20302c206f72207468697320636f6e74726163742077696c6c206d69736560648201526e7261626c79206661696c2c2062726f60881b608482015260a4016102b8565b60006040518060c00160405280878152602001848152602001868152602001858152602001600081526020016000151581525090508060036000896001600160a01b03166001600160a01b03168152602001908152602001600020600082015181600001556020820151816001015560408201518160020155606082015181600301556080820151816004015560a08201518160050160006101000a81548160ff021916908315150217905550905050600260009054906101000a90046001600160a01b03166001600160a01b03166323b872dd6104f66108e4565b6040516001600160e01b031960e084901b1681526001600160a01b039091166004820152306024820152604481018990526064016020604051808303816000875af1158015610549573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061056d9190610f16565b50866001600160a01b03167f969705509595726740fe60cc30769bbd53c883efff4d8e70108a82817e0392a9876040516105a991815260200190565b60405180910390a2506105bb60018055565b505050505050565b6105cb610cf9565b6105d3610d51565b6001600160a01b038116600090815260036020818152604092839020835160c081018552815481526001820154928101929092526002810154938201939093529082015460608201526004820154608082015260059091015460ff1615801560a08301526106755760405162461bcd60e51b815260206004820152600f60248201526e105b1c9958591e481c995d9bdad959608a1b60448201526064016102b8565b600160a082018181526001600160a01b03841660008181526003602081815260408084208851815591880151968201969096558587015160028201556060870151918101919091556080860151600482015592516005909301805460ff19169315159390931790925591517f68d870ac0aff3819234e8a1fc8f357b40d75212f2dc8594b97690fa205b3bab29190a2600061070f83610bbd565b905060008260800151826107239190610f4e565b905080156107a25760025460405163a9059cbb60e01b81526001600160a01b039091169063a9059cbb9061075d9087908590600401610f67565b6020604051808303816000875af115801561077c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a09190610f16565b505b82516000906107b2908490610f4e565b1115610840576002546001600160a01b031663a9059cbb6107d16108e4565b85516107de908690610f4e565b6040518363ffffffff1660e01b81526004016107fb929190610f67565b6020604051808303816000875af115801561081a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061083e9190610f16565b505b50505061084c60018055565b50565b610857610cf9565b61085f610d7b565b565b600054600160a01b900460ff1690565b610879610cf9565b61085f6000610dca565b61088b610cf9565b61085f610e1a565b61089b610cf9565b6001600160a01b038116600081815260046020526040808220805460ff19169055517f1ecef1b5180dc14b16608c5c5ec1fa28998e2f94e460b91c1b50bdfb643cc1389190a250565b6000546001600160a01b031690565b6108fb610cf9565b6001600160a01b0381166109435760405162461bcd60e51b815260206004820152600f60248201526e496e76616c6964206164647265737360881b60448201526064016102b8565b6001600160a01b038116600081815260046020526040808220805460ff19166001179055517f07e751107375f503d05dfa76b5038ce1c5b7d46e9e45768f913ac333780394399190a250565b610997610d2b565b61099f610d51565b33600081815260036020818152604092839020835160c081018552815481526001820154928101929092526002810154938201939093529082015460608201526004820154608082015260059091015460ff1615801560a0830152610a415760405162461bcd60e51b815260206004820152601860248201527715995cdd1a5b99c81cd8da19591d5b19481c995d9bdad95960421b60448201526064016102b8565b6000610a4c83610bbd565b90506000826080015182610a609190610f4e565b905080600003610aa75760405162461bcd60e51b81526020600482015260126024820152714e6f20746f6b656e7320746f20636c61696d60701b60448201526064016102b8565b6001600160a01b03841660008181526003602052604090819020600401849055517f896e034966eaaf1adc54acc0f257056febbd300c9e47182cf761982cf1f5e43090610af79084815260200190565b60405180910390a260025460405163a9059cbb60e01b81526001600160a01b039091169063a9059cbb90610b319087908590600401610f67565b6020604051808303816000875af1158015610b50573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b749190610f16565b505050505061085f60018055565b610b8a610cf9565b6001600160a01b038116610bb4576000604051631e4fbdf760e01b81526004016102b89190610f02565b61084c81610dca565b6001600160a01b0381166000908152600360208181526040808420815160c08101835281548082526001830154948201949094526002820154928101929092529283015460608201526004830154608082015260059092015460ff16151560a0830152610c695760405162461bcd60e51b815260206004820152601a60248201527915995cdd1a5b99c81cd8da19591d5b19481b9bdd08199bdd5b9960321b60448201526064016102b8565b604081015160208201514291600091610c829190610f80565b9050808211610c9657506000949350505050565b82606001518360200151610caa9190610f80565b8210610cb95750505192915050565b6000836020015183610ccb9190610f4e565b905060008460600151828660000151610ce49190610f93565b610cee9190610faa565b979650505050505050565b33610d026108e4565b6001600160a01b03161461085f573360405163118cdaa760e01b81526004016102b89190610f02565b610d33610861565b1561085f5760405163d93c066560e01b815260040160405180910390fd5b600260015403610d7457604051633ee5aeb560e01b815260040160405180910390fd5b6002600155565b610d83610e5d565b6000805460ff60a01b191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b604051610dc09190610f02565b60405180910390a1565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b610e22610d2b565b6000805460ff60a01b1916600160a01b1790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258610db33390565b610e65610861565b61085f57604051638dfc202b60e01b815260040160405180910390fd5b80356001600160a01b0381168114610e9957600080fd5b919050565b600080600080600060a08688031215610eb657600080fd5b610ebf86610e82565b97602087013597506040870135966060810135965060800135945092505050565b600060208284031215610ef257600080fd5b610efb82610e82565b9392505050565b6001600160a01b0391909116815260200190565b600060208284031215610f2857600080fd5b81518015158114610efb57600080fd5b634e487b7160e01b600052601160045260246000fd5b81810381811115610f6157610f61610f38565b92915050565b6001600160a01b03929092168252602082015260400190565b80820180821115610f6157610f61610f38565b8082028115828204841417610f6157610f61610f38565b600082610fc757634e487b7160e01b600052601260045260246000fd5b50049056fea2646970667358221220c959251d2b14022f03b3227478d3f07bbba6712558cbf46497703f39879c40a464736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} From 5195c5c64c931cdba8a7ddbeb08c93da3ab16ea5 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:47:47 +0700 Subject: [PATCH 08/14] feat(challenge-3): add link to token vesting portal and update footer links --- challenge-3-frontend/app/page.tsx | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/challenge-3-frontend/app/page.tsx b/challenge-3-frontend/app/page.tsx index fb01185..b438f32 100644 --- a/challenge-3-frontend/app/page.tsx +++ b/challenge-3-frontend/app/page.tsx @@ -26,6 +26,9 @@ export default function Home() {
  • Mint/Redeem LST Bifrost
  • +
  • + Token vesting portal +
  • - Maintained by buildstation.org with support from OpenGuild + Maintained by{" "} + + buildstation.org + {" "} + with support from{" "} + + OpenGuild +
    From bcf784f2093dd8d1a95b0a0f741cf8a1f0f23629 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:50:04 +0700 Subject: [PATCH 09/14] refactor(challenge-3): edit use-toast hook and utils --- challenge-3-frontend/hooks/use-toast.ts | 29 ++++++++++--------------- challenge-3-frontend/lib/utils.ts | 17 +++++++++++---- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/challenge-3-frontend/hooks/use-toast.ts b/challenge-3-frontend/hooks/use-toast.ts index 02e111d..af106b7 100644 --- a/challenge-3-frontend/hooks/use-toast.ts +++ b/challenge-3-frontend/hooks/use-toast.ts @@ -3,15 +3,12 @@ // Inspired by react-hot-toast library import * as React from "react" -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/toast" -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 5 +const TOAST_REMOVE_DELAY = 5000 -type ToasterToast = ToastProps & { +type ToasterToastProps = ToastProps & { id: string title?: React.ReactNode description?: React.ReactNode @@ -37,23 +34,23 @@ type ActionType = typeof actionTypes type Action = | { type: ActionType["ADD_TOAST"] - toast: ToasterToast + toast: ToasterToastProps } | { type: ActionType["UPDATE_TOAST"] - toast: Partial + toast: Partial } | { type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] + toastId?: string } | { type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] + toastId?: string } interface State { - toasts: ToasterToast[] + toasts: ToasterToastProps[] } const toastTimeouts = new Map>() @@ -93,8 +90,6 @@ export const reducer = (state: State, action: Action): State => { case "DISMISS_TOAST": { const { toastId } = action - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId) } else { @@ -129,7 +124,7 @@ export const reducer = (state: State, action: Action): State => { } } -const listeners: Array<(state: State) => void> = [] +const listeners: ((state: State) => void)[] = [] let memoryState: State = { toasts: [] } @@ -140,12 +135,12 @@ function dispatch(action: Action) { }) } -type Toast = Omit +type Toast = Omit function toast({ ...props }: Toast) { const id = genId() - const update = (props: ToasterToast) => + const update = (props: ToasterToastProps) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, diff --git a/challenge-3-frontend/lib/utils.ts b/challenge-3-frontend/lib/utils.ts index d47db6e..be9cb8b 100644 --- a/challenge-3-frontend/lib/utils.ts +++ b/challenge-3-frontend/lib/utils.ts @@ -1,10 +1,19 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } -export function truncateHash(hash: string, startLength: number = 6, endLength: number = 4) { +export function truncateHash( + hash: string, + startLength: number = 6, + endLength: number = 4 +) { return `${hash.slice(0, startLength)}...${hash.slice(-endLength)}`; } + +export function formatDate(timestamp: number | bigint): string { + const date = new Date(Number(timestamp) * 1000); + return date.toLocaleDateString() + " " + date.toLocaleTimeString(); +} From d82ee46fbc0aba3f16cf7d4cf37a1c3a8d28d28a Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:52:23 +0700 Subject: [PATCH 10/14] feat(challenge-3): add new ui components: badge, card, dropdown menu, progress bar, table --- challenge-3-frontend/components/ui/badge.tsx | 36 ++++ challenge-3-frontend/components/ui/card.tsx | 85 ++++++++ .../components/ui/dropdown-menu.tsx | 200 ++++++++++++++++++ .../components/ui/progress.tsx | 28 +++ challenge-3-frontend/components/ui/table.tsx | 117 ++++++++++ 5 files changed, 466 insertions(+) create mode 100644 challenge-3-frontend/components/ui/badge.tsx create mode 100644 challenge-3-frontend/components/ui/card.tsx create mode 100644 challenge-3-frontend/components/ui/dropdown-menu.tsx create mode 100644 challenge-3-frontend/components/ui/progress.tsx create mode 100644 challenge-3-frontend/components/ui/table.tsx diff --git a/challenge-3-frontend/components/ui/badge.tsx b/challenge-3-frontend/components/ui/badge.tsx new file mode 100644 index 0000000..bb6f6a2 --- /dev/null +++ b/challenge-3-frontend/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +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/challenge-3-frontend/components/ui/card.tsx b/challenge-3-frontend/components/ui/card.tsx new file mode 100644 index 0000000..0e899e9 --- /dev/null +++ b/challenge-3-frontend/components/ui/card.tsx @@ -0,0 +1,85 @@ +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< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + 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/challenge-3-frontend/components/ui/dropdown-menu.tsx b/challenge-3-frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/challenge-3-frontend/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/challenge-3-frontend/components/ui/progress.tsx b/challenge-3-frontend/components/ui/progress.tsx new file mode 100644 index 0000000..339efec --- /dev/null +++ b/challenge-3-frontend/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } \ No newline at end of file diff --git a/challenge-3-frontend/components/ui/table.tsx b/challenge-3-frontend/components/ui/table.tsx new file mode 100644 index 0000000..bb0650a --- /dev/null +++ b/challenge-3-frontend/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; From 7dd83a278f7ec26212bc966ef7208f741c2a518f Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:52:47 +0700 Subject: [PATCH 11/14] feat(challenge-3): add TypeScript interfaces for vesting schedules and beneficiary information --- challenge-3-frontend/lib/types.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 challenge-3-frontend/lib/types.ts diff --git a/challenge-3-frontend/lib/types.ts b/challenge-3-frontend/lib/types.ts new file mode 100644 index 0000000..b28dbfb --- /dev/null +++ b/challenge-3-frontend/lib/types.ts @@ -0,0 +1,30 @@ +import { Address } from "viem"; + +export interface VestingSchedule { + totalAmount: bigint; + startTime: bigint; + cliffDuration: bigint; + vestDuration: bigint; + amountClaimed: bigint; + revoked: boolean; +} + +export interface BeneficiaryInfo { + address: Address; + isWhitelisted: boolean; + vestingSchedule?: VestingSchedule; + vestedAmount?: bigint; + claimableAmount?: bigint; +} + +export interface VestingFormData { + beneficiary: string; + amount: string; + cliffDuration: string; + vestDuration: string; + startTimestamp: string; +} + +export interface WhitelistFormData { + beneficiary: string; +} From bdb74cd0f7949f7b69686c910799bfc2b976f296 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:54:48 +0700 Subject: [PATCH 12/14] feat(challenge-3): improve navbar - Add wallet connect functionality directly into navbar - Add href link to token-vesting page to navbar --- challenge-3-frontend/components/navbar.tsx | 155 +++++++++++++++++---- 1 file changed, 130 insertions(+), 25 deletions(-) diff --git a/challenge-3-frontend/components/navbar.tsx b/challenge-3-frontend/components/navbar.tsx index d212169..d96ace8 100644 --- a/challenge-3-frontend/components/navbar.tsx +++ b/challenge-3-frontend/components/navbar.tsx @@ -1,32 +1,137 @@ +"use client"; + import Link from "next/link"; +import { useAccount, useConnect, useDisconnect } from "wagmi"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +// import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { useEffect, useState } from "react"; +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import { Wallet } from "lucide-react"; +import PortfolioCard from "./portfolio-card"; export default function Navbar() { + const { address, isConnected } = useAccount(); + const { disconnect } = useDisconnect(); + const [mounted, setMounted] = useState(false); + + // This effect is to prevent hydration mismatch + useEffect(() => { + setMounted(true); + }, []); + + // Format address for display + const formatAddress = (addr: string) => { + if (!addr) return ""; + return `${addr.slice(0, 6)}...${addr.slice(-4)}`; + }; + + // Get initials for avatar + const getInitials = (addr: string) => { + if (!addr) return ""; + return addr.slice(0, 2); + }; + return ( -
    - - Home - - - Wallet - - - Send transaction - - - Write contract - - - Mint/Redeem LST Bifrost - +
    +
    + + Home + + + Wallet + + + Send Transaction + + + Write Contract + + + Token Vesting + + + Mint/Redeem LST + +
    + + {mounted && ( +
    + {isConnected ? ( + + + + + + My Account + + { + navigator.clipboard.writeText(address || ""); + }} + > + Copy Address + + disconnect()} + > + Disconnect + + + + ) : ( + + {({ + account, + chain, + openAccountModal, + openChainModal, + openConnectModal, + mounted: rainbowMounted, + }) => ( + + )} + + )} +
    + )}
    ); } From fa2657fadca8608a7de500d00af1166eb1d463b0 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:55:13 +0700 Subject: [PATCH 13/14] refactor(challenge-3): use new navbar in /wallet page --- challenge-3-frontend/app/wallet/page.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/challenge-3-frontend/app/wallet/page.tsx b/challenge-3-frontend/app/wallet/page.tsx index b77996d..ae32f98 100644 --- a/challenge-3-frontend/app/wallet/page.tsx +++ b/challenge-3-frontend/app/wallet/page.tsx @@ -1,18 +1,15 @@ "use client"; import SigpassKit from "@/components/sigpasskit"; -import Link from "next/link"; +import Navbar from "@/components/navbar"; export default function WalletPage() { return ( -
    -
    - Home - Wallet - Send transaction - Write contract -
    -

    Wallet

    - +
    + +
    +

    Wallet

    + +
    ); } \ No newline at end of file From 976a6b74e317a2f3f2de63e8fc9a5f03d470c4f8 Mon Sep 17 00:00:00 2001 From: ducmint864 Date: Mon, 31 Mar 2025 01:55:56 +0700 Subject: [PATCH 14/14] feat(challenge-3): implement pages for token vesting from challenge-1 --- .../app/token-vesting/page.tsx | 160 ++++ .../components/token-vesting/admin-panel.tsx | 840 ++++++++++++++++++ .../token-vesting/beneficiary-panel.tsx | 571 ++++++++++++ 3 files changed, 1571 insertions(+) create mode 100644 challenge-3-frontend/app/token-vesting/page.tsx create mode 100644 challenge-3-frontend/components/token-vesting/admin-panel.tsx create mode 100644 challenge-3-frontend/components/token-vesting/beneficiary-panel.tsx diff --git a/challenge-3-frontend/app/token-vesting/page.tsx b/challenge-3-frontend/app/token-vesting/page.tsx new file mode 100644 index 0000000..c653eb2 --- /dev/null +++ b/challenge-3-frontend/app/token-vesting/page.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import Navbar from "@/components/navbar"; +import { Separator } from "@/components/ui/separator"; +import { useAccount, useReadContract } from "wagmi"; +import AdminPanel from "@/components/token-vesting/admin-panel"; +import BeneficiaryPanel from "@/components/token-vesting/beneficiary-panel"; +import { Address } from "viem"; +import { useToast } from "@/hooks/use-toast"; +import { Toaster } from "@/components/ui/toaster"; +import { Button } from "@/components/ui/button"; +import { abi as TokenVestingAbi } from "@/abis/TokenVesting.json"; +// import { westendAssetHub } from "@/app/providers"; +import { sepolia, moonbaseAlpha } from "wagmi/chains"; + +// Simulated contract address - in production this would come from environment variables or a config +const CONTRACT_ADDRESS = + "0xc5BF7a8634721D1366396707E24533C6ac786Fae" as Address; +const TOKEN_ADDRESS = "0xd5954beef69b90978ec667b1fcf696d102dcde97" as Address; + +export default function TokenVestingPage() { + const { address, isConnected } = useAccount(); + const [isAdmin, setIsAdmin] = useState(false); + const { toast } = useToast(); + const { data: owner, error: ownerError } = useReadContract({ + address: CONTRACT_ADDRESS, + abi: TokenVestingAbi, + functionName: "owner", + chainId: sepolia.id, + }); + + useEffect(() => { + if (!address) return; + if (ownerError) { + console.error("Error fetching contract owner", ownerError); + return; + } + console.log("Owner: ", owner); + setIsAdmin(owner === address); + }, [owner, address]); + + // In a real application, you'd check if the connected address is the contract owner + // For now, we'll have a toggle to simulate admin/beneficiary view + const toggleRole = () => { + setIsAdmin(!isAdmin); + toast({ + title: `Switched to ${!isAdmin ? "Admin" : "Beneficiary"} view`, + description: "This is for demonstration purposes only", + }); + }; + + return ( +
    + + +
    +

    + Token Vesting Portal +

    + {/* */} +
    + + {!isConnected ? ( +
    +
    + Connect your wallet to continue +
    +

    + You need to connect your wallet to interact with the vesting + contract +

    +
    + ) : ( +
    +
    +

    + {isAdmin ? "Administrator Panel" : "Beneficiary Dashboard"} +

    + +
    + + + + Overview + {isAdmin && ( + Create Schedule + )} + {isAdmin && ( + Manage Beneficiaries + )} + {!isAdmin && ( + Claim Tokens + )} + + + + {isAdmin ? ( + + ) : ( + + )} + + + {isAdmin && ( + + + + )} + + {isAdmin && ( + + + + )} + + {!isAdmin && ( + + + + )} + +
    + )} + +
    +

    Built with DotUI • Polkadot's UI Kit for Web3

    +
    + + +
    + ); +} diff --git a/challenge-3-frontend/components/token-vesting/admin-panel.tsx b/challenge-3-frontend/components/token-vesting/admin-panel.tsx new file mode 100644 index 0000000..5672e99 --- /dev/null +++ b/challenge-3-frontend/components/token-vesting/admin-panel.tsx @@ -0,0 +1,840 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Address, parseUnits, formatUnits } from "viem"; +import { + useAccount, + useWriteContract, + useWaitForTransactionReceipt, +} from "wagmi"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/hooks/use-toast"; +import { abi as TokenVestingAbi } from "@/abis/TokenVesting.json"; +import { abi as TokenAbi } from "@/abis/Token.json"; +import { + BeneficiaryInfo, + VestingFormData, + WhitelistFormData, +} from "@/lib/types"; +import { + Table, + TableHeader, + TableBody, + TableHead, + TableRow, + TableCell, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { formatDate } from "@/lib/utils"; +import { Separator } from "@/components/ui/separator"; +import { reverse } from "dns"; + +interface AdminPanelProps { + contractAddress: Address; + tokenAddress: Address; + view: "overview" | "create" | "manage"; +} + +export default function AdminPanel({ + contractAddress, + tokenAddress, + view, +}: AdminPanelProps) { + const { address } = useAccount(); + const { toast } = useToast(); + + // State for vesting form + const [vestingForm, setVestingForm] = useState({ + beneficiary: "", + amount: "", + cliffDuration: "", + vestDuration: "", + startTimestamp: "", + }); + + // State for whitelist form + const [whitelistForm, setWhitelistForm] = useState({ + beneficiary: "", + }); + + // State for beneficiaries + const [beneficiaries, setBeneficiaries] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [pendingRemoval, setPendingRemoval] = useState
    (null); + const [pendingRevoke, setPendingRevoke] = useState
    (null); + + // Contract write operations + const { data: approveHash, writeContract: approveToken } = useWriteContract(); + const { data: createHash, writeContract: createSchedule } = + useWriteContract(); + const { data: whitelistHash, writeContract: addToWhitelist } = + useWriteContract(); + const { data: removeHash, writeContract: removeFromWhitelist } = + useWriteContract(); + const { data: revokeHash, writeContract: revokeVesting } = useWriteContract(); + + // Handle transaction receipts + const { isLoading: isApproveLoading, status: approveStatus } = + useWaitForTransactionReceipt({ + hash: approveHash, + }); + + useEffect(() => { + if (isApproveLoading) { + return; + } + if (approveStatus === "success") { + toast({ + title: "Approval successful", + description: "You can now create the vesting schedule", + }); + handleCreateSchedule(); + } + }, [approveStatus]); + + const { isLoading: isCreateLoading, status: createStatus } = + useWaitForTransactionReceipt({ + hash: createHash, + }); + + useEffect(() => { + if (isCreateLoading) { + return; + } + if (createStatus === "success") { + toast({ + title: "Success!", + description: "Vesting schedule created successfully", + }); + + // Add/update the beneficiary with vesting schedule in localStorage + const currentTime = Math.floor(Date.now() / 1000); + const startTime = vestingForm.startTimestamp + ? parseInt(vestingForm.startTimestamp) + : currentTime + 60; + + const newBeneficiary: BeneficiaryInfo = { + address: vestingForm.beneficiary as Address, + isWhitelisted: true, // When creating a vesting schedule, beneficiary is automatically whitelisted + vestingSchedule: { + totalAmount: parseUnits(vestingForm.amount, 18), + startTime: BigInt(startTime), + cliffDuration: BigInt( + parseInt(vestingForm.cliffDuration) * 24 * 60 * 60 + ), + vestDuration: BigInt( + parseInt(vestingForm.vestDuration) * 24 * 60 * 60 + ), + amountClaimed: BigInt(0), + revoked: false, + }, + vestedAmount: BigInt(0), + claimableAmount: BigInt(0), + }; + + // Update or add the beneficiary + const updatedBeneficiaries = [...beneficiaries]; + const existingIndex = updatedBeneficiaries.findIndex( + (b) => b.address === newBeneficiary.address + ); + + if (existingIndex >= 0) { + updatedBeneficiaries[existingIndex] = { + ...updatedBeneficiaries[existingIndex], + ...newBeneficiary, + }; + } else { + updatedBeneficiaries.push(newBeneficiary); + } + + // Persist to localStorage + try { + localStorage.setItem( + "vestingBeneficiaries", + JSON.stringify( + updatedBeneficiaries, + (key, value) => { + // Convert bigint to string + return typeof value === "bigint" ? String(value) : value; + }, + 2 + ) + ); + console.log("Updated vesting schedules stored in localStorage"); + } catch (err) { + console.error( + "Failed to store vesting schedules in localStorage:", + err + ); + } + + resetVestingForm(); + fetchBeneficiaries(); // Refresh the beneficiaries list + } else if (createStatus === "error") { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to create vesting schedule", + }); + } + }, [createStatus]); + + const { isLoading: isWhitelistLoading, status: whitelistStatus } = + useWaitForTransactionReceipt({ + hash: whitelistHash, + }); + + useEffect(() => { + if (isWhitelistLoading) { + return; + } + if (whitelistStatus === "success") { + toast({ + title: "Success!", + description: "Beneficiary added to whitelist", + }); + + // Create new beneficiary object + const newBeneficiary: BeneficiaryInfo = { + address: whitelistForm.beneficiary as Address, + isWhitelisted: true, + }; + + // Check if the beneficiary already exists in the array + const updatedBeneficiaries = [...beneficiaries]; + const existingIndex = updatedBeneficiaries.findIndex( + (b) => b.address === newBeneficiary.address + ); + + // If exists, update it, otherwise add it + if (existingIndex >= 0) { + updatedBeneficiaries[existingIndex] = { + ...updatedBeneficiaries[existingIndex], + isWhitelisted: true, + }; + } else { + updatedBeneficiaries.push(newBeneficiary); + } + + // Persist to localStorage for later retrieval + try { + localStorage.setItem( + "vestingBeneficiaries", + JSON.stringify( + updatedBeneficiaries, + (key, value) => { + // Convert bigint to string + return typeof value === "bigint" ? String(value) : value; + }, + 2 + ) + ); + console.log("Beneficiaries stored in localStorage"); + } catch (err) { + console.error("Failed to store beneficiaries in localStorage:", err); + } + + // Clear whitelist input form + setWhitelistForm((prev) => ({ ...prev, beneficiary: "" })); + } else if (whitelistStatus === "error") { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to add to whitelist", + }); + } + fetchBeneficiaries(); // Refresh the beneficiaries list + }, [whitelistStatus]); + + const { isLoading: isRemoveLoading, status: removeStatus } = + useWaitForTransactionReceipt({ + hash: removeHash, + }); + + useEffect(() => { + if (isRemoveLoading) { + return; + } + if (removeStatus === "success") { + // Changed from revokeStatus to removeStatus + toast({ + title: "Success!", + description: "Beneficiary removed from whitelist", + }); + + // Update localStorage to mark the beneficiary as not whitelisted + const updatedBeneficiaries = beneficiaries.map((ben) => { + if (ben.address === pendingRemoval) { + return { + ...ben, + isWhitelisted: false, + }; + } + return ben; + }); + + // Persist to localStorage + try { + localStorage.setItem( + "vestingBeneficiaries", + JSON.stringify( + updatedBeneficiaries, + (key, value) => { + // Convert bigint to string + return typeof value === "bigint" ? String(value) : value; + }, + 2 + ) + ); + console.log("Updated whitelist stored in localStorage"); + } catch (err) { + console.error("Failed to store whitelist in localStorage:", err); + } + + fetchBeneficiaries(); // Refresh the beneficiaries list + } else if (removeStatus === "error") { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to remove from whitelist", + }); + } + }, [removeStatus]); + + const { isLoading: isRevokeLoading, status: revokeStatus } = + useWaitForTransactionReceipt({ + hash: revokeHash, + }); + + useEffect(() => { + if (isRevokeLoading) { + return; + } + if (revokeStatus === "success") { + toast({ + title: "Success!", + description: "Vesting schedule revoked", + }); + + // Update localStorage to mark the schedule as revoked + const updatedBeneficiaries = beneficiaries.map((ben) => { + if (ben.address === pendingRevoke) { + return { + ...ben, + vestingSchedule: ben.vestingSchedule + ? { + ...ben.vestingSchedule, + revoked: true, + } + : undefined, + }; + } + return ben; + }); + + // Persist to localStorage + try { + localStorage.setItem( + "vestingBeneficiaries", + JSON.stringify( + updatedBeneficiaries, + (key, value) => { + // Convert bigint to string + return typeof value === "bigint" ? String(value) : value; + }, + 2 + ) + ); + console.log("Updated vesting schedules stored in localStorage"); + } catch (err) { + console.error( + "Failed to store vesting schedules in localStorage:", + err + ); + } + + fetchBeneficiaries(); // Refresh the beneficiaries list + } + }, [revokeStatus]); + + // Helper function to fetch beneficiary data + const fetchBeneficiaries = async () => { + // In a real implementation, you would query events or use a subgraph + // For this demo, we'll just use a mock + let mockBeneficiaries: BeneficiaryInfo[] = []; + + try { + const storedData = localStorage.getItem("vestingBeneficiaries"); + if (storedData && storedData !== "") { + mockBeneficiaries = JSON.parse(storedData, (key, value) => { + // Convert string to bigint + return key === "totalAmount" || + key === "startTime" || + key === "cliffDuration" || + key === "vestDuration" || + key === "amountClaimed" || + key === "vestedAmount" || + key === "claimableAmount" + ? BigInt(value) + : value; + }); + } + } catch (error) { + console.error("Error parsing localStorage data:", error); + } + + if (mockBeneficiaries.length === 0) { + mockBeneficiaries = [ + { + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" as Address, + isWhitelisted: true, + vestingSchedule: { + totalAmount: parseUnits("10000", 18), + startTime: BigInt(Math.floor(Date.now() / 100)), + cliffDuration: BigInt(30 * 24 * 60 * 60), // 30 days + vestDuration: BigInt(365 * 24 * 60 * 60), // 1 year + amountClaimed: BigInt(0), + revoked: false, + }, + vestedAmount: parseUnits("2000", 18), + claimableAmount: parseUnits("2000", 18), + }, + { + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" as Address, + isWhitelisted: true, + vestingSchedule: undefined, + }, + { + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" as Address, + isWhitelisted: false, + }, + ]; + } + + setBeneficiaries(mockBeneficiaries); + }; + + useEffect(() => { + fetchBeneficiaries(); + }, []); + + const handleInputChange = ( + e: React.ChangeEvent, + formType: "vesting" | "whitelist" + ) => { + const { name, value } = e.target; + if (formType === "vesting") { + setVestingForm((prev) => ({ ...prev, [name]: value })); + } else { + setWhitelistForm((prev) => ({ ...prev, [name]: value })); + } + }; + + const resetVestingForm = () => { + setVestingForm({ + beneficiary: "", + amount: "", + cliffDuration: "", + vestDuration: "", + startTimestamp: "", + }); + }; + + const handleSubmitVesting = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + // In a real implementation, validate the form first + + // First approve tokens for the vesting contract + approveToken({ + address: tokenAddress, + abi: TokenAbi, + functionName: "approve", + args: [ + contractAddress, + parseUnits(vestingForm.amount, 18), // Assuming token has 18 decimals + ], + }); + } catch (error) { + console.error("Error creating vesting schedule:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to create vesting schedule", + }); + } + }; + + const handleCreateSchedule = () => { + const currentTime = Math.floor(Date.now() / 1000); + const startTime = vestingForm.startTimestamp + ? parseInt(vestingForm.startTimestamp) + : currentTime + 60; // Default to 1 minute from now + + createSchedule({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "createVestingSchedule", + args: [ + vestingForm.beneficiary as Address, + parseUnits(vestingForm.amount, 18), // Assuming token has 18 decimals + BigInt(parseInt(vestingForm.cliffDuration) * 24 * 60 * 60), // Convert days to seconds + BigInt(parseInt(vestingForm.vestDuration) * 24 * 60 * 60), // Convert days to seconds + BigInt(startTime), + ], + }); + }; + + const handleWhitelistSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + try { + addToWhitelist({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "addToWhitelist", + args: [whitelistForm.beneficiary as Address], + }); + } catch (error) { + console.error("Error adding to whitelist:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to add to whitelist", + }); + } + }; + + const handleRemoveFromWhitelist = (beneficiaryAddress: Address) => { + try { + setPendingRemoval(beneficiaryAddress); + removeFromWhitelist({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "removeFromWhitelist", + args: [beneficiaryAddress], + }); + } catch (error) { + console.error("Error removing from whitelist:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to remove from whitelist", + }); + setPendingRemoval(null); + } + }; + + const handleRevokeVesting = (beneficiaryAddress: Address) => { + try { + setPendingRevoke(beneficiaryAddress); + revokeVesting({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "revokeVesting", + args: [beneficiaryAddress], + }); + } catch (error) { + console.error("Error revoking vesting:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to revoke vesting", + }); + setPendingRevoke(null); + } + }; + return ( +
    + {view === "overview" && ( + + + Vesting Schedule Overview + View all active vesting schedules + + + + + + Beneficiary + Whitelist Status + Total Amount + Vested Amount + Status + Actions + + + + {beneficiaries.map((beneficiary) => ( + + + {beneficiary.address} + + + {beneficiary.isWhitelisted ? ( + Whitelisted + ) : ( + Not Whitelisted + )} + + + {beneficiary.vestingSchedule + ? formatUnits( + beneficiary.vestingSchedule.totalAmount, + 18 + ) + : "-"} + + + {beneficiary.vestedAmount + ? formatUnits(beneficiary.vestedAmount, 18) + : "-"} + + + {beneficiary.vestingSchedule?.revoked ? ( + Revoked + ) : beneficiary.vestingSchedule ? ( + Active + ) : ( + No Schedule + )} + + +
    + {beneficiary.isWhitelisted ? ( + + ) : ( + + )} + + {beneficiary.vestingSchedule && + !beneficiary.vestingSchedule.revoked && ( + + )} +
    +
    +
    + ))} + + {beneficiaries.length === 0 && ( + + + No beneficiaries found + + + )} +
    +
    +
    +
    + )} + + {view === "create" && ( + + + Create Vesting Schedule + + Set up a new token vesting schedule for a beneficiary + + + +
    +
    + + handleInputChange(e, "vesting")} + /> +
    + +
    + + handleInputChange(e, "vesting")} + /> +
    + +
    +
    + + handleInputChange(e, "vesting")} + /> +
    + +
    + + handleInputChange(e, "vesting")} + /> +
    +
    + +
    + + handleInputChange(e, "vesting")} + /> +

    + Leave empty to use current time + 1 minute. Current timestamp:{" "} + {Math.floor(Date.now() / 1000)} +

    +
    + + +
    +
    +
    + )} + + {view === "manage" && ( + + + Manage Beneficiaries + + Add or remove beneficiaries from the whitelist + + + +
    +
    + + handleInputChange(e, "whitelist")} + /> +
    + + +
    + + + +
    +

    Current Whitelist

    +
    + {beneficiaries + .filter((b) => b.isWhitelisted) + .map((beneficiary) => ( +
    + + {beneficiary.address} + + +
    + ))} + + {beneficiaries.filter((b) => b.isWhitelisted).length === 0 && ( +

    + No whitelisted beneficiaries +

    + )} +
    +
    +
    +
    + )} +
    + ); +} diff --git a/challenge-3-frontend/components/token-vesting/beneficiary-panel.tsx b/challenge-3-frontend/components/token-vesting/beneficiary-panel.tsx new file mode 100644 index 0000000..ba93613 --- /dev/null +++ b/challenge-3-frontend/components/token-vesting/beneficiary-panel.tsx @@ -0,0 +1,571 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Address, formatUnits } from "viem"; +import { + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, +} from "wagmi"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { useToast } from "@/hooks/use-toast"; +import { abi as TokenVestingAbi } from "@/abis/TokenVesting.json"; +import { abi as TokenAbi } from "@/abis/Token.json"; +import { VestingSchedule } from "@/lib/types"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { InfoCircledIcon } from "@radix-ui/react-icons"; +// import { westendAssetHub } from "@/app/providers"; +import { moonbaseAlpha, sepolia } from "wagmi/chains"; + +interface BeneficiaryPanelProps { + contractAddress: Address; + address: Address; + view: "overview" | "claim"; +} + +export default function BeneficiaryPanel({ + contractAddress, + address, + view, +}: BeneficiaryPanelProps) { + const { toast } = useToast(); + const [isWhitelisted, setIsWhitelisted] = useState(null); + const [vestingSchedule, setVestingSchedule] = + useState(null); + const [vestedAmount, setVestedAmount] = useState(null); + const [claimableAmount, setClaimableAmount] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [refetchNow, setRefetchNow] = useState(false); + + // For scrolling to claim card + const claimCardRef = useRef(null); + + // Contract write operation + const { data: claimHash, writeContract: claimTokens } = useWriteContract(); + + // Handle transaction receipt + const { isLoading: isClaimLoading, status: claimStatus } = + useWaitForTransactionReceipt({ + hash: claimHash, + }); + + useEffect(() => { + if (isClaimLoading) { + return; + } + if (claimStatus === "success") { + toast({ + title: "Success!", + description: "Tokens claimed successfully", + }); + setRefetchNow(true); + fetchBeneficiaryData(); // Refresh data after claiming + + return () => { + setRefetchNow(false); + }; + } + }, [claimStatus]); + + // Fetch vesting schedule with better error handling and logging + const REFETCH_INTERVAL = 1000; // Miliseconds + const { + data: schedule, + error: scheduleError, + isLoading: scheduleLoading, + } = useReadContract({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "vestingSchedules", + args: [address], + chainId: sepolia.id, + query: { + refetchInterval(query) { + return refetchNow ? 0 : REFETCH_INTERVAL; + }, + }, + }); + + // Log both successful data and errors + useEffect(() => { + if (scheduleError) { + console.error("Schedule fetch error:", scheduleError); + } + if (schedule) { + console.log("Raw schedule data received:", schedule); + // Log structure to understand the format + console.log("Schedule data type:", typeof schedule); + if (typeof schedule === "object") { + console.log("Schedule keys:", Object.keys(schedule)); + } + } + }, [schedule, scheduleError]); + + // Similar for the whitelist check + const { data: whitelisted, error: whitelistError } = useReadContract({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "whitelist", + args: [address], + chainId: sepolia.id, + query: { + refetchInterval(query) { + return refetchNow ? 0 : REFETCH_INTERVAL; + }, + }, + }); + + // Log whitelist errors + useEffect(() => { + if (whitelistError) { + console.error("Whitelist fetch error:", whitelistError); + } + }, [whitelistError]); + + // Fetch vested amount + const { data: vested } = useReadContract({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "calculateVestedAmount", + args: [address], + chainId: sepolia.id, + query: { + refetchInterval(query) { + return refetchNow ? 0 : REFETCH_INTERVAL; + }, + }, + }); + + // Helper function to fetch all beneficiary data + const fetchBeneficiaryData = async () => { + setIsLoading(true); + + try { + // Set whitelist status + setIsWhitelisted( + whitelisted !== undefined ? (whitelisted as boolean) : false + ); + + // Set vesting schedule if there's one, handling both array and object formats + console.log("Processing schedule data:", schedule); + + if (schedule) { + let scheduleData; + + // Handle if schedule is returned as an array (tuple) + if (Array.isArray(schedule)) { + console.log("Schedule is an array with length:", schedule.length); + // Map the tuple to our expected object structure + scheduleData = { + totalAmount: schedule[0], + startTime: schedule[1], + cliffDuration: schedule[2], + vestDuration: schedule[3], + amountClaimed: schedule[4], + revoked: schedule[5], + }; + } else { + scheduleData = schedule as any; + } + + // Check if we have a valid schedule with a non-zero amount + if ( + scheduleData && + scheduleData.totalAmount && + scheduleData.totalAmount > 0 + ) { + console.log("Valid schedule found:", scheduleData); + + setVestingSchedule({ + totalAmount: scheduleData.totalAmount, + startTime: scheduleData.startTime, + cliffDuration: scheduleData.cliffDuration, + vestDuration: scheduleData.vestDuration, + amountClaimed: scheduleData.amountClaimed, + revoked: scheduleData.revoked, + }); + + // Set vested amount if available + if (vested !== undefined) { + setVestedAmount(vested as bigint); + // Calculate claimable amount + const claimable = (vested as bigint) - scheduleData.amountClaimed; + setClaimableAmount(claimable); + } else { + // Calculate vested amount based on current time + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + const startTime = scheduleData.startTime; + const cliffTime = startTime + scheduleData.cliffDuration; + const endTime = startTime + scheduleData.vestDuration; + + if (currentTime < cliffTime) { + // During cliff period, nothing is vested + setVestedAmount(BigInt(0)); + setClaimableAmount(BigInt(0)); + } else if (currentTime >= endTime) { + // After vesting period, everything is vested + setVestedAmount(scheduleData.totalAmount); + setClaimableAmount( + BigInt(scheduleData.totalAmount) - + BigInt(scheduleData.amountClaimed) + ); + } else { + // Linear vesting during the vesting period + const timeFromCliff = currentTime - cliffTime; + const vestingPeriod = endTime - cliffTime; + // Convert all values to bigint to avoid type mismatch in division + const vestedAmt = + (scheduleData.totalAmount * timeFromCliff) / + BigInt(vestingPeriod); + + setVestedAmount(vestedAmt); + setClaimableAmount( + BigInt(vestedAmt) - BigInt(scheduleData.amountClaimed) + ); + } + } + } else { + console.log("Schedule exists but has zero amount or is invalid"); + setVestingSchedule(null); + setVestedAmount(null); + setClaimableAmount(null); + } + } else { + console.info("No schedule data received from contract"); + setVestingSchedule(null); + setVestedAmount(null); + setClaimableAmount(null); + } + } catch (error) { + console.error("Error in fetchBeneficiaryData:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to fetch vesting data", + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchBeneficiaryData(); + }, [address, whitelisted, schedule, vested]); + + const handleClaimTokens = () => { + try { + claimTokens({ + address: contractAddress, + abi: TokenVestingAbi, + functionName: "claimVestedTokens", + }); + } catch (error) { + console.error("Error claiming tokens:", error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to claim tokens", + }); + } + }; + + const formatTimestamp = (timestamp: bigint) => { + return new Date(Number(timestamp) * 1000).toLocaleString(); + }; + + const calculateProgress = () => { + if (!vestingSchedule || !vestedAmount) return 0; + return Number((vestedAmount * BigInt(100)) / vestingSchedule.totalAmount); + }; + + const isCliffPeriod = () => { + if (!vestingSchedule) return false; + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + return ( + currentTime < vestingSchedule.startTime + vestingSchedule.cliffDuration + ); + }; + + const isVestingComplete = () => { + if (!vestingSchedule) return false; + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + return ( + currentTime >= vestingSchedule.startTime + vestingSchedule.vestDuration + ); + }; + + useEffect(() => { + if (!view) return; + + console.log("view value changed to: ", view); + if (view === "claim") { + // Add a small delay to ensure the DOM is updated + setTimeout(() => { + console.log("Attempting to scroll to claim card..."); + if (claimCardRef.current) { + claimCardRef.current.scrollIntoView({ behavior: "smooth" }); + } else { + console.log("claimCardRef still not defined after delay"); + } + }, 100); + } + }, [view]); + + return ( +
    + {schedule as any} + {isLoading ? ( + + +
    + + + + +
    +
    +
    + ) : !isWhitelisted ? ( + + + Not Whitelisted + + Your address has not been whitelisted by the administrator yet + + + +
    + +

    + You need to be whitelisted by the administrator to participate + in the token vesting program. +

    +
    +
    +
    + ) : !vestingSchedule ? ( + + + No Vesting Schedule + + You are whitelisted but don't have an active vesting schedule + + + +
    + +

    + Contact the administrator to create a vesting schedule for your + address. +

    +
    +
    +
    + ) : ( +
    + + +
    + Your Vesting Schedule + {vestingSchedule.revoked && ( + Revoked + )} + {isVestingComplete() && ( + Completed + )} + {isCliffPeriod() && ( + In Cliff Period + )} +
    + + Track the progress of your token vesting schedule + +
    + +
    +
    +
    + Progress + {calculateProgress()}% +
    + +
    + +
    +
    +

    + Total Amount +

    +

    + {formatUnits(vestingSchedule.totalAmount, 18)} tokens +

    +
    + +
    +

    + Vested Amount +

    +

    + {vestedAmount ? formatUnits(vestedAmount, 18) : "0"}{" "} + tokens +

    +
    + +
    +

    + Claimed Amount +

    +

    + {formatUnits(vestingSchedule.amountClaimed, 18)} tokens +

    +
    + +
    +

    + Claimable Now +

    +

    + {claimableAmount ? formatUnits(claimableAmount, 18) : "0"}{" "} + tokens +

    +
    +
    + +
    +
    +
    +

    + Start Date +

    +

    + {formatTimestamp(vestingSchedule.startTime)} +

    +
    + +
    +

    + Cliff End Date +

    +

    + {formatTimestamp( + vestingSchedule.startTime + + vestingSchedule.cliffDuration + )} +

    +
    + +
    +

    + Vesting End Date +

    +

    + {formatTimestamp( + vestingSchedule.startTime + + vestingSchedule.vestDuration + )} +

    +
    +
    +
    +
    +
    +
    + + {view === "claim" && ( + + + Claim Tokens + Claim your vested tokens + + +
    +
    +
    +
    +

    + Claimable Amount +

    +

    + {claimableAmount + ? formatUnits(claimableAmount, 18) + : "0"}{" "} + tokens +

    +
    + + {vestingSchedule.revoked && ( + + Vesting Revoked + + )} + + {isCliffPeriod() && ( + + In Cliff Period + + )} +
    +
    + +
    + + + {(!claimableAmount || claimableAmount <= 0) && + !isCliffPeriod() && + !vestingSchedule.revoked && ( +

    + You don't have any tokens to claim at the moment. +

    + )} + + {isCliffPeriod() && ( +

    + You are still in the cliff period. Tokens will be + available to claim after{" "} + {formatTimestamp( + vestingSchedule.startTime + + vestingSchedule.cliffDuration + )} +

    + )} + + {vestingSchedule.revoked && ( +

    + Your vesting schedule has been revoked by the + administrator. +

    + )} +
    +
    +
    +
    + )} +
    + )} +
    + ); +}