diff --git a/projects/ui/src/components/App/index.tsx b/projects/ui/src/components/App/index.tsx index 480fb7ddba..aec49264f9 100644 --- a/projects/ui/src/components/App/index.tsx +++ b/projects/ui/src/components/App/index.tsx @@ -62,6 +62,7 @@ import VotingPowerPage from '~/pages/governance/votingPower'; import MorningUpdater from '~/state/beanstalk/sun/morning'; import MorningFieldUpdater from '~/state/beanstalk/field/morning'; import BeanstalkCaseUpdater from '~/state/beanstalk/case/updater'; +import MigrationPreview from '../../pages/preview'; // import Snowflakes from './theme/winter/Snowflakes'; BigNumber.set({ EXPONENTIAL_AT: [-12, 20] }); @@ -203,6 +204,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/projects/ui/src/components/Nav/routes.ts b/projects/ui/src/components/Nav/routes.ts index 9a763bd232..985bda2f47 100644 --- a/projects/ui/src/components/Nav/routes.ts +++ b/projects/ui/src/components/Nav/routes.ts @@ -11,6 +11,7 @@ import disclosuresIcon from '~/img/beanstalk/interface/nav/disclosures.svg'; import analyticsIcon from '~/img/beanstalk/interface/nav/stats.svg'; import basinIcon from '~/img/beanstalk/interface/nav/basin.svg'; import pipelineIcon from '~/img/beanstalk/interface/nav/pipeline.svg'; +import migrationIcon from '~/img/beanstalk/interface/nav/migration.svg'; export type RouteData = { /** Nav item title */ @@ -83,6 +84,12 @@ const ROUTES: { [key in RouteKeys]: RouteData[] } = { icon: governanceIcon, small: true, }, + { + path: '/preview', + title: 'Migration Preview', + icon: migrationIcon, + small: true, + }, { path: 'docs', href: 'https://docs.bean.money/almanac', diff --git a/projects/ui/src/img/beanstalk/interface/nav/migration.svg b/projects/ui/src/img/beanstalk/interface/nav/migration.svg new file mode 100644 index 0000000000..9c3f921974 --- /dev/null +++ b/projects/ui/src/img/beanstalk/interface/nav/migration.svg @@ -0,0 +1 @@ +migrate \ No newline at end of file diff --git a/projects/ui/src/pages/preview.tsx b/projects/ui/src/pages/preview.tsx new file mode 100644 index 0000000000..5294165dd1 --- /dev/null +++ b/projects/ui/src/pages/preview.tsx @@ -0,0 +1,987 @@ +import React, { useEffect, useState } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + Card, + Container, + Dialog, + Divider, + InputAdornment, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import useAccount from '../hooks/ledger/useAccount'; +import PageHeader from '~/components/Common/PageHeader'; +import Row from '~/components/Common/Row'; +import { FC } from '~/types'; +import { useParams } from 'react-router-dom'; +import { ethers } from 'ethers'; +import { TokenValue } from '@beanstalk/sdk-core'; +import useSdk from '~/hooks/sdk'; +import { BeanstalkPalette, IconSize } from '~/components/App/muiTheme'; +import { + StyledDialogContent, + StyledDialogTitle, +} from '~/components/Common/Dialog'; +import TokenIcon from '~/components/Common/TokenIcon'; + +const MigrationPreview: FC<{}> = () => { + const connectedAccount = useAccount(); + const { address: accountUrl } = useParams(); + const theme = useTheme(); + const [isAccountValid, setIsAccountValid] = useState(false); + const [account, setAccount] = useState(); + const [data, setData] = useState(); + const [openDialog, setOpenDialog] = useState(false); + + const sdk = useSdk(); + + const BEAN = sdk.tokens.BEAN; + + useEffect(() => { + if (accountUrl) { + validateAddress(accountUrl); + getMigrationData(); + } + }, []); + + function validateAddress(address: string) { + setAccount(address); + if (address) { + const isValid = ethers.utils.isAddress(address); + setIsAccountValid(isValid); + } else { + setIsAccountValid(false); + } + } + + function useConnectedWallet() { + if (!connectedAccount) return; + validateAddress(connectedAccount); + getMigrationData(); + } + + async function getMigrationData() { + const _account = account || accountUrl; + const _isValid = account + ? isAccountValid + : ethers.utils.isAddress(accountUrl || ''); + if (!_account || !_isValid) return; + try { + const migrationData = await fetch( + `https://api.bean.money/migration?account=${_account}` + ).then((response) => response.json()); + if (migrationData) { + const totalPerToken: any[] = []; + migrationData.silo.deposits.forEach( + (deposit: any, index: number, deposits: any[]) => { + const _token = Array.from( + sdk.tokens.getMap(), + ([key, value]) => value + ).find( + (token) => + token.displayName.toLowerCase() === deposit.token.toLowerCase() + ); + const _amount = TokenValue.fromBlockchain( + deposit.amount, + _token?.decimals || 6 + ); + const _recordedBdv = TokenValue.fromBlockchain( + deposit.recordedBdv, + BEAN.decimals + ); + const _currentStalk = TokenValue.fromBlockchain( + deposit.currentStalk, + 16 + ); + const _stalkAfterMow = TokenValue.fromBlockchain( + deposit.stalkAfterMow, + 16 + ); + const _deposit = { + token: _token, + amount: _amount, + recordedBdv: _recordedBdv, + currentStalk: _currentStalk, + stalkAfterMow: _stalkAfterMow, + }; + deposits[index] = _deposit; + const totalIndex = totalPerToken.findIndex( + (token) => token.token.displayName === _token?.displayName + ); + if (totalIndex === -1) { + const total = { + token: _token, + amount: _amount, + }; + totalPerToken.push(total); + } else { + totalPerToken[totalIndex].amount = _amount.add( + totalPerToken[totalIndex].amount + ); + } + } + ); + migrationData.silo.currentStalk = TokenValue.fromBlockchain( + migrationData.silo.currentStalk, + 16 + ); + migrationData.silo.earnedBeans = TokenValue.fromBlockchain( + migrationData.silo.earnedBeans, + BEAN.decimals + ); + migrationData.silo.stalkAfterMow = TokenValue.fromBlockchain( + migrationData.silo.stalkAfterMow, + 16 + ); + migrationData.silo.totalPerToken = totalPerToken; + migrationData.field.totalPods = TokenValue.fromBlockchain( + migrationData.field.totalPods, + BEAN.decimals + ); + migrationData.barn.totalFert = TokenValue.fromBlockchain( + migrationData.barn.totalFert, + 0 + ); + migrationData.barn.totalRinsable = TokenValue.fromBlockchain( + migrationData.barn.totalRinsable, + BEAN.decimals + ); + migrationData.barn.totalUnrinsable = TokenValue.fromBlockchain( + migrationData.barn.totalUnrinsable, + BEAN.decimals + ); + migrationData.farm.forEach((token: any, index: number, farm: any[]) => { + const _token = Array.from( + sdk.tokens.getMap(), + ([key, value]) => value + ).find( + (tokenMap) => + tokenMap.displayName.toLowerCase() === token.token.toLowerCase() + ); + const _currentInternal = TokenValue.fromBlockchain( + token.currentInternal, + _token?.decimals || 6 + ); + const _withdrawn = TokenValue.fromBlockchain( + token.withdrawn, + _token?.decimals || 6 + ); + const _unpicked = TokenValue.fromBlockchain( + token.unpicked, + _token?.decimals || 6 + ); + const _rinsable = TokenValue.fromBlockchain( + token.rinsable, + _token?.decimals || 6 + ); + const _total = TokenValue.fromBlockchain( + token.total, + _token?.decimals || 6 + ); + const farmData = { + token: _token, + currentInternal: _currentInternal, + withdrawn: _withdrawn, + unpicked: _unpicked, + rinsable: _rinsable, + total: _total, + }; + farm[index] = farmData; + }); + setData(migrationData); + } + } catch (e) { + console.error('Migration Preview - Error Fetching Data'); + } + } + + return ( + + + + + + + + Enter Address + + {account && !isAccountValid && ( + + )} + {account && isAccountValid && ( + + )} + + ), + }} + value={account} + onChange={(e) => { + validateAddress(e.target.value); + }} + /> + + + + + + + + + {data && ( + <> + + setOpenDialog(false)} open={openDialog}> + setOpenDialog(false)}> + BIP-50 Balance Migration + + +
+ BIP-50 proposes to migrate Beanstalk state to Arbitrum.{' '} + In doing so,{' '} + + all Deposits, Plots, Fertilizer and Beanstalk-related Farm + Balances + {' '} + (Beans, BEANWETH, BEAN3CRV (migrated to BEANUSDC), BEANwstETH, + urBEAN and urBEANwstETH) are migrated. +
+
+ As part of the migration process: +
    +
  • + Grown Stalk is Mown; +
  • +
  • + Earned Beans are Planted; and +
  • +
  • + + Rinsable Sprouts, Unpicked Unripe assets and unclaimed + Silo V2 Withdrawals + {' '} + are put into the respective{' '} + accounts' Farm Balances. +
  • +
