diff --git a/.env.local_zq2 b/.env.local_zq2 index 4a246ea..ce02623 100644 --- a/.env.local_zq2 +++ b/.env.local_zq2 @@ -1,2 +1,2 @@ -CHAIN_ID=32769 +ZQ2_STAKING_CHAIN_ID=32769 ZQ2_STAKING_WALLET_CONNECT_API_KEY=ewewewejwje \ No newline at end of file diff --git a/.env.mocked b/.env.mocked index 38b0526..26c815a 100644 --- a/.env.mocked +++ b/.env.mocked @@ -1,2 +1,2 @@ -CHAIN_ID=9999999 +ZQ2_STAKING_CHAIN_ID=9999999 ZQ2_STAKING_WALLET_CONNECT_API_KEY=ewewewejwje \ No newline at end of file diff --git a/render_config_staging.yaml b/render_config_staging.yaml index c3030f0..f1b20a2 100644 --- a/render_config_staging.yaml +++ b/render_config_staging.yaml @@ -17,7 +17,7 @@ apps_to_clusters: cluster: staging dns_name: zq2-staking.zilstg.dev healthcheck: - request_path: /health + request_path: /api/health image_name: zq2-staking-frontend namespace: zq2-staking-stg pipeline: diff --git a/src/components/stakingCalculator.tsx b/src/components/stakingCalculator.tsx index b0c23de..450622c 100644 --- a/src/components/stakingCalculator.tsx +++ b/src/components/stakingCalculator.tsx @@ -10,7 +10,6 @@ import Link from "next/link"; const StakingCalculator: React.FC = () => { - const { appConfig } = AppConfigStorage.useContainer(); @@ -160,9 +159,6 @@ const StakingCalculator: React.FC = () => {
Max transaction cost: {zilToStake ? '0.01' : '0'}$
-
- Unbonding Period: {zilToStake ? '0.01' : '0'}$ -
@@ -198,8 +194,7 @@ const StakingCalculator: React.FC = () => {
- - + { stakingCallTxHash !== undefined && (
@@ -215,9 +210,6 @@ const StakingCalculator: React.FC = () => { {stakeContractCallError.message}
)} - - - ) ); diff --git a/src/components/stakingPoolDetailsView.tsx b/src/components/stakingPoolDetailsView.tsx index 30f8dd5..32ca025 100644 --- a/src/components/stakingPoolDetailsView.tsx +++ b/src/components/stakingPoolDetailsView.tsx @@ -1,14 +1,12 @@ import StakingCalculator from "@/components/stakingCalculator"; import UnstakingCalculator from "@/components/unstakingCalculator"; import WithdrawZilPanel from "@/components/withdrawUnstakedZilPanel"; -import { StakingOperations } from "@/contexts/stakingOperations"; import { WalletConnector } from "@/contexts/walletConnector"; import { formatPercentage, formatUnitsToHumanReadable } from "@/misc/formatting"; import { StakingPool } from "@/misc/stakingPoolsConfig"; import { UserStakingPoolData, UserUnstakingPoolData } from "@/misc/walletsConfig"; import { DateTime } from "luxon"; import { useState } from "react"; -import { Button } from "antd"; interface StakingPoolDetailsViewProps { stakingPoolData: StakingPool; @@ -27,10 +25,6 @@ const StakingPoolDetailsView: React.FC = ({ zilAvailable, } = WalletConnector.useContainer(); - const { - unstake, - } = StakingOperations.useContainer(); - const [selectedPane, setSelectedPane] = useState('Stake'); const colorInfoEntry = (title: string, value: string | null) => ( @@ -81,7 +75,8 @@ const StakingPoolDetailsView: React.FC = ({ return (
+ scrollbar-thin scrollbar-thumb-gray1 scrollbar-track-gray3 hover:scrollbar-thumb-gray2" + >
@@ -91,26 +86,26 @@ const StakingPoolDetailsView: React.FC = ({ {stakingPoolData.definition.tokenSymbol}
-
+
{doesUserHoldAnyFundsInThisPool && -
+
{ colorInfoEntry("Available to stake", `${formatUnitsToHumanReadable(zilAvailable || 0n, 18)} ZIL`) } { colorInfoEntry("Staked", `${humanReadableStakingToken(userStakingPoolData?.stakingTokenAmount || 0n)} ${stakingPoolData.definition.tokenSymbol}`) } { colorInfoEntry("Unstake requests", pendingUnstakesValue ? `${humanReadableStakingToken(pendingUnstakesValue)} ${stakingPoolData.definition.tokenSymbol}`: "-" ) } { colorInfoEntry("Available to claim", availableToClaim ? `${humanReadableStakingToken(availableToClaim)} ${stakingPoolData.definition.tokenSymbol}` : "-") }
} -
+
{ greyInfoEntry("Voting power", stakingPoolData.data && formatPercentage(stakingPoolData.data.votingPower)) } { greyInfoEntry("Total supply", stakingPoolData.data && `${humanReadableStakingToken(stakingPoolData.data.tvl)} ${stakingPoolData.definition.tokenSymbol}`) } { greyInfoEntry("Commission", stakingPoolData.data && formatPercentage(stakingPoolData.data.commission)) } { greyInfoEntry("", stakingPoolData.data && ( <> - 1 ZIL =
- {stakingPoolData.data.zilToTokenRate} {stakingPoolData.definition.tokenSymbol} + 1 ZIL ~
+ {stakingPoolData.data.zilToTokenRate.toPrecision(3)} {stakingPoolData.definition.tokenSymbol} )) }
diff --git a/src/components/unstakingCalculator.tsx b/src/components/unstakingCalculator.tsx index a68d10f..d9b46f4 100644 --- a/src/components/unstakingCalculator.tsx +++ b/src/components/unstakingCalculator.tsx @@ -5,9 +5,11 @@ import { formatPercentage, convertTokenToZil, formatUnitsToHumanReadable, + getHumanFormDuration, } from '@/misc/formatting'; import { formatUnits, parseEther } from 'viem'; import { StakingOperations } from '@/contexts/stakingOperations'; +import { DateTime } from 'luxon'; const UnstakingCalculator: React.FC = () => { const { stakingPoolForView } = StakingPoolsStorage.useContainer(); @@ -72,6 +74,10 @@ const UnstakingCalculator: React.FC = () => { ); }; + const unboudingPeriod = getHumanFormDuration(( + DateTime.now().plus({ minutes: stakingPoolForView?.stakingPool.definition.withdrawPeriodInMinutes || 0 }) + )); + return ( stakingPoolForView && (
@@ -111,7 +117,7 @@ const UnstakingCalculator: React.FC = () => { )} ZIL - ~5 days + { unboudingPeriod }
@@ -149,7 +155,7 @@ const UnstakingCalculator: React.FC = () => { Max transaction cost: {zilToUnstake ? '0.01' : '0'}$
- Unbonding Period: {zilToUnstake ? '0.01' : '0'}$ + Unbonding Period: { unboudingPeriod }
diff --git a/src/contexts/stakingOperations.tsx b/src/contexts/stakingOperations.tsx index 248c741..aee8b72 100644 --- a/src/contexts/stakingOperations.tsx +++ b/src/contexts/stakingOperations.tsx @@ -46,6 +46,7 @@ const useStakingOperations = () => { if (isDummyWalletConnected) { setDummyWalletPopupContent(`Now User gonna approve the wallet transaction for staking ZIL`); setIsDummyWalletPopupOpen(true); + setStakingCallTxHash("0x1234567890234567890234567890234567890" as Address); } else { writeContract( wagmiConfig, @@ -110,6 +111,7 @@ const useStakingOperations = () => { if (isDummyWalletConnected) { setDummyWalletPopupContent(`Now User gonna approve the wallet transaction for unstaking ${tokensToUnstake} staked tokens`); setIsDummyWalletPopupOpen(true); + setUnstakingCallTxHash("0x1234567890234567890234567890234567890" as Address); } else { writeContract( wagmiConfig, diff --git a/src/misc/stakingAbis.ts b/src/misc/stakingAbis.ts index 5e633d0..1666a21 100644 --- a/src/misc/stakingAbis.ts +++ b/src/misc/stakingAbis.ts @@ -1,3 +1,32 @@ +export const depositAbi = [ + { + "inputs": [], + "name": "getFutureTotalStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawalPeriod", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] + export const delegatorAbi = [ { "inputs": [], @@ -44,5 +73,44 @@ export const delegatorAbi = [ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "getCommissionNumerator", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "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 788c548..4fea7ab 100644 --- a/src/misc/stakingPoolsConfig.ts +++ b/src/misc/stakingPoolsConfig.ts @@ -1,6 +1,12 @@ -import { Address, erc20Abi } from "viem"; +import { Address, erc20Abi, formatUnits, parseUnits } from "viem"; import { CHAIN_ZQ2_PROTOTESTNET, CHAIN_ZQ2_DOCKERCOMPOSE, getViemClient, MOCK_CHAIN } from "./chainConfig"; import { readContract } from "viem/actions"; +import { delegatorAbi, depositAbi } from "./stakingAbis"; + +/** + * Deposit address is always the same + */ +const DEPOSIT_ADDRESS = "0x00000000005a494c4445504f53495450524f5859" as Address; export interface StakingPoolDefinition { id: string; @@ -11,6 +17,7 @@ export interface StakingPoolDefinition { tokenSymbol: string; iconUrl: string; minimumStake: bigint; + withdrawPeriodInMinutes: number; } export interface StakingPoolData { @@ -39,17 +46,76 @@ async function mockDelegatorDataProvider(mockData: StakingPoolData, loadingMilis }); } -async function fetchDelegatorDataFromNetwork(mockData: StakingPoolData, definition: StakingPoolDefinition, chainId: number): Promise { - try { - const totalSupply = await readContract(getViemClient(chainId), { +async function fetchDelegatorDataFromNetwork(definition: StakingPoolDefinition, chainId: number): Promise { + + const viemClient = getViemClient(chainId); + + const readDelegatorContract = async (functionName: string): Promise => { + return (await readContract(viemClient, { + address: definition.address as Address, + abi: delegatorAbi, + functionName, + })) as T + } + + const readTokenContract = async (functionName: "symbol" | "name" | "totalSupply" | "allowance" | "balanceOf" | "decimals"): Promise => { + return await (readContract(viemClient, { address: definition.tokenAddress as Address, abi: erc20Abi, - functionName: "totalSupply", + functionName, + })) as T + } + + const readDepositContract = async (functionName: string): Promise => { + return await (readContract(viemClient, { + address: DEPOSIT_ADDRESS, + abi: depositAbi, + functionName, + })) as T + } + + try { + const [ + totalSupply, + commissionNumerator, + zilToTokenRateWei, + delegatorStake, + depositTotalStake, + ] = await Promise.all([ + readTokenContract("totalSupply"), + readDelegatorContract("getCommissionNumerator"), + readDelegatorContract("getPrice"), + readDelegatorContract("getStake"), + readDepositContract("getFutureTotalStake"), + ]); + + 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 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 { - ...mockData, tvl: totalSupply, + commission, + zilToTokenRate, + votingPower, + apr: apr + } } catch (error) { console.error("Error fetching total supply:", error); @@ -57,6 +123,8 @@ async function fetchDelegatorDataFromNetwork(mockData: StakingPoolData, definiti } } +const twoWeeksInMinutes = 60 * 24 * 14; + export const stakingPoolsConfigForChainId: Record> = { [MOCK_CHAIN.id]: [ { @@ -68,11 +136,12 @@ export const stakingPoolsConfigForChainId: Record = [ stakingTokenAmount: [ { address: "0x1234567890234567890234567890234567890", - stakingTokenAmount: 1000n, + stakingTokenAmount: parseUnits("1000.50", 18), rewardAcumulated: 10 }, { address: "0x96525678902345678902345678918278372212", - stakingTokenAmount: 60n, + stakingTokenAmount: parseUnits("60.50", 18), rewardAcumulated: 50 }, ], @@ -82,12 +82,12 @@ export const dummyWallets: Array = [ stakingTokenAmount: [ { address: "0x1234567890234567890234567890234567890", - stakingTokenAmount: 1000n, + stakingTokenAmount: parseUnits("1000", 18), rewardAcumulated: 10 }, { address: "0x96525678902345678902345678918278372212", - stakingTokenAmount: 60n, + stakingTokenAmount: parseUnits("9991119", 18), rewardAcumulated: 50 }, ], @@ -126,12 +126,12 @@ export const dummyWallets: Array = [ stakingTokenAmount: [ { address: "0x96525678902345678902345678918278372212", - stakingTokenAmount: 123n, + stakingTokenAmount: parseUnits("123.522039320", 18), rewardAcumulated: 40 }, { address: "0x82245678902345678902345678918278372382", - stakingTokenAmount: 999n, + stakingTokenAmount: parseUnits("99999", 18), rewardAcumulated: 0 }, ], diff --git a/src/script/fetchPoolStaticData.ts b/src/script/fetchPoolStaticData.ts index 269351b..0ccd5f1 100644 --- a/src/script/fetchPoolStaticData.ts +++ b/src/script/fetchPoolStaticData.ts @@ -43,51 +43,55 @@ const argv = yargs(hideBin(process.argv)) console.log(`Network RPC URL: ${argv.network_id}`); console.log(`Contract Address: ${argv.contract_address}`); + const chainid = parseInt(argv.network_id); - const tokenAddress = await readContract(getViemClient(chainid), { - address: argv.contract_address as Address, - abi: delegatorAbi, - functionName: "getLST", - }) as string; + const readDelegatorContract = async (functionName: string): Promise => { + return (await readContract(getViemClient(chainid), { + address: argv.contract_address as Address, + abi: delegatorAbi, + functionName, + })) as T + } + + const [tokenAddress] = await Promise.all([ + readDelegatorContract
("getLST"), + ]); + + const readTokenContract = async (functionName: string): Promise => { + return await (readContract(getViemClient(chainid), { + address: tokenAddress, + abi: erc20Abi, + functionName: "decimals", + })) as T + } const [ tokenDecimals, tokenSymbol, minimumStake, ] = await Promise.all([ - readContract(getViemClient(chainid), { - address: tokenAddress as Address, - abi: erc20Abi, - functionName: "decimals", - }), - readContract(getViemClient(chainid), { - address: tokenAddress as Address, - abi: erc20Abi, - functionName: "symbol", - }), - readContract(getViemClient(chainid), { - address: argv.contract_address as Address, - abi: delegatorAbi, - functionName: "MIN_DELEGATION", - }), + readTokenContract("decimals"), + readTokenContract("symbol"), + readTokenContract("MIN_DELEGATION") ]); const hash = Buffer.from(argv.contract_address + tokenAddress).toString('base64').slice(0, 8); + const twoWeeksInMinutes = 60 * 24 * 14; const definition: StakingPoolDefinition = { id: hash, - address: argv.contract_address, - tokenAddress, - iconUrl: argv.icon_url, - name: argv.name, - tokenDecimals, - tokenSymbol, - minimumStake: minimumStake as bigint, + address: argv.contract_address, + tokenAddress, + iconUrl: argv.icon_url, + name: argv.name, + tokenDecimals, + tokenSymbol, + minimumStake: minimumStake as bigint, + withdrawPeriodInMinutes: twoWeeksInMinutes, } console.log("Add following definition to stakingPoolsConfig.ts"); - // console.log(JSON.stringify({ definition }, null, 2)); console.log({ definition, }); } )();