diff --git a/packages/dapp/components/staking/bonding-shares-explorer.tsx b/packages/dapp/components/staking/bonding-shares-explorer.tsx new file mode 100644 index 000000000..014420123 --- /dev/null +++ b/packages/dapp/components/staking/bonding-shares-explorer.tsx @@ -0,0 +1,319 @@ +import { BigNumber, ethers } from "ethers"; +import { memo, useCallback, useState } from "react"; + +import { formatEther } from "@/lib/format"; +import { performTransaction, useAsyncInit } from "@/lib/utils"; +import withLoadedContext, { LoadedContext } from "@/lib/with-loaded-context"; + +import DepositShare from "./deposit-share"; +import useBalances from "../lib/hooks/use-balances"; +import useTransactionLogger from "../lib/hooks/use-transaction-logger"; +import Button from "../ui/button"; +import Icon from "../ui/icon"; +import Loading from "../ui/loading"; + +type ShareData = { + id: number; + // cspell: disable-next-line + ugov: BigNumber; + bond: { + minter: string; + lpFirstDeposited: BigNumber; + creationBlock: BigNumber; + lpRewardDebt: BigNumber; + endBlock: BigNumber; + lpAmount: BigNumber; + }; + sharesBalance: BigNumber; + weeksLeft: number; +}; + +type Model = { + shares: ShareData[]; + totalShares: BigNumber; + walletLpBalance: BigNumber; + processing: ShareData["id"][]; +}; + +type Actions = { + onWithdrawLp: (payload: { id: number; amount: null | number }) => void; + onClaimUbq: (id: number) => void; + onStake: (payload: { amount: BigNumber; weeks: BigNumber }) => void; +}; + +const USD_TO_LP = 0.7460387929; +const LP_TO_USD = 1 / USD_TO_LP; + +export const BondingSharesExplorerContainer = ({ managedContracts, web3Provider, walletAddress, signer }: LoadedContext) => { + const [model, setModel] = useState(null); + const [, doTransaction] = useTransactionLogger(); + const [, refreshBalances] = useBalances(); + // cspell: disable-next-line + const { staking: bonding, masterChef, stakingToken: bondingToken, dollarMetapool: metaPool } = managedContracts; + + useAsyncInit(fetchSharesInformation); + async function fetchSharesInformation(processedShareId?: ShareData["id"]) { + console.time("BondingShareExplorerContainer contract loading"); + const currentBlock = await web3Provider.getBlockNumber(); + // cspell: disable-next-line + const blockCountInAWeek = +(await bonding.blockCountInAWeek()).toString(); + const totalShares = await masterChef.totalShares(); + // cspell: disable-next-line + const bondingShareIds = await bondingToken.holderTokens(walletAddress); + const walletLpBalance = await metaPool.balanceOf(walletAddress); + + const shares: ShareData[] = []; + await Promise.all( + // cspell: disable-next-line + bondingShareIds.map(async (id: BigNumber) => { + // cspell: disable-next-line + const [ugov, bond, bondingShareInfo, tokenBalance] = await Promise.all([ + // cspell: disable-next-line + masterChef.pendingGovernance(id), + // cspell: disable-next-line + bondingToken.getStake(id), + masterChef.getStakingShareInfo(id), + // cspell: disable-next-line + bondingToken.balanceOf(walletAddress, id), + ]); + + const endBlock = +bond.endBlock.toString(); + const blocksLeft = endBlock - currentBlock; + const weeksLeft = Math.round((blocksLeft / blockCountInAWeek) * 100) / 100; + + // If this is 0 it means the share ERC1155 token was transferred to another account + if (+tokenBalance.toString() > 0) { + // cspell: disable-next-line + shares.push({ id: +id.toString(), ugov, bond, sharesBalance: bondingShareInfo[0], weeksLeft }); + } + }) + ); + + const sortedShares = shares.sort((a, b) => a.id - b.id); + + console.timeEnd("BondingShareExplorerContainer contract loading"); + setModel((model) => ({ + processing: model ? model.processing.filter((id) => id !== processedShareId) : [], + shares: sortedShares, + totalShares, + walletLpBalance, + })); + } + + function allLpAmount(id: number): BigNumber { + if (!model) throw new Error("No model"); + const lpAmount = model.shares.find((s) => s.id === id)?.bond?.lpAmount; + if (!lpAmount) throw new Error("Could not find share in model"); + return lpAmount; + } + + const actions: Actions = { + onWithdrawLp: useCallback( + async ({ id, amount }) => { + if (!model || model.processing.includes(id)) return; + console.log(`Withdrawing ${amount ? amount : "ALL"} LP from ${id}`); + setModel((prevModel) => (prevModel ? { ...prevModel, processing: [...prevModel.processing, id] } : null)); + doTransaction("Withdrawing LP...", async () => { + try { + // cspell: disable-next-line + const isAllowed = await bondingToken.isApprovedForAll(walletAddress, bonding.address); + if (!isAllowed) { + // cspell: disable-next-line + // Allow bonding contract to control account share + // cspell: disable-next-line + if (!(await performTransaction(bondingToken.connect(signer).setApprovalForAll(bonding.address, true)))) { + return; // TODO: Show transaction errors to user + } + } + + const bigNumberAmount = amount ? ethers.utils.parseEther(amount.toString()) : allLpAmount(id); + // cspell: disable-next-line + await performTransaction(bonding.connect(signer).removeLiquidity(bigNumberAmount, BigNumber.from(id))); + } catch (error) { + console.log(`Withdrawing LP from ${id} failed:`, error); + // throws exception to update the transaction log + throw error; + } finally { + fetchSharesInformation(id); + refreshBalances(); + } + }); + }, + [model] + ), + + onClaimUbq: useCallback( + async (id) => { + if (!model) return; + console.log(`Claiming Ubiquity Governance token rewards from ${id}`); + setModel((prevModel) => (prevModel ? { ...prevModel, processing: [...prevModel.processing, id] } : null)); + doTransaction("Claiming Ubiquity Governance tokens...", async () => { + try { + await performTransaction(masterChef.connect(signer).getRewards(BigNumber.from(id))); + } catch (error) { + console.log(`Claiming Ubiquity Governance token rewards from ${id} failed:`, error); + // throws exception to update the transaction log + throw error; + } finally { + fetchSharesInformation(id); + refreshBalances(); + } + }); + }, + [model] + ), + + onStake: useCallback( + async ({ amount, weeks }) => { + if (!model || model.processing.length) return; + console.log(`Staking ${amount} for ${weeks} weeks`); + doTransaction("Staking...", async () => {}); + // cspell: disable-next-line + const allowance = await metaPool.allowance(walletAddress, bonding.address); + console.log("allowance", ethers.utils.formatEther(allowance)); + console.log("lpsAmount", ethers.utils.formatEther(amount)); + if (allowance.lt(amount)) { + // cspell: disable-next-line + await performTransaction(metaPool.connect(signer).approve(bonding.address, amount)); + // cspell: disable-next-line + const allowance2 = await metaPool.allowance(walletAddress, bonding.address); + console.log("allowance2", ethers.utils.formatEther(allowance2)); + } + // cspell: disable-next-line + await performTransaction(bonding.connect(signer).deposit(amount, weeks)); + + fetchSharesInformation(); + refreshBalances(); + }, + [model] + ), + }; + + return ; +}; + +export const BondingSharesExplorer = memo(({ model, actions }: { model: Model | null; actions: Actions }) => { + return <>{model ? : }; +}); + +export const BondingSharesInformation = ({ shares, totalShares, onWithdrawLp, onClaimUbq, onStake, processing, walletLpBalance }: Model & Actions) => { + const totalUserShares = shares.reduce((sum, val) => { + return sum.add(val.sharesBalance); + }, BigNumber.from(0)); + + const totalLpBalance = shares.reduce((sum, val) => { + return sum.add(val.bond.lpAmount); + }, BigNumber.from(0)); + // cspell: disable-next-line + const totalPendingUgov = shares.reduce((sum, val) => sum.add(val.ugov), BigNumber.from(0)); + + const poolPercentage = formatEther(totalUserShares.mul(ethers.utils.parseEther("100")).div(totalShares)); + // cspell: disable-next-line + const filteredShares = shares.filter(({ bond: { lpAmount }, ugov }) => lpAmount.gt(0) || ugov.gt(0)); + + return ( +
+ 0} maxLp={walletLpBalance} /> + + + + + + + + + + {filteredShares.length > 0 ? ( + + {filteredShares.map((share) => ( + + ))} + + ) : ( + + + + + + )} +
ActionRewardsUnlock TimeEst. Deposit
Nothing staked yet
+ + + + + + + + + + + + + + + + + + + +
+ Pending + + + + {/* cspell: disable-next-line */} + {formatEther(totalPendingUgov)} +
+ Staked LP + + + {formatEther(totalLpBalance)}
Ownership%{poolPercentage}
+
+ ); +}; + +type BondingShareRowProps = ShareData & { disabled: boolean; onWithdrawLp: Actions["onWithdrawLp"]; onClaimUbq: Actions["onClaimUbq"] }; +// cspell: disable-next-line +const BondingShareRow = ({ id, ugov, sharesBalance, bond, weeksLeft, disabled, onWithdrawLp, onClaimUbq }: BondingShareRowProps) => { + const [withdrawAmount] = useState(""); + + const numLpAmount = +formatEther(bond.lpAmount); + const usdAmount = numLpAmount * LP_TO_USD; + + function onClickWithdraw() { + const parsedUsdAmount = parseFloat(withdrawAmount); + if (parsedUsdAmount) { + onWithdrawLp({ id, amount: parsedUsdAmount * USD_TO_LP }); + } else { + onWithdrawLp({ id, amount: null }); + } + } + + return ( + + + {weeksLeft <= 0 && bond.lpAmount.gt(0) ? ( + + ) : // cspell: disable-next-line + ugov.gt(0) ? ( + + ) : null} + + +
+ {/* cspell: disable-next-line */} + {formatEther(ugov)} +
+ + {weeksLeft <= 0 ? "Ready" : {weeksLeft}w} + + ${Math.round(usdAmount * 100) / 100} + + ); +}; + +export default withLoadedContext(BondingSharesExplorerContainer); diff --git a/packages/dapp/pages/staking.tsx b/packages/dapp/pages/staking.tsx index a66923e56..e26d6cd9e 100644 --- a/packages/dapp/pages/staking.tsx +++ b/packages/dapp/pages/staking.tsx @@ -1,11 +1,12 @@ import { FC } from "react"; +import BondingSharesExplorer from "@/components/staking/bonding-shares-explorer"; import dynamic from "next/dynamic"; const WalletConnectionWall = dynamic(() => import("@/components/ui/wallet-connection-wall"), { ssr: false }); //@note Fix: (Hydration Error) const Staking: FC = (): JSX.Element => { return ( - + ); };