From e29282cdc6cb9fca37c9b4e596a65b54e5929c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosiak?= Date: Thu, 12 Dec 2024 12:22:53 +0100 Subject: [PATCH 1/2] claiming implemented --- src/components/stakingPoolDetailsView.tsx | 4 +- src/components/withdrawUnstakedZilPanel.tsx | 35 +++++-- src/components/withdrawZilView.tsx | 34 +++--- src/contexts/stakingOperations.tsx | 83 +++++++++++++-- src/contexts/stakingPoolsStorage.tsx | 9 +- src/misc/stakingAbis.ts | 64 ++++++++++-- src/misc/stakingPoolsConfig.ts | 18 +--- src/misc/walletsConfig.ts | 110 ++++++++++++++++---- src/script/fetchPoolStaticData.ts | 2 +- 9 files changed, 284 insertions(+), 75 deletions(-) diff --git a/src/components/stakingPoolDetailsView.tsx b/src/components/stakingPoolDetailsView.tsx index 32ca025..92ac2a5 100644 --- a/src/components/stakingPoolDetailsView.tsx +++ b/src/components/stakingPoolDetailsView.tsx @@ -58,14 +58,14 @@ const StakingPoolDetailsView: React.FC = ({ const pendingUnstakesValue = userUnstakingPoolData?.filter( (item) => item.availableAt > DateTime.now() ).reduce( - (acc, item) => acc + item.unstakingTokenAmount, + (acc, item) => acc + item.zilAmount, 0n ); const availableToClaim = userUnstakingPoolData?.filter( (item) => item.availableAt <= DateTime.now() ).reduce( - (acc, item) => acc + item.unstakingTokenAmount, + (acc, item) => acc + item.zilAmount, 0n ); diff --git a/src/components/withdrawUnstakedZilPanel.tsx b/src/components/withdrawUnstakedZilPanel.tsx index e9c33c7..fa0b9a1 100644 --- a/src/components/withdrawUnstakedZilPanel.tsx +++ b/src/components/withdrawUnstakedZilPanel.tsx @@ -1,9 +1,12 @@ +import { AppConfigStorage } from "@/contexts/appConfigStorage"; import { StakingOperations } from "@/contexts/stakingOperations"; -import { convertTokenToZil, getHumanFormDuration } from "@/misc/formatting"; +import { formatAddress, getHumanFormDuration, getTxExplorerUrl } from "@/misc/formatting"; import { StakingPool } from "@/misc/stakingPoolsConfig"; import { UserUnstakingPoolData } from "@/misc/walletsConfig"; import { Button } from "antd"; import { DateTime } from "luxon"; +import Link from "next/link"; +import { formatUnits } from "viem"; interface WithdrawZilPanelProps { stakingPoolData: StakingPool; @@ -15,9 +18,15 @@ const WithdrawZilPanel: React.FC = ({ stakingPoolData, }) => { const { - claim + claim, + isClaimingInProgress, + claimCallTxHash, } = StakingOperations.useContainer(); + const { + appConfig + } = AppConfigStorage.useContainer(); + const pendingUnstake = userUnstakingPoolData?.filter( (claim) => claim.availableAt > DateTime.now() ).toSorted((claimA, claimB) => claimA.availableAt.diff(claimB.availableAt).milliseconds) @@ -28,6 +37,17 @@ const WithdrawZilPanel: React.FC = ({ return (
+ + { + claimCallTxHash !== undefined && ( +
+ + Last staking transaction: {formatAddress(claimCallTxHash)} + +
+ ) + } + { !!availableUnstake?.length ? ( availableUnstake.map( @@ -36,12 +56,13 @@ const WithdrawZilPanel: React.FC = ({
{ stakingPoolData.data ?
- ~{convertTokenToZil(item.unstakingTokenAmount, stakingPoolData.data.zilToTokenRate)} ZIL + {parseFloat(formatUnits(item.zilAmount, 18)).toFixed(3)} ZIL
:
} @@ -60,7 +81,7 @@ const WithdrawZilPanel: React.FC = ({
{ stakingPoolData.data ?
- ~{convertTokenToZil(pendingUnstake[0].unstakingTokenAmount, stakingPoolData.data.zilToTokenRate)} ZIL + {parseFloat(formatUnits(pendingUnstake[0].zilAmount, 18)).toFixed(3)} ZIL
:
}
@@ -76,7 +97,7 @@ const WithdrawZilPanel: React.FC = ({ !!pendingUnstake?.length && (
- Pending requests + All pending requests
{ @@ -85,7 +106,7 @@ const WithdrawZilPanel: React.FC = ({ { stakingPoolData.data ?
- {claim.unstakingTokenAmount} {stakingPoolData.definition.tokenSymbol} ~= {convertTokenToZil(claim.unstakingTokenAmount, stakingPoolData.data.zilToTokenRate)} ZILs + {parseFloat(formatUnits(claim.zilAmount, 18)).toFixed(3)} ZIL
:
}
diff --git a/src/components/withdrawZilView.tsx b/src/components/withdrawZilView.tsx index 97620ec..e65aef5 100644 --- a/src/components/withdrawZilView.tsx +++ b/src/components/withdrawZilView.tsx @@ -1,6 +1,6 @@ import { StakingOperations } from "@/contexts/stakingOperations"; import { StakingPoolsStorage } from "@/contexts/stakingPoolsStorage"; -import { convertTokenToZil, formatUnitsToHumanReadable } from "@/misc/formatting"; +import { convertTokenToZil, formatUnitsToHumanReadable, getHumanFormDuration } from "@/misc/formatting"; import { Button } from "antd"; import Image from 'next/image'; @@ -8,6 +8,8 @@ const WithdrawZilView: React.FC = () => { const { availableForUnstaking, pendingUnstaking, + selectStakingPoolForView, + isUnstakingDataLoading } = StakingPoolsStorage.useContainer(); const { @@ -61,7 +63,7 @@ const WithdrawZilView: React.FC = () => { item.stakingPool.data ? <> { formatUnitsToHumanReadable( - convertTokenToZil(item.unstakeInfo.unstakingTokenAmount, item.stakingPool.data!.zilToTokenRate), + convertTokenToZil(item.unstakeInfo.zilAmount, item.stakingPool.data!.zilToTokenRate), 18 ) } ZIL @@ -73,7 +75,7 @@ const WithdrawZilView: React.FC = () => { }
-
{item.unstakeInfo.unstakingTokenAmount} {item.stakingPool.definition.tokenSymbol}
+
{item.unstakeInfo.zilAmount} {item.stakingPool.definition.tokenSymbol}
@@ -81,17 +83,19 @@ const WithdrawZilView: React.FC = () => {
-
+ +
@@ -99,8 +103,14 @@ const WithdrawZilView: React.FC = () => { } ) : ( -
- WoW such empty +
+ { + isUnstakingDataLoading ? ( +
+ ) : ( + WoW such empty + ) + }
) } diff --git a/src/contexts/stakingOperations.tsx b/src/contexts/stakingOperations.tsx index aee8b72..36d7dea 100644 --- a/src/contexts/stakingOperations.tsx +++ b/src/contexts/stakingOperations.tsx @@ -1,6 +1,6 @@ import { notification } from "antd"; import { useEffect, useState } from "react"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { useWaitForTransactionReceipt } from "wagmi"; import { createContainer } from "./context"; import { WalletConnector } from "./walletConnector"; import { StakingPoolsStorage } from "./stakingPoolsStorage"; @@ -125,6 +125,14 @@ const useStakingOperations = () => { (txHash) => { setUnstakingCallTxHash(txHash); } + ).catch( + (error) => { + notification.error({ + message: "Unstaking failed", + description: error?.message || "There was an error while unstaking ZIL", + placement: "topRight" + }); + } ) } } @@ -153,11 +161,66 @@ const useStakingOperations = () => { * CLAIMING */ - const claim = (zilToStake: bigint) => { - setDummyWalletPopupContent(`Now User gonna approve the wallet transaction for withdrawing/claiming ${zilToStake} ZIL`); - setIsDummyWalletPopupOpen(true); + const [claimCallTxHash, setClaimCallTxHash] = useState
(undefined); + + const { + isLoading: isClaimingInProgress, + error: claimContractCallError, + status: claimCallReceiptStatus, + } = useWaitForTransactionReceipt({ + hash: claimCallTxHash, + }) + + const claim = (delegatorAddress: string) => { + if (isDummyWalletConnected) { + setDummyWalletPopupContent(`Now User gonna approve the wallet transaction for withdrawing/claiming ZIL`); + setIsDummyWalletPopupOpen(true); + setClaimCallTxHash("0x1234567890234567890234567890234567890" as Address); + } else { + writeContract( + wagmiConfig, + { + address: delegatorAddress as Address, + abi: delegatorAbi, + functionName: 'claim', + args: [] + } + ).then( + (txHash) => { + setClaimCallTxHash(txHash); + } + ).catch( + (error) => { + notification.error({ + message: "Claiming failed", + description: error?.message || "There was an error while claiming ZIL", + placement: "topRight" + }); + } + ) + } } + useEffect( + () => { + if (claimCallReceiptStatus === "success") { + notification.success({ + message: "Claiming successful", + description: `You have successfully claimed ZIL`, + placement: "topRight" + }); + reloadUserStakingPoolsData(); + updateWalletBalance(); + } else if (claimCallReceiptStatus === "error") { + notification.error({ + message: "Claiming failed", + description: `There was an error while claiming ZIL`, + placement: "topRight" + }); + } + }, [claimCallReceiptStatus] + ) + /** * OTHER */ @@ -166,6 +229,7 @@ const useStakingOperations = () => { function clearStateOnDelegatorChange() { setStakingCallTxHash(undefined); setUnstakingCallTxHash(undefined); + setClaimCallTxHash(undefined); }, [stakingPoolId] ) @@ -174,14 +238,21 @@ const useStakingOperations = () => { isDummyWalletPopupOpen, dummyWalletPopupContent, setIsDummyWalletPopupOpen, + stake, - unstake, - claim, isStakingInProgress, stakingCallTxHash, stakeContractCallError, + + unstake, isUnstakingInProgress, + unstakingCallTxHash, unstakeContractCallError, + + claim, + isClaimingInProgress, + claimCallTxHash, + claimContractCallError, } }; diff --git a/src/contexts/stakingPoolsStorage.tsx b/src/contexts/stakingPoolsStorage.tsx index 03e0cac..8779a89 100644 --- a/src/contexts/stakingPoolsStorage.tsx +++ b/src/contexts/stakingPoolsStorage.tsx @@ -27,6 +27,8 @@ const useStakingPoolsStorage = () => { const [stakingPoolForStaking, setStakingPoolForStaking] = useState(null); const [stakingPoolForUnstaking, setStakingPoolForUnstaking] = useState(null); + const [isUnstakingDataLoading, setIsUnstakingDataLoading] = useState(false); + const reloadUserStakingPoolsData = () => { if (!walletAddress) { setUserStakingPoolsData([]); @@ -34,7 +36,11 @@ const useStakingPoolsStorage = () => { } getWalletStakingData(walletAddress, appConfig!.chainId).then(setUserStakingPoolsData).catch(console.error); - getWalletUnstakingData(walletAddress).then(setUserUnstakesData).catch(console.error); + setIsUnstakingDataLoading(true); + getWalletUnstakingData(walletAddress, appConfig!.chainId) + .then(setUserUnstakesData) + .catch(console.error) + .finally(() => setIsUnstakingDataLoading(false)); } useEffect( @@ -192,6 +198,7 @@ const useStakingPoolsStorage = () => { availableForUnstaking, pendingUnstaking, reloadUserStakingPoolsData, + isUnstakingDataLoading, }; }; diff --git a/src/misc/stakingAbis.ts b/src/misc/stakingAbis.ts index 1666a21..b403fb4 100644 --- a/src/misc/stakingAbis.ts +++ b/src/misc/stakingAbis.ts @@ -28,6 +28,9 @@ export const depositAbi = [ ] export const delegatorAbi = [ + /** + * from Delegation.sol + */ { "inputs": [], "name": "stake", @@ -50,12 +53,19 @@ export const delegatorAbi = [ }, { "inputs": [], - "name": "getLST", + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getPendingClaims", "outputs": [ { - "internalType": "address", - "name": "", - "type": "address" + "internalType": "uint256[2][]", + "name": "claims", + "type": "uint256[2][]" } ], "stateMutability": "view", @@ -63,7 +73,7 @@ export const delegatorAbi = [ }, { "inputs": [], - "name": "MIN_DELEGATION", + "name": "getMinDelegation", "outputs": [ { "internalType": "uint256", @@ -76,8 +86,13 @@ export const delegatorAbi = [ }, { "inputs": [], - "name": "getCommissionNumerator", + "name": "getCommission", "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, { "internalType": "uint256", "name": "", @@ -89,11 +104,11 @@ export const delegatorAbi = [ }, { "inputs": [], - "name": "getPrice", + "name": "getStake", "outputs": [ { "internalType": "uint256", - "name": "amount", + "name": "", "type": "uint256" } ], @@ -102,7 +117,7 @@ export const delegatorAbi = [ }, { "inputs": [], - "name": "getStake", + "name": "getClaimable", "outputs": [ { "internalType": "uint256", @@ -112,5 +127,34 @@ export const delegatorAbi = [ ], "stateMutability": "view", "type": "function" - } + }, + /** + * From ILiquidDelegation.sol + */ + { + "inputs": [], + "name": "getLST", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, ] \ No newline at end of file diff --git a/src/misc/stakingPoolsConfig.ts b/src/misc/stakingPoolsConfig.ts index 4fea7ab..f1c1435 100644 --- a/src/misc/stakingPoolsConfig.ts +++ b/src/misc/stakingPoolsConfig.ts @@ -77,38 +77,28 @@ async function fetchDelegatorDataFromNetwork(definition: StakingPoolDefinition, try { const [ totalSupply, - commissionNumerator, zilToTokenRateWei, delegatorStake, depositTotalStake, + [commissionNumerator, commissionDenominator] ] = await Promise.all([ readTokenContract("totalSupply"), - readDelegatorContract("getCommissionNumerator"), readDelegatorContract("getPrice"), readDelegatorContract("getStake"), readDepositContract("getFutureTotalStake"), + readDelegatorContract<[bigint, bigint]>("getCommission"), ]); - const commissionDenominator = 10000; const zilToTokenRate = 1 / parseFloat(formatUnits(zilToTokenRateWei, 18)); - const commission = (parseInt(commissionNumerator.toString()) / commissionDenominator); // percent - const votingPower = parseFloat(((delegatorStake * 100n) / depositTotalStake).toString()) / 100; // percent + const commission = Number((commissionNumerator * 100n) / commissionDenominator) / 100; + const votingPower = Number(((delegatorStake * 100n) / depositTotalStake)) / 100; const rewardsPerYearInZil = 51000 * 24 * 365; const delegatorYearReward = votingPower * rewardsPerYearInZil; const delegatorRewardForShare = delegatorYearReward * (1 - commission); const apr = delegatorRewardForShare / parseFloat(formatUnits(delegatorStake, 18)); - console.log({ - commission, - votingPower, - rewardsPerYearInZil, - delegatorYearReward, - delegatorRewardForShare, - apr - }) - return { tvl: totalSupply, commission, diff --git a/src/misc/walletsConfig.ts b/src/misc/walletsConfig.ts index 9a5040b..61e732e 100644 --- a/src/misc/walletsConfig.ts +++ b/src/misc/walletsConfig.ts @@ -3,6 +3,7 @@ import { getViemClient, MOCK_CHAIN } from "./chainConfig"; import { stakingPoolsConfigForChainId } from "./stakingPoolsConfig"; import { readContract } from "viem/actions"; import { Address, erc20Abi, parseUnits } from "viem"; +import { delegatorAbi } from "./stakingAbis"; export interface UserStakingPoolData { address: string; @@ -12,7 +13,7 @@ export interface UserStakingPoolData { export interface UserUnstakingPoolData { address: string; - unstakingTokenAmount: bigint; + zilAmount: bigint; availableAt: DateTime; } @@ -20,7 +21,7 @@ export interface DummyWallet { name: string; address: string; stakingTokenAmount: Array; - unstakingTokenAmount: Array; + zilAmount: Array; currentZil: bigint; } @@ -30,22 +31,22 @@ export const dummyWallets: Array = [ address: "0xCF671756a8238cBeB19BCB4D77FC9091E2fCe1A3", currentZil: 0n, stakingTokenAmount: [], - unstakingTokenAmount: [], + zilAmount: [], }, { name: "No Zil, No ZIL staked, Some ZIL unstaked", address: "0xCF671756a8238cBeB19BCB4D77FC9091E2fCeYYY", currentZil: 0n, stakingTokenAmount: [], - unstakingTokenAmount: [ + zilAmount: [ { address: "0x1234567890234567890234567890234567890", - unstakingTokenAmount: parseUnits("62712.323", 18), + zilAmount: parseUnits("62712.323", 18), availableAt: DateTime.now().minus({ days: 1 }), }, { address: "0x96525678902345678902345678918278372212", - unstakingTokenAmount: parseUnits("1000", 18), + zilAmount: parseUnits("1000", 18), availableAt: DateTime.now().plus({ days: 1 }), }, ], @@ -55,7 +56,7 @@ export const dummyWallets: Array = [ address: "0xf0a9953B539f9E7c4953279859F924d9212B2111", currentZil: 1000000000000000000n, stakingTokenAmount: [], - unstakingTokenAmount: [], + zilAmount: [], }, { name: "Some Zil, Some ZIL staked, No ZIL unstaked", @@ -73,7 +74,7 @@ export const dummyWallets: Array = [ rewardAcumulated: 50 }, ], - unstakingTokenAmount: [], + zilAmount: [], }, { name: "Some Zil, Some ZIL staked, Some ZIL unstaked", @@ -91,30 +92,30 @@ export const dummyWallets: Array = [ rewardAcumulated: 50 }, ], - unstakingTokenAmount: [ + zilAmount: [ { address: "0x1234567890234567890234567890234567890", - unstakingTokenAmount: parseUnits("9000", 18), + zilAmount: parseUnits("9000", 18), availableAt: DateTime.now().minus({ days: 1 }), }, { address: "0x82245678902345678902345678918278372382", - unstakingTokenAmount: parseUnits("1044", 18), + zilAmount: parseUnits("1044", 18), availableAt: DateTime.now().minus({ days: 5 }), }, { address: "0x82245678902345678902345678918278372382", - unstakingTokenAmount: parseUnits("1000000", 18), + zilAmount: parseUnits("1000000", 18), availableAt: DateTime.now().plus({ days: 1 }), }, { address: "0x96525678902345678902345678918278372212", - unstakingTokenAmount: parseUnits("500", 18), + zilAmount: parseUnits("500", 18), availableAt: DateTime.now().plus({ days: 5 }), }, { address: "0x96525678902345678902345678918278372212", - unstakingTokenAmount: parseUnits("10000", 18), + zilAmount: parseUnits("10000", 18), availableAt: DateTime.now().plus({ days: 13 }), }, ], @@ -135,7 +136,7 @@ export const dummyWallets: Array = [ rewardAcumulated: 0 }, ], - unstakingTokenAmount: [], + zilAmount: [], }, ] @@ -168,10 +169,75 @@ export async function getWalletStakingData(wallet: string, chainId: number): Pro } } -export function getWalletUnstakingData(wallet: string): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(dummyWallets.find((dw) => dw.address === wallet)?.unstakingTokenAmount || []); - }, 1000); - }); -} \ No newline at end of file +export async function getWalletUnstakingData(wallet: string, chainId: number): Promise { + if (chainId === MOCK_CHAIN.id) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(dummyWallets.find((dw) => dw.address === wallet)?.zilAmount || []); + }, 1000); + }); + } else { + const currentBlockNumber = await getViemClient(chainId).getBlockNumber(); + + // get unstaking data from contracts + const unstakingWalletData = await Promise.all( + stakingPoolsConfigForChainId[chainId].map( + async (pool) => { + return { + address: pool.definition.address, + blockNumberAndAmount: (await readContract(getViemClient(chainId), { + address: pool.definition.address as Address, + abi: delegatorAbi, + functionName: "getPendingClaims", + account: wallet as Address, + })) as bigint[][], + caimableNow: await readContract(getViemClient(chainId), { + address: pool.definition.address as Address, + abi: delegatorAbi, + functionName: "getClaimable", + account: wallet as Address, + }) as bigint + } + } + ) + ) + + // convert contracts raw data into application data + const result: UserUnstakingPoolData[] = unstakingWalletData.filter( + (uwd) => uwd.blockNumberAndAmount.length > 0 || uwd.caimableNow > 0 + ).map( + (uwd) => { + + const claims: UserUnstakingPoolData[] = []; + + if (uwd.caimableNow > 0) { + claims.push({ + zilAmount: uwd.caimableNow, + availableAt: DateTime.now().minus({ days: 1 }), // just to make sure it displays + address: uwd.address, + }); + } + + if (uwd.blockNumberAndAmount.length > 0) { + claims.push( + ...uwd.blockNumberAndAmount.map( + (bna) => { + const blocksRemaining = Number(bna[0] - currentBlockNumber); + + return { + zilAmount: bna[1], + availableAt: DateTime.now().plus({ seconds: blocksRemaining }), // we assume block takes a second + address: uwd.address, + } + } + ) + ); + } + + return claims; + } + ).flat(); + + return result; + } +} diff --git a/src/script/fetchPoolStaticData.ts b/src/script/fetchPoolStaticData.ts index 0ccd5f1..e8a25d5 100644 --- a/src/script/fetchPoolStaticData.ts +++ b/src/script/fetchPoolStaticData.ts @@ -73,7 +73,7 @@ const argv = yargs(hideBin(process.argv)) ] = await Promise.all([ readTokenContract("decimals"), readTokenContract("symbol"), - readTokenContract("MIN_DELEGATION") + readDelegatorContract("getMinDelegation") ]); const hash = Buffer.from(argv.contract_address + tokenAddress).toString('base64').slice(0, 8); From 58ceb60728c26f75bfa116a12dc8eb79c9007c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosiak?= Date: Thu, 12 Dec 2024 12:55:03 +0100 Subject: [PATCH 2/2] typo --- src/misc/walletsConfig.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/misc/walletsConfig.ts b/src/misc/walletsConfig.ts index 61e732e..89389be 100644 --- a/src/misc/walletsConfig.ts +++ b/src/misc/walletsConfig.ts @@ -191,7 +191,7 @@ export async function getWalletUnstakingData(wallet: string, chainId: number): P functionName: "getPendingClaims", account: wallet as Address, })) as bigint[][], - caimableNow: await readContract(getViemClient(chainId), { + claimableNow: await readContract(getViemClient(chainId), { address: pool.definition.address as Address, abi: delegatorAbi, functionName: "getClaimable", @@ -204,15 +204,15 @@ export async function getWalletUnstakingData(wallet: string, chainId: number): P // convert contracts raw data into application data const result: UserUnstakingPoolData[] = unstakingWalletData.filter( - (uwd) => uwd.blockNumberAndAmount.length > 0 || uwd.caimableNow > 0 + (uwd) => uwd.blockNumberAndAmount.length > 0 || uwd.claimableNow > 0 ).map( (uwd) => { const claims: UserUnstakingPoolData[] = []; - if (uwd.caimableNow > 0) { + if (uwd.claimableNow > 0) { claims.push({ - zilAmount: uwd.caimableNow, + zilAmount: uwd.claimableNow, availableAt: DateTime.now().minus({ days: 1 }), // just to make sure it displays address: uwd.address, });