diff --git a/src/App.jsx b/src/App.jsx index cb1470a..c2d1981 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,7 @@ import Home from './pages/Home'; import Vaults from './pages/vaults/Vaults'; import Vault from './pages/vault/Vault'; import VaultHistory from './pages/vault/VaultHistory'; +import VaultMerkl from './pages/vault/VaultMerkl'; import LiquidationPools from './pages/liquidation-pools/LiquidationPools'; import TstStaking from './pages/tst-staking/TstStaking'; import LegacyPools from './pages/legacy-pools/LegacyPools'; @@ -31,6 +32,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/abis/merkl.jsx b/src/abis/merkl.jsx new file mode 100644 index 0000000..c51e8ca --- /dev/null +++ b/src/abis/merkl.jsx @@ -0,0 +1,32 @@ +export const abi = [ + { + "inputs":[ + { + "internalType":"address[]", + "name":"users", + "type":"address[]" + }, + { + "internalType":"address[]", + "name":"tokens", + "type":"address[]" + }, + { + "internalType":"uint256[]", + "name":"amounts", + "type":"uint256[]" + }, + { + "internalType":"bytes32[][]", + "name":"proofs", + "type":"bytes32[][]" + } + ], + "name":"claim", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + } +]; + +export default abi; diff --git a/src/abis/smartVaultV4.jsx b/src/abis/smartVaultV4.jsx index 8daf9a1..71fc717 100644 --- a/src/abis/smartVaultV4.jsx +++ b/src/abis/smartVaultV4.jsx @@ -62,6 +62,39 @@ export const abi = [ ], "stateMutability": "view", "type": "function" + }, + { + "type": "function", + "name": "merklClaim", + "inputs": [ + { + "name": "_distributor", + "type": "address", + "internalType": "address" + }, + { + "name": "users", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokens", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "amounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "proofs", + "type": "bytes32[][]", + "internalType": "bytes32[][]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" } ]; diff --git a/src/assets/merkl-powered-dark.svg b/src/assets/merkl-powered-dark.svg new file mode 100644 index 0000000..42779b2 --- /dev/null +++ b/src/assets/merkl-powered-dark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/merkl-powered-light.svg b/src/assets/merkl-powered-light.svg new file mode 100644 index 0000000..fab4358 --- /dev/null +++ b/src/assets/merkl-powered-light.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/merkllogo-light.webp b/src/assets/merkllogo-light.webp new file mode 100644 index 0000000..5ebf680 Binary files /dev/null and b/src/assets/merkllogo-light.webp differ diff --git a/src/assets/merkllogo.webp b/src/assets/merkllogo.webp new file mode 100644 index 0000000..217a91b Binary files /dev/null and b/src/assets/merkllogo.webp differ diff --git a/src/assets/merklogo-dark.webp b/src/assets/merklogo-dark.webp new file mode 100644 index 0000000..9fb89e1 Binary files /dev/null and b/src/assets/merklogo-dark.webp differ diff --git a/src/components/tst-staking/StakingRewards.jsx b/src/components/tst-staking/StakingRewards.jsx index 37120af..b851750 100644 --- a/src/components/tst-staking/StakingRewards.jsx +++ b/src/components/tst-staking/StakingRewards.jsx @@ -19,6 +19,8 @@ const StakingRewards = ({ rawStakedSince, collatDaily, priceData, + tstGlobalBalance, + tstGlobalBalanceLoading, }) => { const { erc20Abi } = useErc20AbiStore(); @@ -102,6 +104,8 @@ const StakingRewards = ({ rawStakedSince={rawStakedSince} collatDaily={collatDaily} latestPrices={latestPrices} + tstGlobalBalance={tstGlobalBalance} + tstGlobalBalanceLoading={tstGlobalBalanceLoading} /> { const [open, setOpen] = useState(false); const [shareOpen, setShareOpen] = useState(false); @@ -175,7 +177,15 @@ const StakingSummary = ({ } // formatted amount - const stakedAmount = ethers.formatEther(tstAmount.toString()); + const stakedAmount = ethers.formatUnits(tstAmount.toString(), 18); + + let globalAmount = 0; + + if (tstGlobalBalance) { + globalAmount = ethers.formatUnits(tstGlobalBalance.toString(), 18); + } + + const poolShare = Number((stakedAmount / globalAmount) * 100).toFixed(8); const handleCloseModal = () => { setOpen(false) @@ -220,22 +230,6 @@ const StakingSummary = ({ return since; } - // const calculateSimpleAPY = (stakedAmount, totalRewardsValue, daysStaked) => { - // const dailyEarnings = Number(totalRewardsValue / daysStaked) || 0; - // const annualEarningsPerTST = (dailyEarnings * 365) / Number(stakedAmount) || 0; - // return annualEarningsPerTST * 100; - // } - const calculateSimpleAPY = (stakedAmount, totalRewardsValue, daysStaked) => { - const dailyEarnings = totalRewardsValue || 0; - const annualEarningsPerTST = (dailyEarnings * 365) / Number(stakedAmount) || 0; - return annualEarningsPerTST; - } - - // const calculateSimpleAPY = (totalValueUSD, dailyEarnings) => { - // const apy = Number(dailyEarnings * 365); - // return apy; - // } - const rewardsWithPrices = rewardsData.map(reward => { const useAmount = ethers.formatUnits(reward.amount, reward.decimals); const useRate = ethers.formatUnits(reward.dailyReward, reward.decimals); @@ -267,10 +261,6 @@ const StakingSummary = ({ if (nextTier?.minAmount) { progress = (stakedAmount / nextTier.minAmount) * 100; } - const apyDisplay = calculateSimpleAPY(stakedAmount, totalRateUSD, daysStaked) + '%' || '0%'; - // const apyDisplay = calculateSimpleAPY(totalValueUSD, dailyEarnings).toFixed(8) + '%' || '0%'; - - let stakeRatio = 'TODO.DOTO'; let progressPercentage = `${progress}%`; @@ -306,14 +296,14 @@ const StakingSummary = ({ - {/*
+
- Historical APY + Your Pool Share
-
*/} +
@@ -400,44 +390,34 @@ const StakingSummary = ({ Participate in protocol governance -
+
Total TST Staked - - {stakedAmount} TST - + + + {stakedAmount ? ( + Number.parseFloat(Number(stakedAmount).toFixed(4)) + ) : ('0')} +  TST + +
- {/*
-
- - Historical APY - - - - - - {apyDisplay} - -
-
*/} - {/*
+
Your Pool Share - {stakeRatio} + {tstGlobalBalanceLoading ? ( + + ) : ( + <> + {poolShare || 0}% + + )}
-
*/} +
diff --git a/src/components/ui/TokenIcon.jsx b/src/components/ui/TokenIcon.jsx index adf0bb7..a6164d1 100644 --- a/src/components/ui/TokenIcon.jsx +++ b/src/components/ui/TokenIcon.jsx @@ -12,6 +12,7 @@ import sushilogo from "../../assets/sushilogo.svg"; import susdlogo from "../../assets/usdslogo.svg"; import usdclogo from "../../assets/usdclogo.svg"; import wethlogo from "../../assets/wethlogo.svg"; +import merkllogo from "../../assets/merkllogo.webp"; import Typography from "./Typography"; @@ -19,6 +20,7 @@ const TokenIcon = ({ symbol, style, className, + isMerkl, }) => { switch (symbol) { case 'ETH': @@ -26,7 +28,7 @@ const TokenIcon = ({ ETH logo ); @@ -35,7 +37,7 @@ const TokenIcon = ({ TST logo ); @@ -44,7 +46,7 @@ const TokenIcon = ({ EUROs logo ); @@ -53,7 +55,7 @@ const TokenIcon = ({ WBTC logo ); @@ -62,7 +64,7 @@ const TokenIcon = ({ LINK logo ); @@ -71,7 +73,7 @@ const TokenIcon = ({ ARB logo ); @@ -80,7 +82,7 @@ const TokenIcon = ({ PAXG logo ); @@ -89,7 +91,7 @@ const TokenIcon = ({ gmx logo ); @@ -98,7 +100,7 @@ const TokenIcon = ({ rdnt logo ); @@ -107,7 +109,7 @@ const TokenIcon = ({ sushi logo ); @@ -116,7 +118,7 @@ const TokenIcon = ({ USDs logo ); @@ -125,7 +127,7 @@ const TokenIcon = ({ USDC logo ); @@ -134,7 +136,7 @@ const TokenIcon = ({ WETH logo ); @@ -144,11 +146,21 @@ const TokenIcon = ({ logo ); default: + if (isMerkl) { + return ( + {`${symbol} + ) + } return (
{(amount <= 0 || !yieldEnabled || !tokenYield) ? (null) : ( - + )}
diff --git a/src/components/vault/merkl/ClaimModal.jsx b/src/components/vault/merkl/ClaimModal.jsx new file mode 100644 index 0000000..e6677c9 --- /dev/null +++ b/src/components/vault/merkl/ClaimModal.jsx @@ -0,0 +1,208 @@ +import { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { ethers } from "ethers"; +import { toast } from 'react-toastify'; +import { + useWriteContract, +} from "wagmi"; +import { + ArrowDownCircleIcon, +} from '@heroicons/react/24/outline'; + +import { + useVaultAddressStore, + useMerklAddressStore, + useMerklABIStore, + useSmartVaultV4ABIStore, +} from "../../../store/Store"; + +import Modal from "../../ui/Modal"; +import Button from "../../ui/Button"; +import Typography from "../../ui/Typography"; +import TokenIcon from "../../ui/TokenIcon"; +import CenterLoader from "../../ui/CenterLoader"; + +const ClaimModal = (props) => { + const { + open, + closeModal, + useAssets, + parentLoading, + } = props; + + const { merklDistributorAddress } = useMerklAddressStore(); + const { merklABI } = useMerklABIStore(); + const { vaultAddress } = useVaultAddressStore(); + const { smartVaultV4ABI } = useSmartVaultV4ABIStore(); + + const { vaultId } = useParams(); + + const { writeContract, isError, isPending, isSuccess, error } = useWriteContract(); + + const claimUsers = useAssets && useAssets.length && useAssets.map(function(asset, index) { + return (vaultAddress) + }); + const claimTokens = useAssets && useAssets.length && useAssets.map(function(asset, index) { + return (asset?.tokenAddress) + }); + const claimAmounts = useAssets && useAssets.length && useAssets.map(function(asset, index) { + return (asset?.accumulated) + }); + const claimProofs = useAssets && useAssets.length && useAssets.map(function(asset, index) { + return (asset?.proof) + }); + + const handleClaimToken = async () => { + if (vaultId <= 49) { + // LEGACY CODE FOR OLD VAULTS + try { + writeContract({ + abi: merklABI, + address: merklDistributorAddress, + functionName: "claim", + args: [ + claimUsers, + claimTokens, + claimAmounts, + claimProofs + ], + }); + } catch (error) { + let errorMessage = ''; + if (error && error.shortMessage) { + errorMessage = error.shortMessage; + } + toast.error(errorMessage || 'There was an error'); + } + } else { + // CURRENT CODE + try { + writeContract({ + abi: smartVaultV4ABI, + address: vaultAddress, + functionName: "merklClaim", + args: [ + merklDistributorAddress, + claimUsers, + claimTokens, + claimAmounts, + claimProofs + ], + }); + } catch (error) { + let errorMessage = ''; + if (error && error.shortMessage) { + errorMessage = error.shortMessage; + } + toast.error(errorMessage || 'There was an error'); + } + } + }; + + useEffect(() => { + if (isPending) { + // + } else if (isSuccess) { + toast.success("Claim Successful"); + closeModal(); + } else if (isError) { + console.error(error) + toast.error('There was an error'); + } + }, [ + isPending, + isSuccess, + isError, + error, + ]); + + return ( + <> + + <> + + + Claim Your Rewards + + + + Please confirm that you wish to claim the following tokens: + + + + + + + + + + {parentLoading ? (null) : ( + + {useAssets && useAssets.length && useAssets.map(function(asset, index) { + const symbol = asset?.symbol; + const decimals = asset?.decimals; + const unclaimedRaw = asset?.unclaimed; + + const unclaimed = ethers.formatUnits(unclaimedRaw, decimals); + + return ( + + + + + ) + })} + + )} +
TokenQuantity
+
+
+ +
+
+ {symbol} +
+
+
+ {unclaimed} +
+ {parentLoading ? ( + + ) : (null)} + +
+ + +
+ +
+ + ); +}; + +export default ClaimModal; \ No newline at end of file diff --git a/src/components/vault/merkl/RewardItem.jsx b/src/components/vault/merkl/RewardItem.jsx new file mode 100644 index 0000000..d90affd --- /dev/null +++ b/src/components/vault/merkl/RewardItem.jsx @@ -0,0 +1,121 @@ +import { Fragment } from "react"; +import { ethers } from "ethers"; +import { + Tooltip, +} from 'react-daisyui'; + +import { + ChevronDownIcon, + ChevronUpIcon, +} from '@heroicons/react/24/outline'; + +import Button from "../../ui/Button"; +import TokenIcon from "../../ui/TokenIcon"; + +const RewardItem = ({ + vaultType, + index, + asset, + handleClick, + toggleSubRow, + subRow, +}) => { + + let currencySymbol = ''; + if (vaultType === 'EUROs') { + currencySymbol = '€'; + } + if (vaultType === 'USDs') { + currencySymbol = '$'; + } + + const claimed = asset?.accumulated; + const unclaimed = asset?.unclaimed; + const symbol = asset?.symbol; + const decimals = asset?.decimals; + const balance = asset?.balanceOf; + + return ( + + toggleSubRow(index)} + className={subRow === index + 'sub' ? ( + 'cursor-pointer hover active' + ) : ( + 'cursor-pointer hover' + )} + > + +
+ + + +
{symbol}
+
+ + + {ethers.formatUnits(balance, decimals)} + + + {ethers.formatUnits(claimed, decimals)} + + + + {ethers.formatUnits(unclaimed, decimals)} + + + + + + + + <> +
+ + +
+ + + +
+ ) +}; + +export default RewardItem; \ No newline at end of file diff --git a/src/components/vault/merkl/RewardList.jsx b/src/components/vault/merkl/RewardList.jsx new file mode 100644 index 0000000..9dd0643 --- /dev/null +++ b/src/components/vault/merkl/RewardList.jsx @@ -0,0 +1,225 @@ +import { useState } from "react"; + +import { + useReadContracts, +} from "wagmi"; + +import { + QueueListIcon, +} from '@heroicons/react/24/outline'; + +import { + Tooltip, +} from 'react-daisyui'; + +import { + QuestionMarkCircleIcon, +} from '@heroicons/react/24/outline'; + +import { + useErc20AbiStore, + useVaultAddressStore, +} from "../../../store/Store"; + +import Button from "../../ui/Button"; +import CenterLoader from "../../ui/CenterLoader"; +import Typography from "../../ui/Typography"; +import TokenActions from "./TokenActions"; +import RewardItem from "./RewardItem"; +import ClaimModal from "./ClaimModal"; + +const RewardList = ({ + merklRewards, + merklRewardsLoading, + vaultType, +}) => { + const { erc20Abi } = useErc20AbiStore(); + const { vaultAddress } = useVaultAddressStore(); + + const [claimAllOpen, setClaimAllOpen] = useState(false); + const [actionType, setActionType] = useState(); + const [useAsset, setUseAsset] = useState(); + const [subRow, setSubRow] = useState('0sub'); + + let currencySymbol = ''; + if (vaultType === 'EUROs') { + currencySymbol = '€'; + } + if (vaultType === 'USDs') { + currencySymbol = '$'; + } + + const closeAction = () => { + setActionType(''); + } + + const toggleSubRow = (index) => { + const useRow = index + 'sub'; + + if (subRow === useRow) { + setSubRow(''); + } else { + setSubRow(useRow); + } + } + + const { data: merklBalances, isLoading: merklBalancesLoading } = useReadContracts({ + contracts:merklRewards.map((item) =>({ + address: item?.tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + args: [vaultAddress], + })) + }) + + const merklData = merklRewards.map((item, index) => { + let useBalance = 0n; + if (merklBalances) { + if (merklBalances[index]) { + if (merklBalances[index].result) { + useBalance = merklBalances[index].result; + } + return { + ...merklRewards[index], + balanceOf: useBalance + } + } else { + return {}; + } + } + }); + + const hasClaims = merklData.find(item => item?.unclaimed > 0); + + return ( + <> +
+ + + Merkl Reward Tokens + + + + + + + + + + + + + {merklRewardsLoading || merklBalancesLoading ? (null) : ( + + {merklData && merklData.length ? ( + <> + {merklData.map(function(asset, index) { + const handleClick = (type, asset) => { + setActionType(type); + setUseAsset(asset); + }; + + return ( + + )} + )} + + ) : ( + <> + + + + + )} + + )} +
+ Asset + + Current Balance + + + + + Lifetime Accumulated + + + +   + Unclaimed + + + + +   +
+ No Rewards Earned Yet. +
+ Start earning by placing your tokens into the yield pool. +
+ {merklRewardsLoading || merklBalancesLoading ? ( + + ) : (null)} + +
+ +
+
+ + setClaimAllOpen(false)} + useAssets={merklData} + vaultType={vaultType} + parentLoading={merklRewardsLoading || merklBalancesLoading} + /> + + + ); +}; + +export default RewardList; \ No newline at end of file diff --git a/src/components/vault/merkl/TokenActions.jsx b/src/components/vault/merkl/TokenActions.jsx new file mode 100644 index 0000000..e1b23b1 --- /dev/null +++ b/src/components/vault/merkl/TokenActions.jsx @@ -0,0 +1,61 @@ +import React from "react"; +import { ethers } from "ethers"; +import WithdrawModal from "./WithdrawModal"; +import ClaimModal from "./ClaimModal"; + +const TokenActions = ({ + actionType, + useAsset, + closeModal, + vaultType, +}) => { + let content; + + if (useAsset) { + const symbol = useAsset?.symbol; + const decimals = useAsset?.decimals; + const balanceRaw = useAsset?.balanceOf; + const tokenAddress = useAsset?.tokenAddress; + + const balance = ethers.formatUnits(balanceRaw, decimals); + + switch (actionType) { + case 'WITHDRAW': + content = ( + <> + + + ); + break; + case 'CLAIM': + content = ( + <> + + + ); + break; + default: + content = <>; + break; + } + + return <>{content}; + } + + return <>; +}; + +export default TokenActions; diff --git a/src/components/vault/merkl/WithdrawModal.jsx b/src/components/vault/merkl/WithdrawModal.jsx new file mode 100644 index 0000000..4f0a1d2 --- /dev/null +++ b/src/components/vault/merkl/WithdrawModal.jsx @@ -0,0 +1,190 @@ +import { useEffect, useRef, useState } from "react"; +import { ethers } from "ethers"; +import { toast } from 'react-toastify'; +import { + useWriteContract, + useAccount, + useWaitForTransactionReceipt, +} from "wagmi"; +import { + ArrowDownCircleIcon, +} from '@heroicons/react/24/outline'; + +import { + useVaultAddressStore, + useSmartVaultABIStore, +} from "../../../store/Store"; + +import Modal from "../../ui/Modal"; +import Button from "../../ui/Button"; +import Typography from "../../ui/Typography"; +import Input from "../../ui/Input"; + +const WithdrawModal = (props) => { + const { + open, + closeModal, + symbol, + decimals, + balance, + tokenAddress, + } = props; + + const [amount, setAmount] = useState(0n); + + const { vaultAddress } = useVaultAddressStore(); + const { smartVaultABI } = useSmartVaultABIStore(); + + const [ txdata, setTxdata ] = useState(null); + + const { address } = useAccount(); + const inputRef = useRef(null); + + const handleAmount = (e) => { + setAmount(ethers.parseUnits(e.target.value.toString(), decimals)) + }; + + const { writeContract, isError, isPending, isSuccess } = useWriteContract(); + + const handleWithdrawToken = async () => { + try { + writeContract({ + abi: smartVaultABI, + address: vaultAddress, + functionName: "removeAsset", + args: [ + tokenAddress, + amount, + address, + ], + }); + } catch (error) { + let errorMessage = ''; + if (error && error.shortMessage) { + errorMessage = error.shortMessage; + } + toast.error(errorMessage || 'There was an error'); + } + }; + + useEffect(() => { + if (isPending) { + // + } else if (isSuccess) { + inputRef.current.value = ""; + inputRef.current.focus(); + setTxdata(txRcptData); + toast.success("Withdraw Successful"); + } else if (isError) { + inputRef.current.value = ""; + inputRef.current.focus(); + } + }, [ + isPending, + isSuccess, + isError, + ]); + + const shortenAddress = (address) => { + const prefix = address?.slice(0, 6); + const suffix = address?.slice(-8); + return `${prefix}...${suffix}`; + }; + + const shortenedAddress = shortenAddress(address); + + const { + data: txRcptData, + } = useWaitForTransactionReceipt({ + hash: txdata, + }); + + const handleMaxBalance = async () => { + const formatted = balance; + inputRef.current.value = formatted; + handleAmount({ target: { value: formatted } }); + }; + + return ( + <> + + <> + + + Withdraw {symbol} + +
+ + This transaction may revert if the token you are withdrawing is being used as collateral and would leave your vault undercollateralised. + +
+
+ + Withdraw Amount + + + Available: {balance || ''} + +
+
+ + + +
+
+ {symbol} to address "{shortenedAddress}" +
+ +
+ + +
+ +
+ + ); +}; + +export default WithdrawModal; \ No newline at end of file diff --git a/src/components/vault/yield/YieldClaimModal.jsx b/src/components/vault/yield/YieldClaimModal.jsx index a150134..e4e5527 100644 --- a/src/components/vault/yield/YieldClaimModal.jsx +++ b/src/components/vault/yield/YieldClaimModal.jsx @@ -263,7 +263,7 @@ const YieldClaimModal = ({ variant="outline" onClick={() => setYieldStage('')} > - Return to Vault + Back
diff --git a/src/components/vault/yield/YieldList.jsx b/src/components/vault/yield/YieldList.jsx index 9b9362f..fe4ee67 100644 --- a/src/components/vault/yield/YieldList.jsx +++ b/src/components/vault/yield/YieldList.jsx @@ -164,7 +164,7 @@ const YieldList = (props) => { ) : ( <> - ${showBalance || ''} + ${showBalance || '0.00'} )} diff --git a/src/components/vault/yield/YieldParent.jsx b/src/components/vault/yield/YieldParent.jsx index 10a5dd1..c75e604 100644 --- a/src/components/vault/yield/YieldParent.jsx +++ b/src/components/vault/yield/YieldParent.jsx @@ -22,7 +22,7 @@ import Card from "../../ui/Card"; import Typography from "../../ui/Typography"; import Button from "../../ui/Button"; -const Vault = (props) => { +const YieldParent = (props) => { const { yieldEnabled } = props; const { vaultAddress } = useVaultAddressStore(); const { smartVaultABI } = useSmartVaultABIStore(); @@ -32,6 +32,8 @@ const Vault = (props) => { const [ gammaReturnsLoading, setGammaReturnsLoading ] = useState(false); const [ gammaStats, setGammaStats ] = useState([]); const [ gammaStatsLoading, setGammaStatsLoading ] = useState(false); + const [ merklRewards, setMerklRewards ] = useState([]); + const [ merklRewardsLoading, setMerklRewardsLoading ] = useState(true); const [ userSummary, setUserSummary ] = useState([]); @@ -65,6 +67,38 @@ const Vault = (props) => { } }, [yieldData, isPending]); + useEffect(() => { + getMerklRewardsData(); + }, [vaultAddress]); + + const getMerklRewardsData = async () => { + try { + setMerklRewardsLoading(true); + const response = await axios.get( + `https://api.merkl.xyz/v3/userRewards?chainId=42161&proof=true&user=${vaultAddress}` + ); + + const useData = response?.data; + + const rewardsArray = []; + + Object.keys(useData).forEach(key => { + const value = useData[key]; + const rewardItem = { + tokenAddress: key, + ... value + } + rewardsArray.push(rewardItem); + }); + + setMerklRewards(rewardsArray); + setMerklRewardsLoading(false); + } catch (error) { + setMerklRewardsLoading(false); + console.log(error); + } + }; + const getGammaUserData = async () => { try { setGammaUserLoading(true); @@ -215,6 +249,8 @@ const Vault = (props) => { gammaUser={gammaUser} gammaUserLoading={gammaUserLoading} userSummary={userSummary} + merklRewards={merklRewards} + merklRewardsLoading={merklRewardsLoading} /> @@ -277,4 +313,4 @@ const Vault = (props) => { ) }; -export default Vault; \ No newline at end of file +export default YieldParent; \ No newline at end of file diff --git a/src/components/vault/yield/YieldPerformanceModal.jsx b/src/components/vault/yield/YieldPerformanceModal.jsx index ec17703..6a0916b 100644 --- a/src/components/vault/yield/YieldPerformanceModal.jsx +++ b/src/components/vault/yield/YieldPerformanceModal.jsx @@ -44,9 +44,9 @@ const YieldPerformanceModal = ({ const initialTokenCurrentUSD = positionUser?.returns?.initialTokenCurrentUSD || 0; const currentUSD = positionUser?.returns?.currentUSD || 0; const hypervisorReturnsUSD = positionUser?.returns?.hypervisorReturnsUSD; - const hypervisorReturnsPercentage = positionUser?.returns?.hypervisorReturnsPercentage; - const netMarketReturnsUSD = positionUser?.returns?.netMarketReturnsUSD; - const netMarketReturnsPercentage = positionUser?.returns?.netMarketReturnsPercentage; + const hypervisorReturnsPercentage = positionUser?.returns?.hypervisorReturnsPercentage || 0; + const netMarketReturnsUSD = positionUser?.returns?.netMarketReturnsUSD || 0; + const netMarketReturnsPercentage = positionUser?.returns?.netMarketReturnsPercentage || 0; const tvlUSD = Number(positionStats?.tvlUSD) || 0; let showApy = '0'; @@ -55,7 +55,7 @@ const YieldPerformanceModal = ({ } const getMarketContext = () => { - const marketReturn = parseFloat(netMarketReturnsPercentage); + const marketReturn = parseFloat(netMarketReturnsPercentage) || 0; if (marketReturn > 0) { return { description: `Market movement has generated ${formatUSD(netMarketReturnsUSD)}`, @@ -190,7 +190,7 @@ const YieldPerformanceModal = ({ Current Value - ~${currentUSD?.toFixed(2) || ''} + ${currentUSD?.toFixed(2) || ''} @@ -249,7 +249,7 @@ const YieldPerformanceModal = ({ variant="outline" onClick={handleCloseModal} > - Return to Vault + Back + {vaultType === 'USDs' ? ( + + ) : null} + + + + { return (
) diff --git a/src/pages/vault/VaultMerkl.jsx b/src/pages/vault/VaultMerkl.jsx new file mode 100644 index 0000000..2793058 --- /dev/null +++ b/src/pages/vault/VaultMerkl.jsx @@ -0,0 +1,207 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + useReadContract, + useChainId, + useBlockNumber, + useWatchBlockNumber, +} from "wagmi"; + +import axios from "axios"; + +import { arbitrumSepolia } from "wagmi/chains"; +import { + ChevronLeftIcon, +} from '@heroicons/react/24/outline'; + +import { + useLocalThemeModeStore, + usesUSDContractAddressStore, + useVaultManagerAbiStore, + useVaultAddressStore, + useVaultStore, +} from "../../store/Store"; + +import MerklPoweredDark from "../../assets/merkl-powered-dark.svg"; +import MerklPoweredLight from "../../assets/merkl-powered-light.svg"; + +import RewardList from "../../components/vault/merkl/RewardList"; + +import CenterLoader from "../../components/ui/CenterLoader"; +import Card from "../../components/ui/Card"; +import Button from "../../components/ui/Button"; + +const VaultMerkl = () => { + const chainId = useChainId(); + + const { + arbitrumsUSDSepoliaContractAddress, + arbitrumsUSDContractAddress + } = usesUSDContractAddressStore(); + + const { vaultManagerAbi } = useVaultManagerAbiStore(); + const { setVaultAddress } = useVaultAddressStore(); + const { vaultStore, setVaultStore } = useVaultStore(); + const { vaultType, vaultId } = useParams(); + const { localThemeModeStore } = useLocalThemeModeStore(); + const navigate = useNavigate(); + + const { data: blockNumber } = useBlockNumber(); + const [renderedBlock, setRenderedBlock] = useState(blockNumber); + + const [vaultsLoading, setVaultsLoading] = useState(true); + const [ merklRewards, setMerklRewards ] = useState({}); + const [ merklRewardsLoading, setMerklRewardsLoading ] = useState(true); + + const isLight = localThemeModeStore && localThemeModeStore.includes('light'); + + const vaultNav = () => { + return ( +
+
+ + +
+
+ Powered By Merkl +
+
+ ) + }; + + useEffect(() => { + // fixes flashing "no vault found" on first load + setVaultsLoading(true); + setTimeout(() => { + setVaultsLoading(false); + }, 1000); + }, []); + + const sUSDVaultManagerAddress = + chainId === arbitrumSepolia.id + ? arbitrumsUSDSepoliaContractAddress + : arbitrumsUSDContractAddress; + + const { data: vaultData, refetch } = useReadContract({ + abi: vaultManagerAbi, + address: sUSDVaultManagerAddress, + functionName: "vaultData", + args: [vaultId], + }); + + useWatchBlockNumber({ + onBlockNumber() { + refetch(); + }, + }) + + const currentVault = vaultData; + + useEffect(() => { + getMerklRewardsData(); + }, [currentVault, vaultsLoading]); + + const getMerklRewardsData = async () => { + const { vaultAddress } = currentVault.status; + + try { + setMerklRewardsLoading(true); + const response = await axios.get( + `https://api.merkl.xyz/v3/userRewards?chainId=42161&proof=true&user=${vaultAddress}` + ); + + const useData = response?.data; + + const rewardsArray = []; + + Object.keys(useData).forEach(key => { + const value = useData[key]; + const rewardItem = { + tokenAddress: key, + ... value + } + rewardsArray.push(rewardItem); + }); + + setMerklRewards(rewardsArray); + setMerklRewardsLoading(false); + } catch (error) { + setMerklRewardsLoading(false); + console.log(error); + } + }; + + if (vaultsLoading) { + return ( +
+ +
+ {vaultNav()} + +
+
+
+ ) + } + + if (!currentVault) { + return ( +
+ +
+ {vaultNav()} + Vault Not Found +
+
+
+ ); + } + + const { vaultAddress } = currentVault.status; + + if ( + vaultStore.tokenId !== currentVault.tokenId || + blockNumber !== renderedBlock + ) { + setVaultStore(currentVault); + setVaultAddress(vaultAddress); + setRenderedBlock(blockNumber); + } + + return ( +
+ +
+ {vaultNav()} + + + +
+
+ +
+ ) +}; + +export default VaultMerkl; diff --git a/src/pages/vaults/Vaults.jsx b/src/pages/vaults/Vaults.jsx index b72ea99..4f3682e 100644 --- a/src/pages/vaults/Vaults.jsx +++ b/src/pages/vaults/Vaults.jsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { ethers } from "ethers"; import { @@ -19,11 +20,16 @@ import { import VaultCreate from "../../components/vaults/VaultCreate"; import VaultList from "../../components/vaults/VaultList"; +import Card from "../../components/ui/Card"; +import Typography from "../../components/ui/Typography"; +import Button from "../../components/ui/Button"; const Vaults = () => { const { address: accountAddress } = useAccount(); const { vaultManagerAbi } = useVaultManagerAbiStore(); + const navigate = useNavigate(); + const { arbitrumSepoliaContractAddress, arbitrumContractAddress @@ -182,6 +188,36 @@ const Vaults = () => { return (
+ +
+
+
+ + Stake TST Today | Earn Protocol Fees + Early Governance | Join Before $10M TVL + + + 💰 1% of all yield pool deposits
+ 💸 Up to 5% of debt minting fees
+ 💎 1% of all collateral trades +
+
+
+ +
+ +
+
+
({ + merklABI, + }) +); + +export const useMerklAddressStore = create() ( + (set) => ({ + merklDistributorAddress: "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae", + setContractAddress: (merklDistributorAddress) => + set(() => ({ merklDistributorAddress: merklDistributorAddress })), + }) +); + export const useContractAddressStore = create() ( (set) => ({ arbitrumContractAddress: "0xba169cceCCF7aC51dA223e04654Cf16ef41A68CC", @@ -278,4 +293,11 @@ export const useLocalThemeModePrefStore = create( localThemeModePrefStore: 'device', setLocalThemeModePrefStore: (localThemeModePrefStore) => set(() => ({ localThemeModePrefStore: localThemeModePrefStore })), }) -); \ No newline at end of file +); + +export const useMerklRewardsUSD = create( + (set) => ({ + merklRewardsUSD: 0, + setMerklRewardsUSD: (merklRewardsUSD) => set(() => ({ merklRewardsUSD: merklRewardsUSD })), + }) +);