+
+
+ The following balances show the state of the selected Farmer + as a result of the migration,{' '} + + assuming the migration was executed based on balances at + block {data.meta.block} (i.e., roughly + {` ${new Date(data.meta.timestamp * 1000).toLocaleString()}`} + ).{' '} + + You should cross reference this with your balances in the rest + of the Beanstalk UI (assuming your balances haven't changed + since block {data.meta.block}). +
+
+ Note that{' '} + + Circulating Balances and smart contract account balances are + not migrated automatically. + {' '} + See Discord +  and{' '} + + BIP-50 + {' '} + for more information. +
+
+
+ + + Silo + {`(as of ${new Date(data.meta.timestamp * 1000).toLocaleString()})`} + + {data.silo.earnedBeans.gt(0) || + data.silo.currentStalk.gt(0) || + data.silo.stalkAfterMow.gt(0) || + (data.silo.totalPerToken && + data.silo.totalPerToken.length > 0) ? ( + + + + + Earned Beans + + + + + + {data.silo.earnedBeans.toHuman('short')} + + + + + Current Stalk + + {data.silo.currentStalk.toHuman('short')} + + + + + Stalk After Mow + + {data.silo.stalkAfterMow.toHuman('short')} + + + + + + + Deposited Tokens + + + {data.silo.totalPerToken && + data.silo.totalPerToken.map( + (tokenData: any, index: number, array: any[]) => ( + <> + + + + + {tokenData.token.displayName} + + + + {tokenData.amount.toHuman('short')} + + + {index !== array.length - 1 && } + + ) + )} + + + + {data.silo.deposits.length > 0 && ( + + + } + > + View Deposits + + + + + + + Token + Amount + + Recorded BDV + + + Current Stalk + + + Stalk After Mow + + + + + {data.silo.deposits.map( + (deposit: any, i: number) => { + const stalkIncrease = + deposit.stalkAfterMow.gt( + deposit.currentStalk + ); + return ( + + + +
{deposit.token.displayName}
+
+ + {deposit.amount.toHuman('short')} + + + {deposit.recordedBdv.toHuman('short')} + + + {deposit.currentStalk.toHuman('short')} + + + + {deposit.stalkAfterMow.toHuman( + 'short' + )} + + +
+ ); + } + )} +
+
+
+
+
+ )} +
+ ) : ( + + This account has no assets in the Silo. + + )} +
+ + + Field + {`(as of ${new Date(data.meta.timestamp * 1000).toLocaleString()})`} + + {data.field.totalPods.gt(0) || + (data.field.plots && data.field.plots.length > 0) ? ( + + + + Total Pods + + {data.field.totalPods.toHuman('short')} + + + + {data.field.plots.length > 0 && ( + + + } + > + View Plots + + + + + + + Index + Amount + + + + {data.field.plots.map((plot: any, i: number) => ( + + + {TokenValue.fromBlockchain( + plot.index, + 6 + ).toHuman('short')} + + + {TokenValue.fromBlockchain( + plot.amount, + 6 + ).toHuman('short')} + + + ))} + +
+
+
+
+ )} +
+ ) : ( + + This account has no assets in the Field. + + )} +
+ + + Barn + {`(as of ${new Date(data.meta.timestamp * 1000).toLocaleString()})`} + + {data.barn.totalFert.gt(0) || + data.barn.totalRinsable.gt(0) || + data.barn.totalUnrinsable.gt(0) || + (data.barn.fert && data.barn.fert.length > 0) ? ( + + + + Total Fertilizer + + {data.barn.totalFert.toHuman('short')} + + + + + Rinsable Sprouts + + + + + + + {data.barn.totalRinsable.toHuman('short')} + + + + Unrinsable Sprouts + + {data.barn.totalUnrinsable.toHuman('short')} + + + + {data.barn.fert.length > 0 && ( + + + } + > + View Fertilizer + + + + + + + ID + Amount + + Rinsable Sprouts + + + Unrinsable Sprouts + + Humidity + + + + {data.barn.fert.map((fert: any, i: number) => ( + + + {Number(fert.fertilizerId)} + + + {TokenValue.fromBlockchain( + fert.amount, + 0 + ).toHuman('short')} + + + {TokenValue.fromBlockchain( + fert.rinsableSprouts, + 6 + ).toHuman('short')} + + + {TokenValue.fromBlockchain( + fert.unrinsableSprouts, + 6 + ).toHuman('short')} + + {`${fert.humidity * 100}%`} + + ))} + +
+
+
+
+ )} +
+ ) : ( + + This account has no assets in the Barn. + + )} +
+ + + Farm + {`(as of ${new Date(data.meta.timestamp * 1000).toLocaleString()})`} + + + {data.farm.length > 0 ? ( + + + + + Token + Internal Balance + Withdrawn + Unpicked + Rinsable + Total + + + + {data.farm.map((farmData: any, i: number) => { + return ( + + + +
{farmData.token.displayName}
+
+ + {farmData.currentInternal.toHuman('short')} + + + {farmData.withdrawn.toHuman('short')} + + + {farmData.token.isUnripe ? farmData.unpicked.toHuman('short') : '-'} + + + {farmData.token.displayName === "Bean" ? farmData.rinsable.toHuman('short') : '-'} + + + {farmData.total.toHuman('short')} + +
+ ); + })} +
+
+
+ ) : ( + This account has no Farm Balances. + )} +
+
+ + )} +
+
+ ); +}; + +export default MigrationPreview;