diff --git a/.env.example b/.env.example index 8abf0440..abc118f5 100644 --- a/.env.example +++ b/.env.example @@ -49,3 +49,17 @@ NEXT_PUBLIC_CURRENCIES=usd, eur, gbp, cny, jpy, krw, aud # https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP NEXT_PUBLIC_CONTENT_SECURITY_POLICY=https://app.safe.global https://*.blockscout.com + +# Blocking variables must have any value that Boolean(value) returns true. + +# NEXT_PUBLIC_DISABLE_STAKE= +# NEXT_PUBLIC_DISABLE_UNSTAKE= + +# NEXT_PUBLIC_DISABLE_MINT= +# NEXT_PUBLIC_DISABLE_BURN= + +# NEXT_PUBLIC_DISABLE_BOOST= +# NEXT_PUBLIC_DISABLE_UNBOOST= + +# NEXT_PUBLIC_DISABLE_UNBOOST_QUEUE= +# NEXT_PUBLIC_DISABLE_UNSTAKE_QUEUE= diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index bc0e9900..c454c6e7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -50,7 +50,7 @@ jobs: - name: Awaiting WEB deployment to be ready uses: UnlyEd/github-action-await-vercel@v1.1.1 env: - VERCEL_TOKEN: ${{ secrets.VERCEl_TOKEN }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} with: deployment-url: ${{ steps.preview.outputs.preview_url }} timeout: 600 @@ -69,6 +69,11 @@ jobs: VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} run: npm run e2e + - name: Sanitize reports before upload + if: ${{ failure() }} + run: | + find ./playwright-report/ -name "*.html" -exec sed -i 's/${{ secrets.[A-Z_]* }}/***/g' {} \; + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: diff --git a/package-lock.json b/package-lock.json index 3313cef5..13b2e04e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,12 @@ "@cowprotocol/cow-sdk": "5.10.3", "@headlessui/react": "2.2.4", "@ledgerhq/hw-app-eth": "6.45.5", - "@ledgerhq/hw-transport-web-ble": "^6.29.12", + "@ledgerhq/hw-transport-web-ble": "6.29.12", "@ledgerhq/hw-transport-webhid": "6.30.1", "@ledgerhq/hw-transport-webusb": "6.29.5", "@metamask/onboarding": "1.0.1", "@reduxjs/toolkit": "2.8.2", - "@stakewise/v3-sdk": "3.7.1", + "@stakewise/v3-sdk": "^4.1.2", "@tailwindcss/postcss": "4.1.7", "@types/react-redux": "7.1.34", "@wagmi/connectors": "5.8.3", @@ -6195,9 +6195,9 @@ "license": "MIT" }, "node_modules/@stakewise/v3-sdk": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@stakewise/v3-sdk/-/v3-sdk-3.7.1.tgz", - "integrity": "sha512-NqTwnjELrsinXb0XPvrtfP5EdgOODD08EB/6eqynwulKIn3XEetQZmVakeisswoo6NKs5nRlQ3CdFzHubUJsLw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@stakewise/v3-sdk/-/v3-sdk-4.1.2.tgz", + "integrity": "sha512-LrYNjp2i4vKV9rlB7mEWdCblGemfBGr0J1xliDd4ZBOAoIM+kdYyvICsKpGlVdCcAd3Pocfr1Ebb5gJOF+jPlg==", "license": "AGPL-3.0-only", "dependencies": { "bignumber.js": "9.3.0" diff --git a/package.json b/package.json index ce8476df..ee97eebc 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@ledgerhq/hw-transport-webusb": "6.29.5", "@metamask/onboarding": "1.0.1", "@reduxjs/toolkit": "2.8.2", - "@stakewise/v3-sdk": "3.7.1", + "@stakewise/v3-sdk": "4.1.2", "@tailwindcss/postcss": "4.1.7", "@types/react-redux": "7.1.34", "@wagmi/connectors": "5.8.3", diff --git a/scripts/checkLocalEnv.js b/scripts/checkLocalEnv.js index ae5126ff..7ef1197e 100644 --- a/scripts/checkLocalEnv.js +++ b/scripts/checkLocalEnv.js @@ -52,7 +52,7 @@ const getEnvList = (filePath) => { return result } -const validateEnv = (env) => { +const validateEnv = () => { const isEnvExist = fs.existsSync(localEnv) if (!isEnvExist) { diff --git a/scripts/generateColors/index.js b/scripts/generateColors/index.js index 9e01ea0c..62d28e59 100644 --- a/scripts/generateColors/index.js +++ b/scripts/generateColors/index.js @@ -2,6 +2,7 @@ const fs = require('fs') const path = require('path') const hexToRgb = require('./hexToRgb') + const themes = [ 'light', 'dark' ] const destVariables = path.resolve(__dirname, `../../src/styles/variables.scss`) @@ -93,7 +94,7 @@ const generateColors = () => { let newBaseFile = baseFile - Object.keys(colorsBase).forEach((theme, index) => { + Object.keys(colorsBase).forEach((theme) => { newBaseFile = newBaseFile .replace( new RegExp(`:root .body-${theme}-theme {[^}]*}\n`, 'g'), diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 5fd8ea77..b2518d9b 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -3,12 +3,13 @@ import React, { useEffect } from 'react' import cx from 'classnames' import intl from 'modules/intl' import { Inter } from 'next/font/google' -import theme, { ThemeColor } from 'modules/theme' import { cookie, constants } from 'helpers' -import messages from 'views/ErrorView/messages' +import theme, { ThemeColor } from 'modules/theme' + import { Button, Text } from 'components' import languages from 'scripts/languages' +import messages from 'views/ErrorView/messages' import 'styles/globals.scss' import 'styles/tailwind/config.css' diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b6e04530..e5d72a77 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,10 +6,10 @@ import { commonMessages } from 'helpers' import { getLocale } from 'modules/intl/_SSR' import { ThemeColor } from 'modules/theme/enum' import { getServerTheme } from 'modules/theme/_SSR' +import { getVaultBase } from 'helpers/requests/_SSR' import { getServerDevice } from 'modules/device/_SSR' -import GlobalLayout from 'layouts/GlobalLayout/GlobalLayout' import { getNetworkId } from 'config/core/config/_SSR' -import { getVaultBase } from 'helpers/requests/_SSR' +import GlobalLayout from 'layouts/GlobalLayout/GlobalLayout' import 'focus-visible' import 'styles/globals.scss' diff --git a/src/app/page.tsx b/src/app/page.tsx index 570855b4..48ce5de8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1 +1 @@ -export { default } from 'views/HomeView/HomeView' +export { default } from 'views/SwapView/SwapView' diff --git a/src/components/ApyBreakdown/ApyBreakdown.tsx b/src/components/ApyBreakdown/ApyBreakdown.tsx new file mode 100644 index 00000000..c19b629d --- /dev/null +++ b/src/components/ApyBreakdown/ApyBreakdown.tsx @@ -0,0 +1,34 @@ +import React, { ReactNode } from 'react' + +import PopupInfo from '../PopupInfo/PopupInfo' + +import Details, { DetailsProps } from './Details/Details' + + +export type ApyBreakdownProps = { + className?: string + data?: DetailsProps['data'] + withText?: boolean + maxBoostApy: number + children: ReactNode +} + +const ApyBreakdown: React.FC = (props) => { + const { className, children, data, withText, maxBoostApy } = props + + return ( + +
+ + ) +} + + +export default React.memo(ApyBreakdown) diff --git a/src/views/HomeView/common/ApyBreakdown/Details/Details.tsx b/src/components/ApyBreakdown/Details/Details.tsx similarity index 91% rename from src/views/HomeView/common/ApyBreakdown/Details/Details.tsx rename to src/components/ApyBreakdown/Details/Details.tsx index 12feccdd..bbb4c2b6 100644 --- a/src/views/HomeView/common/ApyBreakdown/Details/Details.tsx +++ b/src/components/ApyBreakdown/Details/Details.tsx @@ -2,12 +2,12 @@ import React, { useMemo } from 'react' import cx from 'classnames' import date from 'modules/date' import intl from 'modules/intl' -import methods from 'helpers/methods' +import { useStore } from 'hooks' import { useConfig } from 'config' -import { useSelector } from 'react-redux' -import { commonMessages } from 'helpers' +import { commonMessages, methods } from 'helpers' -import { Text, Logo } from 'components' +import Logo from '../../Logo/Logo' +import Text from '../../Text/Text' import messages from './messages' @@ -22,9 +22,10 @@ type Data = { endTimestamp?: string } -type DetailsProps = { +export type DetailsProps = { className?: string data: Data[] + maxBoostApy: number withText?: boolean } @@ -34,16 +35,18 @@ const Details: React.FC = (props) => { const now = date.time() const { sdk } = useConfig() const intlRef = intl.useIntlRef() - const { maxBoostApy } = useSelector(storeSelector) + const { maxBoostApy } = useStore(storeSelector) const tokenList = useMemo(() => { const SSV = sdk.config.addresses.tokens.ssv.toLocaleLowerCase() + const obol = sdk.config.addresses.tokens.obol.toLocaleLowerCase() const SWISE = sdk.config.addresses.tokens.swise.toLocaleLowerCase() const mintToken = sdk.config.addresses.tokens.mintToken.toLocaleLowerCase() const depositToken = sdk.config.addresses.tokens.depositToken.toLocaleLowerCase() return ({ [SSV]: sdk.config.tokens.ssv, + [obol]: sdk.config.tokens.obol, [SWISE]: sdk.config.tokens.swise, [mintToken]: sdk.config.tokens.mintToken, [depositToken]: sdk.config.tokens.depositToken, diff --git a/src/views/HomeView/common/ApyBreakdown/Details/messages.ts b/src/components/ApyBreakdown/Details/messages.ts similarity index 100% rename from src/views/HomeView/common/ApyBreakdown/Details/messages.ts rename to src/components/ApyBreakdown/Details/messages.ts diff --git a/src/components/ButtonBase/ButtonBase.tsx b/src/components/ButtonBase/ButtonBase.tsx index 421d7af2..b1852e97 100644 --- a/src/components/ButtonBase/ButtonBase.tsx +++ b/src/components/ButtonBase/ButtonBase.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import cx from 'classnames' import s from './ButtonBase.module.scss' diff --git a/src/components/Chart/Legend/Amount/Amount.tsx b/src/components/Chart/Legend/Amount/Amount.tsx index 2ed02724..69dc8d01 100644 --- a/src/components/Chart/Legend/Amount/Amount.tsx +++ b/src/components/Chart/Legend/Amount/Amount.tsx @@ -1,7 +1,7 @@ import React, { useRef, useCallback } from 'react' import { createSelector } from '@reduxjs/toolkit' -import { useSelector } from 'react-redux' -import methods from 'helpers/methods' +import { methods } from 'helpers' +import { useStore } from 'hooks' import cx from 'classnames' import Text from '../../../Text/Text' @@ -29,7 +29,7 @@ const storeSelector = createSelector([ const Amount: React.FC = (props) => { const { className, token, chart, series } = props - const { fiatRates, currency, currencySymbol } = useSelector(storeSelector) + const { fiatRates, currency, currencySymbol } = useStore(storeSelector) const fiatTooltipRef = useRef(null) const tokenValueTooltipRef = useRef(null) diff --git a/src/components/Chart/Legend/Percentage/Percentage.tsx b/src/components/Chart/Legend/Percentage/Percentage.tsx index 0558eb14..5442d2cb 100644 --- a/src/components/Chart/Legend/Percentage/Percentage.tsx +++ b/src/components/Chart/Legend/Percentage/Percentage.tsx @@ -1,5 +1,5 @@ import React, { useRef, useCallback } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import cx from 'classnames' import Text from '../../../Text/Text' diff --git a/src/components/Chart/util/useDefaultSettings.ts b/src/components/Chart/util/useDefaultSettings.ts index ddf33344..4c7604e5 100644 --- a/src/components/Chart/util/useDefaultSettings.ts +++ b/src/components/Chart/util/useDefaultSettings.ts @@ -1,7 +1,7 @@ import { useMemo, useCallback } from 'react' import theme, { ThemeColor } from 'modules/theme' +import { methods } from 'helpers' import intl from 'modules/intl' -import methods from 'helpers/methods' import { LineStyle, LineWidth, CrosshairMode } from 'lightweight-charts' diff --git a/src/components/Dropdown/DropdownView/DropdownView.tsx b/src/components/Dropdown/DropdownView/DropdownView.tsx index 32cc914d..ef49a64e 100644 --- a/src/components/Dropdown/DropdownView/DropdownView.tsx +++ b/src/components/Dropdown/DropdownView/DropdownView.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, KeyboardEventHandler, ReactElement, ReactNode } from 'react' +import React, { Fragment, KeyboardEventHandler, ReactElement, ReactNode, useRef } from 'react' import cx from 'classnames' import { offset, shift, VirtualElement, OffsetOptions } from '@floating-ui/react' import type { Placement } from '@floating-ui/react' @@ -15,6 +15,7 @@ type ButtonInput = { export type DropdownViewProps = { className?: string + contentClassName?: string children: ReactNode disabled?: boolean // The child component must inherit the props, so be sure to make @@ -29,6 +30,7 @@ export type DropdownViewProps = { autoUpdate?: boolean offsetOptions?: Omit } + onOpen?: () => void onClose?: () => void onChange?: (value: any) => void onOptionsClick?: () => void @@ -41,10 +43,12 @@ type DropdownViewComponent = React.FC & { const DropdownView: DropdownViewComponent = (props: DropdownViewProps) => { const { - className, children, button, value, disabled, withArrow, middleware, - placement = 'bottom-end', dataTestId, onClose, onChange, onOptionsClick, onOptionsKeyDown, + className, contentClassName, children, button, value, disabled, withArrow, middleware, + placement = 'bottom-end', dataTestId, onOpen, onClose, onChange, onOptionsClick, onOptionsKeyDown, } = props + const isOpenRef = useRef(false) + const { refs, floatingStyles } = useFloating({ placement, middleware: [ @@ -73,10 +77,16 @@ const DropdownView: DropdownViewComponent = (props: DropdownViewProps) => { ({ open }) => { const arrow = open ? 'up' : 'down' - if (!open && typeof onClose === 'function') { + if (open && !isOpenRef.current && typeof onOpen === 'function') { + onOpen() + } + + if (!open && isOpenRef.current && typeof onClose === 'function') { setTimeout(onClose) } + isOpenRef.current = open + if (typeof button === 'function') { return button({ ref: refs.setReference, @@ -94,7 +104,11 @@ const DropdownView: DropdownViewComponent = (props: DropdownViewProps) => { & { className?: string text: Intl.Message + center?: boolean + link?: string type?: 'error' | 'info' | 'success' | 'warning' dataTestId?: string } const Note: React.FC = (props) => { - const { className, text, type = 'info', dataTestId, ...rest } = props + const { className, text, center, link, type = 'info', dataTestId, ...rest } = props const textColor = useMemo(() => { if (type === 'error') { @@ -35,15 +39,35 @@ const Note: React.FC = (props) => { 'bg-success-light/10 border border-success-light/30': type === 'success', 'bg-warning/10 border border-warning/30': type === 'warning', 'bg-error/10 border border-error/30': type === 'error', + 'justify-between': link && !center, + 'justify-center': link && center, + 'flex items-center': link, })} data-testid={dataTestId} > + { + Boolean(link) && ( + + + + ) + } ) } diff --git a/src/components/PopupInfo/PopupInfo.tsx b/src/components/PopupInfo/PopupInfo.tsx index 40a3b0bb..007fd98d 100644 --- a/src/components/PopupInfo/PopupInfo.tsx +++ b/src/components/PopupInfo/PopupInfo.tsx @@ -69,7 +69,7 @@ const PopupInfo: React.FC = (props) => { return (
diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index c6dcb4ad..fd3d1f48 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -1,5 +1,5 @@ import React from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import Text from '../Text/Text' import FieldValue from '../FieldValue/FieldValue' diff --git a/src/components/Select/SelectView/SelectView.tsx b/src/components/Select/SelectView/SelectView.tsx index 8a80e50e..4425795e 100644 --- a/src/components/Select/SelectView/SelectView.tsx +++ b/src/components/Select/SelectView/SelectView.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import Dropdown, { DropdownProps } from '../../Dropdown/Dropdown' diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 107c48b9..bd1bbfe2 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -13,6 +13,8 @@ export type TabsProps = Omit = (props) => { const { className, + dataTestId, + tabsClassName, panelClassName, panelsClassName, field, @@ -31,6 +33,8 @@ const Tabs: React.FC = (props) => { return ( = (props) => { const { - children, className, panelClassName, panelsClassName, - tabsList, selectedId, defaultActiveTabId, borderMin, + children, className, panelClassName, panelsClassName, tabsClassName, + tabsList, selectedId, defaultActiveTabId, borderMin, dataTestId, noteNode, onChange, } = props @@ -94,7 +96,10 @@ const TabsView: React.FC = (props) => { }, [ tabsList, selectedIndex, locale, handleLine ]) return ( -
+
= (props) => { [s.borderFill]: !borderMin, })} ref={containerRef} + data-testid={`${dataTestId}-filters`} >
diff --git a/src/components/Text/Text.tsx b/src/components/Text/Text.tsx index 1ea3ce80..0c26af1d 100644 --- a/src/components/Text/Text.tsx +++ b/src/components/Text/Text.tsx @@ -1,9 +1,7 @@ import React from 'react' import cx from 'classnames' -import methods from 'helpers/methods' import intl from 'modules/intl' - -import { constants, replaceReactComponents } from '../../helpers' +import { methods, constants, replaceReactComponents } from 'helpers' const sizesMap = { diff --git a/src/components/TokenAmount/TokenAmount.tsx b/src/components/TokenAmount/TokenAmount.tsx index a412981d..0e57a304 100644 --- a/src/components/TokenAmount/TokenAmount.tsx +++ b/src/components/TokenAmount/TokenAmount.tsx @@ -1,10 +1,9 @@ import React from 'react' import cx from 'classnames' -import methods from 'helpers/methods' +import { methods, constants } from 'helpers' import Logo from '../Logo/Logo' import Text from '../Text/Text' -import { constants } from '../../helpers' export const tokens = [ diff --git a/src/components/TokenAmountInput/Balance/Balance.tsx b/src/components/TokenAmountInput/Balance/Balance.tsx index e4378d77..2b6479f7 100644 --- a/src/components/TokenAmountInput/Balance/Balance.tsx +++ b/src/components/TokenAmountInput/Balance/Balance.tsx @@ -1,5 +1,5 @@ import React from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import device from 'modules/device' import Text from '../../Text/Text' diff --git a/src/components/TokenAmountInput/Input/Input.tsx b/src/components/TokenAmountInput/Input/Input.tsx index 4742c23b..e79fd5ee 100644 --- a/src/components/TokenAmountInput/Input/Input.tsx +++ b/src/components/TokenAmountInput/Input/Input.tsx @@ -1,5 +1,5 @@ import React, { RefObject, useCallback } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import AmountInput from '../../AmountInput/AmountInput' diff --git a/src/components/TokenAmountInputView/Balance/Balance.tsx b/src/components/TokenAmountInputView/Balance/Balance.tsx index 746e2863..96d3fcbd 100644 --- a/src/components/TokenAmountInputView/Balance/Balance.tsx +++ b/src/components/TokenAmountInputView/Balance/Balance.tsx @@ -1,5 +1,5 @@ import React from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import device from 'modules/device' import Text from '../../Text/Text' diff --git a/src/components/TokenAmountInputView/Input/Input.tsx b/src/components/TokenAmountInputView/Input/Input.tsx index 4742c23b..e79fd5ee 100644 --- a/src/components/TokenAmountInputView/Input/Input.tsx +++ b/src/components/TokenAmountInputView/Input/Input.tsx @@ -1,5 +1,5 @@ import React, { RefObject, useCallback } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import AmountInput from '../../AmountInput/AmountInput' diff --git a/src/components/TokenDropdown/TokenDropdown.tsx b/src/components/TokenDropdown/TokenDropdown.tsx index 3f7e33df..fdfef161 100644 --- a/src/components/TokenDropdown/TokenDropdown.tsx +++ b/src/components/TokenDropdown/TokenDropdown.tsx @@ -1,30 +1,33 @@ import React, { useCallback } from 'react' -import forms from 'modules/forms' import device from 'modules/device' import { useConfig } from 'config' +import forms from 'modules/forms' +import { Network } from 'sdk' import Icon from '../Icon/Icon' import ButtonBase from '../ButtonBase/ButtonBase' import DropdownView, { DropdownViewProps } from '../Dropdown/DropdownView/DropdownView' import TokenBase from './TokenBase/TokenBase' - import TokenOptions from './TokenOptions/TokenOptions' +import { useTokenDropdown } from './util' export type TokenDropdownProps = Omit & { className?: string + contentClassName?: string value: Tokens tokens: SwapToken[] - isFetching?: boolean + isDisabled?: boolean onChange?: (value: string) => void } const TokenDropdown: React.FC = (props) => { - const { className, value, tokens, dataTestId, isFetching, onChange, ...rest } = props + const { className, contentClassName, value, tokens, dataTestId, isDisabled, onChange, ...rest } = props const { isMobile } = device.useData() - const { isReadOnlyMode } = useConfig() + const { isReadOnlyMode, chainId } = useConfig() + const { isFetching, open } = useTokenDropdown() const field = forms.useField({ valueType: 'string', @@ -39,24 +42,24 @@ const TokenDropdown: React.FC = (props) => { field.setValue('') }, [ field, onChange ]) - const isSwapEnabled = tokens.length > 1 + const isSwapEnabled = [ Network.Mainnet, Network.Gnosis ].includes(chainId) const tokenBaseNode = ( ) - if (isFetching || !isSwapEnabled) { + if (!isSwapEnabled) { return tokenBaseNode } return ( = (props) => { // @ts-ignore ref={ref} className="flex items-center gap-8" - disabled={isReadOnlyMode} + disabled={isReadOnlyMode || isDisabled} > {tokenBaseNode} = (props) => { }) } }} - onClose={field.reset} + onOpen={() => open(true)} + onClose={() => { + field.reset() + open(false) + }} onChange={handleChange} {...rest} > diff --git a/src/components/TokenDropdown/TokenOptions/Option/Option.tsx b/src/components/TokenDropdown/TokenOptions/Option/Option.tsx index f7859983..b226b459 100644 --- a/src/components/TokenDropdown/TokenOptions/Option/Option.tsx +++ b/src/components/TokenDropdown/TokenOptions/Option/Option.tsx @@ -1,6 +1,6 @@ import React from 'react' import cx from 'classnames' -import methods from 'helpers/methods' +import { methods } from 'helpers' import { useConfig } from 'config' import { formatUnits } from 'ethers' import { ListboxOption } from '@headlessui/react' diff --git a/src/components/TokenDropdown/TokenOptions/TokenOptions.tsx b/src/components/TokenDropdown/TokenOptions/TokenOptions.tsx index df36f6db..3496a4a6 100644 --- a/src/components/TokenDropdown/TokenOptions/TokenOptions.tsx +++ b/src/components/TokenDropdown/TokenOptions/TokenOptions.tsx @@ -1,26 +1,30 @@ import React, { useMemo } from 'react' import cx from 'classnames' import forms from 'modules/forms' -import device from 'modules/device' import { useConfig } from 'config' +import device from 'modules/device' import Text from '../../Text/Text' import Option from './Option/Option' import TokenSearch from './TokenSearch/TokenSearch' +import TokenSkeleton from './TokenSkeleton/TokenSkeleton' import ScrollableContainer from '../../ScrollableContainer/ScrollableContainer' import messages from './messages' -export type TokenDropdownProps = { +const tokensMock = [ ...new Array(5) ] + +export type TokenOptionsProps = { field: Forms.Field tokens: SwapToken[] + isFetching?: boolean dataTestId?: string } -const TokenDropdown: React.FC = (props) => { - const { field, tokens, dataTestId } = props +const TokenOptions: React.FC = (props) => { + const { field, tokens, isFetching, dataTestId } = props const { sdk, isGnosis } = useConfig() @@ -35,7 +39,7 @@ const TokenDropdown: React.FC = (props) => { const { name, address } = token const lowerCasedName = name.toLowerCase() - const lowerCasedAddress = address.toLowerCase() + const lowerCasedAddress = (address || '').toLowerCase() return ( lowerCasedName.includes(lowerCasedSearch) @@ -58,46 +62,64 @@ const TokenDropdown: React.FC = (props) => { data-testid={`${dataTestId}-input`} /> { - filteredTokens.length ? ( + isFetching ? ( 5 - ? `calc(5 * 56rem - 28rem)` - : `calc(${filteredTokens.length} * 56rem)`, + height: `calc(5 * 56rem - 28rem)`, }} >
{ - filteredTokens.map((swapToken, index) => { - const { name, address } = swapToken - - const swapTokenAddress = name === sdk.config.tokens.depositToken && isGnosis - ? sdk.config.addresses.tokens.depositToken - : address - - return ( -
) : ( - + filteredTokens.length ? ( + 5 + ? `calc(5 * 56rem - 28rem)` + : `calc(${filteredTokens.length} * 56rem)`, + }} + > +
+ { + filteredTokens.map((swapToken, index) => { + const { name, address } = swapToken + + const swapTokenAddress = name === sdk.config.tokens.depositToken && isGnosis + ? sdk.config.addresses.tokens.depositToken + : address + + return ( +
+
+ ) : ( + + ) ) } @@ -105,4 +127,4 @@ const TokenDropdown: React.FC = (props) => { } -export default React.memo(TokenDropdown) +export default React.memo(TokenOptions) diff --git a/src/components/TokenDropdown/TokenOptions/TokenSkeleton/TokenSkeleton.tsx b/src/components/TokenDropdown/TokenOptions/TokenSkeleton/TokenSkeleton.tsx new file mode 100644 index 00000000..5e246465 --- /dev/null +++ b/src/components/TokenDropdown/TokenOptions/TokenSkeleton/TokenSkeleton.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import cx from 'classnames' + +import Bone from '../../../Bone/Bone' + + +type TokenSkeletonProps = { + className?: string +} + +const TokenSkeleton: React.FC = (props) => { + const { className } = props + + return ( +
+
+ +
+ + +
+
+
+ + +
+
+ ) +} + + +export default React.memo(TokenSkeleton) diff --git a/src/layouts/AppLayout/util/_SSR/fetchSwapTokenRates.ts b/src/components/TokenDropdown/util/fetchSwapTokenRates.ts similarity index 82% rename from src/layouts/AppLayout/util/_SSR/fetchSwapTokenRates.ts rename to src/components/TokenDropdown/util/fetchSwapTokenRates.ts index ee368787..de9ee2f7 100644 --- a/src/layouts/AppLayout/util/_SSR/fetchSwapTokenRates.ts +++ b/src/components/TokenDropdown/util/fetchSwapTokenRates.ts @@ -1,7 +1,5 @@ -'use server' import { Network } from 'sdk' -import { swapTokens } from 'helpers' -import methods from 'helpers/methods' +import { swapTokens, methods } from 'helpers' import cacheStorage from 'modules/cache-storage' @@ -17,18 +15,17 @@ const cache = { type Input = { values: (number | null)[] - cacheData: Record | null chainTokens: Record } -const modifyResult = ({ values, cacheData, chainTokens }: Input) => { +const modifyResult = ({ values, chainTokens }: Input) => { const result: Record = {} const tokenNames = Object.keys(chainTokens) values.forEach((value, index) => { const token = tokenNames[index] - result[token] = value === null ? cacheData?.[token] || 0 : value + result[token] = value || 0 }) return result @@ -65,7 +62,7 @@ const fetchSwapTokenRates = async (chainId: Network) => { Object.values(chainTokens).map((address) => fetchSwapTokenRate({ chainId, address })) ) - const data = modifyResult({ values, cacheData, chainTokens }) + const data = modifyResult({ values, chainTokens }) cache[chainId as keyof typeof cache].setData(data, cacheLimit) diff --git a/src/components/TokenDropdown/util/index.ts b/src/components/TokenDropdown/util/index.ts new file mode 100644 index 00000000..af9a2e00 --- /dev/null +++ b/src/components/TokenDropdown/util/index.ts @@ -0,0 +1 @@ +export { default as useTokenDropdown } from './useTokenDropdown' diff --git a/src/components/TokenDropdown/util/useSwapTokenRates.ts b/src/components/TokenDropdown/util/useSwapTokenRates.ts new file mode 100644 index 00000000..a7df26d5 --- /dev/null +++ b/src/components/TokenDropdown/util/useSwapTokenRates.ts @@ -0,0 +1,58 @@ +import { useCallback } from 'react' +import { useConfig } from 'config' +import { useActions } from 'hooks' +import { swapTokens, methods } from 'helpers' + +import fetchSwapTokenRates from './fetchSwapTokenRates' + + +const useFiatRates = () => { + const actions = useActions() + const { sdk, chainId } = useConfig() + + const chainTokens = swapTokens[chainId as keyof typeof swapTokens] + + return useCallback(async () => { + if (!chainTokens) { + actions.swapTokenRates.setFetching(false) + + return + } + + actions.swapTokenRates.setFetching(true) + + try { + const [ fiatRates, swapTokenRates ] = await Promise.all([ + sdk.utils.getFiatRates(), + fetchSwapTokenRates(chainId), + ]) + + const setValues = methods.createSetValues({ + EUR: fiatRates['USD/EUR'], + GBP: fiatRates['USD/GBP'], + CNY: fiatRates['USD/CNY'], + JPY: fiatRates['USD/JPY'], + KRW: fiatRates['USD/KRW'], + AUD: fiatRates['USD/AUD'], + }) + + if (!swapTokenRates) { + console.error('Fetch swap token rates error (swapTokenRates is null)') + } + + const swapTokenData = Object.keys(swapTokenRates || {}).reduce((acc, key) => { + acc[key] = setValues(swapTokenRates[key]) + + return acc + }, {} as Record>) + + actions.swapTokenRates.setData(swapTokenData) + } + catch (error: any) { + console.error('Fetch swap token rates error', error) + } + }, [ sdk, actions, chainId, chainTokens ]) +} + + +export default useFiatRates diff --git a/src/components/TokenDropdown/util/useTokenDropdown.ts b/src/components/TokenDropdown/util/useTokenDropdown.ts new file mode 100644 index 00000000..118bc79d --- /dev/null +++ b/src/components/TokenDropdown/util/useTokenDropdown.ts @@ -0,0 +1,64 @@ +import { useCallback, useMemo } from 'react' +import { useConfig } from 'config' +import { useBalances, useChainChanged, useAutoFetch, useObjectState } from 'hooks' + +import useSwapTokenRates from './useSwapTokenRates' + + +type State = { + fetchedKey: string + isOpen: boolean + isFetching: boolean +} + +const useTokenDropdown = () => { + const { address, chainId } = useConfig() + const [ { fetchedKey, isOpen, isFetching }, setState ] = useObjectState({ + fetchedKey: '', + isOpen: false, + isFetching: true, + }) + + const fetchRates = useSwapTokenRates() + const { refetchSwapTokenBalances } = useBalances() + + const dataKey = `${address}-${chainId}` + + const handleFetch = useCallback(async () => { + if (isOpen) { + setState({ isFetching: true }) + + await Promise.all([ + refetchSwapTokenBalances(), + fetchRates(), + ]) + + setState({ fetchedKey: dataKey, isFetching: false }) + } + }, [ isOpen, dataKey, setState, fetchRates, refetchSwapTokenBalances ]) + + useChainChanged(handleFetch) + + useAutoFetch({ + action: handleFetch, + interval: 15 * 60 * 1000, + skip: !isOpen, + }) + + const open = useCallback((isOpen: boolean) => { + setState({ isOpen }) + }, []) + + return useMemo(() => ({ + isFetching: isFetching && fetchedKey !== dataKey, + open, + }), [ + dataKey, + fetchedKey, + isFetching, + open, + ]) +} + + +export default useTokenDropdown diff --git a/src/components/Transactions/types.d.ts b/src/components/Transactions/types.d.ts index 2e0d44ad..5a981964 100644 --- a/src/components/Transactions/types.d.ts +++ b/src/components/Transactions/types.d.ts @@ -5,4 +5,6 @@ export type SetTransaction = (id: string | number, status: TransactionStatus) => export type StepData = Partial> +export type SetNextTransactionsFailed = (id: string | number) => void + export type StepsData = StepData[] diff --git a/src/components/Transactions/util/useLogic.ts b/src/components/Transactions/util/useLogic.ts index 0592dd8d..1094a36c 100644 --- a/src/components/Transactions/util/useLogic.ts +++ b/src/components/Transactions/util/useLogic.ts @@ -1,4 +1,6 @@ import { useCallback, useMemo, useState } from 'react' +import type { SetNextTransactionsFailed } from 'components' + import type { SetTransaction } from '../types' @@ -46,19 +48,44 @@ const useLogic = (initialTransactions: Transaction[] = []) => { setTransactions(initialTransactions) }, [ initialTransactions ]) + const setNextTransactionsFailed: SetNextTransactionsFailed = useCallback((id) => { + setTransactions((steps) => { + const failedIndex = steps.findIndex((step) => step.id === id) + + if (failedIndex === -1) { + return steps + } + + return steps.map((step, index) => { + if (index >= failedIndex) { + return { + ...step, + status: TransactionStatus.Fail, + } + } + + return step + }) + }) + }, []) + return useMemo(() => ({ transactions: transactions.map(({ onCancel, ...transaction }) => ({ ...transaction, - onCancel: typeof onCancel === 'function' ? () => onCancel({ setTransaction }) : undefined, + onCancel: typeof onCancel === 'function' + ? () => onCancel({ setTransaction }) + : undefined, })), setTransaction, setTransactions, resetTransactions, + setNextTransactionsFailed, }), [ transactions, setTransaction, setTransactions, resetTransactions, + setNextTransactionsFailed, ]) } diff --git a/src/components/UserApy/BoostPercent/BoostPercent.tsx b/src/components/UserApy/BoostPercent/BoostPercent.tsx index 7bf070af..8aed513c 100644 --- a/src/components/UserApy/BoostPercent/BoostPercent.tsx +++ b/src/components/UserApy/BoostPercent/BoostPercent.tsx @@ -1,8 +1,8 @@ import React from 'react' import cx from 'classnames' import intl from 'modules/intl' +import { methods } from 'helpers' import { useConfig } from 'config' -import methods from 'helpers/methods' import Logo from '../../Logo/Logo' import Icon from '../../Icon/Icon' diff --git a/src/components/UserApy/UserApy.tsx b/src/components/UserApy/UserApy.tsx index 5a451ca8..9a383219 100644 --- a/src/components/UserApy/UserApy.tsx +++ b/src/components/UserApy/UserApy.tsx @@ -1,5 +1,5 @@ import React from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import Text from '../Text/Text' import Loading from '../Loading/Loading' diff --git a/src/components/index.ts b/src/components/index.ts index 01802db3..3ce3c458 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,9 @@ export { default as AmountInput } from './AmountInput/AmountInput' export type { AmountInputProps } from './AmountInput/AmountInput' +export { default as ApyBreakdown } from './ApyBreakdown/ApyBreakdown' +export type { ApyBreakdownProps } from './ApyBreakdown/ApyBreakdown' + export { default as AutoHeightToggle } from './AutoHeightToggle/AutoHeightToggle' export type { AutoHeightToggleProps } from './AutoHeightToggle/AutoHeightToggle' @@ -110,7 +113,7 @@ export { default as TabsView } from './TabsView/TabsView' export type { TabsViewProps } from './TabsView/TabsView' export { default as Text } from './Text/Text' -export type { TextProps } from './Text/Text' +export type { TextProps, TextColor } from './Text/Text' export { default as TextWithTooltip } from './TextWithTooltip/TextWithTooltip' export type { TextWithTooltipProps } from './TextWithTooltip/TextWithTooltip' @@ -134,7 +137,7 @@ export { default as Tooltip } from './Tooltip/Tooltip' export type { TooltipProps } from './Tooltip/Tooltip' export { default as Transactions } from './Transactions/Transactions' -export type { SetTransaction } from './Transactions/types' +export type { SetTransaction, SetNextTransactionsFailed, StepsData } from './Transactions/types' export { TransactionStatus } from './Transactions/util' export type { Transaction } from './Transactions/util' export type { TransactionsProps } from './Transactions/Transactions' diff --git a/src/config/core/config/types.d.ts b/src/config/core/config/types.d.ts index b1add23c..b232d5d7 100644 --- a/src/config/core/config/types.d.ts +++ b/src/config/core/config/types.d.ts @@ -1,5 +1,8 @@ import type { BrowserProvider } from 'ethers' +import useCancelOnChange from './util/useCancelOnChange' +import type { Subscription } from './util/useWallet/useCallsOnChange' + declare global { @@ -32,6 +35,8 @@ declare global { setAddress: (address: string) => void changeChain: (networkId: NetworkIds) => Promise connect: (walletName: WalletIds, transport?: 'usb' | 'ble') => Promise + subscribeBeforeChange: Subscription + unsubscribeBeforeChange: Subscription } type CancelOnChangeInput = { @@ -44,7 +49,7 @@ declare global { wallet: Wallet chainId: ChainIds isReadOnlyMode: boolean - cancelOnChange: (values: CancelOnChangeInput) => any + cancelOnChange: ReturnType } type Callbacks = { diff --git a/src/config/core/config/util/useWallet/index.ts b/src/config/core/config/util/useWallet/index.ts index 92d5ca3b..d995fed6 100644 --- a/src/config/core/config/util/useWallet/index.ts +++ b/src/config/core/config/util/useWallet/index.ts @@ -7,6 +7,7 @@ import useChangeChain from './useChangeChain' import useAutoConnect from './useAutoConnect' import useUpdateWallet from './useUpdateWallet' import { BaseInput } from '../useConfigContext' +import useCallsOnChange from './useCallsOnChange' type Input = Omit & { @@ -26,8 +27,21 @@ const useWallet = (values: Input): ConfigProvider.Wallet => { onFinishConnect, } = values + const { + onChangeChain, + onChangeAddress, + subscribeBeforeChange, + unsubscribeBeforeChange, + } = useCallsOnChange() + const disconnect = useDisconnect({ configState, onError, onDisconnect }) - const changeChain = useChangeChain({ configState, supportedNetworkIds, onError }) + + const changeChain = useChangeChain({ + configState, + supportedNetworkIds, + onChangeChain, + onError, + }) const connect = useConnect({ configState, @@ -58,6 +72,8 @@ const useWallet = (values: Input): ConfigProvider.Wallet => { configState, chainId, disconnect, + onChangeChain, + onChangeAddress, }) return useMemo(() => ({ @@ -65,11 +81,15 @@ const useWallet = (values: Input): ConfigProvider.Wallet => { disconnect, setAddress, changeChain, + subscribeBeforeChange, + unsubscribeBeforeChange, }), [ connect, disconnect, setAddress, changeChain, + subscribeBeforeChange, + unsubscribeBeforeChange, ]) } diff --git a/src/config/core/config/util/useWallet/useCallsOnChange.ts b/src/config/core/config/util/useWallet/useCallsOnChange.ts new file mode 100644 index 00000000..729c0d2a --- /dev/null +++ b/src/config/core/config/util/useWallet/useCallsOnChange.ts @@ -0,0 +1,47 @@ +import { useCallback, useRef, useMemo } from 'react' +import EventAggregator from 'modules/event-aggregator' + + +const types = { + chain: 'chain', + address: 'address', +} + +type Type = keyof typeof types +type Handler = Parameters[1] +export type Subscription = (type: Type, handler: Handler) => void + +const useCallsOnChange = () => { + const eventsRef = useRef(new EventAggregator()) + + const subscribeBeforeChange = useCallback((type: Type, handler: Handler) => { + eventsRef.current.subscribe(type, handler) + }, []) + + const unsubscribeBeforeChange = useCallback((type: Type, handler: Handler) => { + eventsRef.current.unsubscribe(type, handler) + }, []) + + const onChangeChain = useCallback(() => { + eventsRef.current.dispatch(types.chain) + }, []) + + const onChangeAddress = useCallback(() => { + eventsRef.current.dispatch(types.address) + }, []) + + return useMemo(() => ({ + onChangeChain, + onChangeAddress, + subscribeBeforeChange, + unsubscribeBeforeChange, + }), [ + onChangeChain, + onChangeAddress, + subscribeBeforeChange, + unsubscribeBeforeChange, + ]) +} + + +export default useCallsOnChange diff --git a/src/config/core/config/util/useWallet/useChangeChain.ts b/src/config/core/config/util/useWallet/useChangeChain.ts index 30c0323c..b9354ffe 100644 --- a/src/config/core/config/util/useWallet/useChangeChain.ts +++ b/src/config/core/config/util/useWallet/useChangeChain.ts @@ -13,10 +13,11 @@ type Input = { supportedNetworkIds: NetworkIds[] configState: ConfigProvider.ConfigState onError?: ConfigProvider.Callbacks['onError'] + onChangeChain: () => void } const useChangeChain = (values: Input) => { - const { configState, supportedNetworkIds, onError } = values + const { configState, supportedNetworkIds, onError, onChangeChain } = values const { dataRef, setData } = configState const { @@ -47,6 +48,7 @@ const useChangeChain = (values: Input) => { const isMonitorAddress = activeWallet === wallets.monitorAddress.id if (!dataRef.current.address || isMonitorAddress) { + onChangeChain() setData({ networkId }) return @@ -81,6 +83,7 @@ const useChangeChain = (values: Input) => { clearNotificationTimeout() } + onChangeChain() setData({ library, networkId }) } catch (error: any) { @@ -99,6 +102,7 @@ const useChangeChain = (values: Input) => { supportedNetworkIds, setNotificationTimeout, clearNotificationTimeout, + onChangeChain, setData, onError, ]) diff --git a/src/config/core/config/util/useWallet/useUpdateWallet.ts b/src/config/core/config/util/useWallet/useUpdateWallet.ts index 31f65017..70d4ce15 100644 --- a/src/config/core/config/util/useWallet/useUpdateWallet.ts +++ b/src/config/core/config/util/useWallet/useUpdateWallet.ts @@ -1,7 +1,7 @@ import { useEffect, useCallback } from 'react' import { getAddress, BrowserProvider } from 'ethers' import type { Eip1193Provider } from 'ethers' -import methods from 'helpers/methods' +import { methods } from 'helpers' import networks from '../networks' @@ -11,10 +11,19 @@ type Input = { supportedNetworkIds: NetworkIds[] configState: ConfigProvider.ConfigState disconnect: () => Promise + onChangeAddress: () => void + onChangeChain: () => void } const useUpdateWallet = (values: Input) => { - const { chainId, configState, supportedNetworkIds, disconnect } = values + const { + chainId, + configState, + supportedNetworkIds, + disconnect, + onChangeChain, + onChangeAddress, + } = values const { data, dataRef, setData } = configState const { address, autoConnectChecked } = data @@ -72,6 +81,7 @@ const useUpdateWallet = (values: Input) => { const provider = await connector.getProvider() const library = new BrowserProvider(provider as Eip1193Provider) + onChangeChain() setData({ library, networkId }) } } @@ -83,6 +93,7 @@ const useUpdateWallet = (values: Input) => { const handleAccountsChanged = (address: string) => { if (address) { + onChangeAddress() setData({ address: getAddress(address), accountName: null, @@ -119,7 +130,16 @@ const useUpdateWallet = (values: Input) => { connector?.events?.unsubscribe('change', handleUpdate) connector?.events?.unsubscribe('disconnect', disconnect) } - }, [ autoConnectChecked, address, dataRef, supportedNetworkIds, disconnect, setData ]) + }, [ + address, + dataRef, + autoConnectChecked, + supportedNetworkIds, + setData, + disconnect, + onChangeChain, + onChangeAddress, + ]) } diff --git a/src/config/core/connectors/BinanceConnector.ts b/src/config/core/connectors/BinanceConnector.ts index bb3a970a..f228fb7a 100644 --- a/src/config/core/connectors/BinanceConnector.ts +++ b/src/config/core/connectors/BinanceConnector.ts @@ -1,8 +1,8 @@ 'use client' import { getProvider } from '@binance/w3w-ethereum-provider' import EventAggregator from 'modules/event-aggregator' -import { AbstractProvider } from 'ethers' import apiUrls from 'helpers/methods/apiUrls' +import { AbstractProvider } from 'ethers' import { Network } from 'sdk' import networks from '../config/util/networks' diff --git a/src/config/core/connectors/LedgerConnector/LedgerProvider.ts b/src/config/core/connectors/LedgerConnector/LedgerProvider.ts index 4a690d95..30f84753 100644 --- a/src/config/core/connectors/LedgerConnector/LedgerProvider.ts +++ b/src/config/core/connectors/LedgerConnector/LedgerProvider.ts @@ -1,6 +1,6 @@ -import methods from 'helpers/methods' import { configs, Network } from 'sdk' import AppEth from '@ledgerhq/hw-app-eth' +import * as methods from 'helpers/methods' import { EIP712Message } from '@ledgerhq/types-live' import { Signature, Transaction, isAddress, TypedDataEncoder } from 'ethers' import type { TransactionLike, Eip1193Provider } from 'ethers' diff --git a/src/config/core/global.d.ts b/src/config/core/global.d.ts index 301fe69c..cfbabacd 100644 --- a/src/config/core/global.d.ts +++ b/src/config/core/global.d.ts @@ -6,7 +6,6 @@ const walletsIds = Object.values(wallets).map(({ id }) => id) declare global { type WalletIds = typeof walletsIds[number] - type ReadOnlyConnector = ReadOnlyConnectorType type NetworkIds = OneOfArray type Connectors = Unpromise> diff --git a/src/config/index.tsx b/src/config/index.tsx index c6057b77..32c49737 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useRef } from 'react' import { configs, chains } from 'sdk' import modal from 'modules/modal' -import methods from 'helpers/methods' +import { methods } from 'helpers' import useActions from '../hooks/data/useActions' import { createConfig, networks, wallets } from './core' @@ -78,6 +78,12 @@ const ConfigProvider: React.FC = (props) => { const isActivationMessageVisible = useRef(false) const activationMessageTimeoutRef = useRef(null) + const resetAccount = useCallback(() => { + actions.account.balances.resetData() + actions.account.vestings.resetData() + actions.account.swapTokenBalances.resetData() + }, [ actions ]) + const setLoader = useCallback((activationMessage: Intl.Message | string) => { activationMessageTimeoutRef.current = setTimeout(() => { isActivationMessageVisible.current = true @@ -96,10 +102,11 @@ const ConfigProvider: React.FC = (props) => { }, [ actions ]) const handleConnectError = useCallback(() => { + resetAccount() modal.closeModal(connectModalId) setTimeout(() => actions.ui.resetBottomLoader()) - }, [ actions ]) + }, [ actions, resetAccount ]) return ( = (props) => { supportedNetworkIds={supportedNetworkIds} onConnectError={handleConnectError} onFinishConnect={resetLoader} + onDisconnect={resetAccount} onStartConnect={setLoader} > {children} diff --git a/src/helpers/constants/blockchain.ts b/src/helpers/constants/blockchain.ts index 9300ddf3..3d584344 100644 --- a/src/helpers/constants/blockchain.ts +++ b/src/helpers/constants/blockchain.ts @@ -7,4 +7,7 @@ export default { emptyBalance: 100000000000000000000000000n, emptyBalance6: 100000000000000n, emptyBalance8: 10000000000000000n, + + // Addresses + emptyAddress: '0x1111111111111111111111111111111111111111', } diff --git a/src/helpers/constants/index.ts b/src/helpers/constants/index.ts index 33d7f572..681057d6 100644 --- a/src/helpers/constants/index.ts +++ b/src/helpers/constants/index.ts @@ -1,26 +1,22 @@ +import links from './links' import tokens from './tokens' import colors from './colors' -import walletList from './walletList' import queryNames from './queryNames' import blockchain from './blockchain' -import walletNames from './walletNames' import headerNames from './headerNames' import cookieNames from './cookieNames' -import walletTitles from './walletTitles' import localStorageNames from './localStorageNames' import sessionStorageNames from './sessionStorageNames' export { + links, tokens, colors, - walletList, queryNames, blockchain, - walletNames, headerNames, cookieNames, - walletTitles, localStorageNames, sessionStorageNames, } diff --git a/src/helpers/constants/links.ts b/src/helpers/constants/links.ts new file mode 100644 index 00000000..ead97f6e --- /dev/null +++ b/src/helpers/constants/links.ts @@ -0,0 +1,3 @@ +export default { + boostUpgrade: 'https://forum.stakewise.io/t/swip-33-upgrade-stakewise-boost-strategy-after-changes-in-aave-contracts/1950', +} diff --git a/src/helpers/constants/walletList.ts b/src/helpers/constants/walletList.ts deleted file mode 100644 index bbcd41ac..00000000 --- a/src/helpers/constants/walletList.ts +++ /dev/null @@ -1,74 +0,0 @@ -import walletNames from './walletNames' -import walletTitles from './walletTitles' - - -const walletList = [ - { - id: walletNames.metaMask, - title: walletTitles.metaMask, - logo: 'connector/metamask', - }, - { - id: walletNames.walletConnect, - title: walletTitles.walletConnect, - logo: 'connector/walletConnect', - }, - { - id: walletNames.braveWallet, - title: walletTitles.braveWallet, - logo: 'connector/braveBrowser', - }, - { - id: walletNames.rabby, - title: walletTitles.rabbyWallet, - logo: 'connector/rabby', - }, - { - id: walletNames.ledger, - title: walletTitles.ledger, - logo: 'connector/ledger', - }, - { - id: walletNames.coinbase, - title: walletTitles.coinbase, - logo: 'connector/coinbase', - }, - { - id: walletNames.zenGo, - title: walletTitles.zenGo, - logo: 'connector/zengo', - }, - { - id: walletNames.taho, - title: walletTitles.taho, - logo: 'connector/taho', - }, - { - id: walletNames.okx, - title: walletTitles.okx, - logo: 'connector/okx', - }, - { - id: walletNames.trustWallet, - title: walletTitles.trustWallet, - logo: 'connector/trustWallet', - }, - { - id: walletNames.monitorAddress, - title: walletTitles.monitorAddress, - logo: 'connector/monitorAddress', - }, - { - id: walletNames.dAppBrowser, - title: walletTitles.dAppBrowser, - logo: 'connector/monitorAddress', - }, - { - id: walletNames.gnosisSafe, - title: walletTitles.gnosisSafe, - logo: 'connector/gnosisSafe', - }, -] as const - - -export default walletList diff --git a/src/helpers/constants/walletNames.ts b/src/helpers/constants/walletNames.ts deleted file mode 100644 index c5da70df..00000000 --- a/src/helpers/constants/walletNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default { - monitorAddress: 'monitorAddress', - walletConnect: 'walletConnect', - braveWallet: 'braveWallet', - trustWallet: 'trustWallet', - dAppBrowser: 'dAppBrowser', - gnosisSafe: 'gnosisSafe', - coinbase: 'coinbase', - metaMask: 'metaMask', - ledger: 'ledger', - rabby: 'rabby', - zenGo: 'zenGo', - taho: 'taho', - okx: 'okx', -} as const diff --git a/src/helpers/constants/walletTitles.ts b/src/helpers/constants/walletTitles.ts deleted file mode 100644 index 1b06eedf..00000000 --- a/src/helpers/constants/walletTitles.ts +++ /dev/null @@ -1,16 +0,0 @@ -export default { - okx: 'OKX', - taho: 'Taho', - zenGo: 'ZenGo', - ledger: 'Ledger', - metaMask: 'MetaMask', - coinbase: 'Coinbase', - ledgerLive: 'Ledger Live', - gnosisSafe: 'Gnosis Safe', - dAppBrowser: 'DApp Browser', - braveWallet: 'Brave Wallet', - rabbyWallet: 'Rabby Wallet', - trustWallet: 'Trust Wallet', - monitorAddress: 'Check wallet', - walletConnect: 'WalletConnect', -} as const diff --git a/src/helpers/contracts/addresses.ts b/src/helpers/contracts/addresses.ts index 6f133f97..58ea1e07 100644 --- a/src/helpers/contracts/addresses.ts +++ b/src/helpers/contracts/addresses.ts @@ -13,7 +13,7 @@ const addresses = { }, [Network.Hoodi]: { base: { - merkleDistributorV2: '0xc61847D6fc1F64162ff9f1D06205d9C4cDb2F239', + merkleDistributorV2: '0xc61847D6Fc1F64162fF9F1d06205D9c4cDb2f239', }, cow: { vaultRelayer: ZeroAddress, diff --git a/src/helpers/enums.ts b/src/helpers/enums.ts index 25820cb9..779dbea0 100644 --- a/src/helpers/enums.ts +++ b/src/helpers/enums.ts @@ -1,8 +1,14 @@ export enum BoostStep { + Upgrade = 'Upgrade', Permit = 'Permit', Boost = 'Boost', } +export enum UnboostStep { + Upgrade = 'Upgrade', + Unboost = 'Unboost', +} + export enum StakeStep { SwapApprove = 'SwapApprove', Approve = 'Approve', diff --git a/src/helpers/getters/getDefaultNetwork.ts b/src/helpers/getters/getDefaultNetwork.ts index e2d6a6a8..0e1cad86 100644 --- a/src/helpers/getters/getDefaultNetwork.ts +++ b/src/helpers/getters/getDefaultNetwork.ts @@ -1,7 +1,7 @@ import getVaultAddress from './getVaultAddress' -const networks: NetworkIds[] = [ 'mainnet', 'gnosis', 'chiado' ] +const networks: NetworkIds[] = [ 'mainnet', 'gnosis', 'chiado', 'hoodi' ] const getDefaultNetwork = () => networks.find(getVaultAddress) diff --git a/src/helpers/index.ts b/src/helpers/index.ts index c88752d9..39e9d9d8 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,10 +1,11 @@ export { BigDecimal, getGas } from 'sdk' -export * as polyfills from 'helpers/polyfills' -export { default as cookie } from 'helpers/cookie' -export { default as initContext } from 'helpers/initContext' +export * as polyfills from './polyfills' +export { default as cookie } from './cookie' +export { default as initContext } from './initContext' export { default as replaceReactComponents } from './replaceReactComponents' export * from './swapTokens' +export * as methods from './methods' export * as getters from './getters' export * as requests from './requests' export * as modifiers from './modifiers' diff --git a/src/helpers/messages/index.ts b/src/helpers/messages/index.ts index f3f280e4..10775d1a 100644 --- a/src/helpers/messages/index.ts +++ b/src/helpers/messages/index.ts @@ -19,6 +19,15 @@ export default { transaction, notification, accessibility, + upgradeLeverageStrategy: { + en: 'Upgrade leverage strategy contract', + ru: 'Обновить контракт стратегии с плечом', + fr: 'Mettre à niveau le contrat de stratégie de levier', + es: 'Actualizar el contrato de estrategia de apalancamiento', + pt: 'Atualizar contrato de estratégia de alavancagem', + de: 'Leverage-Strategievertrag aktualisieren', + zh: '升级杠杆策略合约', + }, staked: { en: 'Staked {depositToken}', ru: 'Застейкано {depositToken}', diff --git a/src/helpers/messages/notification.ts b/src/helpers/messages/notification.ts index 691025c4..7b2ce6ab 100644 --- a/src/helpers/messages/notification.ts +++ b/src/helpers/messages/notification.ts @@ -71,4 +71,13 @@ export default { de: 'Sie müssen warten, bis diese Anforderung zum Entboosten abgeschlossen ist, bevor Sie mehr osETH entboosten können.', zh: '您需要等到此解除加速请求完成后,才能进一步解除osETH的加速。', }, + boostUpgrade: { + en: 'Boost position upgrade will be included in the transaction batch.', + ru: 'Обновление Буст позиции будет включено в пакет транзакций.', + fr: 'La mise à niveau de la position Boost sera incluse dans le lot de transactions.', + es: 'La actualización de la posición Boost se incluirá en el lote de transacciones.', + pt: 'A atualização da posição Boost será incluída no lote de transações.', + de: 'Die Boost-Position-Aufrüstung wird in den Transaktionsstapel aufgenommen.', + zh: '提升位置将包含在交易批处理中。', + }, } diff --git a/src/helpers/methods/fetch/index.ts b/src/helpers/methods/fetch/index.ts index 8aa25161..6b261115 100644 --- a/src/helpers/methods/fetch/index.ts +++ b/src/helpers/methods/fetch/index.ts @@ -1,5 +1,7 @@ import { AbortRequest } from 'sdk' +import { sessionStorageNames } from '../../constants' + import { handleJson, extractOpName, @@ -12,12 +14,14 @@ type FetchOptions = RequestInit & { retryCount?: number } +const sessionErrorUrl = sessionStorageNames.moduleErrorUrl + const fetchMethod = ( urls: string | readonly string[], options: FetchOptions = {} ): AbortRequest => { const retryCount = options.retryCount ?? 0 - const baseUrl = getRequestUrl(urls) + const baseUrl = getRequestUrl({ urls, sessionErrorUrl }) const opName = extractOpName(options.body) const sep = baseUrl.includes('?') ? '&' : '?' const urlToUse = opName @@ -61,7 +65,7 @@ const fetchMethod = ( const hasBackup = Array.isArray(urls) && urls.length > 1 if (hasBackup && retryCount < 1) { - saveErrorUrlToSessionStorage(baseUrl) + saveErrorUrlToSessionStorage({ baseUrl, sessionErrorUrl }) return fetchMethod(urls, { ...options, retryCount: retryCount + 1 }).promise } diff --git a/src/helpers/methods/fetch/utils/getRequestUrl.ts b/src/helpers/methods/fetch/utils/getRequestUrl.ts index 2cde2805..27c072c2 100644 --- a/src/helpers/methods/fetch/utils/getRequestUrl.ts +++ b/src/helpers/methods/fetch/utils/getRequestUrl.ts @@ -1,20 +1,22 @@ import { localStorage } from 'sdk' -import * as constants from 'helpers/constants' import type { ErrorRecord } from './types' -const sessionErrorUrl = constants.sessionStorageNames.moduleErrorUrl +type Input = { + sessionErrorUrl: string + urls: string | ReadonlyArray +} -const getErroredUrlFromSessionStorage = (): string | null => { - const sessionRecord = localStorage.getSessionItem(sessionErrorUrl) +const getErroredUrlFromSessionStorage = (url: string): string | null => { + const sessionRecord = localStorage.getSessionItem(url) if (!sessionRecord) { return null } if (sessionRecord.expiresAt <= Date.now()) { - localStorage.removeSessionItem(sessionErrorUrl) + localStorage.removeSessionItem(url) return null } @@ -22,7 +24,9 @@ const getErroredUrlFromSessionStorage = (): string | null => { return sessionRecord.url } -const getRequestUrl = (urls: string | ReadonlyArray): string => { +const getRequestUrl = (input: Input): string => { + const { urls, sessionErrorUrl } = input + if (typeof urls === 'string') { return urls } @@ -38,7 +42,7 @@ const getRequestUrl = (urls: string | ReadonlyArray): string => { const primary = urls[0] const backup = urls[1] - const errored = getErroredUrlFromSessionStorage() + const errored = getErroredUrlFromSessionStorage(sessionErrorUrl) return errored === primary ? backup : primary } diff --git a/src/helpers/methods/fetch/utils/saveErrorUrlToSessionStorage.ts b/src/helpers/methods/fetch/utils/saveErrorUrlToSessionStorage.ts index 9db828b1..4cd4d9a8 100644 --- a/src/helpers/methods/fetch/utils/saveErrorUrlToSessionStorage.ts +++ b/src/helpers/methods/fetch/utils/saveErrorUrlToSessionStorage.ts @@ -1,16 +1,21 @@ import { localStorage } from 'sdk' -import * as constants from 'helpers/constants' import type { ErrorRecord } from './types' -const sessionErrorUrl = constants.sessionStorageNames.moduleErrorUrl +type Input = { + baseUrl: string + sessionErrorUrl: string +} + const expireTime = 60 * 60 * 1_000 // 1hr -const saveErrorUrlToSessionStorage = (baseUrl: string): void => { +const saveErrorUrlToSessionStorage = (input: Input): void => { + const { baseUrl, sessionErrorUrl } = input + const record: ErrorRecord = { url: baseUrl, - expiresAt: Date.now() + expireTime, + expiresAt: Date.now() + expireTime } localStorage.setSessionItem(sessionErrorUrl, record) diff --git a/src/helpers/methods/getInjectedProvider.ts b/src/helpers/methods/getInjectedProvider.ts deleted file mode 100644 index 5c7659f2..00000000 --- a/src/helpers/methods/getInjectedProvider.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as constants from 'helpers/constants' - - -const getInjectedProvider = (walletName: WalletIds) => { - if (walletName === constants.walletNames.okx) { - return window.okxwallet - } - - if (walletName === constants.walletNames.rabby) { - return window.rabby - } - - if (walletName === constants.walletNames.taho) { - return window.taho - } - - if (walletName === constants.walletNames.trustWallet) { - return window.trustwallet - } - - if (walletName === constants.walletNames.braveWallet) { - return window.braveEthereum - } - - if (walletName === constants.walletNames.metaMask) { - return window.ethereum - } - - return null -} - - -export default getInjectedProvider diff --git a/src/helpers/methods/getSDK/index.ts b/src/helpers/methods/getSDK/index.ts index c058c1f8..3be88801 100644 --- a/src/helpers/methods/getSDK/index.ts +++ b/src/helpers/methods/getSDK/index.ts @@ -7,7 +7,7 @@ import getUrls from './getUrls' type Input = { chainId: Network - library?: StakeWise.Provider + library?: any } const sdkList = {} as Record diff --git a/src/helpers/methods/index.ts b/src/helpers/methods/index.ts index 0774894b..8e879047 100644 --- a/src/helpers/methods/index.ts +++ b/src/helpers/methods/index.ts @@ -12,18 +12,16 @@ import downloadFile from './downloadFile' import shortenAddress from './shortenAddress' import fetchWithRetry from './fetchWithRetry' import formatFiatValue from './formatFiatValue' -import isInjectedWallet from './isInjectedWallet' import formatTokenValue from './formatTokenValue' import getOriginHostName from './getOriginHostName' import getArrUniqueItems from './getArrUniqueItems' import numericalReduction from './numericalReduction' import getGlobalHtmlAttrs from './getGlobalHtmlAttrs' import addNumberSeparator from './addNumberSeparator' -import getInjectedProvider from './getInjectedProvider' import fetchFiatRates, { createSetValues } from './fetchFiatRates' -export default { +export { ens, fetch, getSDK, @@ -40,12 +38,10 @@ export default { fetchFiatRates, formatFiatValue, createSetValues, - isInjectedWallet, formatTokenValue, getOriginHostName, getArrUniqueItems, numericalReduction, getGlobalHtmlAttrs, addNumberSeparator, - getInjectedProvider, } diff --git a/src/helpers/methods/isInjectedWallet.ts b/src/helpers/methods/isInjectedWallet.ts deleted file mode 100644 index 3ef9e362..00000000 --- a/src/helpers/methods/isInjectedWallet.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as constants from 'helpers/constants' - - -const injectedWalletNames = [ - constants.walletNames.okx, - constants.walletNames.taho, - constants.walletNames.rabby, - constants.walletNames.metaMask, - constants.walletNames.braveWallet, - constants.walletNames.trustWallet, -] as const - -const isInjectedWallet = (walletName: WalletIds) => injectedWalletNames.includes(walletName as any) - - -export default isInjectedWallet diff --git a/src/helpers/requests/_SSR/vault/getVaultBase.ts b/src/helpers/requests/_SSR/vault/getVaultBase.ts index 1a6091e0..bee0ce24 100644 --- a/src/helpers/requests/_SSR/vault/getVaultBase.ts +++ b/src/helpers/requests/_SSR/vault/getVaultBase.ts @@ -1,7 +1,9 @@ -import methods from 'helpers/methods' import { cookies } from 'next/headers' import { networks } from 'config/core' -import { constants, getters } from 'helpers' +import { Network } from 'sdk' + +import { constants, getters } from '../../../index' +import { getSDK } from '../../../methods' const getNetworkData = async () => { @@ -33,14 +35,22 @@ const getVaultBase = async () => { } const chainId = networks.chainById[networkId as NetworkIds] - const sdk = methods.getSDK({ chainId }) + const sdk = getSDK({ chainId }) const data = await sdk.vault.getVault({ vaultAddress, withTime: true }) const versions = await sdk.getVaultVersion(vaultAddress) + const feePercent = await sdk.contracts.base.mintTokenController.feePercent() + + const isEditableInGnosis = sdk.network === Network.Gnosis && versions.version >= 3 + const isEditableInEthereum = sdk.network === Network.Mainnet && versions.version >= 5 + const isPostPectra = isEditableInGnosis || isEditableInEthereum + return { data: { ...data, versions, + isPostPectra, + protocolFeePercent: String(feePercent / 100n), }, isSSR: true, isFetching: false, diff --git a/src/helpers/requests/approve.ts b/src/helpers/requests/approve.ts index af3560ca..8eddf2cb 100644 --- a/src/helpers/requests/approve.ts +++ b/src/helpers/requests/approve.ts @@ -1,5 +1,6 @@ import { MaxUint256, parseEther } from 'ethers' -import methods from 'helpers/methods' + +import { getGasMargin } from '../methods' type Input = { @@ -25,7 +26,7 @@ const approve = async (values: Input) => { const { maxFeePerGas, maxPriorityFeePerGas } = feeData - const gasLimit = methods.getGasMargin(gasCost) + const gasLimit = getGasMargin(gasCost) const overrides: Parameters[2] = { gasLimit, } diff --git a/src/helpers/requests/fetchBoostSupplyCaps.ts b/src/helpers/requests/fetchBoostSupplyCaps.ts index 76a0a554..6d356c03 100644 --- a/src/helpers/requests/fetchBoostSupplyCaps.ts +++ b/src/helpers/requests/fetchBoostSupplyCaps.ts @@ -1,4 +1,4 @@ -import methods from '../methods' +import { fetch } from '../methods' type Input = { @@ -17,7 +17,7 @@ type BoostSupplyCapsQueryPayload = { const fetchBoostSupplyCaps = async (values: Input): Promise => { const { url } = values - return methods.fetch(url, { + return fetch(url, { method: 'POST', body: JSON.stringify({ query: ` diff --git a/src/helpers/requests/fetchCreatedAt.ts b/src/helpers/requests/fetchCreatedAt.ts index c03ba5db..9db6d08e 100644 --- a/src/helpers/requests/fetchCreatedAt.ts +++ b/src/helpers/requests/fetchCreatedAt.ts @@ -1,4 +1,4 @@ -import methods from 'helpers/methods' +import { fetch } from '../methods' type Input = { @@ -17,7 +17,7 @@ type CreatedAtVariables = { } const fetchCreatedAt = ({ url, variables }: Input) => { - return methods.fetch(url, { + return fetch(url, { method: 'POST', body: JSON.stringify({ query: ` diff --git a/src/helpers/requests/fetchDistributorClaims.ts b/src/helpers/requests/fetchDistributorClaims.ts index 319808b1..a7b7a37a 100644 --- a/src/helpers/requests/fetchDistributorClaims.ts +++ b/src/helpers/requests/fetchDistributorClaims.ts @@ -1,5 +1,6 @@ import { getAddress } from 'ethers' -import methods from 'helpers/methods' + +import { fetch } from '../methods' type Input = { @@ -21,7 +22,7 @@ type DistributorClaimsQueryPayload = { const fetchDistributorClaims = async (values: Input): Promise => { const { address, url } = values - return methods.fetch(url, { + return fetch(url, { method: 'POST', body: JSON.stringify({ query: ` diff --git a/src/helpers/requests/fetchStakeStats.ts b/src/helpers/requests/fetchStakeStats.ts deleted file mode 100644 index 609bad87..00000000 --- a/src/helpers/requests/fetchStakeStats.ts +++ /dev/null @@ -1,54 +0,0 @@ -import methods from 'helpers/methods' - - -type Input = { - url: string | readonly string[] - variables: StakeStatsVariables -} - -type StakeStatsQueryPayload = { - osTokenHolder: { - apy: string - timestamp: string - totalAssets: string - earnedAssets: string - }[] -} - -type StakeStatsVariables = { - first?: number - where: { - osTokenHolder: string - timestamp_gte?: string - timestamp_lte?: string - } -} - -const fetchStakeStats = ({ url, variables }: Input) => { - return methods.fetch(url, { - method: 'POST', - body: JSON.stringify({ - query: ` - query StakeStats( - $where: OsTokenHolderStats_filter - $first: Int - ) { - osTokenHolder: osTokenHolderStats_collection( - interval: day - first: $first - where: $where - ) { - apy - timestamp - totalAssets - earnedAssets - } - } - `, - variables, - }), - }) -} - - -export default fetchStakeStats diff --git a/src/helpers/requests/fetchStakeSwapData.ts b/src/helpers/requests/fetchStakeSwapData.ts new file mode 100644 index 00000000..e9ab7309 --- /dev/null +++ b/src/helpers/requests/fetchStakeSwapData.ts @@ -0,0 +1,37 @@ +import { VoidSigner } from 'ethers' +import { blockchain } from '../constants' + + +type Input = { + sdk: SDK + amount: bigint + vaultAddress: string + userAddress?: string | null +} + +const fetchStakeSwapData = async (values: Input) => { + const { amount, sdk, userAddress, vaultAddress } = values + + const signer = new VoidSigner(blockchain.emptyAddress, sdk.provider) + + const { params } = await sdk.vault.getHarvestParams({ vaultAddress }) + const signedContract = sdk.contracts.special.stakeCalculator.connect(signer) + + const { + exchangeRate, + receivedOsTokenShares: receiveShares, + } = await signedContract.calculateStake.staticCall({ + user: userAddress || blockchain.emptyAddress, + harvestParams: params, + vault: vaultAddress, + stakeAssets: amount, + }) + + return { + receiveShares, + exchangeRate, + } +} + + +export default fetchStakeSwapData diff --git a/src/helpers/requests/index.ts b/src/helpers/requests/index.ts index c1713da2..118483f7 100644 --- a/src/helpers/requests/index.ts +++ b/src/helpers/requests/index.ts @@ -3,6 +3,6 @@ export { default as getApproveGas } from './getApproveGas' export { default as increaseDelay } from './increaseDelay' export { default as fetchXlsxFile } from './fetchXlsxFile' export { default as fetchCreatedAt } from './fetchCreatedAt' -export { default as fetchStakeStats } from './fetchStakeStats' +export { default as fetchStakeSwapData } from './fetchStakeSwapData' export { default as fetchBoostSupplyCaps } from './fetchBoostSupplyCaps' export { default as fetchDistributorClaims } from './fetchDistributorClaims' diff --git a/src/hooks/controls/useAddressChanged.ts b/src/hooks/controls/useAddressChanged.ts index 623906bf..9b02ae1f 100644 --- a/src/hooks/controls/useAddressChanged.ts +++ b/src/hooks/controls/useAddressChanged.ts @@ -1,27 +1,18 @@ -import { useEffect, useRef } from 'react' import { useConfig } from 'config' +import useChangeEffect from './useChangeEffect' -const useAddressChanged = (callback: () => any) => { - const { address, autoConnectChecked } = useConfig() - - const isInitRef = useRef(false) - const addressRef = useRef(address) - const callbackRef = useRef(callback) - callbackRef.current = callback +type Callback = () => any - useEffect(() => { - if (!isInitRef.current && autoConnectChecked) { - addressRef.current = address - isInitRef.current = true - } +const useAddressChanged = (callback: Callback) => { + const { address, autoConnectChecked } = useConfig() - if (autoConnectChecked && address !== addressRef.current) { - callbackRef.current() - addressRef.current = address + useChangeEffect<[ string | null, boolean, Callback ]>((prevAddress, prevAutoConnectChecked) => { + if (prevAutoConnectChecked && prevAddress !== address) { + callback() } - }, [ address, autoConnectChecked ]) + }, [ address, autoConnectChecked, callback ]) } diff --git a/src/hooks/controls/useChainChanged.ts b/src/hooks/controls/useChainChanged.ts index 296a6c05..640fe342 100644 --- a/src/hooks/controls/useChainChanged.ts +++ b/src/hooks/controls/useChainChanged.ts @@ -1,20 +1,16 @@ -import { useEffect, useRef } from 'react' import { useConfig } from 'config' +import useChangeEffect from './useChangeEffect' -const useChainChanged = (callback: () => any) => { - const { chainId } = useConfig() - const chainIdRef = useRef(chainId) - const callbackRef = useRef(callback) - callbackRef.current = callback +const useChainChanged = (callback: (chainId: ChainIds) => any) => { + const { chainId } = useConfig() - useEffect(() => { - if (chainIdRef.current !== chainId) { - callbackRef.current() - chainIdRef.current = chainId + useChangeEffect<[ ChainIds, (chainId: ChainIds) => any ]>((prevChainId) => { + if (prevChainId !== chainId) { + callback(chainId) } - }, [ chainId ]) + }, [ chainId, callback ]) } diff --git a/src/hooks/controls/useChangeEffect.ts b/src/hooks/controls/useChangeEffect.ts new file mode 100644 index 00000000..69dc582e --- /dev/null +++ b/src/hooks/controls/useChangeEffect.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef } from 'react' + + +type ChangeEffectDeps = ReadonlyArray + +const useUpdateEffect: typeof useEffect = (effect, deps) => { + const firstMount = useRef(true) + + useEffect(() => { + if (firstMount.current) { + firstMount.current = false + return + } + effect() + }, deps) +} + +const usePrevious = (deps: T): T => { + const prevRef = useRef(deps) + + useEffect(() => { + prevRef.current = deps + }, deps) + + return prevRef.current +} + +const useChangeEffect = ( + effect: (...prevValue: T) => void, + deps: T +): void => { + const prevValue = usePrevious(deps) + + useUpdateEffect(() => { + effect(...prevValue) + }, deps) +} + + +export default useChangeEffect diff --git a/src/hooks/controls/useFieldListener.ts b/src/hooks/controls/useFieldListener.ts index 5218faa5..301a625b 100644 --- a/src/hooks/controls/useFieldListener.ts +++ b/src/hooks/controls/useFieldListener.ts @@ -1,6 +1,6 @@ 'use client' import { useEffect } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' type Procedure = (...args: any[]) => void diff --git a/src/hooks/controls/useModalClose.ts b/src/hooks/controls/useModalClose.ts index 5e396bd8..44b3a8e5 100644 --- a/src/hooks/controls/useModalClose.ts +++ b/src/hooks/controls/useModalClose.ts @@ -1,5 +1,5 @@ -import useChainChanged from './useChainChanged' -import useAddressChanged from './useAddressChanged' +import { useEffect } from 'react' +import { useConfig } from 'config' type Input = { @@ -9,8 +9,16 @@ type Input = { const useModalClose = (values: Input) => { const { closeModal } = values - useChainChanged(closeModal) - useAddressChanged(closeModal) + const { wallet } = useConfig() + + useEffect(() => { + wallet.subscribeBeforeChange('chain', closeModal) + wallet.subscribeBeforeChange('address', closeModal) + return () => { + wallet.unsubscribeBeforeChange('chain', closeModal) + wallet.unsubscribeBeforeChange('address', closeModal) + } + }, []) } diff --git a/src/hooks/data/useBalances.ts b/src/hooks/data/useBalances.ts index 7fc9bde3..2635f604 100644 --- a/src/hooks/data/useBalances.ts +++ b/src/hooks/data/useBalances.ts @@ -14,7 +14,7 @@ type Output = { } const storeSelector = (store: Store) => ({ - balances: store.account.balances.data, + balances: store.account.balances, }) const useBalances = (): Output => { @@ -35,10 +35,10 @@ const useBalances = (): Output => { } const balance = await sdk.contracts.helpers.multicallContract.getEthBalance(address) - const isChanged = balancesRef.current.nativeTokenBalance !== balance + const isChanged = balancesRef.current.nativeToken !== balance if (isChanged) { - actions.account.balances.setNativeTokenBalance(balance) + actions.account.balances.setNativeToken(balance) } }, [ address, sdk, actions ]) @@ -58,10 +58,10 @@ const useBalances = (): Output => { balance = await depositTokenContract.balanceOf(address) } - const isChanged = balancesRef.current.depositTokenBalance !== balance + const isChanged = balancesRef.current.depositToken !== balance if (isChanged) { - actions.account.balances.setDepositTokenBalance(balance) + actions.account.balances.setDepositToken(balance) } }, [ address, sdk, actions, isStakeNativeToken ]) @@ -71,10 +71,10 @@ const useBalances = (): Output => { } const balance = await sdk.contracts.tokens.mintToken.balanceOf(address) - const isChanged = balancesRef.current.mintTokenBalance !== balance + const isChanged = balancesRef.current.mintToken !== balance if (isChanged) { - actions.account.balances.setMintTokenBalance(balance) + actions.account.balances.setMintToken(balance) } }, [ address, sdk, actions ]) diff --git a/src/hooks/data/useClaimsTotal.ts b/src/hooks/data/useClaimsTotal.ts index 57a5ef67..62bd98c3 100644 --- a/src/hooks/data/useClaimsTotal.ts +++ b/src/hooks/data/useClaimsTotal.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import { useConfig } from 'config' import { formatEther } from 'ethers' diff --git a/src/hooks/data/useFiatValues.ts b/src/hooks/data/useFiatValues.ts index cba5d3de..8aec3832 100644 --- a/src/hooks/data/useFiatValues.ts +++ b/src/hooks/data/useFiatValues.ts @@ -1,8 +1,9 @@ -import methods from 'helpers/methods' -import { useSelector } from 'react-redux' +import { methods } from 'helpers' import { useMemo, useCallback } from 'react' import { createSelector } from '@reduxjs/toolkit' +import useStore from '../data/useStore' + type Input = Record(values: Input): Output => { - const { fiatRates, swapTokenRates, currency, currencySymbol, isFetching } = useSelector(storeSelector) + const { fiatRates, swapTokenRates, currency, currencySymbol, isFetching } = useStore(storeSelector) const getFiatValue = useCallback((params: Input[T]) => { const { token, value, isMinimal } = params diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 45daf8d6..53999376 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,8 @@ +// Actions export { default as useCopyToClipboard } from './actions/useCopyToClipboard' export { default as useAddTokenToWallet } from './actions/useAddTokenToWallet' +// Controls export { default as useDeepMemo } from './controls/useDeepMemo' export { default as useTabButton } from './controls/useTabButton' export { default as useAutoFetch } from './controls/useAutoFetch' @@ -16,6 +18,7 @@ export { default as useAddressChanged } from './controls/useAddressChanged' export { default as useActiveBrowserTab } from './controls/useActiveBrowserTab' export { default as useIsomorphicLayoutEffect } from './controls/useIsomorphicLayoutEffect' +// Data export { default as useStore } from './data/useStore' export { default as useActions } from './data/useActions' export { default as useBalances } from './data/useBalances' @@ -24,16 +27,8 @@ export { default as useFiatValues } from './data/useFiatValues' export { default as useClaimsTotal } from './data/useClaimsTotal' export { default as useSwapTokenBalances } from './data/useSwapTokenBalances' +// Fetch export { default as useApprove } from './fetch/useApprove' export { default as useAllowance } from './fetch/useAllowance' export { default as useTransaction } from './fetch/useTransaction' export { default as useSubgraphUpdate } from './fetch/useSubgraphUpdate' - -export { default as useSwap } from './stake/useSwap' -export { default as useSwapQuote } from './stake/useSwapQuote' -export { default as useSwapTokens } from './stake/useSwapTokens' -export { default as useStakeField } from './stake/useStakeField' -export { default as useStakeSubmit } from './stake/useStakeSubmit' -export { default as useStakeApprove } from './stake/useStakeApprove' -export { default as useStakeApproveGas } from './stake/useStakeApproveGas' -export { default as useApproveRequired } from './stake/useApproveRequired' diff --git a/src/hooks/stake/useApproveRequired.ts b/src/hooks/stake/useApproveRequired.ts deleted file mode 100644 index b989c37a..00000000 --- a/src/hooks/stake/useApproveRequired.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import useFieldListener from '../controls/useFieldListener' - - -type Input = { - amountField: Forms.Field - allowance: bigint - skip?: boolean -} - -const useApproveRequired = ({ amountField, allowance, skip }: Input) => { - const [ isApproveRequired, setApproveRequired ] = useState((amountField.value || 0n) > allowance) - - const handleChange = useCallback((amountField: Forms.Field) => { - const amount = amountField.value || 0n - - setApproveRequired(amount > allowance) - }, [ allowance, setApproveRequired ]) - - useFieldListener(amountField, handleChange) - - useEffect(() => { - handleChange(amountField) - }, [ amountField, handleChange ]) - - return !skip && isApproveRequired -} - - -export default useApproveRequired diff --git a/src/hooks/stake/useStakeApprove.ts b/src/hooks/stake/useStakeApprove.ts deleted file mode 100644 index 2704132d..00000000 --- a/src/hooks/stake/useStakeApprove.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { StakeStep } from 'helpers/enums' -import type { SetTransaction } from '../../components/Transactions/types' -import Transactions from '../../components/Transactions/Transactions' - -import useApprove from '../fetch/useApprove' - -import useApproveRequired from './useApproveRequired' - - -type SubmitInput = { - setTransaction: SetTransaction -} - -type Input = { - step: StakeStep - field: Forms.Field - tokenAddress: string - recipient: string - skip?: boolean -} - -const useStakeApprove = ({ step, field, tokenAddress, recipient, skip }: Input) => { - const { allowance, isFetching, getGas, approve, checkAllowance } = useApprove({ - tokenAddress, - recipient, - skip, - }) - - const isApproveRequired = useApproveRequired({ - amountField: field, - allowance, - skip, - }) - - const handleApprove = useCallback(async (values: SubmitInput) => { - const { setTransaction } = values - - try { - setTransaction(step, Transactions.Status.Confirm) - - const hash = await approve() - - setTransaction(step, Transactions.Status.Processing) - - await checkAllowance({ hash, allowance }) - - setTransaction(step, Transactions.Status.Success) - } - catch (error) { - const failedSteps = step === StakeStep.SwapApprove - ? [ StakeStep.SwapApprove, StakeStep.Swap, StakeStep.Approve, StakeStep.Stake ] - : [ StakeStep.Approve, StakeStep.Stake ] - - failedSteps.forEach((step) => { - setTransaction(step, Transactions.Status.Fail) - }) - - return Promise.reject(error) - } - }, [ step, allowance, approve, checkAllowance ]) - - return useMemo(() => ({ - isFetching, - isRequired: isApproveRequired, - approve: handleApprove, - getGas, - }), [ - isApproveRequired, - isFetching, - getGas, - handleApprove, - ]) -} - - -export default useStakeApprove diff --git a/src/hooks/stake/useStakeApproveGas.ts b/src/hooks/stake/useStakeApproveGas.ts deleted file mode 100644 index 08f3adf0..00000000 --- a/src/hooks/stake/useStakeApproveGas.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useConfig } from 'config' -import addresses from 'helpers/contracts/addresses' -import { StakeStep } from 'helpers/enums' - -import useStakeApprove from './useStakeApprove' - - -type Input = { - field: Forms.Field - swapToken: SwapToken - vaultAddress: string | null -} - -const useStakeApproveGas = (values: Input) => { - const { field, swapToken, vaultAddress } = values - - const { sdk, chainId, address, isGnosis } = useConfig() - const [ approveGas, setApproveGas ] = useState(0n) - - const swapApprove = useStakeApprove({ - field, - step: StakeStep.SwapApprove, - recipient: addresses[chainId].cow.vaultRelayer, - tokenAddress: swapToken.address, - skip: !swapToken.address, - }) - - const stakeApprove = useStakeApprove({ - field, - step: StakeStep.Approve, - recipient: vaultAddress as string, - tokenAddress: sdk.config.addresses.tokens.depositToken, - skip: !isGnosis || !vaultAddress, - }) - - const getApproveGas = useCallback(async () => { - let approveGas = 0n - - if (address) { - const [ swapApproveGas, stakeApproveGas ] = await Promise.all([ - swapApprove.isRequired ? swapApprove.getGas() : Promise.resolve(0n), - stakeApprove.isRequired ? stakeApprove.getGas() : Promise.resolve(0n), - ]) - - approveGas = swapApproveGas + stakeApproveGas - } - - setApproveGas(approveGas) - }, [ address, swapApprove, stakeApprove ]) - - useEffect(() => { - getApproveGas() - }, [ getApproveGas ]) - - return useMemo(() => ({ - approveGas, - swapApprove, - stakeApprove, - }), [ - approveGas, - swapApprove, - stakeApprove, - ]) -} - - -export default useStakeApproveGas diff --git a/src/hooks/stake/useStakeSubmit.ts b/src/hooks/stake/useStakeSubmit.ts deleted file mode 100644 index 42bc1796..00000000 --- a/src/hooks/stake/useStakeSubmit.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { useConfig } from 'config' -import { StakeStep } from 'helpers/enums' -import notifications from 'modules/notifications' -import { commonMessages, getters } from 'helpers' - -import type { SetTransaction, StepsData } from '../../components/Transactions/types' -import Transactions from '../../components/Transactions/Transactions' - -import useStore from '../data/useStore' -import useActions from '../data/useActions' -import useBalances from '../data/useBalances' -import useSubgraphUpdate from '../fetch/useSubgraphUpdate' - -import useSwap from './useSwap' -import useStakeApproveGas from './useStakeApproveGas' - - -const storeSelector = (store: Store) => ({ - vaultAddress: store.vault.base.data.vaultAddress, -}) - -type SubmitInput = { - closeModal: () => void -} - -type OnSuccessInput = { - assets?: bigint - shares?: bigint - hash: string -} - -type StakeInput = SubmitInput & { - assets: bigint - setTransaction: SetTransaction -} - -type OnStartInput = SubmitInput & { - assets: bigint - setTransaction?: SetTransaction -} - -type Input = { - field: Forms.Field - swapToken: SwapToken - stakeStep: StakeStep.Stake - onSwap?: (buyAmount: bigint) => void - onSuccess?: (values: OnSuccessInput) => void - openTransactionsFlowModal: (props: { - flow: 'stake' - stepsData?: StepsData - onStart: (values: { setTransaction: SetTransaction }) => Promise - }) => void -} - -const useStakeSubmit = ({ field, swapToken, stakeStep, onSwap, onSuccess, openTransactionsFlowModal }: Input) => { - const actions = useActions() - const { swap, cancelSwap } = useSwap() - const { vaultAddress } = useStore(storeSelector) - const [ isSubmitting, setSubmitting ] = useState(false) - const { sdk, signSDK, address, chainId, cancelOnChange } = useConfig() - - const subgraphUpdate = useSubgraphUpdate() - const { refetchNativeTokenBalance, refetchDepositTokenBalance } = useBalances() - const { approveGas, swapApprove, stakeApprove } = useStakeApproveGas({ - field, - swapToken, - vaultAddress, - }) - - const handleSuccess = useCallback((values: OnSuccessInput) => { - field.reset() - - cancelOnChange({ - address, - chainId, - logic: () => { - refetchNativeTokenBalance() - refetchDepositTokenBalance() - - if (typeof onSuccess === 'function') { - onSuccess(values) - } - }, - }) - }, [ - field, - chainId, - address, - onSuccess, - cancelOnChange, - refetchNativeTokenBalance, - refetchDepositTokenBalance, - ]) - - const stake = useCallback(async (values: StakeInput) => { - const { assets, closeModal, setTransaction } = values - - try { - setTransaction(StakeStep.Stake, Transactions.Status.Confirm) - - const referrerAddress = getters.getReferrer() - - const hash = await signSDK.vault.deposit({ - userAddress: address as string, - assets, - vaultAddress, - referrerAddress, - }) - - setTransaction(StakeStep.Stake, Transactions.Status.Processing) - - if (hash) { - await subgraphUpdate({ hash }) - - setTransaction(StakeStep.Stake, Transactions.Status.Success) - - closeModal() - - handleSuccess({ hash, assets }) - } - else { - setTransaction(StakeStep.Stake, Transactions.Status.Fail) - - return Promise.reject('TxHash is not defined') - } - } - catch (error) { - setTransaction(StakeStep.Stake, Transactions.Status.Fail) - - return Promise.reject(error) - } - }, [ signSDK, address, vaultAddress, subgraphUpdate, handleSuccess ]) - - const stepsData = useMemo(() => { - const result: StepsData = [] - - if (swapApprove.isRequired) { - result.push({ - id: StakeStep.SwapApprove, - title: { - ...commonMessages.buttonTitle.approve, - values: { - token: swapToken.name, - }, - }, - }) - } - - if (swapToken.address) { - result.push({ - id: StakeStep.Swap, - onCancel: cancelSwap, - }) - } - - if (stakeApprove.isRequired) { - result.push({ - id: StakeStep.Approve, - title: { - ...commonMessages.buttonTitle.approve, - values: { - token: sdk.config.tokens.depositToken, - }, - }, - }) - } - - result.push({ id: stakeStep }) - - return result - }, [ sdk, stakeStep, swapToken, swapApprove, stakeApprove ]) - - const onStart = useCallback(async (values: OnStartInput) => { - const { assets, closeModal, setTransaction = () => {} } = values - - setSubmitting(true) - - try { - let stakeAssets = assets - - for (let i = 0; i < stepsData.length; i += 1) { - const step = stepsData[i] - - if (step.id === StakeStep.SwapApprove) { - await swapApprove.approve({ setTransaction }) - } - - if (step.id === StakeStep.Swap) { - const buyAmount = await swap({ - amount: assets, - fromToken: swapToken.address, - setTransaction, - }) - - if (buyAmount) { - stakeAssets = buyAmount - - if (typeof onSwap === 'function') { - onSwap(buyAmount) - } - } - } - - if (step.id === StakeStep.Approve) { - await stakeApprove.approve({ setTransaction }) - } - - if (step.id === StakeStep.Stake) { - await stake({ assets: stakeAssets, closeModal, setTransaction }) - } - } - } - catch (error) { - actions.ui.resetBottomLoader() - console.error('Deposit send transaction error', error as Error) - - notifications.open({ - type: 'error', - text: commonMessages.notification.failed, - }) - } - finally { - setSubmitting(false) - } - }, [ actions, swapToken, stepsData, swapApprove, stakeApprove, swap, stake, onSwap ]) - - const submit = useCallback((values?: SubmitInput) => { - const { closeModal = () => {} } = values || {} - - const assets = field.value - - if (!address || !assets || !vaultAddress) { - return - } - - if (stepsData.length > 1) { - openTransactionsFlowModal({ - flow: 'stake', - stepsData, - onStart: ({ setTransaction }) => onStart({ assets, closeModal, setTransaction }), - }) - } - else { - onStart({ assets, closeModal }) - } - }, [ field, address, vaultAddress, stepsData, onStart, openTransactionsFlowModal ]) - - const isAllowanceFetching = swapApprove.isFetching || stakeApprove.isFetching - - return useMemo(() => ({ - approveGas, - isSubmitting, - isAllowanceFetching, - submit, - }), [ - approveGas, - isSubmitting, - isAllowanceFetching, - submit, - ]) -} - - -export default useStakeSubmit diff --git a/src/hooks/stake/useSwapQuote.ts b/src/hooks/stake/useSwapQuote.ts deleted file mode 100644 index 2f2cd9cf..00000000 --- a/src/hooks/stake/useSwapQuote.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useCallback, useEffect, useMemo } from 'react' -import { BigDecimal } from 'sdk' -import useObjectState from '../controls/useObjectState' - -import useSwap from './useSwap' - - -type State = { - fee: bigint - buyAmount: bigint - isFetching: boolean -} - -type Input = { - amount: bigint - fromToken: string -} - -const initialState = { - fee: 0n, - buyAmount: 0n, - isFetching: false, -} - -const useSwapQuote = ({ amount, fromToken }: Input) => { - const skip = !fromToken || !amount - - const [ state, setState ] = useObjectState({ - ...initialState, - isFetching: !skip, - }) - - const { fetchQuote } = useSwap() - - const getBuyAmount = useCallback((value: bigint) => { - if (amount && state.buyAmount) { - const amountPercent = new BigDecimal(amount).divide(100) - const percent = new BigDecimal(value).divide(amountPercent) - const result = new BigDecimal(state.buyAmount).divide(100).multiply(percent).decimals(0).toNumber() - - return BigInt(result) - } - - return 0n - }, [ amount, state.buyAmount ]) - - const handleFetchQuote = useCallback(async ({ amount, fromToken }: Input) => { - setState({ ...initialState, isFetching: true }) - - let fee = '0' - let buyAmount = '0' - - try { - const quote = await fetchQuote({ - amount, - fromToken, - }) - - fee = quote.feeAmount - buyAmount = quote.buyAmount - } - catch (error: any) { - if (error?.feeAmount) { - fee = error.feeAmount as string - } - } - - setState({ - fee: BigInt(fee), - buyAmount: BigInt(buyAmount), - isFetching: false, - }) - }, [ fetchQuote, setState ]) - - useEffect(() => { - if (skip) { - setState(initialState) - } - else { - handleFetchQuote({ - amount: amount as bigint, - fromToken: fromToken, - }) - } - }, [ skip, amount, fromToken, handleFetchQuote, setState ]) - - return useMemo(() => ({ - ...state, - getBuyAmount, - }), [ - state, - getBuyAmount, - ]) -} - - -export default useSwapQuote diff --git a/src/layouts/AppLayout/AppLayout.tsx b/src/layouts/AppLayout/AppLayout.tsx index 11c5f01e..73fb4d7d 100644 --- a/src/layouts/AppLayout/AppLayout.tsx +++ b/src/layouts/AppLayout/AppLayout.tsx @@ -7,14 +7,14 @@ import { useViewportHeight, useImagesPrefetch } from 'hooks' import { imagesUrls } from 'components' import Header from './Header/Header' +import CommonModals from './CommonModals/CommonModals' + import { useAccount, useFiatRates, - useVaultData, useQueryParams, useMintTokenData, - useAutoFetchBalances, } from './util' import s from './AppLayout.module.scss' @@ -31,11 +31,9 @@ const Notifications = dynamic(() => import('./Notifications/Notifications'), { const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { useAccount() useFiatRates() - useVaultData() useQueryParams() useMintTokenData() useViewportHeight() - useAutoFetchBalances() useImagesPrefetch(imagesUrls) return ( @@ -46,6 +44,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
{children}
+
diff --git a/src/layouts/AppLayout/CommonModals/CommonModals.tsx b/src/layouts/AppLayout/CommonModals/CommonModals.tsx new file mode 100644 index 00000000..50b8e42c --- /dev/null +++ b/src/layouts/AppLayout/CommonModals/CommonModals.tsx @@ -0,0 +1,27 @@ +import React from 'react' + +import { + GuideModal, + TxCompletedModal, + SwitchAccountModal, + ConnectWalletModal, + ExportRewardsModal, + TransactionsFlowModal, + DistributorClaimsModal, +} from 'layouts/modals' + + +const CommonModals: React.FC = () => ( + <> + + + + + + + + +) + + +export default React.memo(CommonModals) diff --git a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/AccountMenu.tsx b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/AccountMenu.tsx index 7e9d47cb..7e211f64 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/AccountMenu.tsx +++ b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/AccountMenu.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { useConfig } from 'config' -import { constants } from 'helpers' +import { useConfig, wallets } from 'config' import { useClaimsTotal } from 'hooks' import type { LogoProps } from 'components' @@ -22,7 +21,7 @@ const AccountMenu: React.FC = (props) => { const { activeWallet } = useConfig() const claimsTotal = useClaimsTotal() - const isDappBrowser = activeWallet === constants.walletNames.dAppBrowser + const isDappBrowser = activeWallet === wallets.dAppBrowser.id return ( <> diff --git a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Address/Address.tsx b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Address/Address.tsx index d50e3b6e..87f02302 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Address/Address.tsx +++ b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Address/Address.tsx @@ -1,8 +1,7 @@ import React, { useCallback } from 'react' -import { commonMessages } from 'helpers' +import { commonMessages, methods } from 'helpers' import { useCopyToClipboard } from 'hooks' import { useConfig } from 'config' -import methods from 'helpers/methods' import cx from 'classnames' import { Text, Logo, RoundButton } from 'components' diff --git a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balance/Balance.tsx b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balance/Balance.tsx index 53b69977..bcc054c4 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balance/Balance.tsx +++ b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balance/Balance.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useMemo } from 'react' -import { commonMessages, constants } from 'helpers' +import { commonMessages, constants, methods } from 'helpers' import { useAddTokenToWallet } from 'hooks' import { useConfig, wallets } from 'config' -import methods from 'helpers/methods' import cx from 'classnames' import { imagesUrls, FiatAmount, TokenAmount, Text, RoundButton, Bone } from 'components' @@ -66,7 +65,7 @@ const Balance: React.FC = (props) => { const withAddTokenButton = Boolean( isInjectedWallet && token !== sdk.config.tokens.nativeToken - && activeWallet !== constants.walletNames.monitorAddress + && activeWallet !== wallets.monitorAddress.id ) return ( diff --git a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balances.tsx b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balances.tsx index cc5511aa..1b72dddc 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balances.tsx +++ b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Balances/Balances.tsx @@ -11,9 +11,9 @@ type BalancesProps = { } const storeSelector = (store: Store) => ({ - mintTokenBalance: store.account.balances.data.mintTokenBalance, - nativeTokenBalance: store.account.balances.data.nativeTokenBalance, - depositTokenBalance: store.account.balances.data.depositTokenBalance, + mintTokenBalance: store.account.balances.mintToken, + nativeTokenBalance: store.account.balances.nativeToken, + depositTokenBalance: store.account.balances.depositToken, isFetching: store.account.balances.isFetching, }) diff --git a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Menu/Menu.tsx b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Menu/Menu.tsx index a9568060..2ceaf0c4 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Menu/Menu.tsx +++ b/src/layouts/AppLayout/Header/Connect/Account/AccountMenu/Menu/Menu.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react' import cx from 'classnames' -import { useConfig } from 'config' -import { constants } from 'helpers' +import { useConfig, wallets } from 'config' import MenuItem from './MenuItem/MenuItem' import { openSwitchAccountModal } from 'layouts/modals' @@ -19,7 +18,7 @@ const Menu: React.FC = (props) => { const { activeWallet } = useConfig() const items = useMemo(() => { - const isLedger = activeWallet === constants.walletNames.ledger + const isLedger = activeWallet === wallets.ledger.id if (isLedger) { return [ diff --git a/src/layouts/AppLayout/Header/Connect/Account/util/useAccount.ts b/src/layouts/AppLayout/Header/Connect/Account/util/useAccount.ts index a1bbb60d..1197c467 100644 --- a/src/layouts/AppLayout/Header/Connect/Account/util/useAccount.ts +++ b/src/layouts/AppLayout/Header/Connect/Account/util/useAccount.ts @@ -1,30 +1,20 @@ import { useMemo } from 'react' +import { methods } from 'helpers' import device from 'modules/device' -import { constants } from 'helpers' -import { useConfig } from 'config' -import { useStore } from 'hooks' -import methods from 'helpers/methods' +import { useConfig, wallets } from 'config' import type { LogoProps } from 'components' -const storeSelector = (store: Store) => ({ - isMMI: store.account.wallet.isMMI, -}) - const useAccount = () => { const { isMobile } = device.useData() - const { isMMI } = useStore(storeSelector) const { address, accountName, activeWallet } = useConfig() const addressOption = accountName || methods.shortenAddress(address) - const logoFromWalletList = constants.walletList.find(({ id }) => id === activeWallet)?.logo - - let logo: LogoProps['name'] = logoFromWalletList || 'connector/monitorAddress' - if (isMMI && activeWallet === constants.walletNames.metaMask) { - logo = 'connector/MMI' - } + const logo: LogoProps['name'] = activeWallet + ? wallets[activeWallet].logo + : 'connector/monitorAddress' return useMemo(() => ({ logo, diff --git a/src/layouts/AppLayout/Header/Connect/NetworkSelect/util/useChangeChainDisabled.ts b/src/layouts/AppLayout/Header/Connect/NetworkSelect/util/useChangeChainDisabled.ts index 8feeded6..76cdad4f 100644 --- a/src/layouts/AppLayout/Header/Connect/NetworkSelect/util/useChangeChainDisabled.ts +++ b/src/layouts/AppLayout/Header/Connect/NetworkSelect/util/useChangeChainDisabled.ts @@ -1,20 +1,19 @@ -import { useConfig } from 'config' -import { constants } from 'helpers' +import { useConfig, wallets } from 'config' const useChangeChainDisabled = () => { const { activeWallet } = useConfig() const isSupportedDAppBrowser = ( - activeWallet === constants.walletNames.dAppBrowser + activeWallet === wallets.dAppBrowser.id && typeof window !== 'undefined' && Boolean(window.ethereum?.isMetaMask) ) const disabledChainSwitchWallets: WalletIds[] = [ - constants.walletNames.dAppBrowser, - constants.walletNames.gnosisSafe, - constants.walletNames.zenGo, + wallets.dAppBrowser.id, + wallets.gnosisSafe.id, + wallets.zenGo.id, ] return ( diff --git a/src/layouts/AppLayout/util/_SSR/index.ts b/src/layouts/AppLayout/util/_SSR/index.ts deleted file mode 100644 index e2561030..00000000 --- a/src/layouts/AppLayout/util/_SSR/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as fetchSwapTokenRates } from './fetchSwapTokenRates' diff --git a/src/layouts/AppLayout/util/index.ts b/src/layouts/AppLayout/util/index.ts index 9c1ff255..3cd912f5 100644 --- a/src/layouts/AppLayout/util/index.ts +++ b/src/layouts/AppLayout/util/index.ts @@ -1,6 +1,4 @@ export { default as useAccount } from './useAccount' export { default as useFiatRates } from './useFiatRates' -export { default as useVaultData } from './useVaultData' export { default as useQueryParams } from './useQueryParams' export { default as useMintTokenData } from './useMintTokenData' -export { default as useAutoFetchBalances } from './useAutoFetchBalances' diff --git a/src/layouts/AppLayout/util/useAccount.ts b/src/layouts/AppLayout/util/useAccount/index.ts similarity index 93% rename from src/layouts/AppLayout/util/useAccount.ts rename to src/layouts/AppLayout/util/useAccount/index.ts index 6d7c4987..9845eaa7 100644 --- a/src/layouts/AppLayout/util/useAccount.ts +++ b/src/layouts/AppLayout/util/useAccount/index.ts @@ -4,15 +4,20 @@ import { useActions } from 'hooks' import { requests } from 'helpers' import notifications from 'modules/notifications' +import useAutoFetchBalances from './useAutoFetchBalances' + import messages from './messages' const useAccount = () => { + useAutoFetchBalances() + const actions = useActions() const { sdk, address } = useConfig() const addressRef = useRef(null) + const fetchDistributorClaims = useCallback(async (address: string) => { try { actions.account.distributorClaims.resetData() diff --git a/src/layouts/AppLayout/util/useAccount/messages.ts b/src/layouts/AppLayout/util/useAccount/messages.ts new file mode 100644 index 00000000..52e031cf --- /dev/null +++ b/src/layouts/AppLayout/util/useAccount/messages.ts @@ -0,0 +1,11 @@ +export default { + error: { + en: 'Failed to fetch your account data. Please try later.', + ru: 'Не удалось получить данные вашего аккаунта. Пожалуйста, попробуйте позже.', + fr: 'Échec de la récupération des données de votre compte. Veuillez réessayer plus tard.', + es: 'No se pudo obtener los datos de su cuenta. Por favor, inténtelo más tarde.', + pt: 'Falha ao buscar os dados da sua conta. Por favor, tente novamente mais tarde.', + de: 'Fehler beim Abrufen Ihrer Kontodaten. Bitte versuchen Sie es später erneut.', + zh: '未能获取您的账户数据。请稍后再试。', + }, +} diff --git a/src/layouts/AppLayout/util/useAutoFetchBalances.ts b/src/layouts/AppLayout/util/useAccount/useAutoFetchBalances.ts similarity index 90% rename from src/layouts/AppLayout/util/useAutoFetchBalances.ts rename to src/layouts/AppLayout/util/useAccount/useAutoFetchBalances.ts index b7892461..6f456fb5 100644 --- a/src/layouts/AppLayout/util/useAutoFetchBalances.ts +++ b/src/layouts/AppLayout/util/useAccount/useAutoFetchBalances.ts @@ -13,7 +13,6 @@ const useAutoFetchBalances = () => { const { refetchDepositTokenBalance, refetchNativeTokenBalance, - refetchSwapTokenBalances, refetchMintTokenBalance, } = useBalances() @@ -25,7 +24,6 @@ const useAutoFetchBalances = () => { await Promise.all([ refetchDepositTokenBalance(), refetchNativeTokenBalance(), - refetchSwapTokenBalances(), refetchMintTokenBalance(), ]) } @@ -37,14 +35,15 @@ const useAutoFetchBalances = () => { networkId, refetchDepositTokenBalance, refetchNativeTokenBalance, - refetchSwapTokenBalances, refetchMintTokenBalance, ]) const fetchBalances = useCallback(async () => { actions.account.balances.setFetching(true) + actions.account.swapTokenBalances.setFetching(true) await handleFetchBalances() actions.account.balances.setFetching(false) + actions.account.swapTokenBalances.setFetching(false) }, [ actions, handleFetchBalances ]) useEffect(() => { @@ -59,6 +58,7 @@ const useAutoFetchBalances = () => { } else if (autoConnectChecked) { actions.account.balances.setFetching(false) + actions.account.swapTokenBalances.setFetching(false) } }, [ actions, address, autoConnectChecked, fetchBalances, handleFetchBalances ]) } diff --git a/src/layouts/AppLayout/util/useFiatRates.ts b/src/layouts/AppLayout/util/useFiatRates.ts index e8f08ff3..aeb39366 100644 --- a/src/layouts/AppLayout/util/useFiatRates.ts +++ b/src/layouts/AppLayout/util/useFiatRates.ts @@ -1,10 +1,7 @@ import { useCallback } from 'react' import { useActions, useStore, useAutoFetch, useChainChanged } from 'hooks' -import { swapTokens } from 'helpers' import { useConfig } from 'config' -import methods from 'helpers/methods' - -import { fetchSwapTokenRates } from './_SSR' +import { methods } from 'helpers' const storeSelector = (store: Store) => ({ @@ -12,20 +9,18 @@ const storeSelector = (store: Store) => ({ }) const useFiatRates = () => { - const { sdk, chainId } = useConfig() + const { sdk } = useConfig() const actions = useActions() const { mintTokenRate } = useStore(storeSelector) - const chainTokens = swapTokens[chainId as keyof typeof swapTokens] - const handleFetchFiatPrices = useCallback(async () => { if (!mintTokenRate) { return } try { - const fiatRates = await methods.fetchFiatRates(chainId) + const fiatRates = await methods.fetchFiatRates(sdk.config.network.chainId) if (fiatRates) { actions.fiatRates.setData(fiatRates) @@ -34,55 +29,15 @@ const useFiatRates = () => { catch (error: any) { console.error('Fetch fiat rates error', error) } - }, [ chainId, actions, mintTokenRate ]) - - const handleFetchSwapTokenRates = useCallback(async () => { - if (!chainTokens) { - return - } - - try { - const [ swapTokenRates, rates ] = await Promise.all([ - fetchSwapTokenRates(chainId), - sdk.utils.getFiatRates(), - ]) - - const setValues = methods.createSetValues({ - EUR: rates['USD/EUR'], - GBP: rates['USD/GBP'], - CNY: rates['USD/CNY'], - JPY: rates['USD/JPY'], - KRW: rates['USD/KRW'], - AUD: rates['USD/AUD'], - }) - - const swapTokenData = Object.keys(swapTokenRates).reduce((acc, key) => { - acc[key] = setValues(swapTokenRates[key]) - - return acc - }, {} as Record>) - - actions.swapTokenRates.setData(swapTokenData) - } - catch (error: any) { - console.error('Fetch swap token rates error', error) - } - }, [ sdk, actions, chainId, chainTokens ]) + }, [ sdk, actions, mintTokenRate ]) useChainChanged(handleFetchFiatPrices) - useChainChanged(handleFetchSwapTokenRates) useAutoFetch({ action: handleFetchFiatPrices, interval: 15 * 60 * 1000, skip: !Number(mintTokenRate), }) - - useAutoFetch({ - action: handleFetchSwapTokenRates, - interval: 15 * 60 * 1000, - skip: !chainTokens, - }) } diff --git a/src/layouts/AppLayout/util/useMintTokenData.ts b/src/layouts/AppLayout/util/useMintTokenData.ts index 6397ab96..4b5fd294 100644 --- a/src/layouts/AppLayout/util/useMintTokenData.ts +++ b/src/layouts/AppLayout/util/useMintTokenData.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react' import { useActions, useAutoFetch, useMintToken, useChainChanged } from 'hooks' import { useConfig } from 'config' -import methods from 'helpers/methods' +import { methods } from 'helpers' type ExitStatsQueryPayload = { diff --git a/src/layouts/AppLayout/util/useVaultData.ts b/src/layouts/AppLayout/util/useVaultData.ts deleted file mode 100644 index 76d30ca8..00000000 --- a/src/layouts/AppLayout/util/useVaultData.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react' -import { useActions, useAutoFetch, useStore } from 'hooks' -import { useConfig } from 'config' -import { getters } from 'helpers' - - -const storeSelector = (store: Store) => ({ - isSSR: store.vault.base.isSSR, -}) - -const useVaultData = () => { - const actions = useActions() - const { sdk, networkId } = useConfig() - const { isSSR } = useStore(storeSelector) - - const fetchedNetworkRef = useRef(isSSR ? networkId : null) - const vaultAddress = getters.getVaultAddress(networkId) - - const fetchVaultData = useCallback(async () => { - if (fetchedNetworkRef.current !== networkId) { - const vault = await sdk.vault.getVault({ vaultAddress }) - const versions = await sdk.getVaultVersion(vaultAddress) - - actions.vault.base.setData({ - ...vault, - versions, - }) - } - - fetchedNetworkRef.current = networkId - }, [ sdk, actions, vaultAddress, networkId ]) - - useEffect(() => { - fetchVaultData() - }, [ fetchVaultData ]) - - useAutoFetch({ - action: fetchVaultData, - interval: 15 * 60 * 1000, - }) -} - - -export default useVaultData diff --git a/src/layouts/modals/ConnectWalletModal/ConnectorsView/ConnectorsView.tsx b/src/layouts/modals/ConnectWalletModal/ConnectorsView/ConnectorsView.tsx index 54ada41c..6cd8329f 100644 --- a/src/layouts/modals/ConnectWalletModal/ConnectorsView/ConnectorsView.tsx +++ b/src/layouts/modals/ConnectWalletModal/ConnectorsView/ConnectorsView.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useMemo, useState } from 'react' import { usePathname } from 'next/navigation' +import { methods } from 'helpers' +import { wallets } from 'config/core' import device from 'modules/device' -import { constants } from 'helpers' -import methods from 'helpers/methods' -import { useStore } from 'hooks' +import { useConfig } from 'config' import cx from 'classnames' import { LogoProps } from 'components' @@ -16,41 +16,19 @@ type ConnectorsViewProps = { onSelect: (walletId: WalletIds) => void } -const storeSelector = (store: Store) => ({ - isMMI: store.account.wallet.isMMI, -}) - -const desktopWallets = constants.walletList.filter(({ id }) => ( - id !== constants.walletNames.dAppBrowser - && id !== constants.walletNames.gnosisSafe -)) - -const mobileWallets = constants.walletList.filter(({ id }) => { - const list = [ - constants.walletNames.metaMask, - constants.walletNames.coinbase, - constants.walletNames.walletConnect, - constants.walletNames.monitorAddress, - ] as string[] - - return list.includes(id) -}) - -const setIsDisabled = (id: WalletIds): boolean => { - const provider = methods.getInjectedProvider(id) - - return provider === null ? false : !provider -} +const walletsArr = Object.values(wallets) +const mobileWallets = walletsArr.filter(({ location }) => location.includes('mobile')) +const desktopWallets = walletsArr.filter(({ location }) => location.includes('desktop')) const ConnectorsView: React.FC = (props) => { const { className, onSelect } = props + const { chainId } = useConfig() const pathname = usePathname() const { isDesktop } = device.useData() - const { isMMI } = useStore(storeSelector) const setDeepLink = useCallback((id: WalletIds) => { - if (id === constants.walletNames.metaMask && !isDesktop) { + if (id === wallets.metaMask.id && !isDesktop) { const hostname = methods.getHostName() return `https://metamask.app.link/dapp/${hostname}${pathname}` @@ -58,33 +36,31 @@ const ConnectorsView: React.FC = (props) => { }, [ isDesktop, pathname ]) const walletsList = useMemo(() => { - const wallets = isDesktop ? desktopWallets : mobileWallets + const walletsItems = isDesktop ? desktopWallets : mobileWallets - const list = wallets + const list = walletsItems + .filter((wallet) => wallet.networks.includes(chainId)) .map((wallet) => { - let title: Intl.Message | string = wallet.title - let logo: LogoProps['name'] = wallet.logo - - if (wallet.id === constants.walletNames.metaMask && isMMI) { - logo = 'connector/MMI' - title = 'MMI' - } + const title: Intl.Message | string = wallet.title + const logo: LogoProps['name'] = wallet.logo return { ...wallet, logo, title, deepLink: setDeepLink(wallet.id), - isDisabled: setIsDisabled(wallet.id), + isDisabled: wallet.isInjectedWallet + ? wallet.isDisabled(isDesktop) + : false, } }) if (!process.env.NEXT_PUBLIC_WALLET_CONNECT_ID) { - return list.filter(({ id }) => id !== constants.walletNames.walletConnect) + return list.filter(({ id }) => id !== wallets.walletConnect.id) } return list - }, [ isDesktop, isMMI, setDeepLink ]) + }, [ isDesktop, chainId, setDeepLink ]) const [ selectedId, setSelectedId ] = useState(null) diff --git a/src/layouts/modals/ConnectWalletModal/MonitorAddressView/MonitorAddressView.tsx b/src/layouts/modals/ConnectWalletModal/MonitorAddressView/MonitorAddressView.tsx index efd6daee..ef1b39ea 100644 --- a/src/layouts/modals/ConnectWalletModal/MonitorAddressView/MonitorAddressView.tsx +++ b/src/layouts/modals/ConnectWalletModal/MonitorAddressView/MonitorAddressView.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react' import cx from 'classnames' -import methods from 'helpers/methods' +import { methods } from 'helpers' import { useConfig } from 'config' import { isAddress } from 'ethers' import forms from 'modules/forms' diff --git a/src/layouts/modals/DistributorClaimsModal/DistributorClaimsModal.tsx b/src/layouts/modals/DistributorClaimsModal/DistributorClaimsModal.tsx index 9b08b81f..73810a7f 100644 --- a/src/layouts/modals/DistributorClaimsModal/DistributorClaimsModal.tsx +++ b/src/layouts/modals/DistributorClaimsModal/DistributorClaimsModal.tsx @@ -2,13 +2,12 @@ import React, { useCallback, useMemo, useState } from 'react' import cx from 'classnames' import intl from 'modules/intl' import modal from 'modules/modal' -import methods from 'helpers/methods' import { useConfig } from 'config' import { formatEther } from 'ethers' +import notifications from 'modules/notifications' import { createContracts } from 'helpers/contracts' -import { commonMessages, requests } from 'helpers' +import { commonMessages, requests, methods } from 'helpers' import { useStore, useActions, useClaimsTotal, useSubgraphUpdate } from 'hooks' -import notifications from 'modules/notifications' import { Button, FiatAmount, Modal, Text, TokenAmount } from 'components' diff --git a/src/layouts/modals/ExportRewardsModal/ExportRewardsModal.tsx b/src/layouts/modals/ExportRewardsModal/ExportRewardsModal.tsx index 2eaca6eb..6e10d3a0 100644 --- a/src/layouts/modals/ExportRewardsModal/ExportRewardsModal.tsx +++ b/src/layouts/modals/ExportRewardsModal/ExportRewardsModal.tsx @@ -1,7 +1,7 @@ import React from 'react' import { commonMessages } from 'helpers' -import device from 'modules/device' import { useModalClose } from 'hooks' +import device from 'modules/device' import modal from 'modules/modal' import { Input, Modal, Select, FormValid, Button } from 'components' @@ -14,12 +14,11 @@ export type ExportRewardsModalProps = Omit export const [ ExportRewardsModal, openExportRewardsModal ] = ( modal.wrapper(UNIQUE_FILE_ID, (props) => { - const { vaultAddress, statsType, closeModal } = props + const { vaultAddress, closeModal } = props const { isDesktop } = device.useData() const { form, isFetching, onSubmit } = useExport({ - statsType, vaultAddress, closeModal, }) diff --git a/src/layouts/modals/ExportRewardsModal/util/useExport.ts b/src/layouts/modals/ExportRewardsModal/util/useExport.ts index f2e90b91..099ffae7 100644 --- a/src/layouts/modals/ExportRewardsModal/util/useExport.ts +++ b/src/layouts/modals/ExportRewardsModal/util/useExport.ts @@ -5,22 +5,21 @@ import intl from 'modules/intl' import useForm from './useForm' import useXLSX from './useXLSX' import messages from './messages' -import useRewards, { StatsType } from './useRewards' +import useRewards from './useRewards' export type Input = { vaultAddress: string - statsType: StatsType closeModal: () => void } const useExport = (input: Input) => { - const { vaultAddress, statsType, closeModal } = input + const { vaultAddress, closeModal } = input const form = useForm() const intlRef = intl.useIntlRef() const getFile = useXLSX({ form, vaultAddress }) - const fetchRewards = useRewards({ form, statsType, vaultAddress }) + const fetchRewards = useRewards({ form, vaultAddress }) const [ isFetching, setFetching ] = useState(false) diff --git a/src/layouts/modals/ExportRewardsModal/util/useRewards.ts b/src/layouts/modals/ExportRewardsModal/util/useRewards.ts index 650468dc..7cc75f1d 100644 --- a/src/layouts/modals/ExportRewardsModal/util/useRewards.ts +++ b/src/layouts/modals/ExportRewardsModal/util/useRewards.ts @@ -1,16 +1,14 @@ import { useCallback } from 'react' -import { useStore } from 'hooks' +import { modifiers } from 'helpers' import { useConfig } from 'config' -import { mergeRewardsFiat, StakeWiseSDK } from 'sdk' -import date from 'modules/date' +import { StakeWiseSDK } from 'sdk' import forms from 'modules/forms' -import { modifiers, requests } from 'helpers' +import { useStore } from 'hooks' +import date from 'modules/date' import type { ExportForm } from './useForm' -export type StatsType = 'osToken' | 'allocator' - type FetcherParams = { userAddress: string dateTo: number @@ -21,7 +19,6 @@ type FetcherReturn = Awaited type Input = { vaultAddress: string - statsType: StatsType form: Forms.Form } @@ -34,45 +31,13 @@ const formatFiat = (value: number) => { } const useRewards = (input: Input) => { - const { form, statsType, vaultAddress } = input + const { form, vaultAddress } = input const { sdk, address } = useConfig() const { currency } = useStore(storeSelector) const { values: { from, to } } = forms.useFormValues(form) - const fetchAllocatorStats = useCallback((params: FetcherParams) => { - return sdk.vault.getUserRewards({ - ...params, - vaultAddress, - }) - }, [ sdk, vaultAddress ]) - - const fetchOsTokenStats = useCallback(async (params: FetcherParams) => { - const { - dateTo, - dateFrom, - userAddress, - } = params - - const data = await requests.fetchStakeStats({ - url: sdk.config.api.subgraph, - variables: { - where: { - osTokenHolder: userAddress.toLowerCase(), - timestamp_gte: String(dateFrom * 1_000), - timestamp_lte: String(dateTo * 1_000), - }, - }, - }) - - const rewards = data?.osTokenHolder || [] - - const fiatRates = await sdk.utils.getFiatRatesByDay({ dateTo, dateFrom }) - - return mergeRewardsFiat({ rewards, fiatRates }) - }, [ sdk ]) - return useCallback(async () => { if (!address || !from || !to) { return @@ -82,20 +47,16 @@ const useRewards = (input: Input) => { const fromInMs = date.time(from).utcOffset(0, true).valueOf() const toInMs = date.time(to).utcOffset(0, true).valueOf() - let data: FetcherReturn = [] - const params: FetcherParams = { userAddress: address, dateTo: toInMs, dateFrom: fromInMs, } - if (statsType === 'osToken') { - data = await fetchOsTokenStats(params) - } - else { - data = await fetchAllocatorStats(params) - } + const data: FetcherReturn = await sdk.vault.getUserRewards({ + ...params, + vaultAddress, + }) const response = data.map((values) => { const { @@ -144,7 +105,7 @@ const useRewards = (input: Input) => { catch (error: any) { console.error('Fetch user rewards fail', error) } - }, [ address, currency, from, to, statsType, fetchAllocatorStats, fetchOsTokenStats ]) + }, [ sdk, address, vaultAddress, currency, from, to ]) } diff --git a/src/layouts/modals/ExportRewardsModal/util/useXLSX.ts b/src/layouts/modals/ExportRewardsModal/util/useXLSX.ts index 4a617394..cc64fb2d 100644 --- a/src/layouts/modals/ExportRewardsModal/util/useXLSX.ts +++ b/src/layouts/modals/ExportRewardsModal/util/useXLSX.ts @@ -1,10 +1,9 @@ import { useCallback, useMemo } from 'react' -import { requests, modifiers } from 'helpers' +import { requests, modifiers, methods } from 'helpers' import forms from 'modules/forms' import intl from 'modules/intl' import { useConfig } from 'config' import { useStore } from 'hooks' -import methods from 'helpers/methods' import type { ExportForm } from './useForm' import messages from './messages' diff --git a/src/layouts/modals/SwitchAccountModal/AccountItem/util/useAccountItem.ts b/src/layouts/modals/SwitchAccountModal/AccountItem/util/useAccountItem.ts index a71aa9a6..41add546 100644 --- a/src/layouts/modals/SwitchAccountModal/AccountItem/util/useAccountItem.ts +++ b/src/layouts/modals/SwitchAccountModal/AccountItem/util/useAccountItem.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import cacheStorage from 'modules/cache-storage' import { formatEther } from 'ethers' import { useConfig } from 'config' -import methods from 'helpers/methods' +import { methods } from 'helpers' import type { AccountItemProps } from '../AccountItem' diff --git a/src/layouts/modals/TransactionsFlowModal/TransactionsFlowModal.tsx b/src/layouts/modals/TransactionsFlowModal/TransactionsFlowModal.tsx index f1803d3d..6897ae15 100644 --- a/src/layouts/modals/TransactionsFlowModal/TransactionsFlowModal.tsx +++ b/src/layouts/modals/TransactionsFlowModal/TransactionsFlowModal.tsx @@ -1,9 +1,12 @@ import React, { useCallback, useEffect } from 'react' -import { useModalClose } from 'hooks' import modal from 'modules/modal' import { commonMessages } from 'helpers' +import useModalClose from 'hooks/controls/useModalClose' + +import TransactionsModal from '../../../components/TransactionsModal/TransactionsModal' +import type { SetTransaction, SetNextTransactionsFailed } from '../../../components/Transactions/types' +import { TransactionStatus } from '../../../components/Transactions/util' -import { SetTransaction, TransactionsModal, TransactionStatus } from 'components' import { useTransactionsFlow } from './util' import type { TransactionsFlow, StepsData } from './types' @@ -11,6 +14,7 @@ import type { TransactionsFlow, StepsData } from './types' type OnStartInput = { setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed } type Input = Modals.VisibilityProps & { @@ -23,7 +27,7 @@ export const [ TransactionsFlowModal, openTransactionsFlowModal ] = ( modal.wrapper(UNIQUE_FILE_ID, (props: Input) => { const { flow, stepsData, onStart, closeModal } = props - const { transactions, setTransaction } = useTransactionsFlow({ + const { transactions, setTransaction, setNextTransactionsFailed } = useTransactionsFlow({ flow, stepsData, }) @@ -31,10 +35,10 @@ export const [ TransactionsFlowModal, openTransactionsFlowModal ] = ( useModalClose({ closeModal }) const handleStart = useCallback(async () => { - await onStart({ setTransaction }) + await onStart({ setTransaction, setNextTransactionsFailed }) closeModal() - }, [ onStart, closeModal, setTransaction ]) + }, [ onStart, closeModal, setTransaction, setNextTransactionsFailed ]) useEffect(() => { handleStart() diff --git a/src/layouts/modals/TransactionsFlowModal/types.d.ts b/src/layouts/modals/TransactionsFlowModal/types.d.ts index e056e1eb..8bb650d3 100644 --- a/src/layouts/modals/TransactionsFlowModal/types.d.ts +++ b/src/layouts/modals/TransactionsFlowModal/types.d.ts @@ -1,7 +1,7 @@ import { Transaction } from '../../../components' -export type TransactionsFlow = 'boost' | 'stake' | 'unstake' +export type TransactionsFlow = 'boost' | 'stake' | 'unstake' | 'unboost' export type StepData = Partial> diff --git a/src/layouts/modals/TransactionsFlowModal/util/steps.ts b/src/layouts/modals/TransactionsFlowModal/util/steps.ts index 6e2013d6..dccd944f 100644 --- a/src/layouts/modals/TransactionsFlowModal/util/steps.ts +++ b/src/layouts/modals/TransactionsFlowModal/util/steps.ts @@ -1,17 +1,16 @@ -import { BoostStep, StakeStep, UnstakeStep } from 'helpers/enums' +import { BoostStep, StakeStep, UnstakeStep, UnboostStep } from 'helpers/enums' import { commonMessages, constants } from 'helpers' -import { Transaction, Transactions } from 'components' +import { Transaction } from 'components' -import { TransactionsFlow } from '../types' +import type { TransactionsFlow } from '../types' import messages from './messages' -const boostSteps: Transaction[] = [ +const boostSteps: Omit[] = [ { id: BoostStep.Permit, - status: Transactions.Status.Confirm, title: { ...commonMessages.buttonTitle.approve, values: { @@ -22,57 +21,69 @@ const boostSteps: Transaction[] = [ }, { id: BoostStep.Boost, - status: Transactions.Status.Waiting, title: commonMessages.buttonTitle.boost, testId: 'step-boost', }, ] -const stakeSteps: Transaction[] = [ +const unboostSteps: Omit[] = [ + { + id: UnboostStep.Upgrade, + title: commonMessages.upgradeLeverageStrategy, + testId: 'step-upgrade', + }, + { + id: UnboostStep.Unboost, + title: commonMessages.buttonTitle.unboost, + testId: 'step-unboost', + }, +] + +const stakeSteps: Omit[] = [ { id: StakeStep.SwapApprove, - status: Transactions.Status.Confirm, title: '', testId: 'step-swap-approve', }, { id: StakeStep.Swap, - status: Transactions.Status.Waiting, title: messages.swap, testId: 'step-swap', }, { id: StakeStep.Approve, - status: Transactions.Status.Waiting, title: '', testId: 'step-approve', }, { id: StakeStep.Stake, - status: Transactions.Status.Waiting, title: commonMessages.buttonTitle.stake, testId: 'step-stake', }, ] -const unstakeSteps: Transaction[] = [ +const unstakeSteps: Omit[] = [ + { + id: UnstakeStep.Approve, + title: '', + testId: 'step-approve', + }, { id: UnstakeStep.Swap, - status: Transactions.Status.Waiting, title: messages.swapOnExchange, testId: 'step-swap', }, { id: UnstakeStep.Queue, - status: Transactions.Status.Waiting, title: messages.queue, testId: 'step-queue', }, ] -const steps: Record = { +const steps: Record[]> = { stake: stakeSteps, boost: boostSteps, + unboost: unboostSteps, unstake: unstakeSteps, } diff --git a/src/layouts/modals/TransactionsFlowModal/util/useTransactionsFlow.ts b/src/layouts/modals/TransactionsFlowModal/util/useTransactionsFlow.ts index 10b130bb..534f201b 100644 --- a/src/layouts/modals/TransactionsFlowModal/util/useTransactionsFlow.ts +++ b/src/layouts/modals/TransactionsFlowModal/util/useTransactionsFlow.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' -import { Transaction, Transactions } from 'components' +import { TransactionStatus, Transaction } from '../../../../components/Transactions/util' +import Transactions from '../../../../components/Transactions/Transactions' import steps from './steps' import { TransactionsFlow, StepsData } from '../types' @@ -15,37 +16,46 @@ const useTransactionsFlow = ({ flow, stepsData }: Input) => { let result = steps[flow] if (stepsData) { - const stepsById: Record = {} + const stepsById: Record> = {} steps[flow].forEach((step) => { stepsById[step.id] = step }) result = stepsData - .map((stepData, index) => { + .map((stepData) => { const defaultStepData = stepsById[stepData.id as keyof typeof stepsById] return { ...defaultStepData, ...stepData, - status: index ? defaultStepData.status : Transactions.Status.Confirm, } }) } - return result + return result.map((step, index) => ({ + ...step, + status: index ? TransactionStatus.Waiting : TransactionStatus.Confirm, + })) }, [ flow, stepsData ]) - const { transactions, setTransaction, resetTransactions } = Transactions.useLogic(flowSteps) + const { + transactions, + setTransaction, + resetTransactions, + setNextTransactionsFailed, + } = Transactions.useLogic(flowSteps) return useMemo(() => ({ transactions, setTransaction, resetTransactions, + setNextTransactionsFailed, }), [ transactions, setTransaction, resetTransactions, + setNextTransactionsFailed, ]) } diff --git a/src/layouts/modals/TxCompletedModal/AddMintTokenButton/AddMintTokenButton.tsx b/src/layouts/modals/TxCompletedModal/AddMintTokenButton/AddMintTokenButton.tsx index f3e74f36..74975fc7 100644 --- a/src/layouts/modals/TxCompletedModal/AddMintTokenButton/AddMintTokenButton.tsx +++ b/src/layouts/modals/TxCompletedModal/AddMintTokenButton/AddMintTokenButton.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react' -import methods from 'helpers/methods' +import { methods } from 'helpers' import { useConfig } from 'config' import { useAddTokenToWallet } from 'hooks' diff --git a/src/layouts/modals/TxCompletedModal/TxCompletedModal.tsx b/src/layouts/modals/TxCompletedModal/TxCompletedModal.tsx index 312ed3e5..5839ac4f 100644 --- a/src/layouts/modals/TxCompletedModal/TxCompletedModal.tsx +++ b/src/layouts/modals/TxCompletedModal/TxCompletedModal.tsx @@ -5,7 +5,8 @@ import device from 'modules/device' import { useConfig, wallets } from 'config' import useModalClose from 'hooks/controls/useModalClose' -import { Button, Modal } from 'components' +import Button from 'components/Button/Button' +import Modal from 'components/Modal/Modal' import Token, { TokenProps } from './Token/Token' import TransactionButton from './TransactionButton/TransactionButton' diff --git a/src/store/store/account/balances.ts b/src/store/store/account/balances.ts index 4532e528..6d6d0422 100644 --- a/src/store/store/account/balances.ts +++ b/src/store/store/account/balances.ts @@ -5,26 +5,16 @@ import storageNames from '../../utils/storageNames' export interface BalancesState { - data: { - mintTokenBalance: bigint - swiseTokenBalance: bigint - nativeTokenBalance: bigint - depositTokenBalance: bigint - v2StakeTokenBalance: bigint - v2RewardTokenBalance: bigint - } + mintToken: bigint + nativeToken: bigint + depositToken: bigint isFetching: boolean } export const initialState: BalancesState = { - data: { - depositTokenBalance: 0n, - nativeTokenBalance: 0n, - swiseTokenBalance: 0n, - mintTokenBalance: 0n, - v2StakeTokenBalance: 0n, - v2RewardTokenBalance: 0n, - }, + mintToken: 0n, + nativeToken: 0n, + depositToken: 0n, isFetching: true, } @@ -32,26 +22,17 @@ export const balancesSlice = createSlice({ name: storageNames.accountBalances, initialState, reducers: { - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - setSwiseTokenBalance: (state, action: PayloadAction) => { - state.data.swiseTokenBalance = action.payload + setMintToken: (state, action: PayloadAction) => { + state.mintToken = action.payload }, - setNativeTokenBalance: (state, action: PayloadAction) => { - state.data.nativeTokenBalance = action.payload + setNativeToken: (state, action: PayloadAction) => { + state.nativeToken = action.payload }, - setDepositTokenBalance: (state, action: PayloadAction) => { - state.data.depositTokenBalance = action.payload + setDepositToken: (state, action: PayloadAction) => { + state.depositToken = action.payload }, - setMintTokenBalance: (state, action: PayloadAction) => { - state.data.mintTokenBalance = action.payload - }, - setV2StakeTokenBalance: (state, action: PayloadAction) => { - state.data.v2StakeTokenBalance = action.payload - }, - setV2RewardTokenBalance: (state, action: PayloadAction) => { - state.data.v2RewardTokenBalance = action.payload + setFetching: (state, action: PayloadAction) => { + state.isFetching = action.payload }, resetData: () => ({ ...initialState, diff --git a/src/store/store/account/encodings.ts b/src/store/store/account/encodings.ts deleted file mode 100644 index b6ede04b..00000000 --- a/src/store/store/account/encodings.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../utils/storageNames' - - -export interface EncodingsState { - data: { - address: string | null - referralQuery: string | null - } - isFetching: boolean -} - -export const initialState: EncodingsState = { - data: { - address: null, - referralQuery: null, - }, - isFetching: true, -} - -export const encodingsSlice = createSlice({ - name: storageNames.encodings, - initialState, - reducers: { - setData: (state, action: PayloadAction) => ({ - ...state, - isFetching: false, - data: action.payload, - }), - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = encodingsSlice.actions - -export default encodingsSlice.reducer diff --git a/src/store/store/account/index.ts b/src/store/store/account/index.ts index f722b493..d19a6d5d 100644 --- a/src/store/store/account/index.ts +++ b/src/store/store/account/index.ts @@ -1,33 +1,28 @@ import { combineReducers } from '@reduxjs/toolkit' -import * as wallet from './wallet' import * as balances from './balances' import * as vestings from './vestings' -import * as encodings from './encodings' import * as distributorClaims from './distributorClaims' import * as swapTokenBalances from './swapTokenBalances' -export const accountMethods = { wallet: wallet.methods, +export const accountMethods = { balances: balances.methods, vestings: vestings.methods, - encodings: encodings.methods, distributorClaims: distributorClaims.methods, swapTokenBalances: swapTokenBalances.methods, } -export const initialState = { wallet: wallet.initialState, +export const initialState = { balances: balances.initialState, vestings: vestings.initialState, - encodings: encodings.initialState, distributorClaims: distributorClaims.initialState, swapTokenBalances: swapTokenBalances.initialState, } -export default combineReducers({ wallet: wallet.default, +export default combineReducers({ balances: balances.default, vestings: vestings.default, - encodings: encodings.default, distributorClaims: distributorClaims.default, swapTokenBalances: swapTokenBalances.default, }) diff --git a/src/store/store/account/wallet.ts b/src/store/store/account/wallet.ts deleted file mode 100644 index acb92433..00000000 --- a/src/store/store/account/wallet.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../utils/storageNames' - - -export interface AccountWalletState { - isMMI: boolean // MetaMast Institutional -} - -export const initialState: AccountWalletState = { - isMMI: false, -} - -export const accountWalletSlice = createSlice({ - name: storageNames.accountWallet, - initialState, - reducers: { - setMMI: (state, action: PayloadAction) => { - state.isMMI = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = accountWalletSlice.actions - -export default accountWalletSlice.reducer diff --git a/src/store/store/vault/allocatorActions.ts b/src/store/store/vault/allocatorActions.ts deleted file mode 100644 index c0b02b31..00000000 --- a/src/store/store/vault/allocatorActions.ts +++ /dev/null @@ -1,53 +0,0 @@ -import swMethods from 'helpers/methods' -import { StakeWiseSDK } from 'sdk' -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../utils/storageNames' - - -export interface AllocatorActionsState { - data: Awaited> - isFetching: boolean - isLoadMore: boolean -} - -export const initialState: AllocatorActionsState = { - data: [], - isFetching: true, - isLoadMore: true, -} - -export const allocatorActionsSlice = createSlice({ - name: storageNames.allocatorActions, - initialState, - reducers: { - addItem: (state, action: PayloadAction) => { - const newActions = action.payload - const newItems = [ - ...state.data, - ...newActions, - ] - - const updatedActionsArray = swMethods.getArrUniqueItems(newItems, 'id') - - return { - ...state, - isFetching: false, - data: updatedActionsArray, - } - }, - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - setLoadMore: (state, action: PayloadAction) => { - state.isLoadMore = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = allocatorActionsSlice.actions - -export default allocatorActionsSlice.reducer diff --git a/src/store/store/vault/base.ts b/src/store/store/vault/base.ts index 394bc9fd..590b965a 100644 --- a/src/store/store/vault/base.ts +++ b/src/store/store/vault/base.ts @@ -8,6 +8,8 @@ import storageNames from '../../utils/storageNames' type GetVaultData = Awaited> type BaseData = Omit> }, 'version'> @@ -37,25 +39,29 @@ export const initialState: BaseState = { feeRecipient: '', blocklistCount: 0, whitelistCount: 0, + lastFeePercent: 0, queuedShares: '0', depositDataRoot: '', whitelistManager: '', blocklistManager: '', validatorsManager: '', depositDataManager: '', + protocolFeePercent: '0', allocatorMaxBoostApy: 0, - osTokenHolderMaxBoostApy: 0, + lastFeeUpdateTimestamp: '0', isErc20: false, isPrivate: false, isGenesis: false, + isMetaVault: false, isBlocklist: false, + isPostPectra: false, isSmoothingPool: false, isCollateralized: false, osTokenConfig: { - ltvPercent: '', - liqThresholdPercent: '', + ltvPercent: '0', + liqThresholdPercent: '0', }, versions: { @@ -82,6 +88,9 @@ export const baseSlice = createSlice({ setFetching: (state, action: PayloadAction) => { state.isFetching = action.payload }, + resetSSR: (state) => { + state.isSSR = false + }, resetData: () => initialState, }, }) diff --git a/src/store/store/vault/index.ts b/src/store/store/vault/index.ts index c6ed3331..aab89c87 100644 --- a/src/store/store/vault/index.ts +++ b/src/store/store/vault/index.ts @@ -3,8 +3,6 @@ import { combineReducers } from '@reduxjs/toolkit' import * as user from './user' import * as base from './base' import * as chart from './chart' -import * as validators from './validators' -import * as allocatorActions from './allocatorActions' import { vaultUserMethods, initialState as vaultUserInitialState } from './user' @@ -12,22 +10,16 @@ export const vaultMethods = { base: base.methods, chart: chart.methods, user: vaultUserMethods, - validators: validators.methods, - allocatorActions: allocatorActions.methods, } export const initialState = { base: base.initialState, chart: chart.initialState, user: vaultUserInitialState, - validators: validators.initialState, - allocatorActions: allocatorActions.initialState, } export default combineReducers({ user: user.default, base: base.default, chart: chart.default, - validators: validators.default, - allocatorActions: allocatorActions.default, }) diff --git a/src/store/store/vault/user/allocatorActions.ts b/src/store/store/vault/user/allocatorActions.ts deleted file mode 100644 index 29ae369b..00000000 --- a/src/store/store/vault/user/allocatorActions.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { StakeWiseSDK, AllocatorActionType } from 'sdk' -import type { PayloadAction } from '@reduxjs/toolkit' -import { createSlice } from '@reduxjs/toolkit' -import { formatEther } from 'ethers' -import swMethods from 'helpers/methods' - -import storageNames from '../../../utils/storageNames' - - -export interface AllocatorActionsState { - data: Awaited> - isFetching: boolean - isLoadMore: boolean -} - -export const initialState: AllocatorActionsState = { - data: [], - isFetching: true, - isLoadMore: true, -} - -type FirstItemPayload = { - hash: string, - link: string, - shares?: bigint, - assets?: bigint, - actionType: AllocatorActionType, -} - -export const allocatorActionsSlice = createSlice({ - name: storageNames.vaultUserActions, - initialState, - reducers: { - addFirstItem: (state, action: PayloadAction) => { - const { hash, actionType, assets, shares, link } = action.payload - - const firstAction: AllocatorActionsState['data'][0] = { - id: hash, - actionType, - createdAt: Date.now(), - shares: shares ? formatEther(shares) : '0', - assets: assets ? formatEther(assets) : '0', - link: `${link}/tx/${hash.replace(/-.*/, '')}`, - } - - const newItems = [ - firstAction, - ...state.data, - ] - - const updatedActionsArray = swMethods.getArrUniqueItems(newItems, 'id') - - return { - ...state, - isFetching: false, - data: updatedActionsArray, - } - }, - addItem: (state, action: PayloadAction) => { - const newActions = action.payload - const newItems = [ - ...state.data, - ...newActions, - ] - - const updatedActionsArray = swMethods.getArrUniqueItems(newItems, 'id') - - return { - ...state, - isFetching: false, - data: updatedActionsArray, - } - }, - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - setLoadMore: (state, action: PayloadAction) => { - state.isLoadMore = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = allocatorActionsSlice.actions - -export default allocatorActionsSlice.reducer diff --git a/src/store/store/vault/user/balances.ts b/src/store/store/vault/user/balances.ts index 1e4e1d5f..5ad2cd2a 100644 --- a/src/store/store/vault/user/balances.ts +++ b/src/store/store/vault/user/balances.ts @@ -1,22 +1,26 @@ import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' -import { StakeWiseSDK, OsTokenPositionHealth, BorrowStatus } from 'sdk' +import { StakeWiseSDK, BorrowStatus } from 'sdk' import storageNames from '../../../utils/storageNames' -type MintTokenData = Awaited> -type BoostData = Awaited> +type BoostData = Omit< + Awaited>, + 'vaultApy' | 'maxMintShares' | 'allocatorMaxBoostApy' +> export interface BalancesState { userAPY: number - withdraw: { - maxAssets: bigint - } - stake: { - assets: bigint - }, - mintToken: MintTokenData & { + stakedAssets: bigint + totalEarnedAssets: bigint + maxWithdrawAssets: bigint + totalRewardingAssets: bigint + totalStakeEarnedAssets: bigint + totalBoostEarnedAssets: bigint + mintToken: { + mintedAssets: bigint + mintedShares: bigint maxMintShares: bigint hasMintBalance: boolean isDisabled: boolean | null // UI has two view of fetching @@ -27,37 +31,30 @@ export interface BalancesState { export const initialState: BalancesState = { userAPY: 0, - withdraw: { - maxAssets: 0n, - }, - stake: { - assets: 0n, - }, + stakedAssets: 0n, + totalEarnedAssets: 0n, + maxWithdrawAssets: 0n, + totalRewardingAssets: 0n, + totalStakeEarnedAssets: 0n, + totalBoostEarnedAssets: 0n, mintToken: { + mintedAssets: 0n, + mintedShares: 0n, isDisabled: null, maxMintShares: 0n, hasMintBalance: false, - protocolFeePercent: 0n, - minted: { - assets: 0n, - shares: 0n, - }, - healthFactor: { - health: OsTokenPositionHealth.Healthy, - value: 0, - }, }, boost: { shares: 0n, - vaultApy: 0, totalShares: 0n, rewardAssets: 0n, - maxMintShares: 0n, exitingPercent: 0, borrowedAssets: 0n, - allocatorMaxBoostApy: 0, - osTokenHolderMaxBoostApy: 0, borrowStatus: BorrowStatus.Healthy, + leverageStrategyData: { + version: 2, + isUpgradeRequired: false, + }, }, isFetching: true, } diff --git a/src/store/store/vault/user/index.ts b/src/store/store/vault/user/index.ts index 2ba20dff..b0ff7074 100644 --- a/src/store/store/vault/user/index.ts +++ b/src/store/store/vault/user/index.ts @@ -1,40 +1,28 @@ import { combineReducers } from '@reduxjs/toolkit' -import * as roles from './roles' import * as rewards from './rewards' import * as balances from './balances' -import * as exitQueue from './exitQueue' +import * as unstakeQueue from './unstakeQueue' import * as unboostQueue from './unboostQueue' -import * as rewardSplitter from './rewardSplitter' -import * as allocatorActions from './allocatorActions' export const vaultUserMethods = { - roles: roles.methods, rewards: rewards.methods, balances: balances.methods, - exitQueue: exitQueue.methods, + unstakeQueue: unstakeQueue.methods, unboostQueue: unboostQueue.methods, - rewardSplitter: rewardSplitter.methods, - allocatorActions: allocatorActions.methods, } export const initialState = { - roles: roles.initialState, rewards: rewards.initialState, balances: balances.initialState, - exitQueue: exitQueue.initialState, + unstakeQueue: unstakeQueue.initialState, unboostQueue: unboostQueue.initialState, - rewardSplitter: rewardSplitter.initialState, - allocatorActions: allocatorActions.initialState, } export default combineReducers({ - roles: roles.default, rewards: rewards.default, balances: balances.default, - exitQueue: exitQueue.default, + unstakeQueue: unstakeQueue.default, unboostQueue: unboostQueue.default, - rewardSplitter: rewardSplitter.default, - allocatorActions: allocatorActions.default, }) diff --git a/src/store/store/vault/user/rewardSplitter.ts b/src/store/store/vault/user/rewardSplitter.ts deleted file mode 100644 index 2e33024a..00000000 --- a/src/store/store/vault/user/rewardSplitter.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../../utils/storageNames' - - -type FeeRecipient = { - shares: bigint - percent: number - address: string -} - -type RewardSplitter = { - owner: string - address: string - totalShares: bigint - feeRecipients: FeeRecipient[] -} - -export interface AllocatorActionsState { - data: { - feeRecipients: FeeRecipient[] - rewardSplitter: RewardSplitter | null - rewardSplitterAddresses: string[] - } - claimAmount: bigint - isFetching: boolean -} - -export const initialState: AllocatorActionsState = { - data: { - rewardSplitterAddresses: [], - rewardSplitter: null, - feeRecipients: [], - }, - claimAmount: 0n, - isFetching: true, -} - -export const rewardSplitterSlice = createSlice({ - name: storageNames.rewardSplitter, - initialState, - reducers: { - setData: (state, action: PayloadAction) => ({ - ...state, - isFetching: false, - data: action.payload, - }), - setClaimAmount: (state, action: PayloadAction) => ({ - ...state, - isFetching: false, - claimAmount: action.payload, - }), - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = rewardSplitterSlice.actions - -export default rewardSplitterSlice.reducer diff --git a/src/store/store/vault/user/roles.ts b/src/store/store/vault/user/roles.ts deleted file mode 100644 index 9710a77f..00000000 --- a/src/store/store/vault/user/roles.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../../utils/storageNames' - - -export interface RolesState { - data: { - isVaultAdmin: boolean - isWhitelisted: boolean - isBlocklisted: boolean - isWhitelistManager: boolean - isBlocklistManager: boolean - isDepositDataManager: boolean - isNativeValidatorsManager: boolean - } - isFetching: boolean -} - -export const initialState: RolesState = { - data: { - isVaultAdmin: false, - isWhitelisted: true, - isBlocklisted: false, - isWhitelistManager: false, - isBlocklistManager: false, - isDepositDataManager: false, - isNativeValidatorsManager: true, - }, - isFetching: true, -} - -export const rolesSlice = createSlice({ - name: storageNames.vaultRoles, - initialState, - reducers: { - setData: (_, action: PayloadAction) => ({ - isFetching: false, - data: action.payload, - }), - setFetching: (state, action: PayloadAction) => ({ - ...state, - isFetching: action.payload, - }), - resetData: (_, action: PayloadAction) => ({ - ...initialState, - isFetching: action.payload, - }), - }, -}) - - -export const methods = rolesSlice.actions - -export default rolesSlice.reducer diff --git a/src/store/store/vault/user/unboostQueue.ts b/src/store/store/vault/user/unboostQueue.ts index 4a884e41..c7ceb029 100644 --- a/src/store/store/vault/user/unboostQueue.ts +++ b/src/store/store/vault/user/unboostQueue.ts @@ -14,6 +14,7 @@ export interface UnboostQueueState { export const initialState: UnboostQueueState = { data: { + version: 2, duration: null, position: null, exitingShares: 0n, diff --git a/src/store/store/vault/user/exitQueue.ts b/src/store/store/vault/user/unstakeQueue.ts similarity index 66% rename from src/store/store/vault/user/exitQueue.ts rename to src/store/store/vault/user/unstakeQueue.ts index 440cc8ee..d4f4b7e0 100644 --- a/src/store/store/vault/user/exitQueue.ts +++ b/src/store/store/vault/user/unstakeQueue.ts @@ -13,7 +13,7 @@ type Position = { type ExitRequests = Awaited>['requests'] -export interface ExitQueueState { +export interface UnstakeQueueState { data: { total: bigint withdrawable: bigint @@ -24,7 +24,7 @@ export interface ExitQueueState { isFetching: boolean } -export const initialState: ExitQueueState = { +export const initialState: UnstakeQueueState = { data: { total: 0n, requests: [], @@ -35,16 +35,16 @@ export const initialState: ExitQueueState = { isFetching: false, } -export const exitQueueStateSlice = createSlice({ - name: storageNames.vaultExitQueue, +export const unstakeQueueStateSlice = createSlice({ + name: storageNames.vaultUnstakeQueue, initialState, reducers: { - setData: (state, action: PayloadAction) => ({ + setData: (state, action: PayloadAction) => ({ ...state, isFetching: false, data: action.payload, }), - setFetching: (state, action: PayloadAction) => { + setFetching: (state, action: PayloadAction) => { state.isFetching = action.payload }, resetData: () => initialState, @@ -52,6 +52,6 @@ export const exitQueueStateSlice = createSlice({ }) -export const methods = exitQueueStateSlice.actions +export const methods = unstakeQueueStateSlice.actions -export default exitQueueStateSlice.reducer +export default unstakeQueueStateSlice.reducer diff --git a/src/store/store/vault/validators.ts b/src/store/store/vault/validators.ts deleted file mode 100644 index c87b1039..00000000 --- a/src/store/store/vault/validators.ts +++ /dev/null @@ -1,53 +0,0 @@ -import swMethods from 'helpers/methods' -import { StakeWiseSDK } from 'sdk' -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' - -import storageNames from '../../utils/storageNames' - - -export interface ValidatorsState { - data: Awaited> - isFetching: boolean - isLoadMore: boolean -} - -export const initialState: ValidatorsState = { - data: [], - isFetching: true, - isLoadMore: true, -} - -export const validatorsSlice = createSlice({ - name: storageNames.vaultValidators, - initialState, - reducers: { - addItems: (state, action: PayloadAction) => { - const newValidators = action.payload - const newItems = [ - ...state.data, - ...newValidators, - ] - - const updatedValidatorsArray = swMethods.getArrUniqueItems(newItems, 'publicKey') - - return { - ...state, - isFetching: false, - data: updatedValidatorsArray, - } - }, - setFetching: (state, action: PayloadAction) => { - state.isFetching = action.payload - }, - setLoadMore: (state, action: PayloadAction) => { - state.isLoadMore = action.payload - }, - resetData: () => initialState, - }, -}) - - -export const methods = validatorsSlice.actions - -export default validatorsSlice.reducer diff --git a/src/store/utils/storageNames.ts b/src/store/utils/storageNames.ts index d2a57287..d7fca133 100644 --- a/src/store/utils/storageNames.ts +++ b/src/store/utils/storageNames.ts @@ -2,27 +2,15 @@ export default { swapTokenBalances: 'swapTokenBalances', distributorClaims: 'distributorClaims', vaultUserRewards: 'vaultUserRewards', - allocatorActions: 'allocatorActions', - vaultUserActions: 'vaultUserActions', vaultUnboostQueue: 'vaultBoostQueue', + vaultUnstakeQueue: 'vaultUnstakeQueue', accountBalances: 'accountBalances', - vaultValidators: 'vaultValidators', - depositsVaults: 'depositsVaults', - vaultExitQueue: 'vaultExitQueue', - rewardSplitter: 'rewardSplitter', swapTokenRates: 'swapTokenRates', - osTokenVaults: 'osTokenVaults', - operateVaults: 'operateVaults', vaultBalances: 'vaultBalances', - accountWallet: 'accountWallet', - vaultOsToken: 'vaultOsToken', - vaultRoles: 'vaultRoles', vaultChart: 'vaultChart', fiatRates: 'fiatRates', mintToken: 'mintToken', - allVaults: 'allVaults', vaultBase: 'vaultBase', - encodings: 'encodings', currency: 'currency', vestings: 'vestings', rewards: 'rewards', diff --git a/src/types/types.d.ts b/src/types/types.d.ts index f3e88b17..0351781d 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -61,7 +61,7 @@ declare global { type SwapToken = { name: string title: string - address: string + address: string | null balance: bigint logo: LogoName units: number diff --git a/src/views/ErrorView/ErrorView.tsx b/src/views/ErrorView/ErrorView.tsx index cc1b704b..e3c2704b 100644 --- a/src/views/ErrorView/ErrorView.tsx +++ b/src/views/ErrorView/ErrorView.tsx @@ -10,10 +10,9 @@ import messages from './messages' type ErrorViewProps = { error: Error - reset: () => void } -const ErrorView: React.FC = ({ error, reset }) => { +const ErrorView: React.FC = ({ error }) => { const { push } = useRouter() const pageMessages = error @@ -40,7 +39,6 @@ const ErrorView: React.FC = ({ error, reset }) => { dataTestId="error-back-button" title={messages.buttonTitle} onClick={() => { - reset() push(links.home) }} /> diff --git a/src/views/HomeView/HomeView.tsx b/src/views/HomeView/HomeView.tsx deleted file mode 100644 index 3aa60d7b..00000000 --- a/src/views/HomeView/HomeView.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' -import React from 'react' - -import { ConnectWalletModal, SwitchAccountModal } from 'layouts/modals' - -import StakeContext from './StakeContext/StakeContext' - - -const HomeView: React.FC = () => ( -
-
- - - -
-
-) - - -export default HomeView diff --git a/src/views/HomeView/StakeContext/StakeContext.tsx b/src/views/HomeView/StakeContext/StakeContext.tsx deleted file mode 100644 index 4688372c..00000000 --- a/src/views/HomeView/StakeContext/StakeContext.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' - -import { TxCompletedModal, ExportRewardsModal, TransactionsFlowModal, DistributorClaimsModal } from 'layouts/modals' - -import Tabs from './Tabs/Tabs' -import { Skeleton } from '../common' -import { StatisticsModal } from '../modals' -import { Balance, Boost, Stake, Mint, Burn, Unboost, Unstake } from '../content' - -import { Tab, stakeCtx } from './util' - - -const components = { - [Tab.Mint]: Mint, - [Tab.Burn]: Burn, - [Tab.Stake]: Stake, - [Tab.Boost]: Boost, - [Tab.Unboost]: Unboost, - [Tab.Unstake]: Unstake, - [Tab.Balance]: Balance, -} - -const StakeContext: React.FC = () => { - const ctx = stakeCtx.useInit() - const Tab = components[ctx.tabs.value as Tab] - - const content = ctx.isFetching ? ( - - ) : ( - <> - -
- -
- - ) - - return ( - - {content} - - - - - - - ) -} - - -export default React.memo(StakeContext) diff --git a/src/views/HomeView/StakeContext/Tabs/util/getTabIds.ts b/src/views/HomeView/StakeContext/Tabs/util/getTabIds.ts deleted file mode 100644 index 442e4081..00000000 --- a/src/views/HomeView/StakeContext/Tabs/util/getTabIds.ts +++ /dev/null @@ -1,11 +0,0 @@ -import getTabsList, { Input } from './getTabsList' - - -const getTabIds = (values: Input) => { - const list = getTabsList(values) - - return list.map(({ id }) => id) -} - - -export default getTabIds diff --git a/src/views/HomeView/StakeContext/Tabs/util/index.ts b/src/views/HomeView/StakeContext/Tabs/util/index.ts deleted file mode 100644 index 77889a14..00000000 --- a/src/views/HomeView/StakeContext/Tabs/util/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as useTabs } from './useTabs' -export { default as getTabsList } from './getTabsList' diff --git a/src/views/HomeView/StakeContext/Tabs/util/useTabs.ts b/src/views/HomeView/StakeContext/Tabs/util/useTabs.ts deleted file mode 100644 index 2139f9a7..00000000 --- a/src/views/HomeView/StakeContext/Tabs/util/useTabs.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useConfig } from 'config' -import { useStore } from 'hooks' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' - -import { stakeCtx } from 'views/HomeView/StakeContext/util' - -import getTabIds from './getTabIds' -import getTabsList from './getTabsList' - - -const storeSelector = (store: Store) => ({ - isMoreV2: store.vault.base.data.versions.isMoreV2, - isMintTokenDisabled: store.vault.user.balances.mintToken.isDisabled, -}) - -const useTabs = () => { - const { isEthereum } = useConfig() - const { tabs } = stakeCtx.useData() - const { isMoreV2, isMintTokenDisabled } = useStore(storeSelector) - - const withMint = !isMintTokenDisabled - const withBoost = withMint && isEthereum && isMoreV2 - const withToggleButton = withMint || withBoost - - const isInitiallyReversed = getTabIds({ withMint, withBoost }).indexOf(tabs.value) === -1 - - const [ isReversed, setReversed ] = useState(isInitiallyReversed) - - const [ tabIds, tabsList ] = useMemo(() => [ - getTabIds({ withMint, withBoost, isReversed }), - getTabsList({ withMint, withBoost, isReversed }), - ], [ withMint, withBoost, isReversed ]) - - const prevIndex = useRef(tabIds.indexOf(tabs.value)) - - const tabIndex = useMemo(() => { - const activeIndex = tabIds.indexOf(tabs.value) - - if (activeIndex === -1) { - return prevIndex.current - } - - prevIndex.current = activeIndex - - return activeIndex - }, [ tabs, tabIds ]) - - useEffect(() => { - const nextTabIndex = tabIds.indexOf(tabs.value) - const isResetNeeded = nextTabIndex === -1 - - if (isResetNeeded) { - const nextTabIndex = withToggleButton ? tabIndex : 0 - - tabs.setTab(tabIds[nextTabIndex]) - } - - if (!withToggleButton) { - setReversed(false) - } - }, [ tabs, tabIds, tabIndex, withToggleButton ]) - - const toggleReversed = useCallback(() => { - setReversed((isReversed) => !isReversed) - }, []) - - return useMemo(() => ({ - tabIndex, - tabsList, - withToggleButton, - toggleReversed, - }), [ - tabIndex, - tabsList, - withToggleButton, - toggleReversed, - ]) -} - - -export default useTabs diff --git a/src/views/HomeView/StakeContext/util/actions/index.ts b/src/views/HomeView/StakeContext/util/actions/index.ts deleted file mode 100644 index f1919562..00000000 --- a/src/views/HomeView/StakeContext/util/actions/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default as useBurn } from './useBurn' -export { default as useMint } from './useMint' -export { default as useBoost } from './useBoost' -export { default as useStake } from './useStake' -export { default as useUnstake } from './useUnstake' -export { default as useUnboost } from './useUnboost' -export { default as useEstimateGas } from './useEstimateGas' diff --git a/src/views/HomeView/StakeContext/util/actions/useBoost.ts b/src/views/HomeView/StakeContext/util/actions/useBoost.ts deleted file mode 100644 index be0097e9..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useBoost.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { useBalances } from 'hooks' -import { useConfig } from 'config' - -import useBoostSubmit from './useBoostSubmit' - - -type SubmitInput = Omit['submit']>[0], 'onSuccess'> - -type Output = { - allowance: bigint - isSubmitting: boolean - isAllowanceFetching: boolean - submit: (input: SubmitInput) => Promise -} - -interface Hook { - (params: StakePage.Params): Output - mock: Output -} - -const useBoost: Hook = (params) => { - const { field, fetch, vaultAddress } = params - - const { signSDK, address, chainId, cancelOnChange } = useConfig() - const { refetchMintTokenBalance, refetchNativeTokenBalance } = useBalances() - const { allowance, isAllowanceFetching, isSubmitting, submit } = useBoostSubmit(vaultAddress) - - const handleGetUserApy = useCallback(async () => { - if (!address) { - return 0 - } - - const userAPY = await signSDK.vault.getUserApy({ - userAddress: address, - vaultAddress, - }) - - return userAPY - }, [ address, signSDK, vaultAddress ]) - - const handleSubmit = useCallback(async (values: SubmitInput) => { - const { amount, setTransaction } = values - - if (!address) { - return - } - - const onSuccess = cancelOnChange({ - address, - chainId, - logic: () => { - field.reset() - - fetch.data() - fetch.balances() - - refetchMintTokenBalance() - refetchNativeTokenBalance() - }, - }) - - await submit({ - amount, - getUserApy: handleGetUserApy, - setTransaction, - onSuccess, - }) - }, [ - field, - fetch, - chainId, - address, - submit, - cancelOnChange, - handleGetUserApy, - refetchMintTokenBalance, - refetchNativeTokenBalance, - ]) - - return useMemo(() => ({ - allowance, - isSubmitting, - isAllowanceFetching, - submit: handleSubmit, - }), [ - allowance, - isSubmitting, - isAllowanceFetching, - handleSubmit, - ]) -} - -useBoost.mock = { - allowance: 0n, - isSubmitting: false, - isAllowanceFetching: false, - submit: async () => {}, -} - - -export default useBoost diff --git a/src/views/HomeView/StakeContext/util/actions/useBoostSubmit.ts b/src/views/HomeView/StakeContext/util/actions/useBoostSubmit.ts deleted file mode 100644 index ba44d50f..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useBoostSubmit.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { useActions, useSubgraphUpdate } from 'hooks' -import notifications from 'modules/notifications' -import { useConfig } from 'config' -import { BoostStep } from 'helpers/enums' -import { commonMessages, getters } from 'helpers' - -import { Transactions, SetTransaction } from 'components' -import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' -import useBoostAllowance from './useBoostAllowance' - - -type ApproveInput = { - setTransaction: SetTransaction -} - -type PermitInput = { - userAddress: string - vaultAddress: string - spenderAddress: string - setTransaction: SetTransaction -} - -type BoostInput = { - amount: bigint - userAddress: string - vaultAddress: string - permitParams?: { - vault: string - amount: bigint - deadline: number - v: number - r: string - s: string - } - setTransaction: SetTransaction -} - -type SubmitInput = { - amount: bigint - permitAddress?: string - getUserApy?: () => Promise - onSuccess?: (hash: string) => Promise - setTransaction?: SetTransaction -} - -type Output = { - allowance: bigint - isSubmitting: boolean - isAllowanceFetching: boolean - submit: (values: SubmitInput) => Promise -} - -const useBoostSubmit = (vaultAddress: string | null): Output => { - const actions = useActions() - const { signSDK, address } = useConfig() - - const subgraphUpdate = useSubgraphUpdate() - const [ isSubmitting, setSubmitting ] = useState(false) - - const { - allowance, - permitAddress, - isFetching, - approve, - checkAllowance, - } = useBoostAllowance(vaultAddress) - - const handleApprove = useCallback(async (values: ApproveInput) => { - const { setTransaction } = values - - try { - const hash = await approve() - - setTransaction(BoostStep.Permit, Transactions.Status.Processing) - - await checkAllowance({ hash, allowance }) - - setTransaction(BoostStep.Permit, Transactions.Status.Success) - } - catch (error) { - setTransaction(BoostStep.Permit, Transactions.Status.Fail) - setTransaction(BoostStep.Boost, Transactions.Status.Fail) - - return Promise.reject(error) - } - }, [ allowance, approve, checkAllowance ]) - - const permit = useCallback(async (values: PermitInput) => { - const { userAddress, vaultAddress, spenderAddress, setTransaction } = values - - try { - const { amount, deadline, v, r, s } = await signSDK.utils.getPermitSignature({ - contract: signSDK.contracts.tokens.mintToken, - ownerAddress: userAddress, - spenderAddress, - }) - - setTransaction(BoostStep.Permit, Transactions.Status.Success) - - return { - amount, - deadline, - vault: vaultAddress, - v, - r, - s, - } - } - catch (error) { - setTransaction(BoostStep.Permit, Transactions.Status.Fail) - setTransaction(BoostStep.Boost, Transactions.Status.Fail) - - return Promise.reject(error) - } - }, [ signSDK ]) - - const boost = useCallback(async (values: BoostInput) => { - const { amount, userAddress, vaultAddress, permitParams, setTransaction } = values - - try { - setTransaction(BoostStep.Boost, Transactions.Status.Confirm) - - const referrerAddress = getters.getReferrer() - - const hash = await signSDK.boost.lock({ - amount, - userAddress, - vaultAddress, - referrerAddress, - permitParams, - }) - - setTransaction(BoostStep.Boost, Transactions.Status.Processing) - - await subgraphUpdate({ hash }) - - setTransaction(BoostStep.Boost, Transactions.Status.Success) - - return hash - } - catch (error) { - setTransaction(BoostStep.Boost, Transactions.Status.Fail) - - return Promise.reject(error) - } - }, [ - signSDK, - subgraphUpdate, - ]) - - const submit = useCallback(async (values: SubmitInput) => { - const { amount, getUserApy, setTransaction = () => {}, onSuccess } = values - - try { - if (!amount || !address || !vaultAddress) { - return - } - - actions.ui.setBottomLoader({ - content: commonMessages.notification.waitingConfirmation, - }) - - console.log({ - category: 'action', - message: 'Boost submit click', - }) - - setSubmitting(true) - - let permitParams - - const isPermitRequired = amount > allowance - - if (permitAddress && isPermitRequired) { - const code = await signSDK.provider.getCode(address) - const isMultiSig = code !== '0x' - - if (isMultiSig) { - await handleApprove({ - setTransaction, - }) - } - else { - permitParams = await permit({ - spenderAddress: permitAddress, - userAddress: address, - vaultAddress, - setTransaction, - }) - } - } - - const hash = await boost({ - amount, - permitParams, - vaultAddress, - userAddress: address, - setTransaction, - }) - - if (typeof onSuccess === 'function') { - await onSuccess(hash) - } - - if (permitParams) { - checkAllowance({ allowance: 0n }) - } - - const userAPY = await getUserApy?.() || 0 - - const tokens = [ - { - apy: userAPY, - value: amount, - action: Action.Boost, - token: signSDK.config.tokens.mintToken, - }, - ] - - openTxCompletedModal({ hash, tokens }) - } - catch (error) { - actions.ui.resetBottomLoader() - console.error('Boost send transaction error', error as Error) - - notifications.open({ - text: commonMessages.notification.failed, - type: 'error', - }) - - return Promise.reject(error) - } - finally { - setSubmitting(false) - } - }, [ - signSDK, - address, - actions, - allowance, - vaultAddress, - permitAddress, - boost, - permit, - handleApprove, - checkAllowance, - ]) - - return useMemo(() => ({ - allowance, - isSubmitting, - isAllowanceFetching: isFetching, - submit, - }), [ - allowance, - isFetching, - isSubmitting, - submit, - ]) -} - - -export default useBoostSubmit diff --git a/src/views/HomeView/StakeContext/util/actions/useBurn/index.ts b/src/views/HomeView/StakeContext/util/actions/useBurn/index.ts deleted file mode 100644 index 05a7d6e4..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useBurn/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useMemo } from 'react' - -import useSubmit from './useSubmit' -import useCalculateBurn from './useCalculateBurn' -import useEstimateGas, { Type } from '../useEstimateGas' - - -type Output = ReturnType & { - getBurnGas: ReturnType - calculateBurn: ReturnType -} - -interface Hook { - (params: StakePage.Params): Output - mock: Output -} - -const useBurn: Hook = (params) => { - const { submit, isSubmitting } = useSubmit(params) - const calculateBurn = useCalculateBurn() - const getBurnGas = useEstimateGas(Type.Burn) - - return useMemo(() => ({ - isSubmitting, - submit, - getBurnGas, - calculateBurn, - }), [ - isSubmitting, - submit, - getBurnGas, - calculateBurn, - ]) -} - -useBurn.mock = { - getBurnGas: useEstimateGas.mock, - isSubmitting: false, - submit: () => Promise.resolve(undefined), - calculateBurn: () => Promise.resolve(0n), -} - -export default useBurn diff --git a/src/views/HomeView/StakeContext/util/actions/useBurn/useCalculateBurn.ts b/src/views/HomeView/StakeContext/util/actions/useBurn/useCalculateBurn.ts deleted file mode 100644 index bc7fdc9f..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useBurn/useCalculateBurn.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useCallback } from 'react' -import { useStore } from 'hooks' -import { useConfig } from 'config' - - -const storeSelector = (store: Store) => ({ - ltvPercent: BigInt(store.vault.base.data.osTokenConfig.ltvPercent), - vaultAddress: store.vault.base.data.vaultAddress, - stakedAssets: store.vault.user.balances.stake.assets, - isMintTokenDataFetching: store.vault.base.isFetching, - isBalancesFetching: store.vault.user.balances.isFetching, - mintedAssets: store.vault.user.balances.mintToken.minted.assets, -}) - -const useCalculateBurn = () => { - const { sdk } = useConfig() - - const { - ltvPercent, - stakedAssets, - mintedAssets, - vaultAddress, - isBalancesFetching, - isMintTokenDataFetching, - } = useStore(storeSelector) - - const isFetching = isBalancesFetching || isMintTokenDataFetching - - return useCallback(async (newStakedAssets: bigint) => { - try { - if (mintedAssets && !isFetching) { - const sharesToBurn = await sdk.osToken.getBurnAmount({ - newStakedAssets, - mintedAssets, - stakedAssets, - vaultAddress, - ltvPercent, - }) - - return sharesToBurn - } - } - catch (error) { - console.error('calculateBurn error', error as Error, { - mintedAssets, - stakedAssets, - }) - - return Promise.reject(error) - } - - return 0n - }, [ sdk, stakedAssets, mintedAssets, ltvPercent, vaultAddress, isFetching ]) -} - - -export default useCalculateBurn diff --git a/src/views/HomeView/StakeContext/util/actions/useEstimateGas.ts b/src/views/HomeView/StakeContext/util/actions/useEstimateGas.ts deleted file mode 100644 index e22c6ac6..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useEstimateGas.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useCallback } from 'react' -import { useStore } from 'hooks' -import { useConfig } from 'config' -import { constants, getters } from 'helpers' - - -export enum Type { - Mint, - Burn, - Deposit, - Withdraw, -} - -interface Hook { - (type: Type): (value: bigint) => Promise - mock: (value: bigint) => Promise -} - -const storeSelector = (store: Store) => ({ - vaultAddress: store.vault.base.data.vaultAddress, -}) - -const useEstimateGas: Hook = (type) => { - const { signSDK, address } = useConfig() - const { vaultAddress } = useStore(storeSelector) - - return useCallback(async (value) => { - if (!vaultAddress) { - return 0n - } - - try { - const referrerAddress = getters.getReferrer() - - const params = { - userAddress: address || vaultAddress, - vaultAddress, - } - - switch (type) { - case Type.Deposit: - return signSDK.vault.deposit.estimateGas({ ...params, referrerAddress, assets: value }) - - case Type.Withdraw: - return signSDK.vault.withdraw.estimateGas({ ...params, assets: value }) - - case Type.Mint: - return signSDK.osToken.mint.estimateGas({ ...params, referrerAddress, shares: value }) - - case Type.Burn: - return signSDK.osToken.burn.estimateGas({ ...params, shares: value }) - - default: - console.error('Incorect estimateGas type', type) - return Promise.resolve(0n) - } - } - catch (error) { - console.error('estimateGas error', error as Error, { value, address, vaultAddress }) - - return 0n - } - }, [ signSDK, type, address, vaultAddress ]) -} - -useEstimateGas.mock = () => Promise.resolve(constants.blockchain.amount0) - - -export default useEstimateGas diff --git a/src/views/HomeView/StakeContext/util/actions/useMint/index.ts b/src/views/HomeView/StakeContext/util/actions/useMint/index.ts deleted file mode 100644 index 941acfe7..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useMint/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useMemo } from 'react' - -import useSubmit from './useSubmit' -import useHealth from './useHealth' -import useEstimateGas, { Type } from '../useEstimateGas' - - -type Output = ReturnType & { - getStyleByHealth: ReturnType['getStyleByHealth'] - getHealthFactor: ReturnType['getHealthFactor'] - getMintGas: ReturnType -} - -interface Hook { - (params: StakePage.Params): Output - mock: Output -} - -const useMint: Hook = (params) => { - const { submit, isSubmitting } = useSubmit(params) - const getMintGas = useEstimateGas(Type.Mint) - const { getStyleByHealth, getHealthFactor } = useHealth() - - return useMemo(() => ({ - isSubmitting, - getStyleByHealth, - getHealthFactor, - getMintGas, - submit, - }), [ - isSubmitting, - getStyleByHealth, - getHealthFactor, - getMintGas, - submit, - ]) -} - -useMint.mock = { - ...useHealth.mock, - getMintGas: useEstimateGas.mock, - isSubmitting: false, - submit: () => Promise.resolve(undefined), -} - - -export default useMint diff --git a/src/views/HomeView/StakeContext/util/actions/useStake/index.ts b/src/views/HomeView/StakeContext/util/actions/useStake/index.ts deleted file mode 100644 index c747772e..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useStake/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { useActions, useStakeSubmit, useSwapTokens } from 'hooks' -import { AllocatorActionType } from 'sdk' -import { useConfig } from 'config' -import { StakeStep } from 'helpers/enums' - -import { Action, openTxCompletedModal, openTransactionsFlowModal } from 'layouts/modals' - -import useMaxStake from './useMaxStake' -import useStakeGas from './useStakeGas' - - -type Input = StakePage.Params & { - swapTokens: StakePage.SwapTokens -} - -type Output = { - gas: { - approve: bigint - deposit: bigint - } - isSubmitting: boolean - isAllowanceFetching: boolean - swapTokens: ReturnType - submit: () => void - onMaxButtonClick: ReturnType -} - -interface Hook { - (params: Input): Output - mock: Output -} - -const useStake: Hook = ({ swapTokens, field, fetch }) => { - const { sdk } = useConfig() - const actions = useActions() - - const swapToken = swapTokens.selected - - const onSwap = useCallback((buyAmount: bigint) => { - field.setValue(buyAmount) - swapTokens.setSelected('') - }, [ field, swapTokens ]) - - const onSuccess = useCallback(({ hash, assets }: { hash: string, assets?: bigint }) => { - fetch.data() - fetch.balances() - - if (assets) { - const blockExplorerUrl = sdk.config.network.blockExplorerUrl - - actions.vault.user.allocatorActions.addFirstItem({ - hash, - assets, - actionType: AllocatorActionType.Deposited, - link: blockExplorerUrl, - }) - - const tokens = [ - { - token: sdk.config.tokens.depositToken, - action: Action.Stake, - value: assets, - }, - ] - - openTxCompletedModal({ tokens, hash }) - } - }, [ sdk, fetch, actions ]) - - const depositGas = useStakeGas() - - const { approveGas, isSubmitting, isAllowanceFetching, submit } = useStakeSubmit({ - field, - swapToken, - stakeStep: StakeStep.Stake, - onSwap, - onSuccess, - openTransactionsFlowModal, - }) - - const onMaxButtonClick = useMaxStake({ - field, - approveGas, - depositGas, - swapToken: swapTokens.selected, - }) - - return useMemo(() => ({ - gas: { - approve: approveGas, - deposit: depositGas, - }, - swapTokens, - isSubmitting, - isAllowanceFetching, - submit, - onMaxButtonClick, - }), [ - approveGas, - depositGas, - swapTokens, - isSubmitting, - isAllowanceFetching, - submit, - onMaxButtonClick, - ]) -} - -useStake.mock = { - gas: { - deposit: 0n, - approve: 0n, - }, - isSubmitting: false, - isAllowanceFetching: false, - swapTokens: useSwapTokens.mock, - submit: () => Promise.resolve(undefined), - onMaxButtonClick: () => Promise.resolve(0n), -} - - -export default useStake diff --git a/src/views/HomeView/StakeContext/util/actions/useStake/useMaxStake.ts b/src/views/HomeView/StakeContext/util/actions/useStake/useMaxStake.ts deleted file mode 100644 index ffe8397a..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useStake/useMaxStake.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback } from 'react' -import { constants } from 'helpers' -import { useConfig } from 'config' - - -type Input = { - approveGas: bigint - depositGas: bigint - field: Forms.Field - swapToken: SwapToken -} - -const useMaxStake = ({ approveGas, depositGas, field, swapToken }: Input) => { - const { isGnosis, activeWallet } = useConfig() - - const isGnosisSafeWallet = activeWallet === constants.walletNames.gnosisSafe - const isNoGasTransaction = Boolean(isGnosis || isGnosisSafeWallet || swapToken.address) - - return useCallback(() => { - const assets = swapToken.balance - - if (isNoGasTransaction) { - field.setValue(assets) - } - else { - const hasAmount = assets > 0 - - if (hasAmount) { - const total = assets - approveGas - (depositGas * 2n) - - field.setValue(total > 0 ? total : 0n) - } - else { - field.setValue(0n) - } - } - }, [ field, approveGas, depositGas, swapToken, isNoGasTransaction ]) -} - - -export default useMaxStake diff --git a/src/views/HomeView/StakeContext/util/actions/useStake/useStakeGas.ts b/src/views/HomeView/StakeContext/util/actions/useStake/useStakeGas.ts deleted file mode 100644 index ffb4b7b2..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useStake/useStakeGas.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import { useConfig } from 'config' -import { constants } from 'helpers' -import { useAutoFetch, useStore } from 'hooks' - -import useEstimateGas, { Type } from '../useEstimateGas' - - -const storeSelector = (store: Store) => ({ - vaultAddress: store.vault.base.data.vaultAddress, - depositTokenBalance: store.account.balances.data.depositTokenBalance, -}) - -const useStakeGas = () => { - const { address } = useConfig() - const [ gas, setGas ] = useState(0n) - const { vaultAddress, depositTokenBalance } = useStore(storeSelector) - - const getDepositGas = useEstimateGas(Type.Deposit) - - const handleGetDepositGas = useCallback(async () => { - let gas = 0n - - const isValidBalance = depositTokenBalance > constants.blockchain.gwei - - try { - if (isValidBalance && vaultAddress) { - const amount = depositTokenBalance / 2n // try to check half of balance to get gas - - gas = await getDepositGas(amount) - } - } - catch {} - - setGas(gas) - }, [ vaultAddress, depositTokenBalance, getDepositGas ]) - - useEffect(() => { - if (!address) { - setGas(0n) - } - }, [ address ]) - - useAutoFetch({ - action: handleGetDepositGas, - interval: 15_000, - skip: !address, - }) - - return gas -} - - -export default useStakeGas diff --git a/src/views/HomeView/StakeContext/util/actions/useUnboost.ts b/src/views/HomeView/StakeContext/util/actions/useUnboost.ts deleted file mode 100644 index 305595d9..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useUnboost.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { useBalances, useStore } from 'hooks' -import { useConfig } from 'config' - -import useUnboostSubmit from './useUnboostSubmit' - - -type Output = { - isDisabled: boolean - isSubmitting: boolean - submit: () => Promise -} - -interface Hook { - (params: StakePage.Params): Output - mock: Output -} - -const storeSelector = (store: Store) => ({ - boostedShares: store.vault.user.balances.boost.shares, - rewardAssets: store.vault.user.balances.boost.rewardAssets, - exitingPercent: store.vault.user.balances.boost.exitingPercent, -}) - -const useUnboost: Hook = (params) => { - const { refetchMintTokenBalance, refetchNativeTokenBalance } = useBalances() - - const { vaultAddress, percentField, fetch } = params - - const { address, chainId, cancelOnChange } = useConfig() - const { boostedShares, rewardAssets, exitingPercent } = useStore(storeSelector) - - const { isSubmitting, submit } = useUnboostSubmit({ - rewards: rewardAssets, - shares: boostedShares, - vaultAddress, - }) - - const isDisabled = boostedShares === 0n || exitingPercent > 0 - - const handleSubmit = useCallback(async () => { - const onSuccess = cancelOnChange({ - address, - chainId, - logic: () => { - percentField.reset() - - fetch.data() - fetch.balances() - fetch.unboostQueue() - - refetchMintTokenBalance() - refetchNativeTokenBalance() - }, - }) - - await submit({ percent: Number(percentField.value), onSuccess }) - }, [ - fetch, - chainId, - address, - percentField, - submit, - cancelOnChange, - refetchMintTokenBalance, - refetchNativeTokenBalance, - ]) - - return useMemo(() => ({ - isDisabled, - isSubmitting, - submit: handleSubmit, - }), [ - isDisabled, - isSubmitting, - handleSubmit, - ]) -} - -useUnboost.mock = { - isDisabled: true, - isSubmitting: false, - submit: async () => {}, -} - - -export default useUnboost diff --git a/src/views/HomeView/StakeContext/util/actions/useUnboostSubmit.ts b/src/views/HomeView/StakeContext/util/actions/useUnboostSubmit.ts deleted file mode 100644 index f4269d64..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useUnboostSubmit.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { useActions, useSubgraphUpdate } from 'hooks' -import { modifiers, commonMessages } from 'helpers' -import notifications from 'modules/notifications' -import { useConfig } from 'config' - -import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' - - -type Input = { - shares: bigint - rewards: bigint - vaultAddress: string | null -} - -type SubmitInput = { - percent: number - onSuccess?: (hash: string) => Promise -} - -type TokenData = { - token: Tokens - value: bigint - action: Action -} - -const useUnboostSubmit = (values: Input) => { - const { shares, rewards, vaultAddress } = values - - const actions = useActions() - const { signSDK, address } = useConfig() - - const subgraphUpdate = useSubgraphUpdate() - const [ isSubmitting, setSubmitting ] = useState(false) - - const submit = useCallback(async (values: SubmitInput) => { - const { percent, onSuccess } = values - - if (!percent || !address || !vaultAddress) { - return - } - - try { - actions.ui.setBottomLoader({ - content: commonMessages.notification.waitingConfirmation, - }) - - console.log({ - category: 'action', - message: 'Submit unboost click', - }) - - setSubmitting(true) - - const hash = await signSDK.boost.unlock({ - percent, - userAddress: address, - vaultAddress: vaultAddress, - }) - - await subgraphUpdate({ hash }) - - if (typeof onSuccess === 'function') { - await onSuccess(hash) - } - - const [ exitShares ] = modifiers.splitPercent(shares, percent) - const [ exitAssets ] = modifiers.splitPercent(rewards, percent) - - const tokens: TokenData[] = [ - { - token: signSDK.config.tokens.mintToken, - value: exitShares, - action: Action.Exiting, - }, - ] - - if (exitAssets) { - tokens.push({ - token: signSDK.config.tokens.depositToken, - value: exitAssets, - action: Action.Exiting, - }) - } - - openTxCompletedModal({ tokens, hash }) - } - catch (error) { - actions.ui.resetBottomLoader() - console.error('Unboost: submit failed', error as Error) - - notifications.open({ - text: commonMessages.notification.failed, - type: 'error', - }) - - return Promise.reject(error) - } - finally { - setSubmitting(false) - } - }, [ - shares, - rewards, - actions, - address, - signSDK, - vaultAddress, - subgraphUpdate, - ]) - - return useMemo(() => ({ - submit, - isSubmitting, - }), [ - submit, - isSubmitting, - ]) -} - - -export default useUnboostSubmit diff --git a/src/views/HomeView/StakeContext/util/actions/useUnstake/index.ts b/src/views/HomeView/StakeContext/util/actions/useUnstake/index.ts deleted file mode 100644 index 98c664ac..00000000 --- a/src/views/HomeView/StakeContext/util/actions/useUnstake/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMemo } from 'react' - -import useSubmit from './useSubmit' -import useEstimateGas, { Type } from '../useEstimateGas' - - -type Output = ReturnType & { - getWithdrawGas: ReturnType -} - -interface Hook { - (params: StakePage.Params): Output - mock: Output -} - -const useUnstake: Hook = (params) => { - const { isSubmitting, submit } = useSubmit(params) - const getWithdrawGas = useEstimateGas(Type.Withdraw) - - return useMemo(() => ({ - isSubmitting, - getWithdrawGas, - submit, - }), [ - isSubmitting, - getWithdrawGas, - submit, - ]) -} - -useUnstake.mock = { - isSubmitting: false, - getWithdrawGas: () => Promise.resolve(0n), - submit: () => Promise.resolve(), -} - - -export default useUnstake diff --git a/src/views/HomeView/StakeContext/util/emptyBalance.ts b/src/views/HomeView/StakeContext/util/emptyBalance.ts deleted file mode 100644 index 192d3a8f..00000000 --- a/src/views/HomeView/StakeContext/util/emptyBalance.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { constants } from 'helpers' - - -export default constants.blockchain.amount100 * 1_000_000n diff --git a/src/views/HomeView/StakeContext/util/index.ts b/src/views/HomeView/StakeContext/util/index.ts deleted file mode 100644 index f40bc93e..00000000 --- a/src/views/HomeView/StakeContext/util/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Tab } from './enum' -import * as stakeCtx from './stakeCtx' -import emptyBalance from './emptyBalance' - - -export { - Tab, - stakeCtx, - emptyBalance, -} diff --git a/src/views/HomeView/StakeContext/util/stakeCtx.ts b/src/views/HomeView/StakeContext/util/stakeCtx.ts deleted file mode 100644 index 19215dd3..00000000 --- a/src/views/HomeView/StakeContext/util/stakeCtx.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { useMemo } from 'react' -import { useConfig } from 'config' -import { initContext } from 'helpers' -import { parseUnits, ZeroAddress } from 'ethers' -import { useStore, useAutoFetch, useSwapQuote, useSwapTokens } from 'hooks' - -import useFields from './useFields' -import useTabs, { tabsMock } from './useTabs' -import useBaseData, { baseDataMock } from './useBaseData' -import { - useBurn, - useMint, - useStake, - useBoost, - useUnboost, - useUnstake, -} from './actions' - -import { - useExitQueue, - useUnboostQueue, -} from './user' - -import { Tab } from './enum' -import useBalances from './useBalances' -import useVaultAddress from './useVaultAddress' - - -export const initialContext: StakePage.Context = { - tabs: tabsMock, - data: baseDataMock, - vaultAddress: ZeroAddress, - - burn: useBurn.mock, - mint: useMint.mock, - boost: useBoost.mock, - stake: { - ...useStake.mock, - isSwapQuoteFetching: false, - getBuyAmount: () => 0n, - }, - unboost: useUnboost.mock, - unstake: useUnstake.mock, - unstakeQueue: { claim: Promise.resolve }, - unboostQueue: { claim: Promise.resolve }, - - field: {} as Forms.Field, - percentField: {} as Forms.Field, - isFetching: false, -} - -const storeSelector = (store: Store) => ({ - isVaultFetching: store.vault.base.isFetching, -}) - -export const { - Provider, - useData, - useInit, -} = initContext(initialContext, () => { - const tabs = useTabs() - const { address } = useConfig() - const swapTokens = useSwapTokens() - const vaultAddress = useVaultAddress() - const fetchBalances = useBalances(vaultAddress) - const { isVaultFetching } = useStore(storeSelector) - const { refetchData, ...data } = useBaseData(vaultAddress) - const { fetchExitQueue, claimExitQueue } = useExitQueue(vaultAddress) - const { fetchUnboostQueue, claimUnboostQueue } = useUnboostQueue({ vaultAddress, fetchBalances }) - - const swapToken = swapTokens.selected - - const { fee, getBuyAmount, isFetching: isSwapQuoteFetching } = useSwapQuote({ - amount: address ? swapToken.balance : parseUnits('1', swapToken.units), - fromToken: swapToken.address, - }) - - const minStakeBalance = fee > 1n ? fee / 100n * 120n : 3n // 20% more than fee - - const { field, percentField } = useFields({ - tabs, - minBalance: tabs.value === Tab.Stake ? minStakeBalance : 0n, - depositTokenBalance: address ? swapToken.balance : swapToken.emptyBalance, - getDepositAmount: tabs.value === Tab.Stake && swapToken.address ? getBuyAmount : undefined, - }) - - const fetch = useMemo(() => ({ - data: refetchData, - balances: fetchBalances, - unstakeQueue: fetchExitQueue, - unboostQueue: fetchUnboostQueue, - }), [ - refetchData, - fetchBalances, - fetchExitQueue, - fetchUnboostQueue, - ]) - - useAutoFetch({ - action: fetchBalances, - interval: 15 * 60 * 1000, - skip: !address, - }) - - const params = useMemo(() => ({ - vaultAddress, - percentField, - field, - fetch, - }), [ - fetch, - field, - percentField, - vaultAddress, - ]) - - const burn = useBurn(params) - const mint = useMint(params) - const boost = useBoost(params) - const stake = useStake({ ...params, swapTokens }) - const unboost = useUnboost(params) - const unstake = useUnstake(params) - - const isFetching = data.isFetching || isVaultFetching - - return useMemo(() => ({ - data, - tabs, - field, - mint, - burn, - stake: { - ...stake, - getBuyAmount, - isSwapQuoteFetching, - }, - boost, - unboost, - unstake, - unstakeQueue: { - claim: claimExitQueue, - }, - unboostQueue: { - claim: claimUnboostQueue, - }, - percentField, - vaultAddress, - isFetching, - }), [ - data, - tabs, - field, - mint, - burn, - stake, - boost, - unstake, - unboost, - percentField, - vaultAddress, - isFetching, - isSwapQuoteFetching, - getBuyAmount, - claimExitQueue, - claimUnboostQueue, - ]) -}) diff --git a/src/views/HomeView/StakeContext/util/useBalances/index.ts b/src/views/HomeView/StakeContext/util/useBalances/index.ts deleted file mode 100644 index 026a2365..00000000 --- a/src/views/HomeView/StakeContext/util/useBalances/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react' -import { useActions, useMountedRef, useStore } from 'hooks' -import notifications from 'modules/notifications' -import { useConfig } from 'config' - -import useStake from './useStake' -import useBoost from './useBoost' -import useUserApy from './useUserApy' -import useWithdraw from './useWithdraw' -import useMintToken from './useMintToken' - -import messages from './messages' - - -const storeSelector = (store: Store) => ({ - ltvPercent: BigInt(store.vault.base.data.osTokenConfig.ltvPercent), - liqThresholdPercent: BigInt(store.vault.base.data.osTokenConfig.liqThresholdPercent), -}) - -const useBalances = (vaultAddress: string) => { - const actions = useActions() - const mountedRef = useMountedRef() - const { address, autoConnectChecked } = useConfig() - - const fetchStake = useStake() - const fetchBoost = useBoost() - const fetchUserApy = useUserApy() - const fetchWithdraw = useWithdraw() - const fetchMintToken = useMintToken() - - const { ltvPercent, liqThresholdPercent } = useStore(storeSelector) - - const storeDataRef = useRef({ ltvPercent, liqThresholdPercent }) - storeDataRef.current = { ltvPercent, liqThresholdPercent } - - const fetchBalances = useCallback(async () => { - const { ltvPercent, liqThresholdPercent } = storeDataRef.current - - if (!address && autoConnectChecked) { - actions.vault.user.balances.setFetching(false) - - return - } - - if (address && vaultAddress) { - try { - actions.vault.user.balances.setFetching(true) - - const stake = await fetchStake({ - userAddress: address, - vaultAddress, - }) - - const mintToken = await fetchMintToken({ - stakedAssets: stake.assets, - userAddress: address as string, - liqThresholdPercent, - vaultAddress, - ltvPercent, - }) - - const withdraw = await fetchWithdraw({ - mintedAssets: mintToken.minted.assets, - stakedAssets: stake.assets, - vaultAddress, - ltvPercent, - }) - - const [ boost, userAPY ] = await Promise.all([ - fetchBoost({ - userAddress: address, - vaultAddress, - }), - fetchUserApy({ - userAddress: address, - vaultAddress, - }), - ]) - - const result: Omit = { - stake, - boost, - userAPY, - withdraw, - mintToken, - } - - if (mountedRef.current) { - actions.vault.user.balances.setData(result) - } - } - catch (error) { - console.error(error) - actions.vault.user.balances.setFetching(false) - - notifications.open({ - type: 'error', - text: messages.error, - }) - } - } - }, [ - address, - actions, - mountedRef, - vaultAddress, - autoConnectChecked, - fetchStake, - fetchBoost, - fetchUserApy, - fetchWithdraw, - fetchMintToken, - ]) - - useEffect(() => { - if (!address && autoConnectChecked) { - actions.vault.user.balances.resetData() - } - }, [ actions, address, autoConnectChecked ]) - - useEffect(() => { - return () => { - actions.vault.user.balances.resetData() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return fetchBalances -} - - -export default useBalances diff --git a/src/views/HomeView/StakeContext/util/useBalances/useStake.ts b/src/views/HomeView/StakeContext/util/useBalances/useStake.ts deleted file mode 100644 index b54bd718..00000000 --- a/src/views/HomeView/StakeContext/util/useBalances/useStake.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useCallback } from 'react' -import { initialState } from 'store/store/vault' -import { constants } from 'helpers' -import { useConfig } from 'config' - - -type Input = { - vaultAddress: string - userAddress: string -} - -const useStake = () => { - const { sdk } = useConfig() - - return useCallback(async (values: Input) => { - try { - const response = await sdk.vault.getStakeBalance(values) - - const result: Store['vault']['user']['balances']['stake'] = response - - return { - assets: result.assets > constants.blockchain.minimalAmount - ? result.assets - : 0n, - } - } - catch (error) { - console.error('fetch vault stake user data error', error as Error) - - return initialState.user.balances.stake - } - }, [ sdk ]) -} - - -export default useStake diff --git a/src/views/HomeView/StakeContext/util/useBaseData/index.ts b/src/views/HomeView/StakeContext/util/useBaseData/index.ts deleted file mode 100644 index 4a871f5d..00000000 --- a/src/views/HomeView/StakeContext/util/useBaseData/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useCallback, useMemo } from 'react' -import { useObjectState, useStore } from 'hooks' -import notifications from 'modules/notifications' -import { commonMessages } from 'helpers' -import { useConfig } from 'config' -import methods from 'helpers/methods' - -import useAPY from './useAPY' -import useUserRewards from './useUserRewards' - - -const storeSelector = (store: Store) => ({ - userAPY: store.vault.user.balances.userAPY, - totalAssets: store.vault.base.data.totalAssets, - isBalancesFetching: store.vault.user.balances.isFetching, -}) - -const initialState: StakePage.BaseData = { - ltvPercent: 0n, - fee: methods.formatApy(0), - userRewards: 0n, - isFetching: true, - apy: { - vault: 0, - maxBoost: 0, - mintToken: 0, - }, -} - -export const baseDataMock: StakePage.Data = { - ...initialState, - apy: { - ...initialState.apy, - user: 0, - }, - tvl: methods.formatApy(0), - refetchData: () => Promise.resolve(), -} - -const useBaseData = (vaultAddress: string) => { - const { sdk } = useConfig() - const [ state, setState ] = useObjectState(initialState) - const { totalAssets, userAPY, isBalancesFetching } = useStore(storeSelector) - - const fetchAPY = useAPY(vaultAddress) - const fetchUserRewards = useUserRewards(vaultAddress) - - const tvl = useMemo(() => { - return `${methods.formatTokenValue(totalAssets)} ${sdk.config.tokens.depositToken}` - }, [ sdk, totalAssets ]) - - const fetchData = useCallback(async () => { - try { - const [ apyData, userRewards ] = await Promise.all([ - fetchAPY(), - fetchUserRewards(), - ]) - - const { apy, fee, ltvPercent } = apyData - - setState({ - apy, - fee, - ltvPercent, - userRewards, - isFetching: false, - }) - } - catch (error) { - console.log({ error }) - notifications.open({ - text: commonMessages.notification.somethingWentWrong, - type: 'error', - }) - } - finally { - setState({ isFetching: false }) - } - }, [ fetchAPY, fetchUserRewards, setState ]) - - useEffect(() => { - fetchData() - }, [ fetchData ]) - - return useMemo(() => ({ - ...state, - apy: { - ...state.apy, - user: userAPY, - }, - tvl, - isFetching: state.isFetching || isBalancesFetching, - refetchData: fetchData, - }), [ - tvl, - userAPY, - state, - isBalancesFetching, - fetchData, - ]) -} - - -export default useBaseData diff --git a/src/views/HomeView/StakeContext/util/useBaseData/useAPY.ts b/src/views/HomeView/StakeContext/util/useBaseData/useAPY.ts deleted file mode 100644 index 5fc3ab98..00000000 --- a/src/views/HomeView/StakeContext/util/useBaseData/useAPY.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useCallback } from 'react' -import { ZeroAddress } from 'ethers' -import { useConfig } from 'config' -import methods from 'helpers/methods' - - -type ApyQueryPayload = { - osToken: { - apy: number - feePercent: number - } - vaults: { - apy: number - feePercent: number - osTokenHolderMaxBoostApy: number - osTokenConfig: { - ltvPercent: number - } - }[] -} - -const useAPY = (vaultAddress: string) => { - const { sdk, address } = useConfig() - - return useCallback(async () => { - try { - const data = await methods.fetch(sdk.config.api.subgraph, { - method: 'POST', - body: JSON.stringify({ - query: ` - query Apy($userAddress: ID!, $vaultAddress: ID!) { - osToken(id: "1") { - apy - feePercent - } - vaults(where: { id: $vaultAddress }) { - apy - feePercent - osTokenHolderMaxBoostApy - osTokenConfig { - ltvPercent - } - } - } - `, - variables: { - vaultAddress: vaultAddress.toLowerCase(), - userAddress: address?.toLowerCase() || ZeroAddress, - }, - }), - }) - - const vaultData = data.vaults[0] - const mintTokenData = data.osToken - - const ltvPercent = BigInt(vaultData?.osTokenConfig?.ltvPercent || 0) - const fee = methods.formatApy((mintTokenData.feePercent + vaultData.feePercent) / 100) - - const apy = { - vault: Number(vaultData.apy), - mintToken: Number(mintTokenData.apy), - maxBoost: Number(vaultData.osTokenHolderMaxBoostApy), - } - - return { - fee, - apy, - ltvPercent, - } - } - catch (error) { - console.error(error) - return Promise.reject('Stake: fetchAPY error') - } - }, [ sdk, address, vaultAddress ]) -} - - -export default useAPY diff --git a/src/views/HomeView/StakeContext/util/useBaseData/useUserRewards.ts b/src/views/HomeView/StakeContext/util/useBaseData/useUserRewards.ts deleted file mode 100644 index b3740c16..00000000 --- a/src/views/HomeView/StakeContext/util/useBaseData/useUserRewards.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useCallback } from 'react' -import { BigDecimal } from 'sdk' -import { parseEther } from 'ethers' -import { useConfig } from 'config' - -import useVaultDays from './useVaultDays' - - -const useUserRewards = (vaultAddress: string) => { - const { sdk, address } = useConfig() - - const fetchDaysSinceVaultCreation = useVaultDays(vaultAddress) - - return useCallback(async () => { - if (!address) { - return 0n - } - - const daysCount = await fetchDaysSinceVaultCreation() - - const data = await sdk.vault.getUserStats({ - userAddress: address, - vaultAddress, - daysCount, - }) - - const rewards = data.rewards.reduce((acc, item) => { - return new BigDecimal(acc).plus(item.value).toNumber() - }, 0) - - return parseEther(new BigDecimal(rewards).toString()) - }, [ sdk, address, vaultAddress, fetchDaysSinceVaultCreation ]) -} - - -export default useUserRewards diff --git a/src/views/HomeView/StakeContext/util/useBaseData/useVaultDays.ts b/src/views/HomeView/StakeContext/util/useBaseData/useVaultDays.ts deleted file mode 100644 index a934cff8..00000000 --- a/src/views/HomeView/StakeContext/util/useBaseData/useVaultDays.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react' -import { getters, requests } from 'helpers' -import { useConfig } from 'config' - - -const day = 24 * 60 * 60 - -const useVaultDays = (vaultAddress: string) => { - const { sdk } = useConfig() - - const daysCountRef = useRef(null) - - useEffect(() => { - daysCountRef.current = null - }, [ vaultAddress ]) - - return useCallback(async () => { - if (daysCountRef.current) { - return daysCountRef.current - } - - if (vaultAddress) { - const data = await requests.fetchCreatedAt({ - url: sdk.config.api.subgraph, - variables: { - address: vaultAddress.toLowerCase(), - }, - }) - - const createdTime = Number(data?.vault?.createdAt || 0) * 1000 - const startOfDay = getters.unixDate.getUnixStartOfDay() - - const time = createdTime / 1000 - - let vaultCreationDay = startOfDay - - if (time < startOfDay) { - const daysOffset = Math.ceil((startOfDay - time) / day) - - vaultCreationDay = getters.unixDate.getUnixStartOfDayOffset(daysOffset) - } - - const timeDiff = startOfDay - vaultCreationDay - - daysCountRef.current = Math.ceil(timeDiff / day) - - return daysCountRef.current - } - - return 0 - }, [ sdk, vaultAddress ]) -} - - -export default useVaultDays diff --git a/src/views/HomeView/StakeContext/util/useFields.ts b/src/views/HomeView/StakeContext/util/useFields.ts deleted file mode 100644 index 7cc56c76..00000000 --- a/src/views/HomeView/StakeContext/util/useFields.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { useRef, useMemo, useCallback } from 'react' -import { useStore, useChainChanged, useAddressChanged, useStakeField } from 'hooks' -import forms from 'modules/forms' - -import { Tab } from './enum' - - -type Input = { - tabs: StakePage.Tabs.Data - minBalance: bigint - depositTokenBalance: bigint - getDepositAmount?: (value: bigint) => bigint -} - - -const storeSelector = (store: Store) => ({ - mintedShares: store.vault.user.balances.mintToken.minted.shares, - maxMintShares: store.vault.user.balances.mintToken.maxMintShares, - mintTokenBalance: store.account.balances.data.mintTokenBalance, - maxWithdrawAssets: store.vault.user.balances.withdraw.maxAssets, -}) - -const useFields = (values: Input) => { - const { tabs, minBalance, depositTokenBalance, getDepositAmount } = values - - const tabRef = useRef(tabs.value) - - const { - mintedShares, - maxMintShares, - mintTokenBalance, - maxWithdrawAssets, - } = useStore(storeSelector) - - const maxBalance = useMemo(() => { - const isStake = tabs.value === Tab.Stake - - let balance = 0n - - if (isStake) { - balance = depositTokenBalance - } - else if (tabs.value === Tab.Unstake) { - balance = maxWithdrawAssets - } - else if (tabs.value === Tab.Mint) { - balance = maxMintShares - } - else if (tabs.value === Tab.Burn) { - balance = mintedShares > mintTokenBalance - ? mintTokenBalance - : mintedShares - } - else { - balance = mintTokenBalance - } - - return balance - }, [ - tabs, - mintedShares, - maxMintShares, - mintTokenBalance, - maxWithdrawAssets, - depositTokenBalance, - ]) - - const { field } = useStakeField({ - minBalance, - maxBalance, - getDepositAmount, - withCapacityCheck: tabs.value === Tab.Stake, - }) - - const percentField = forms.useField({ - valueType: 'string', - initialValue: '', - }) - - const resetForm = useCallback(() => { - field.reset() - percentField.reset() - }, [ field, percentField ]) - - if (tabRef.current !== tabs.value) { - resetForm() - tabRef.current = tabs.value - } - - useChainChanged(resetForm) - useAddressChanged(resetForm) - - return useMemo(() => ({ - field, - percentField, - }), [ - field, - percentField, - ]) -} - - -export default useFields diff --git a/src/views/HomeView/StakeContext/util/useTabs.ts b/src/views/HomeView/StakeContext/util/useTabs.ts deleted file mode 100644 index c7edd35a..00000000 --- a/src/views/HomeView/StakeContext/util/useTabs.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo, useCallback, useState } from 'react' -import { useChainChanged } from 'hooks' - -import { Tab } from './enum' - - -export const tabsMock = { - value: Tab.Stake, - setTab: (() => {}) as StakePage.Tabs.SetTab, -} - -const useTabs = () => { - const [ tab, setTab ] = useState(Tab.Stake) - - const handleSetTab = useCallback((tab: Tab) => { - const isValid = Object.values(Tab).includes(tab) - - if (isValid) { - setTab(tab) - } - }, []) - - useChainChanged(() => setTab(Tab.Stake)) - - return useMemo(() => ({ - value: tab, - setTab: handleSetTab, - }), [ - tab, - handleSetTab, - ]) -} - - -export default useTabs diff --git a/src/views/HomeView/StakeContext/util/user/index.ts b/src/views/HomeView/StakeContext/util/user/index.ts deleted file mode 100644 index 64a0fd77..00000000 --- a/src/views/HomeView/StakeContext/util/user/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as useExitQueue } from './useExitQueue' -export { default as useUnboostQueue } from './useUnboostQueue' diff --git a/src/views/HomeView/StakeContext/util/user/useExitQueue.ts b/src/views/HomeView/StakeContext/util/user/useExitQueue.ts deleted file mode 100644 index 7824f337..00000000 --- a/src/views/HomeView/StakeContext/util/user/useExitQueue.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useActions, useSubgraphUpdate, useMountedRef, useAutoFetch, useBalances } from 'hooks' -import { Action, openTxCompletedModal } from 'layouts/modals' -import notifications from 'modules/notifications' -import { commonMessages } from 'helpers' -import { AllocatorActionType } from 'sdk' -import { useConfig } from 'config' - - -type ClaimExitQueueInput = Store['vault']['user']['exitQueue']['data'] - -const useExitQueue = (vaultAddress: string) => { - const actions = useActions() - const mountedRef = useMountedRef() - const { signSDK, address, isGnosis } = useConfig() - - const subgraphUpdate = useSubgraphUpdate() - const [ autofetch, setAutofetch ] = useState(false) - - const { - refetchNativeTokenBalance, - refetchDepositTokenBalance, - } = useBalances() - - const fetchExitQueue = useCallback(async () => { - if (address && vaultAddress) { - try { - actions.vault.user.exitQueue.setFetching(true) - - const exitQueue = await signSDK.vault.getExitQueuePositions({ - userAddress: address, - isClaimed: false, - vaultAddress, - }) - - if (mountedRef.current) { - actions.vault.user.exitQueue.setData({ - withdrawable: exitQueue.withdrawable, - positions: exitQueue.positions, - duration: exitQueue.duration, - requests: exitQueue.requests, - total: exitQueue.total, - }) - - if (exitQueue.total) { - const needAutofetch = exitQueue.duration === null - - setAutofetch(needAutofetch) - } - } - } - catch (error: any) { - console.error('Fetch ExitQueue error', error) - actions.vault.user.exitQueue.setFetching(false) - } - } - }, [ signSDK, actions, address, mountedRef, vaultAddress ]) - - const claimExitQueue = useCallback(async (exitQueueData: ClaimExitQueueInput) => { - if (vaultAddress && address) { - actions.ui.setBottomLoader({ - content: commonMessages.notification.waitingConfirmation, - }) - - try { - actions.vault.user.exitQueue.setFetching(true) - - const hash = await signSDK.vault.claimExitQueue({ - positions: exitQueueData.positions, - userAddress: address, - vaultAddress, - }) - - if (hash) { - await subgraphUpdate({ hash }) - - fetchExitQueue() - refetchDepositTokenBalance() - - if (isGnosis) { - refetchNativeTokenBalance() - } - - const blockExplorerUrl = signSDK.config.network.blockExplorerUrl - - actions.vault.user.allocatorActions.addFirstItem({ - hash, - assets: exitQueueData.withdrawable, - actionType: AllocatorActionType.ExitedAssetsClaimed, - link: blockExplorerUrl, - }) - - const tokens = [ - { - token: signSDK.config.tokens.depositToken, - value: exitQueueData.withdrawable, - action: Action.Unstake, - }, - ] - - openTxCompletedModal({ tokens, hash }) - } - } - catch (error: any) { - actions.ui.resetBottomLoader() - actions.vault.user.exitQueue.setFetching(false) - console.error('Claim error', error as Error) - - notifications.open({ - type: 'error', - text: commonMessages.notification.failed, - }) - } - } - }, [ - signSDK, - actions, - address, - isGnosis, - vaultAddress, - fetchExitQueue, - subgraphUpdate, - refetchNativeTokenBalance, - refetchDepositTokenBalance, - ]) - - useEffect(() => { - fetchExitQueue() - }, [ fetchExitQueue ]) - - useEffect(() => { - if (!address) { - actions.vault.user.exitQueue.resetData() - } - }, [ actions, address ]) - - useEffect(() => { - return () => { - actions.vault.user.exitQueue.resetData() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useAutoFetch({ - action: fetchExitQueue, - skip: !autofetch, - interval: 60_000, - }) - - return useMemo(() => ({ - fetchExitQueue, - claimExitQueue, - }), [ - fetchExitQueue, - claimExitQueue, - ]) -} - - -export default useExitQueue diff --git a/src/views/HomeView/StakeContext/util/user/useUnboostQueue.ts b/src/views/HomeView/StakeContext/util/user/useUnboostQueue.ts deleted file mode 100644 index acacbf41..00000000 --- a/src/views/HomeView/StakeContext/util/user/useUnboostQueue.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useActions, useSubgraphUpdate, useMountedRef, useAutoFetch, useBalances } from 'hooks' -import { Action, openTxCompletedModal } from 'layouts/modals' -import notifications from 'modules/notifications' -import { commonMessages } from 'helpers' -import { useConfig } from 'config' - - -type Input = { - vaultAddress: string - fetchBalances: () => Promise -} - -type ClaimUnboostQueueInput = Store['vault']['user']['unboostQueue']['data'] - -const useUnboostQueue = (values: Input) => { - const { vaultAddress, fetchBalances } = values - - const actions = useActions() - const mountedRef = useMountedRef() - const [ autofetch, setAutofetch ] = useState(false) - const { sdk, signSDK, address, isEthereum } = useConfig() - - const subgraphUpdate = useSubgraphUpdate() - const { refetchMintTokenBalance, refetchDepositTokenBalance } = useBalances() - - const fetchUnboostQueue = useCallback(async () => { - if (address && vaultAddress && isEthereum) { - try { - actions.vault.user.unboostQueue.setFetching(true) - - const unboostQueue = await sdk.boost.getQueuePosition({ - userAddress: address, - vaultAddress, - }) - - if (mountedRef.current) { - actions.vault.user.unboostQueue.setData(unboostQueue) - - if (unboostQueue.position) { - const needAutofetch = unboostQueue.duration === null - - setAutofetch(needAutofetch) - } - } - } - catch (error: any) { - console.error('Fetch UnboostQueue error', error) - actions.vault.user.unboostQueue.setFetching(false) - } - } - }, [ sdk, actions, address, mountedRef, vaultAddress, isEthereum ]) - - const claimUnboostQueue = useCallback(async (values: ClaimUnboostQueueInput) => { - const { exitingAssets, exitingShares, position } = values - - if (vaultAddress && address && position) { - actions.ui.setBottomLoader({ - content: commonMessages.notification.waitingConfirmation, - }) - - try { - actions.vault.user.unboostQueue.setFetching(true) - - const hash = await signSDK.boost.claimQueue({ - userAddress: address, - vaultAddress, - position, - }) - - if (hash) { - await subgraphUpdate({ hash }) - - fetchBalances() - fetchUnboostQueue() - refetchMintTokenBalance() - refetchDepositTokenBalance() - - const tokens = [ - { - token: signSDK.config.tokens.mintToken, - action: Action.Receive, - value: exitingShares, - }, - { - token: signSDK.config.tokens.depositToken, - action: Action.Receive, - value: exitingAssets, - }, - ] - - openTxCompletedModal({ tokens, hash }) - } - } - catch (error: any) { - actions.ui.resetBottomLoader() - actions.vault.user.unboostQueue.setFetching(false) - console.error('Claim unboost error', error as Error) - - notifications.open({ - type: 'error', - text: commonMessages.notification.failed, - }) - } - } - }, [ - signSDK, - actions, - address, - vaultAddress, - fetchBalances, - subgraphUpdate, - fetchUnboostQueue, - refetchMintTokenBalance, - refetchDepositTokenBalance, - ]) - - useEffect(() => { - fetchUnboostQueue() - }, [ fetchUnboostQueue ]) - - useEffect(() => { - if (!address) { - actions.vault.user.unboostQueue.resetData() - } - }, [ actions, address ]) - - useEffect(() => { - return () => { - actions.vault.user.unboostQueue.resetData() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useAutoFetch({ - action: fetchUnboostQueue, - skip: !autofetch, - interval: 60_000, - }) - - return useMemo(() => ({ - fetchUnboostQueue, - claimUnboostQueue, - }), [ - fetchUnboostQueue, - claimUnboostQueue, - ]) -} - - -export default useUnboostQueue diff --git a/src/views/HomeView/common/ApyBreakdown/ApyBreakdown.tsx b/src/views/HomeView/common/ApyBreakdown/ApyBreakdown.tsx deleted file mode 100644 index c035e2d9..00000000 --- a/src/views/HomeView/common/ApyBreakdown/ApyBreakdown.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { ReactNode } from 'react' -import { useStore } from 'hooks' - -import { PopupInfo } from 'components' - -import Details from './Details/Details' - -import { useApyDetails } from './util' - - -const storeSelector = (store: Store) => ({ - apy: store.vault.base.data.apy, - isMoreV2: store.vault.base.data.versions.isMoreV2, - maxBoostApy: store.vault.base.data.allocatorMaxBoostApy, -}) - -type ApyBreakdownProps = { - className?: string - buttonClassName?: string - children: ReactNode - withText?: boolean -} - -const ApyBreakdown: React.FC = (props) => { - const { className, buttonClassName, children, withText } = props - - const { data } = useApyDetails() - const { apy, isMoreV2, maxBoostApy } = useStore(storeSelector) - - const isBoostProfitable = maxBoostApy > apy && isMoreV2 - const isPopupEnabled = isBoostProfitable && withText || Boolean(data.length) - - if (isPopupEnabled) { - return ( - -
- - ) - } - - return null -} - - -export default React.memo(ApyBreakdown) diff --git a/src/views/HomeView/common/ApyBreakdown/util/index.ts b/src/views/HomeView/common/ApyBreakdown/util/index.ts deleted file mode 100644 index 3c708665..00000000 --- a/src/views/HomeView/common/ApyBreakdown/util/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useApyDetails } from './useApyDetails' diff --git a/src/views/HomeView/common/ApyBreakdown/util/useApyDetails.ts b/src/views/HomeView/common/ApyBreakdown/util/useApyDetails.ts deleted file mode 100644 index 2a3970d6..00000000 --- a/src/views/HomeView/common/ApyBreakdown/util/useApyDetails.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useCallback, useEffect } from 'react' -import { useObjectState, useStore } from 'hooks' -import { useConfig } from 'config' - - -type ApiData = { - apy: string - token: string - endTimestamp?: string -} - -type State = { - data: ApiData[] - isFetching: boolean -} - -const storeSelector = (store: Store) => ({ - apy: store.vault.base.data.apy, - baseApy: store.vault.base.data.baseApy, - vaultAddress: store.vault.base.data.vaultAddress, -}) - -const initialState: State = { - isFetching: false, - data: [], -} - -const useApyDetails = () => { - const { sdk } = useConfig() - const { apy, baseApy, vaultAddress } = useStore(storeSelector) - - const [ state, setState ] = useObjectState(initialState) - - const getPeriodicDistributions = useCallback(async () => { - try { - setState({ isFetching: true }) - - const timestamp = Math.floor(Date.now() / 1000) - - const response = await sdk.vault.getPeriodicDistributions({ - endTimestamp: timestamp, - startTimestamp: timestamp, - vaultAddress: vaultAddress.toLowerCase(), - }) - - if (response.length) { - setState({ - data: [ - ...response, - { - apy: String(baseApy), - token: sdk.config.addresses.tokens.depositToken.toLowerCase(), - }, - ], - isFetching: false, - }) - } - - setState({ isFetching: false }) - } - catch { - setState(initialState) - } - }, [ sdk, baseApy, vaultAddress, setState ]) - - useEffect(() => { - if (vaultAddress && apy !== baseApy) { - getPeriodicDistributions() - } - }, [ apy, baseApy, vaultAddress, getPeriodicDistributions ]) - - return state -} - - -export default useApyDetails diff --git a/src/views/HomeView/common/Button/SubmitButton/SubmitButton.tsx b/src/views/HomeView/common/Button/SubmitButton/SubmitButton.tsx deleted file mode 100644 index 7ab90400..00000000 --- a/src/views/HomeView/common/Button/SubmitButton/SubmitButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import { useConfig } from 'config' -import forms from 'modules/forms' -import { commonMessages } from 'helpers' - -import { Button as ButtonComponent } from 'components' -import { stakeCtx, Tab } from 'views/HomeView/StakeContext/util' -import type { ButtonProps as ButtonComponentProps } from 'components' - - -export type SubmitButtonProps = { - className?: string - title: Intl.Message - loading?: boolean - disabled?: boolean - color?: ButtonComponentProps['color'] - onClick: () => void -} - -const SubmitButton: React.FC = (props) => { - const { className, title, loading, disabled, color, onClick } = props - - const { isReadOnlyMode } = useConfig() - const { tabs, field, percentField } = stakeCtx.useData() - - const { value, error } = forms.useFieldValue(tabs.value === Tab.Unboost ? percentField : field) - - const buttonProps: ButtonComponentProps = { - className, - loading, - size: 'xl', - type: "submit", - fullWidth: true, - color: color || 'primary', - dataTestId: 'submit-button', - title: disabled || value ? title : commonMessages.enterAmount, - disabled: disabled || !value || Boolean(error) || isReadOnlyMode, - onClick, - } - - return ( - - ) -} - - -export default React.memo(SubmitButton) diff --git a/src/views/HomeView/common/Input/Input.tsx b/src/views/HomeView/common/Input/Input.tsx deleted file mode 100644 index a102a4c7..00000000 --- a/src/views/HomeView/common/Input/Input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react' -import { useStore } from 'hooks' -import { useConfig } from 'config' - -import { stakeCtx, emptyBalance, Tab } from 'views/HomeView/StakeContext/util' -import { TokenAmountInput } from 'components' - - -const storeSelector = (store: Store) => ({ - exitingPercent: store.vault.user.balances.boost.exitingPercent, -}) - -type InputProps = { - className?: string - balance: bigint - token: Tokens - isLoading?: boolean - balanceTitle?: Intl.Message - onMaxButtonClick?: () => void -} - -const Input: React.FC = (props) => { - const { className, balance, token, balanceTitle, isLoading, onMaxButtonClick } = props - - const { address } = useConfig() - const { exitingPercent } = useStore(storeSelector) - const { field, tabs, stake, unstake, boost, unboost } = stakeCtx.useData() - - const isDisabled = Boolean(exitingPercent) && [ Tab.Boost, Tab.Unboost ].includes(tabs.value) - const isSubmitting = ( - isLoading - || isDisabled - || stake.isSubmitting - || boost.isSubmitting - || unstake.isSubmitting - || unboost.isSubmitting - ) - - let tokenBalance: bigint | undefined = 0n - - if (!address) { - tokenBalance = tabs.value === Tab.Stake - ? emptyBalance - : undefined - } - else { - tokenBalance = balance - } - - return ( - - ) -} - - -export default React.memo(Input) diff --git a/src/views/HomeView/common/Table/Option/Value/TextValue/TextValue.tsx b/src/views/HomeView/common/Table/Option/Value/TextValue/TextValue.tsx deleted file mode 100644 index a5f49135..00000000 --- a/src/views/HomeView/common/Table/Option/Value/TextValue/TextValue.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' - -import { Text, Icon, TextProps, IconName } from 'components' - - -type Value = Partial & { - icon?: IconName -} - -export type TextValueProps = { - prev: Value - next: Value -} - -const TextValue: React.FC = (props) => { - const { prev, next } = props - - return ( -
- { - prev.icon && ( - - ) - } - - { - next.message && ( - <> - - - - ) - } -
- ) -} - - -export default React.memo(TextValue) diff --git a/src/views/HomeView/common/Table/Option/Value/TokenValue/TokenValue.tsx b/src/views/HomeView/common/Table/Option/Value/TokenValue/TokenValue.tsx deleted file mode 100644 index 369b1608..00000000 --- a/src/views/HomeView/common/Table/Option/Value/TokenValue/TokenValue.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' - -import { Icon, TokenAmount, TokenAmountProps } from 'components' - - -export type TokenValueProps = { - next: { - value: bigint | null - color?: TokenAmountProps['textColor'] - dataTestId?: string - } - prev: { - value: bigint - color?: TokenAmountProps['textColor'] - dataTestId?: string - } - token: Tokens -} - -const TokenValue: React.FC = (props) => { - const { token, prev, next } = props - - return ( -
- - { - typeof next.value === 'bigint' && ( - <> - - - - ) - } -
- ) -} - - -export default React.memo(TokenValue) diff --git a/src/views/HomeView/common/Table/Option/Value/Value.tsx b/src/views/HomeView/common/Table/Option/Value/Value.tsx deleted file mode 100644 index 8c3ca3c7..00000000 --- a/src/views/HomeView/common/Table/Option/Value/Value.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' - -import { Loading } from 'components' - -import TextValue, { TextValueProps } from './TextValue/TextValue' -import TokenValue, { TokenValueProps } from './TokenValue/TokenValue' - - -export type ValueProps = { - isFetching?: boolean - textValue?: TextValueProps - tokenValue?: TokenValueProps -} - -const Value: React.FC = (props) => { - const { textValue, tokenValue, isFetching } = props - - if (isFetching) { - return ( - - ) - } - - if (tokenValue) { - return ( - - ) - } - - if (textValue) { - return ( - - ) - } - - return null -} - - -export default React.memo(Value) diff --git a/src/views/HomeView/content/Balance/ClaimableWithdrawals/ClaimableWithdrawals.tsx b/src/views/HomeView/content/Balance/ClaimableWithdrawals/ClaimableWithdrawals.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/views/HomeView/content/Boost/Boost.tsx b/src/views/HomeView/content/Boost/Boost.tsx deleted file mode 100644 index 27908e65..00000000 --- a/src/views/HomeView/content/Boost/Boost.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' - -import { Form } from 'components' - -import BoostInput from './BoostInput/BoostInput' -import BoostContent from './BoostContent/BoostContent' - - -const Boost: React.FC = () => ( -
- - - -) - - -export default React.memo(Boost) diff --git a/src/views/HomeView/content/Boost/BoostContent/BoostContent.tsx b/src/views/HomeView/content/Boost/BoostContent/BoostContent.tsx deleted file mode 100644 index bd30a91c..00000000 --- a/src/views/HomeView/content/Boost/BoostContent/BoostContent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import { useStore } from 'hooks' - -import { ExitQueueNote } from 'views/HomeView/common' - -import BoostInfo from './BoostInfo/BoostInfo' -import SubmitButton from './SubmitButton/SubmitButton' - - -const storeSelector = (store: Store) => ({ - exitingPercent: store.vault.user.balances.boost.exitingPercent, -}) - -type BoostContentProps = { - className?: string -} - -const BoostContent: React.FC = (props) => { - const { className } = props - - const { exitingPercent } = useStore(storeSelector) - - return ( -
- - { - exitingPercent ? ( - - ) : ( - - ) - } -
- ) -} - - -export default React.memo(BoostContent) diff --git a/src/views/HomeView/content/Boost/BoostContent/BoostInfo/BoostInfo.tsx b/src/views/HomeView/content/Boost/BoostContent/BoostInfo/BoostInfo.tsx deleted file mode 100644 index 1ff99573..00000000 --- a/src/views/HomeView/content/Boost/BoostContent/BoostInfo/BoostInfo.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import { commonMessages } from 'helpers' -import { openGuideModal, GuideModal } from 'layouts/modals' - -import { Table, StakeStats } from 'views/HomeView/common' -import { stakeCtx } from 'views/HomeView/StakeContext/util' - -import { FieldValid, Href, Text } from 'components' - -import { useOptions } from './util' - - -type BoostInfoProps = { - className?: string -} - -const BoostInfo: React.FC = (props) => { - const { className } = props - - const { field } = stakeCtx.useData() - const options = useOptions() - - return ( -
- - { - (isValid) => ( - isValid ? ( - - ) : ( - - ) - ) - } - -
- openGuideModal({ ltv: 100 })} - > - - -
- - - ) -} - - -export default React.memo(BoostInfo) diff --git a/src/views/HomeView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts b/src/views/HomeView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts deleted file mode 100644 index eab7b6a1..00000000 --- a/src/views/HomeView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { stakeCtx } from 'views/HomeView/StakeContext/util' - -import { usePosition } from '../../../../util' - - -const useOptions = () => { - const { field } = stakeCtx.useData() - - return usePosition({ - type: 'boost', - field, - }) -} - - -export default useOptions diff --git a/src/views/HomeView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx b/src/views/HomeView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx deleted file mode 100644 index 6fdc24f3..00000000 --- a/src/views/HomeView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useCallback } from 'react' -import forms from 'modules/forms' -import { commonMessages } from 'helpers' -import { useStore } from 'hooks' -import { useConfig } from 'config' - -import { openTransactionsFlowModal } from 'layouts/modals' -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Button } from 'views/HomeView/common' -import { Tooltip } from 'components' - -import { useBoostSupplyCapsCheck } from './util' - -import messages from './messages' - - -const storeSelector = (store: Store) => ({ - vaultApy: store.vault.base.data.apy, - ltvPercent: store.vault.base.data.osTokenConfig.ltvPercent, - maxBoostApy: store.vault.base.data.allocatorMaxBoostApy, - exitingPercent: store.vault.user.balances.boost.exitingPercent, -}) - -type SubmitButtonProps = { - className?: string -} - -const SubmitButton: React.FC = (props) => { - const { className } = props - - const { isGnosis } = useConfig() - const { boost, field } = stakeCtx.useData() - const { value, error } = forms.useFieldValue(field) - const { vaultApy, ltvPercent, maxBoostApy, exitingPercent } = useStore(storeSelector) - - const { isFetching, checkSupplyCap } = useBoostSupplyCapsCheck({ - ltvPercent: BigInt(ltvPercent), - skip: isGnosis, - }) - - const isDisabled = exitingPercent > 0 - const isNotProfitable = vaultApy >= maxBoostApy - - const title = isNotProfitable - ? messages.notProfitable - : commonMessages.buttonTitle.boost - - const isValidSupplyCap = error - ? true - : checkSupplyCap(value || 0n) - - const disabled = ( - isDisabled - || isFetching - || isNotProfitable - || !isValidSupplyCap - || boost.isSubmitting - || boost.isAllowanceFetching - ) - - const handleClick = useCallback(() => { - const amount = field.value || 0n - const isPermitRequired = amount > boost.allowance - - if (isPermitRequired) { - openTransactionsFlowModal({ - flow: 'boost', - onStart: ({ setTransaction }) => boost.submit({ amount, setTransaction }), - }) - } - else { - boost.submit({ amount }) - } - }, [ field, boost ]) - - const loading = ( - isFetching - || boost.isAllowanceFetching - ) - - const button = ( -
- ) : ( - - ) - ) - } - - ) -} - - -export default React.memo(StakeInfo) diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/useOptions.ts b/src/views/HomeView/content/Stake/StakeInfo/util/useOptions.ts deleted file mode 100644 index e937e415..00000000 --- a/src/views/HomeView/content/Stake/StakeInfo/util/useOptions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useMemo } from 'react' -import { useConfig } from 'config' - -import { Position } from '../../../util' - -import useStakeApy from './useStakeApy' -import useStakeRate from './useStakeRate' -import useStakeAssets from './useStakeAssets' -import useStakeNetworkCost from './useStakeNetworkCost' - - -const useOptions = () => { - const { address } = useConfig() - - const stakeApy = useStakeApy() - const stakeRate = useStakeRate() - const stakeAssets = useStakeAssets() - const stakeNetworkCost = useStakeNetworkCost() - - return useMemo(() => { - const result: Position[] = [ - stakeApy, - stakeAssets, - ] - - if (stakeRate) { - result.push(stakeRate) - } - - if (address) { - result.push(stakeNetworkCost) - } - - return result - }, [ address, stakeApy, stakeRate, stakeAssets, stakeNetworkCost ]) -} - - -export default useOptions diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeApy.ts b/src/views/HomeView/content/Stake/StakeInfo/util/useStakeApy.ts deleted file mode 100644 index 64f379ec..00000000 --- a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeApy.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react' -import { useFieldListener, useObjectState, useStore } from 'hooks' -import methods from 'helpers/methods' -import { useConfig } from 'config' -import { commonMessages } from 'helpers' - -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Position as Item, useGetApy } from 'views/HomeView/content/util' -import messages from 'views/HomeView/content/util/messages' - - -const storeSelector = (store: Store) => ({ - userAPY: store.vault.user.balances.userAPY, -}) - -const useStakeApy = () => { - const { sdk } = useConfig() - const { field, stake } = stakeCtx.useData() - - const { userAPY } = useStore(storeSelector) - - const initialStateRef = useRef({ - newAPY: userAPY, - isFetching: false, - }) - - const [ { newAPY, isFetching }, setState ] = useObjectState(initialStateRef.current) - - const { getBuyAmount, isSwapQuoteFetching } = stake - - const swapToken = stake.swapTokens.selected - - const getAPY = useGetApy({ type: 'stake' }) - - const handleGetAPY = useCallback(async () => { - const inputValue = field.value || 0n - const isValid = inputValue && !field.error - - if (!isValid) { - setState(initialStateRef.current) - - return - } - - const amount = swapToken.address ? getBuyAmount(inputValue) : inputValue - - setState({ isFetching: true }) - - const newAPY = await getAPY(amount) - - if (inputValue === field.value) { - setState({ newAPY, isFetching: false }) - } - }, [ field, swapToken, getAPY, getBuyAmount, setState ]) - - useFieldListener(field, handleGetAPY, 300) - - useEffect(() => { - handleGetAPY() - }, [ handleGetAPY ]) - - return useMemo(() => { - const prev: NonNullable['prev'] = { - message: methods.formatApy(userAPY), - dataTestId: 'apy', - } - - const next: NonNullable['next'] = { - dataTestId: 'apy', - } - - const formattedAPY = methods.formatApy(newAPY) - - if (formattedAPY !== prev.message) { - next.message = formattedAPY - } - - const result: Item = { - title: commonMessages.apy, - textValue: { - prev, - next, - }, - tooltip: { - ...messages.tooltips.apy, - values: { - depositToken: sdk.config.tokens.depositToken, - }, - }, - isFetching: isFetching || isSwapQuoteFetching, - } - - return result - }, [ sdk, userAPY, newAPY, isFetching, isSwapQuoteFetching ]) -} - - -export default useStakeApy diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeAssets.ts b/src/views/HomeView/content/Stake/StakeInfo/util/useStakeAssets.ts deleted file mode 100644 index 2d520fae..00000000 --- a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeAssets.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useMemo } from 'react' -import { useStore } from 'hooks' -import { useConfig } from 'config' -import { commonMessages } from 'helpers' -import forms from 'modules/forms' - -import { Position as Item } from 'views/HomeView/content/util' -import { stakeCtx } from 'views/HomeView/StakeContext/util' - - -const storeSelector = (store: Store) => ({ - stakedAssets: store.vault.user.balances.stake.assets, -}) - -const useStakeAssets = () => { - const { sdk } = useConfig() - const { field, stake } = stakeCtx.useData() - - const { value, error } = forms.useFieldValue(field) - const { stakedAssets } = useStore(storeSelector) - - const { getBuyAmount, isSwapQuoteFetching } = stake - - const depositToken = sdk.config.tokens.depositToken - const swapToken = stake.swapTokens.selected - - return useMemo(() => { - const inputValue = value || 0n - const isValid = Number(value) && typeof value === 'bigint' && !error - - const amount = swapToken.address ? getBuyAmount(inputValue) : inputValue - - const prev: NonNullable['prev'] = { - value: stakedAssets, - dataTestId: 'assets', - } - - const next: NonNullable['next'] = { - value: null, - dataTestId: 'assets', - } - - if (isValid) { - next.value = stakedAssets + amount - } - - const result: Item = { - title: { - ...commonMessages.staked, - values: { depositToken }, - }, - tokenValue: { - prev, - next, - token: depositToken, - }, - isFetching: isSwapQuoteFetching, - } - - return result - }, [ value, error, depositToken, stakedAssets, swapToken, getBuyAmount, isSwapQuoteFetching ]) -} - - -export default useStakeAssets diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeNetworkCost.ts b/src/views/HomeView/content/Stake/StakeInfo/util/useStakeNetworkCost.ts deleted file mode 100644 index f597aae0..00000000 --- a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeNetworkCost.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useMemo } from 'react' -import { useConfig } from 'config' -import { formatEther } from 'ethers' -import { useFiatValues } from 'hooks' -import { commonMessages } from 'helpers' - -import { Position as Item } from 'views/HomeView/content/util' -import { stakeCtx } from 'views/HomeView/StakeContext/util' - - -const useStakeNetworkCost = () => { - const { sdk } = useConfig() - const { stake } = stakeCtx.useData() - const { isSwapQuoteFetching } = stake - - const { fiatGas } = useFiatValues({ - fiatGas: { - token: sdk.config.tokens.nativeToken, - value: formatEther(stake.gas.deposit + stake.gas.approve), - isMinimal: true, - }, - }) - - return useMemo(() => ({ - title: commonMessages.transaction.networkCost, - textValue: { - prev: { - message: fiatGas.formattedValue, - icon: 'icon/gas', - }, - next: {}, - }, - tooltip: { - ...commonMessages.tooltip.gas, - values: { - nativeToken: sdk.config.tokens.nativeToken, - }, - }, - isFetching: isSwapQuoteFetching, - }), [ sdk, fiatGas, isSwapQuoteFetching ]) -} - - -export default useStakeNetworkCost diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeRate.ts b/src/views/HomeView/content/Stake/StakeInfo/util/useStakeRate.ts deleted file mode 100644 index 6d72b79f..00000000 --- a/src/views/HomeView/content/Stake/StakeInfo/util/useStakeRate.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useMemo } from 'react' -import { useConfig } from 'config' -import methods from 'helpers/methods' -import { parseUnits } from 'ethers' -import { commonMessages } from 'helpers' - -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Position as Item } from 'views/HomeView/content/util' -import messages from 'views/HomeView/content/util/messages' - - -const useStakeRate = () => { - const { sdk } = useConfig() - const { stake } = stakeCtx.useData() - - const { getBuyAmount, isSwapQuoteFetching } = stake - const selectedToken = stake.swapTokens.selected - - return useMemo(() => { - if (!selectedToken.address) { - return null - } - - const rateAmount = getBuyAmount(parseUnits('1', selectedToken.units)) - - return { - title: commonMessages.transaction.exchangeRate, - textValue: { - prev: { - message: `1 ${selectedToken.name} = ${methods.formatTokenValue(rateAmount)} ${sdk.config.tokens.depositToken}`, - }, - next: {}, - }, - tooltip: { - ...messages.tooltips.rate, - values: { - swapToken: selectedToken.name, - depositToken: sdk.config.tokens.depositToken, - }, - }, - isFetching: isSwapQuoteFetching, - } as Item - }, [ sdk, selectedToken, isSwapQuoteFetching ]) -} - - -export default useStakeRate diff --git a/src/views/HomeView/content/Stake/StakeInput/StakeInput.tsx b/src/views/HomeView/content/Stake/StakeInput/StakeInput.tsx deleted file mode 100644 index e078edd6..00000000 --- a/src/views/HomeView/content/Stake/StakeInput/StakeInput.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import { useStore } from 'hooks' -import { useConfig } from 'config' - -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { TokenDropdown, TokenAmountInputView } from 'components' - - -const storeSelector = (store: Store) => ({ - isSwapTokenRatesFetching: store.swapTokenRates.isFetching, - isSwapTokenBalancesFetching: store.account.swapTokenBalances.isFetching, -}) - -const StakeInput: React.FC = () => { - const { stake, field } = stakeCtx.useData() - const { address, isReadOnlyMode } = useConfig() - const { isSwapTokenRatesFetching, isSwapTokenBalancesFetching } = useStore(storeSelector) - - return ( - { - if (token !== stake.swapTokens.selected.address) { - field.reset() - stake.swapTokens.setSelected(token) - } - }} - /> - )} - dataTestId="amount-input" - onMaxButtonClick={address ? stake.onMaxButtonClick : undefined} - /> - ) -} - - -export default React.memo(StakeInput) diff --git a/src/views/HomeView/content/Unboost/Unboost.tsx b/src/views/HomeView/content/Unboost/Unboost.tsx deleted file mode 100644 index a2f5909b..00000000 --- a/src/views/HomeView/content/Unboost/Unboost.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' - -import { Form } from 'components' - -import UnboostInput from './UnboostInput/UnboostInput' -import UnboostContent from './UnboostContent/UnboostContent' - - -const Unboost: React.FC = () => ( -
- - - -) - - -export default React.memo(Unboost) diff --git a/src/views/HomeView/content/Unboost/UnboostContent/SubmitButton/SubmitButton.tsx b/src/views/HomeView/content/Unboost/UnboostContent/SubmitButton/SubmitButton.tsx deleted file mode 100644 index 3c4a775f..00000000 --- a/src/views/HomeView/content/Unboost/UnboostContent/SubmitButton/SubmitButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import { useStore } from 'hooks' -import { commonMessages } from 'helpers' - -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Button } from 'views/HomeView/common' - - -const storeSelector = (store: Store) => ({ - boostedShares: store.vault.user.balances.boost.shares, - exitingPercent: store.vault.user.balances.boost.exitingPercent, -}) - -type SubmitButtonProps = { - className?: string -} - -const SubmitButton: React.FC = (props) => { - const { className } = props - - const { data, unboost } = stakeCtx.useData() - const { boostedShares, exitingPercent } = useStore(storeSelector) - - const isDisabled = exitingPercent > 0 || boostedShares === 0n - - return ( -
+ ) : ( + + ) + } + { + leverageStrategyData.isUpgradeRequired && ( + + ) + } +
+ + + +
+ + ) +} + + +export default React.memo(BoostInfo) diff --git a/src/views/HomeView/content/Boost/BoostContent/BoostInfo/util/index.ts b/src/views/SwapView/content/Boost/BoostContent/BoostInfo/util/index.ts similarity index 100% rename from src/views/HomeView/content/Boost/BoostContent/BoostInfo/util/index.ts rename to src/views/SwapView/content/Boost/BoostContent/BoostInfo/util/index.ts diff --git a/src/views/SwapView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts b/src/views/SwapView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts new file mode 100644 index 00000000..951f844f --- /dev/null +++ b/src/views/SwapView/content/Boost/BoostContent/BoostInfo/util/useOptions.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react' +import { useConfig } from 'config' +import { formatEther } from 'ethers' +import { useFiatValues } from 'hooks' +import { commonMessages } from 'helpers' + +import { TableProps } from 'views/SwapView/common' + + +const useOptions = (gasPrice: bigint) => { + const { sdk } = useConfig() + + const { fiatGas } = useFiatValues({ + fiatGas: { + token: sdk.config.tokens.mintToken, + value: formatEther(gasPrice), + isMinimal: true, + }, + }) + + return useMemo(() => { + const initialOptions: TableProps['options'] = [] + + if (gasPrice) { + initialOptions.push({ + text: commonMessages.transaction.price, + value: fiatGas.formattedValue, + dataTestId: 'table-gas', + icon: 'icon/gas', + }) + } + + return initialOptions + }, [ fiatGas, gasPrice ]) +} + + +export default useOptions diff --git a/src/views/SwapView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx b/src/views/SwapView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx new file mode 100644 index 00000000..8089a731 --- /dev/null +++ b/src/views/SwapView/content/Boost/BoostContent/SubmitButton/SubmitButton.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { useStore } from 'hooks' +import { commonMessages } from 'helpers' + +import { vaultHooks } from 'views/SwapView/util' +import { SubmitButtonWrapper } from 'views/SwapView/common' + +import messages from './messages' + + +type BoostParams = Pick< + ReturnType, + 'submit' | 'field' | 'isBoostDisabled' | 'isBoostLoading' | 'boostDisabledTooltip' +> + +type SubmitButtonProps = BoostParams & { + className?: string +} + +const storeSelector = (store: Store) => ({ + vaultApy: store.vault.base.data.apy, + maxBoostApy: store.vault.base.data.allocatorMaxBoostApy, +}) + +const SubmitButton: React.FC = (props) => { + const { + className, + boostDisabledTooltip, + isBoostDisabled, + isBoostLoading, + field, + submit, + } = props + + const { vaultApy, maxBoostApy } = useStore(storeSelector) + + const isNotProfitable = vaultApy >= maxBoostApy + + const title = isNotProfitable + ? messages.notProfitable + : commonMessages.buttonTitle.boost + + return ( + + ) +} + + +export default React.memo(SubmitButton) diff --git a/src/views/HomeView/content/Boost/BoostContent/SubmitButton/messages.ts b/src/views/SwapView/content/Boost/BoostContent/SubmitButton/messages.ts similarity index 100% rename from src/views/HomeView/content/Boost/BoostContent/SubmitButton/messages.ts rename to src/views/SwapView/content/Boost/BoostContent/SubmitButton/messages.ts diff --git a/src/views/HomeView/content/Boost/BoostContent/SubmitButton/util/index.ts b/src/views/SwapView/content/Boost/BoostContent/SubmitButton/util/index.ts similarity index 100% rename from src/views/HomeView/content/Boost/BoostContent/SubmitButton/util/index.ts rename to src/views/SwapView/content/Boost/BoostContent/SubmitButton/util/index.ts diff --git a/src/views/HomeView/content/Boost/BoostContent/SubmitButton/util/useBoostSupplyCapsCheck.ts b/src/views/SwapView/content/Boost/BoostContent/SubmitButton/util/useBoostSupplyCapsCheck.ts similarity index 100% rename from src/views/HomeView/content/Boost/BoostContent/SubmitButton/util/useBoostSupplyCapsCheck.ts rename to src/views/SwapView/content/Boost/BoostContent/SubmitButton/util/useBoostSupplyCapsCheck.ts diff --git a/src/views/SwapView/content/Boost/BoostInput/BoostInput.tsx b/src/views/SwapView/content/Boost/BoostInput/BoostInput.tsx new file mode 100644 index 00000000..ce75eb7e --- /dev/null +++ b/src/views/SwapView/content/Boost/BoostInput/BoostInput.tsx @@ -0,0 +1,39 @@ +import React, { useCallback } from 'react' +import { useConfig } from 'config' + +import { TokenAmountInput } from 'components' + + +type Input = { + className?: string + balance: bigint + field: Forms.Field + isLoading: boolean +} + +const BoostInput: React.FC = (props) => { + const { className, field, balance, isLoading } = props + + const { sdk, address } = useConfig() + + const handleMaxClick = useCallback(() => { + field.setValue(balance) + }, [ field, balance ]) + + return ( + + ) +} + + +export default React.memo(BoostInput) diff --git a/src/views/HomeView/content/Burn/Burn.tsx b/src/views/SwapView/content/Burn/Burn.tsx similarity index 100% rename from src/views/HomeView/content/Burn/Burn.tsx rename to src/views/SwapView/content/Burn/Burn.tsx diff --git a/src/views/HomeView/content/Burn/BurnInfo/BurnInfo.tsx b/src/views/SwapView/content/Burn/BurnInfo/BurnInfo.tsx similarity index 67% rename from src/views/HomeView/content/Burn/BurnInfo/BurnInfo.tsx rename to src/views/SwapView/content/Burn/BurnInfo/BurnInfo.tsx index 936ea55d..fa8f98a1 100644 --- a/src/views/HomeView/content/Burn/BurnInfo/BurnInfo.tsx +++ b/src/views/SwapView/content/Burn/BurnInfo/BurnInfo.tsx @@ -1,9 +1,9 @@ import React from 'react' -import froms from 'modules/forms' +import forms from 'modules/forms' import { useConfig } from 'config' -import { Table } from 'views/HomeView/common' -import { stakeCtx } from 'views/HomeView/StakeContext/util' +import { Table } from 'views/SwapView/common' +import { swapCtx } from 'views/SwapView/util' import { useOptions } from './util' @@ -16,8 +16,8 @@ const BurnInfo: React.FC = (props) => { const { className } = props const { address } = useConfig() - const { field } = stakeCtx.useData() - const { value, error } = froms.useFieldValue(field) + const { burn } = swapCtx.useData() + const { value, error } = forms.useFieldValue(burn.field) const options = useOptions() diff --git a/src/views/HomeView/content/Burn/BurnInfo/util/index.ts b/src/views/SwapView/content/Burn/BurnInfo/util/index.ts similarity index 100% rename from src/views/HomeView/content/Burn/BurnInfo/util/index.ts rename to src/views/SwapView/content/Burn/BurnInfo/util/index.ts diff --git a/src/views/SwapView/content/Burn/BurnInfo/util/useOptions.ts b/src/views/SwapView/content/Burn/BurnInfo/util/useOptions.ts new file mode 100644 index 00000000..71a9de51 --- /dev/null +++ b/src/views/SwapView/content/Burn/BurnInfo/util/useOptions.ts @@ -0,0 +1,80 @@ +import { useMemo } from 'react' +import { useConfig } from 'config' +import { formatEther } from 'ethers' +import { useFiatValues, useStore } from 'hooks' +import { commonMessages, methods } from 'helpers' + +import { swapCtx, vaultHooks } from 'views/SwapView/util' +import { TableProps } from 'views/SwapView/common' + + +const storeSelector = (store: Store) => ({ + userAPY: store.vault.user.balances.userAPY, +}) + +const useOptions = () => { + const { address, sdk, isReadOnlyMode } = useConfig() + + const { userAPY } = useStore(storeSelector) + + const { burn } = swapCtx.useData() + + const { newAPY, isApyHidden, isFetching: isApyFetching } = vaultHooks.helpers.useAPY({ + field: burn.field, + type: 'burn', + }) + + const shares = vaultHooks.helpers.useShares({ + field: burn.field, + type: 'burn', + }) + + const { fiatGas } = useFiatValues({ + fiatGas: { + token: sdk.config.tokens.nativeToken, + value: formatEther(burn.transactionPrice), + isMinimal: true, + }, + }) + + return useMemo(() => { + const result: TableProps['options'] = [] + + if (shares) { + result.push(shares) + } + + if (address && !isReadOnlyMode) { + result.push({ + text: commonMessages.transaction.networkCost, + value: fiatGas.formattedValue, + tooltip: { + ...commonMessages.tooltip.gas, + values: { + nativeToken: sdk.config.tokens.nativeToken, + }, + }, + icon: 'icon/gas', + dataTestId: 'table-gas', + isFetching: isApyFetching, + }) + } + + if (!isApyHidden) { + result.unshift({ + values: { + prev: methods.formatApy(userAPY), + next: methods.formatApy(newAPY), + }, + text: commonMessages.apy, + isFetching: isApyFetching, + dataTestId: 'table-apy', + }) + } + + return result + }, [ address, fiatGas, isApyFetching, isApyHidden, isReadOnlyMode, newAPY, sdk, shares, userAPY ]) +} + + +export default useOptions diff --git a/src/views/SwapView/content/Burn/BurnInput/BurnInput.tsx b/src/views/SwapView/content/Burn/BurnInput/BurnInput.tsx new file mode 100644 index 00000000..6df76dcb --- /dev/null +++ b/src/views/SwapView/content/Burn/BurnInput/BurnInput.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from 'react' +import { useConfig } from 'config' +import { constants } from 'helpers' + +import { swapCtx } from 'views/SwapView/util' +import { TokenAmountInput } from 'components' + +import messages from './messages' + + +const BurnInput: React.FC = () => { + const { sdk, address } = useConfig() + const { burn } = swapCtx.useData() + + const onMaxButtonClick = useCallback(() => { + burn.field.setValue(burn.maxBurnShares) + }, [ burn ]) + + return ( + + ) +} + + +export default React.memo(BurnInput) diff --git a/src/views/HomeView/content/Burn/BurnInput/messages.ts b/src/views/SwapView/content/Burn/BurnInput/messages.ts similarity index 100% rename from src/views/HomeView/content/Burn/BurnInput/messages.ts rename to src/views/SwapView/content/Burn/BurnInput/messages.ts diff --git a/src/views/HomeView/content/Burn/SubmitButton/SubmitButton.tsx b/src/views/SwapView/content/Burn/SubmitButton/SubmitButton.tsx similarity index 59% rename from src/views/HomeView/content/Burn/SubmitButton/SubmitButton.tsx rename to src/views/SwapView/content/Burn/SubmitButton/SubmitButton.tsx index 42ed3b9e..e1c9ed47 100644 --- a/src/views/HomeView/content/Burn/SubmitButton/SubmitButton.tsx +++ b/src/views/SwapView/content/Burn/SubmitButton/SubmitButton.tsx @@ -1,8 +1,8 @@ import React from 'react' import { commonMessages } from 'helpers' -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Button } from 'views/HomeView/common' +import { swapCtx } from 'views/SwapView/util' +import { SubmitButtonWrapper } from 'views/SwapView/common' type SubmitButtonProps = { @@ -12,13 +12,15 @@ type SubmitButtonProps = { const SubmitButton: React.FC = (props) => { const { className } = props - const { burn } = stakeCtx.useData() + const { burn } = swapCtx.useData() return ( -
+ ) + } + + return ( + + ) +} + + +export default React.memo(StakeInfo) diff --git a/src/views/HomeView/content/Stake/StakeInfo/util/index.ts b/src/views/SwapView/content/Stake/StakeInfo/util/index.ts similarity index 100% rename from src/views/HomeView/content/Stake/StakeInfo/util/index.ts rename to src/views/SwapView/content/Stake/StakeInfo/util/index.ts diff --git a/src/views/SwapView/content/Stake/StakeInfo/util/messages.ts b/src/views/SwapView/content/Stake/StakeInfo/util/messages.ts new file mode 100644 index 00000000..48aad3a7 --- /dev/null +++ b/src/views/SwapView/content/Stake/StakeInfo/util/messages.ts @@ -0,0 +1,70 @@ +export default { + receive: { + en: 'You receive', + ru: 'Вы получаете', + fr: 'Vous recevez', + es: 'Recibes', + pt: 'Você recebe', + de: 'Sie erhalten', + zh: '您收到', + }, + fee: { + en: 'Rewards fee', + ru: 'Комиссия за награды', + fr: 'Frais de récompenses', + es: 'Tarifa de recompensas', + pt: 'Taxa de recompensas', + de: 'Belohnungsgebühr', + zh: '奖励费用', + }, + tooltips: { + apy: { + en: 'The percentage you earn on your staked {depositToken} after fees', + ru: 'Процент, который вы зарабатываете на ваших застейканных {depositToken} после вычета комиссий', + fr: 'Le pourcentage que vous gagnez sur vos {depositToken} stakés après frais', + es: 'El porcentaje que gana en su {depositToken} staked después de las tarifas', + pt: 'A porcentagem que você ganha nos seus {depositToken} staked, após taxas', + de: 'Der Prozentsatz, den Sie auf Ihre gestakten {depositToken} nach Abzug der Gebühren verdienen', + zh: '扣除费用后,您在质押的{depositToken}上获得的百分比', + }, + receive: { + en: ` + The amount of {mintToken} tokens you receive to your wallet. + To withdraw your staked {depositToken}, you will need to return the {mintToken} tokens. + `, + ru: ` + Количество токенов {mintToken}, которые вы получаете на свой кошелек. + Чтобы вывести ваш застейканный {depositToken}, вам потребуется вернуть токены {mintToken}. + `, + fr: ` + Le nombre de jetons {mintToken} que vous recevez dans votre portefeuille. + Pour retirer votre {depositToken} staké, vous devrez retourner les jetons {mintToken}. + `, + es: ` + La cantidad de tokens {mintToken} que recibes en tu billetera. + Para retirar tu {depositToken} en staking, necesitarás devolver los tokens {mintToken}. + `, + pt: ` + A quantidade de tokens {mintToken} que você recebe em sua carteira. + Para retirar seu {depositToken} em staking, você precisará devolver os tokens {mintToken}. + `, + de: ` + Die Menge an {mintToken}-Token, die Sie in Ihr Wallet erhalten. + Um Ihre gestakten {depositToken} abzuheben, müssen Sie die {mintToken}-Token zurückgeben. + `, + zh: ` + 您收到的钱包中的 {mintToken} 代币数量。要提取您质押的 {depositToken}, + 您需要返回 {mintToken} 代币。 + `, + }, + rate: { + en: 'The amount of staked {depositToken} you receive for each {swapToken}.', + ru: 'Количество застейканного {depositToken}, которое вы получаете за каждый {swapToken}.', + fr: 'La quantité de {depositToken} staké que vous recevez pour chaque {swapToken}.', + es: 'La cantidad de {depositToken} que recibe por cada {swapToken} en staking.', + pt: 'A quantidade de {depositToken} staked que você recebe por cada {swapToken}.', + de: 'Die Menge an gestakten {depositToken}, die Sie für jedes {swapToken} erhalten.', + zh: '您为每个 {swapToken} 获得的质押 {depositToken} 的数量。', + }, + }, +} diff --git a/src/views/SwapView/content/Stake/StakeInfo/util/useOptions.ts b/src/views/SwapView/content/Stake/StakeInfo/util/useOptions.ts new file mode 100644 index 00000000..a279caf9 --- /dev/null +++ b/src/views/SwapView/content/Stake/StakeInfo/util/useOptions.ts @@ -0,0 +1,140 @@ +import { useMemo } from 'react' +import { useConfig } from 'config' +import { useFiatValues, useStore } from 'hooks' +import { formatEther, parseUnits } from 'ethers' +import { commonMessages, methods } from 'helpers' + +import { TableProps } from 'views/SwapView/common' +import { swapCtx, vaultHooks } from 'views/SwapView/util' + +import messages from './messages' + + +const storeSelector = (store: Store) => ({ + apy: store.vault.base.data.apy, + userApy: store.vault.user.balances.userAPY, +}) + +const useOptions = () => { + const { address, sdk, isReadOnlyMode } = useConfig() + + const { stake } = swapCtx.useData() + + const { newAPY, isApyHidden, isFetching: isApyFetching } = vaultHooks.helpers.useAPY({ + field: stake.field, + type: 'stake', + modifier: stake.getSwappedDepositAmount, + }) + + const { apy, userApy } = useStore(storeSelector) + + const receive = vaultHooks.helpers.useStakeReceive(stake) + + const assets = vaultHooks.helpers.useAssets({ + field: stake.field, + type: 'stake', + }) + + const { fiatGas } = useFiatValues({ + fiatGas: { + token: sdk.config.tokens.nativeToken, + value: formatEther(stake.transactionPrice), + isMinimal: true, + }, + }) + + const diff = userApy - newAPY + + const selectedToken = stake.swapTokens.selected + + const isFetching = receive.isFetching || isApyFetching + + return useMemo(() => { + const rateAmount = selectedToken.address + ? stake.getSwappedDepositAmount(parseUnits('1', selectedToken.units)) + : undefined + + const result: TableProps['options'] = [] + + if (assets) { + result.push(assets) + } + + if (rateAmount) { + result.push({ + text: commonMessages.transaction.exchangeRate, + value: `1 ${selectedToken.name} = ${methods.formatTokenValue(rateAmount)} ${sdk.config.tokens.depositToken}`, + tooltip: { + ...messages.tooltips.rate, + values: { + swapToken: selectedToken.name, + depositToken: sdk.config.tokens.depositToken, + }, + }, + isFetching, + dataTestId: 'table-rate', + }) + } + + if (address && !isReadOnlyMode) { + result.push({ + text: commonMessages.transaction.networkCost, + value: fiatGas.formattedValue, + tooltip: { + ...commonMessages.tooltip.gas, + values: { + nativeToken: sdk.config.tokens.nativeToken, + }, + }, + isFetching, + icon: 'icon/gas', + dataTestId: 'table-gas', + }) + } + + if (!isApyHidden) { + const finallyApyResult = (diff > 0.01 || (!userApy && diff < 0)) + ? { + values: { + prev: methods.formatApy(userApy), + next: methods.formatApy(newAPY), + }, + } + : { + value: methods.formatApy(address ? newAPY : apy), + } + + result.unshift({ + ...finallyApyResult, + text: commonMessages.apy, + tooltip: { + ...messages.tooltips.apy, + values: { + depositToken: sdk.config.tokens.depositToken, + }, + }, + isFetching: isFetching, + dataTestId: 'table-apy', + }) + } + + return result + }, [ + sdk, + apy, + diff, + stake, + assets, + newAPY, + userApy, + address, + fiatGas, + isFetching, + isApyHidden, + selectedToken, + isReadOnlyMode, + ]) +} + + +export default useOptions diff --git a/src/views/SwapView/content/Stake/StakeInput/StakeInput.tsx b/src/views/SwapView/content/Stake/StakeInput/StakeInput.tsx new file mode 100644 index 00000000..734221f5 --- /dev/null +++ b/src/views/SwapView/content/Stake/StakeInput/StakeInput.tsx @@ -0,0 +1,46 @@ +import React, { useCallback } from 'react' +import { useConfig } from 'config' + +import { swapCtx } from 'views/SwapView/util' +import { TokenDropdown, TokenAmountInputView } from 'components' + + +const StakeInput: React.FC = () => { + const { address } = useConfig() + const { stake } = swapCtx.useData() + + const onMaxButtonClick = useCallback(() => { + stake.field.setValue(stake.maxStakeAmount) + }, [ stake ]) + + return ( + { + if (token !== stake.swapTokens.selected.address) { + stake.field.reset() + stake.swapTokens.setSelected(token) + } + }} + /> + )} + dataTestId="amount-input" + onMaxButtonClick={address ? onMaxButtonClick : undefined} + /> + ) +} + + +export default React.memo(StakeInput) diff --git a/src/views/HomeView/content/Stake/SubmitButton/SubmitButton.tsx b/src/views/SwapView/content/Stake/SubmitButton/SubmitButton.tsx similarity index 57% rename from src/views/HomeView/content/Stake/SubmitButton/SubmitButton.tsx rename to src/views/SwapView/content/Stake/SubmitButton/SubmitButton.tsx index c210be12..7a58510e 100644 --- a/src/views/HomeView/content/Stake/SubmitButton/SubmitButton.tsx +++ b/src/views/SwapView/content/Stake/SubmitButton/SubmitButton.tsx @@ -1,8 +1,8 @@ import React from 'react' import { commonMessages } from 'helpers' -import { stakeCtx } from 'views/HomeView/StakeContext/util' -import { Button } from 'views/HomeView/common' +import { swapCtx } from 'views/SwapView/util' +import { SubmitButtonWrapper } from 'views/SwapView/common' type SubmitButtonProps = { @@ -10,13 +10,15 @@ type SubmitButtonProps = { } const SubmitButton: React.FC = ({ className }) => { - const { stake } = stakeCtx.useData() + const { stake } = swapCtx.useData() return ( -
({ + queueDays: store.mintToken.queueDays, + userApy: store.vault.user.balances.userAPY, + vaultVersion: store.vault.base.data.versions.version, + isV2Version: store.vault.base.data.versions.isV2Version, + isCollateralized: store.vault.base.data.isCollateralized, +}) + +const useOptions = () => { + const { unstake } = swapCtx.useData() + const { sdk, address, isReadOnlyMode, isGnosis, isEthereum } = useConfig() + + const { + userApy, + queueDays, + isV2Version, + vaultVersion, + isCollateralized, + } = useStore(storeSelector) + + const { newAPY, isApyHidden, isFetching: isApyFetching } = vaultHooks.helpers.useAPY({ + type: 'unstake', + field: unstake.field, + }) + + const { fiatGas } = useFiatValues({ + fiatGas: { + token: sdk.config.tokens.nativeToken, + value: formatEther(unstake.transactionPrice), + isMinimal: true, + }, + }) + + const assets = vaultHooks.helpers.useAssets({ + field: unstake.field, + type: 'unstake', + }) + + const noteMessage = useMemo(() => { + return { + ...(isV2Version + ? commonMessages.tooltip.unstakeQueueV2 + : commonMessages.tooltip.unstakeQueueV1 + ), + values: { + queueDays, + depositToken: sdk.config.tokens.depositToken, + }, + } + }, [ isV2Version, queueDays, sdk ]) + + const isImmediateInGnosis = isGnosis && vaultVersion >= 3 && !isCollateralized + const isImmediateInEthereum = !isV2Version && isEthereum && !isCollateralized + + const isImmediate = isImmediateInGnosis || isImmediateInEthereum + + return useMemo(() => { + const items: TableProps['options'] = [] + + const fieldAmount = methods.formatTokenValue(unstake.field.value || 0n) + + if (isImmediate) { + items.push({ + text: messages.immediate, + value: fieldAmount, + logo: `token/${sdk.config.tokens.depositToken}`, + dataTestId: 'unstake-queue', + }) + } + else { + items.push({ + text: commonMessages.buttonTitle.unstakeQueue, + tooltip: noteMessage, + value: fieldAmount, + logo: `token/${sdk.config.tokens.depositToken}`, + dataTestId: 'unstake-queue', + }) + } + + if (assets) { + items.push(assets) + } + + if (address && !isReadOnlyMode) { + items.push({ + text: commonMessages.transaction.networkCost, + tooltip: { + ...commonMessages.tooltip.gas, + values: { + nativeToken: sdk.config.tokens.nativeToken, + }, + }, + value: fiatGas.formattedValue, + icon: 'icon/gas', + dataTestId: 'table-gas', + isFetching: isApyFetching, + } as TableProps['options'][number]) + } + + if (!isApyHidden) { + items.unshift({ + values: { + prev: methods.formatApy(userApy), + next: methods.formatApy(newAPY), + }, + text: commonMessages.apy, + isFetching: isApyFetching, + dataTestId: 'table-apy', + }) + } + + return items + }, [ + address, + assets, + fiatGas, + isApyFetching, + isApyHidden, + isImmediate, + isReadOnlyMode, + newAPY, + noteMessage, + sdk, + unstake, + userApy, + ]) +} + + +export default useOptions diff --git a/src/views/SwapView/content/Unstake/UnstakeInput/UnstakeInput.tsx b/src/views/SwapView/content/Unstake/UnstakeInput/UnstakeInput.tsx new file mode 100644 index 00000000..09e8e119 --- /dev/null +++ b/src/views/SwapView/content/Unstake/UnstakeInput/UnstakeInput.tsx @@ -0,0 +1,31 @@ +import React, { useCallback } from 'react' +import { useConfig } from 'config' + +import { swapCtx } from 'views/SwapView/util' +import { TokenAmountInput } from 'components' + + +const UnstakeInput: React.FC = () => { + const { sdk, address } = useConfig() + const { unstake } = swapCtx.useData() + + const onMaxButtonClick = useCallback(() => { + unstake.field.setValue(unstake.maxUnstakeAmount) + }, [ unstake ]) + + return ( + + ) +} + + +export default React.memo(UnstakeInput) diff --git a/src/views/HomeView/content/index.ts b/src/views/SwapView/content/index.ts similarity index 100% rename from src/views/HomeView/content/index.ts rename to src/views/SwapView/content/index.ts diff --git a/src/views/SwapView/modals/StatisticsModal/RewardsChart/RewardsChart.tsx b/src/views/SwapView/modals/StatisticsModal/RewardsChart/RewardsChart.tsx new file mode 100644 index 00000000..0ba1e54b --- /dev/null +++ b/src/views/SwapView/modals/StatisticsModal/RewardsChart/RewardsChart.tsx @@ -0,0 +1,93 @@ +import React, { useMemo, useCallback } from 'react' +import { useConfig } from 'config' + +import { swapCtx } from 'views/SwapView/util' + +import { ChartWithFilter } from '../../../common' +import type { ChartWithFilterProps } from '../../../common' + +import messages from './messages' + + +type RewardsChartProps = { + className?: string + closeModal: () => void +} + +type Fetcher = ChartWithFilterProps['tabsItems'][0]['fetcher'] + +const RewardsChart: React.FC = (props) => { + const { className, closeModal } = props + + const { sdk, address } = useConfig() + const { vaultAddress } = swapCtx.useData() + + const fetchUserData = useCallback(async (days) => { + if (!address) { + return null + } + + const data = await sdk.vault.getUserStats({ + userAddress: address, + daysCount: days, + vaultAddress, + }) + + return data + }, [ sdk, address, vaultAddress ]) + + const fetchVaultData = useCallback(async (days) => { + const data = await sdk.vault.getVaultStats({ + daysCount: days, + vaultAddress, + }) + + return data.reverse().reduce((acc, { time, rewards, apy, balance }) => { + acc.apy.push({ + time, + value: apy, + }) + + acc.rewards.push({ + time, + value: rewards, + }) + + acc.balance.push({ + time, + value: balance, + }) + + return acc + }, { + apy: [], + rewards: [], + balance: [], + } as NonNullable>>) + }, [ sdk, vaultAddress ]) + + const tabsItems = useMemo(() => [ + { + tab: ChartWithFilter.Tab.User, + fetcher: fetchUserData, + }, + { + tab: ChartWithFilter.Tab.Vault, + fetcher: fetchVaultData, + }, + ], [ fetchUserData, fetchVaultData ]) + + return ( + + ) +} + + +export default React.memo(RewardsChart) diff --git a/src/views/HomeView/modals/StatisticsModal/RewardsChart/messages.ts b/src/views/SwapView/modals/StatisticsModal/RewardsChart/messages.ts similarity index 100% rename from src/views/HomeView/modals/StatisticsModal/RewardsChart/messages.ts rename to src/views/SwapView/modals/StatisticsModal/RewardsChart/messages.ts diff --git a/src/views/HomeView/modals/StatisticsModal/StatisticsModal.tsx b/src/views/SwapView/modals/StatisticsModal/StatisticsModal.tsx similarity index 94% rename from src/views/HomeView/modals/StatisticsModal/StatisticsModal.tsx rename to src/views/SwapView/modals/StatisticsModal/StatisticsModal.tsx index b880dcef..58554874 100644 --- a/src/views/HomeView/modals/StatisticsModal/StatisticsModal.tsx +++ b/src/views/SwapView/modals/StatisticsModal/StatisticsModal.tsx @@ -18,7 +18,7 @@ export const [ StatisticsModal, openStatisticsModal ] = ( diff --git a/src/views/HomeView/modals/index.ts b/src/views/SwapView/modals/index.ts similarity index 100% rename from src/views/HomeView/modals/index.ts rename to src/views/SwapView/modals/index.ts diff --git a/src/views/HomeView/StakeContext/util/enum.ts b/src/views/SwapView/util/enum.ts similarity index 100% rename from src/views/HomeView/StakeContext/util/enum.ts rename to src/views/SwapView/util/enum.ts diff --git a/src/views/HomeView/StakeContext/Tabs/util/getTabsList.ts b/src/views/SwapView/util/getTabsList.ts similarity index 95% rename from src/views/HomeView/StakeContext/Tabs/util/getTabsList.ts rename to src/views/SwapView/util/getTabsList.ts index 31c9246f..eb1e4990 100644 --- a/src/views/HomeView/StakeContext/Tabs/util/getTabsList.ts +++ b/src/views/SwapView/util/getTabsList.ts @@ -1,5 +1,6 @@ import { commonMessages } from 'helpers' -import { Tab } from 'views/HomeView/StakeContext/util' + +import { Tab } from './enum' const baseTabsList = [ diff --git a/src/views/SwapView/util/index.ts b/src/views/SwapView/util/index.ts new file mode 100644 index 00000000..4ddf1d4a --- /dev/null +++ b/src/views/SwapView/util/index.ts @@ -0,0 +1,10 @@ +import { Tab } from './enum' +import * as swapCtx from './swapCtx' + + +export { default as vaultHooks } from './vault' + +export { + Tab, + swapCtx, +} diff --git a/src/views/SwapView/util/swapCtx.ts b/src/views/SwapView/util/swapCtx.ts new file mode 100644 index 00000000..4ebaac85 --- /dev/null +++ b/src/views/SwapView/util/swapCtx.ts @@ -0,0 +1,178 @@ +import { useMemo, useEffect, useCallback } from 'react' +import { useConfig } from 'config' +import { ZeroAddress } from 'ethers' +import { initContext } from 'helpers' +import { useStore, useActions, useAutoFetch } from 'hooks' + +import vaultHooks from './vault' +import useStats from './useStats' +import useTabs, { tabsMock } from './useTabs' +import useVaultAddress from './useVaultAddress' + + +export const initialContext: SwapView.Context = { + stake: vaultHooks.actions.useStake.mock, + unstake: vaultHooks.actions.useUnstake.mock, + + boost: vaultHooks.actions.useBoost.mock, + unboost: vaultHooks.actions.useUnboost.mock, + + mint: vaultHooks.actions.useMint.mock, + burn: vaultHooks.actions.useBurn.mock, + + unboostQueue: vaultHooks.actions.useClaimUnboostQueue.mock, + unstakeQueue: vaultHooks.actions.useClaimUnstakeQueue.mock, + + tvl: '', + tabs: tabsMock, + vaultAddress: ZeroAddress, + isFetching: false, +} + +const storeSelector = (store: Store) => ({ + isVaultFetching: store.vault.base.isFetching, + unstakeQueueData: store.vault.user.unstakeQueue.data, + unboostQueueData: store.vault.user.unboostQueue.data, + isUserBalancesFetching: store.vault.user.balances.isFetching, +}) + +export const { + Provider, + useData, + useInit, +} = initContext(initialContext, () => { + const actions = useActions() + const { address, wallet } = useConfig() + + const { + isVaultFetching, + unstakeQueueData, + unboostQueueData, + isUserBalancesFetching, + } = useStore(storeSelector) + + const vaultAddress = useVaultAddress() + + const resetFields = useCallback(() => { + stake.field.reset() + boost.field.reset() + unstake.field.reset() + unboost.percentField.reset() + }, []) + + const tabs = useTabs(resetFields) + const { tvl, isStatsFetching } = useStats() + + const isSkipAutofetchUnstakeQueue = Boolean(!address || !unstakeQueueData.total || unstakeQueueData.duration) + const isSkipAutofetchUnboostQueue = Boolean(!address || !unboostQueueData.position || unboostQueueData.duration) + + const { fetchAllVaultData, resetAllVaultData } = vaultHooks.useVault({ + vaultAddress, + }) + + const { + fetchAllUserData, + resetAllUserData, + fetchUnboostQueue, + fetchUnstakeQueue, + } = vaultHooks.useUser({ + withUserChartStats: false, + }) + + useEffect(() => { + const onChangeChain = () => { + resetFields() + actions.vault.base.resetData() + } + + const onChangeAddress = () => { + resetFields() + actions.vault.user.balances.resetData() + actions.vault.user.unstakeQueue.resetData() + actions.vault.user.unboostQueue.resetData() + } + + wallet.subscribeBeforeChange('chain', onChangeChain) + wallet.subscribeBeforeChange('address', onChangeAddress) + + return () => { + wallet.unsubscribeBeforeChange('chain', onChangeChain) + wallet.unsubscribeBeforeChange('address', onChangeAddress) + } + }, []) + + useEffect(() => { + fetchAllVaultData() + }, [ fetchAllVaultData ]) + + useEffect(() => { + fetchAllUserData() + }, [ fetchAllUserData ]) + + useEffect(() => { + return () => { + resetAllUserData() + resetAllVaultData() + } + }, []) + + useAutoFetch({ + action: fetchUnstakeQueue, + skip: isSkipAutofetchUnstakeQueue, + interval: 60_000, + }) + + useAutoFetch({ + action: fetchUnboostQueue, + skip: isSkipAutofetchUnboostQueue, + interval: 60_000, + }) + + const stake = vaultHooks.actions.useStake({ fetchAllUserData }) + const unstake = vaultHooks.actions.useUnstake({ fetchAllUserData }) + + const boost = vaultHooks.actions.useBoost({ fetchAllUserData }) + const unboost = vaultHooks.actions.useUnboost({ fetchAllUserData }) + + const mint = vaultHooks.actions.useMint({ fetchAllUserData }) + const burn = vaultHooks.actions.useBurn({ fetchAllUserData }) + + const unboostQueue = vaultHooks.actions.useClaimUnboostQueue({ fetchAllUserData }) + const unstakeQueue = vaultHooks.actions.useClaimUnstakeQueue({ fetchAllUserData }) + + const isFetching = ( + isStatsFetching + || isVaultFetching + || isUserBalancesFetching + ) + + return useMemo(() => ({ + tvl, + tabs, + mint, + burn, + stake, + boost, + unboost, + unstake, + unboostQueue, + unstakeQueue, + vaultAddress, + isFetching, + fetchAllUserData, + }), [ + tvl, + mint, + burn, + tabs, + stake, + boost, + unboost, + unstake, + isFetching, + unboostQueue, + unstakeQueue, + vaultAddress, + fetchAllUserData, + ]) +}) diff --git a/src/views/SwapView/util/types.d.ts b/src/views/SwapView/util/types.d.ts new file mode 100644 index 00000000..5cabf034 --- /dev/null +++ b/src/views/SwapView/util/types.d.ts @@ -0,0 +1,46 @@ +import { Tab, vaultHooks } from '../util' + + +declare global { + + declare namespace SwapView { + + namespace Tabs { + + type TabsList = Array<{ + id: Tab, + title: Intl.Message + }> + + type SetTab = (tab: Tab) => void + + type ToggleTabs = () => void + + type Data = { + value: Tab + list: TabsList + setTab: SetTab + toggleTabs: ToggleTabs + } + } + + type Context = { + stake: ReturnType + unstake: ReturnType + + boost: ReturnType + unboost: ReturnType + + burn: ReturnType + mint: ReturnType + + unboostQueue: ReturnType + unstakeQueue: ReturnType + + tvl: string + tabs: Tabs.Data + vaultAddress: string + isFetching: boolean + } + } +} diff --git a/src/views/SwapView/util/useStats.ts b/src/views/SwapView/util/useStats.ts new file mode 100644 index 00000000..19d89155 --- /dev/null +++ b/src/views/SwapView/util/useStats.ts @@ -0,0 +1,50 @@ +import { useCallback, useEffect } from 'react' +import { useObjectState } from 'hooks' +import { useConfig } from 'config' +import { methods } from 'helpers' + + +type State = { + tvl: string + isStatsFetching: boolean +} + +const useStats = () => { + const { sdk } = useConfig() + + const [ state, setState ] = useObjectState({ + isStatsFetching: true, + tvl: '', + }) + + const fetchStats = useCallback(async () => { + try { + const stats = await sdk.utils.getStakewiseStats() + + const token = sdk.config.tokens.depositToken + const value = methods.formatTokenValue(BigInt(stats.totalAssets)) + const tvl = `${value} ${token}` + + setState({ + tvl, + isStatsFetching: false, + }) + } + catch (error) { + console.error('Stake: fetchStats error:', error) + + setState({ isStatsFetching: false }) + + return Promise.reject('Stake: fetchStats error') + } + }, [ sdk, setState ]) + + useEffect(() => { + fetchStats() + }, [ fetchStats ]) + + return state +} + + +export default useStats diff --git a/src/views/SwapView/util/useTabs.ts b/src/views/SwapView/util/useTabs.ts new file mode 100644 index 00000000..917fcc03 --- /dev/null +++ b/src/views/SwapView/util/useTabs.ts @@ -0,0 +1,126 @@ +import { useCallback, useMemo } from 'react' +import { Network } from 'sdk' +import { useConfig } from 'config' +import { useStore, useObjectState, useChainChanged } from 'hooks' + +import getTabsList from './getTabsList' + +import { Tab } from './enum' + + +type State = { + tab: Tab + list: SwapView.Tabs.Data['list'] + isReversed: boolean +} + +export const tabsMock: SwapView.Tabs.Data = { + list: [], + value: Tab.Stake, + setTab: (() => {}) as SwapView.Tabs.SetTab, + toggleTabs: () => {}, +} + +const storeSelector = (store: Store) => ({ + isMoreV2: store.vault.base.data.versions.isMoreV2, + isMintTokenDisabled: store.vault.user.balances.mintToken.isDisabled, +}) + +const useTabs = (resetFields: () => void) => { + const { isEthereum } = useConfig() + + const { isMoreV2, isMintTokenDisabled } = useStore(storeSelector) + + const withMint = !isMintTokenDisabled + const withBoost = withMint && isEthereum && isMoreV2 + const withToggleButton = withMint || withBoost + + const [ { tab, list }, setState ] = useObjectState({ + tab: Tab.Stake, + isReversed: false, + list: getTabsList({ withMint, withBoost, isReversed: false }), + }) + + const getCurrentIndex = useCallback((state: State) => { + const { tab, list } = state + + return list.map(({ id }) => id).indexOf(tab) + }, []) + + const setTab = useCallback((tab: Tab) => { + const isValid = Object.values(Tab).includes(tab) + + if (isValid) { + setState((state) => { + const isExists = Boolean(state.list.some(({ id }) => id === tab)) + + if (!isExists) { + console.error(`Invalid tab "${tab}", on list:`, state.list) + + return state + } + + resetFields() + + return { + ...state, + tab, + } + }) + } + }, [ setState, resetFields ]) + + const toggleTabs = useCallback(() => { + setState((state) => { + if (!withToggleButton) { + const list = getTabsList({ withMint, withBoost, isReversed: false }) + + return { + list, + tab: list[0].id, + isReversed: false, + } + } + + const isReversed = !state.isReversed + const list = getTabsList({ withMint, withBoost, isReversed }) + const index = getCurrentIndex(state) + + return { + list, + isReversed, + tab: list[index].id, + } + }) + }, [ withBoost, withMint, withToggleButton, getCurrentIndex, setState ]) + + const resetTab = useCallback((chainId: ChainIds) => { + const isEth = chainId === Network.Mainnet || chainId === Network.Hoodi + const nextWithBoost = withMint && isEth && isMoreV2 + + const list = getTabsList({ withMint, withBoost: nextWithBoost, isReversed: false }) + + setState({ + list, + tab: list[0].id, + isReversed: false, + }) + }, [ setState, withMint, isMoreV2 ]) + + useChainChanged(resetTab) + + return useMemo(() => ({ + value: tab, + list, + setTab, + toggleTabs, + }), [ + tab, + list, + setTab, + toggleTabs, + ]) +} + + +export default useTabs diff --git a/src/views/HomeView/StakeContext/util/useVaultAddress.ts b/src/views/SwapView/util/useVaultAddress.ts similarity index 100% rename from src/views/HomeView/StakeContext/util/useVaultAddress.ts rename to src/views/SwapView/util/useVaultAddress.ts diff --git a/src/views/SwapView/util/vault/actions/index.ts b/src/views/SwapView/util/vault/actions/index.ts new file mode 100644 index 00000000..28869dbd --- /dev/null +++ b/src/views/SwapView/util/vault/actions/index.ts @@ -0,0 +1,26 @@ +import { default as useStake } from './useStake' +import { default as useUnstake } from './useUnstake' + +import { default as useMint } from './useMint' +import { default as useBurn } from './useBurn' + +import { default as useBoost } from './useBoost' +import { default as useUnboost } from './useUnboost' + +import { default as useClaimUnboostQueue } from './useClaimUnboostQueue' +import { default as useClaimUnstakeQueue } from './useClaimUnstakeQueue' + + +export default { + useStake, + useUnstake, + + useMint, + useBurn, + + useBoost, + useUnboost, + + useClaimUnboostQueue, + useClaimUnstakeQueue, +} diff --git a/src/views/SwapView/util/vault/actions/useBoost/index.ts b/src/views/SwapView/util/vault/actions/useBoost/index.ts new file mode 100644 index 00000000..b53ca836 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/index.ts @@ -0,0 +1,96 @@ +import { useCallback, useMemo } from 'react' +import { useStore } from 'hooks' +import { openGuideModal } from 'layouts/modals/GuideModal/GuideModal' + +import vaultHooks from '../../index' + +import useBoostField from './useBoostField' +import useBoostSubmit from './useBoostSubmit' +import useBoostDisabled from './useBoostDisabled' +import useBoostSupplyCapsCheck from './useBoostSupplyCapsCheck' +import useBoostTransactionPrice from './useBoostTransactionPrice' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + walletMintedShares: store.account.balances.mintToken, + boostedShares: store.vault.user.balances.boost.shares, + ltvPercent: store.vault.base.data.osTokenConfig.ltvPercent, + mintedShares: store.vault.user.balances.mintToken.mintedShares, +}) + +const useBoost = (values: Input) => { + const { fetchAllUserData } = values + + const { + ltvPercent, + vaultAddress, + mintedShares, + boostedShares, + walletMintedShares, + } = useStore(storeSelector) + + const vaultBalance = mintedShares - boostedShares + + const maxBoostShares = vaultBalance > walletMintedShares + ? walletMintedShares + : vaultBalance + + const field = useBoostField(maxBoostShares) + const transactionPrice = useBoostTransactionPrice() + + const { isFetching: isSupplyCapsFetching, checkSupplyCap } = useBoostSupplyCapsCheck() + const { boostDisabledTooltip, isBoostDisabled } = useBoostDisabled({ field, checkSupplyCap }) + + const { isSubmitting, isAllowanceFetching, submit } = useBoostSubmit({ + field, + vaultAddress, + fetchAllUserData, + }) + + const openModal = useCallback(() => { + const ltv = ltvPercent === '999900000000000000' ? 100 : 90 + + openGuideModal({ ltv }) + }, [ ltvPercent ]) + + const isBoostLoading = isSubmitting || isAllowanceFetching || isSupplyCapsFetching + + return useMemo(() => ({ + field, + maxBoostShares, + isBoostLoading, + isBoostDisabled, + transactionPrice, + boostDisabledTooltip, + submit, + openGuideModal: openModal, + }), [ + field, + maxBoostShares, + isBoostLoading, + isBoostDisabled, + transactionPrice, + boostDisabledTooltip, + submit, + openModal, + ]) +} + +useBoost.mock = { + maxBoostShares: 0n, + transactionPrice: 0n, + boostDisabledTooltip: undefined, + isBoostLoading: false, + isBoostDisabled: false, + field: {} as Forms.Field, + submit: () => Promise.resolve(), + openGuideModal: () => {}, +} as ReturnType + + +export default useBoost diff --git a/src/views/SwapView/util/vault/actions/useBoost/messages.ts b/src/views/SwapView/util/vault/actions/useBoost/messages.ts new file mode 100644 index 00000000..4a7c49a9 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/messages.ts @@ -0,0 +1,40 @@ +export default { + boostTooltips: { + needMintToken: { + en: 'To boost your APY, you must have {mintToken} in your wallet.', + ru: 'Чтобы увеличить вашу APY, у вас должно быть {mintToken} в вашем кошельке.', + fr: 'Pour augmenter votre APY, vous devez avoir {mintToken} dans votre portefeuille.', + es: 'Para aumentar tu APY, debes tener {mintToken} en tu billetera.', + pt: 'Para boostear seu APY, você deve ter {mintToken} em sua carteira.', + de: 'Um Ihre APY zu erhöhen, müssen Sie {mintToken} in Ihrem Wallet haben.', + zh: '要提升您的APY,您必须在钱包中拥有{mintToken}。', + }, + needValidators: { + en: 'The boost only works for the vault with validators.', + ru: 'Буст работает только для Волта с валидаторами.', + fr: 'Le boost fonctionne uniquement pour le vault avec des validateurs.', + es: 'El boost solo funciona para el vault con validadores.', + pt: 'O boost só funciona para o vault com validadores.', + de: 'Der Boost funktioniert nur für den Vault mit Validierern.', + zh: '增益仅适用于具有验证器的 vault。', + }, + boostNotProfitable: { + en: 'The boost is currently not profitable.', + ru: 'Буст в настоящее время не приносит прибыль.', + fr: 'Le boost n\'est actuellement pas rentable.', + es: 'El boost actualmente no es rentable.', + pt: 'O boost atualmente não é lucrativo.', + de: 'Der Boost ist derzeit nicht profitabel.', + zh: '当前的增益没有利润。', + }, + needFinishUnboost: { + en: 'The current unboost request must be claimed before boosting again', + ru: 'Текущий запрос на анбуст должен быть выполнен перед повторным бустом', + fr: 'La demande de déboost actuelle doit être réclamée avant de pouvoir booster à nouveau', + es: 'La solicitud de despotenciación actual debe ser reclamada antes de potenciar nuevamente', + pt: 'A solicitação de despotencialização atual deve ser reivindicada antes de impulsionar novamente', + de: 'Die aktuelle Anforderung zum Entboosten muss eingelöst werden, bevor erneut geboostet werden kann', + zh: '当前的取消加速请求必须被领取后才能再次加速', + }, + }, +} diff --git a/src/views/HomeView/StakeContext/util/actions/useBoostAllowance.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostAllowance.ts similarity index 74% rename from src/views/HomeView/StakeContext/util/actions/useBoostAllowance.ts rename to src/views/SwapView/util/vault/actions/useBoost/useBoostAllowance.ts index 8f8314fd..716c0f88 100644 --- a/src/views/HomeView/StakeContext/util/actions/useBoostAllowance.ts +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostAllowance.ts @@ -1,6 +1,11 @@ import { useCallback, useEffect, useMemo } from 'react' import { useConfig } from 'config' -import { useObjectState, useApprove, useAllowance } from 'hooks' +import { + useStore, + useApprove, + useAllowance, + useObjectState, +} from 'hooks' type State = { @@ -8,15 +13,23 @@ type State = { isFetching: boolean } +const storeSelector = (store: Store) => ({ + hasMintBalance: store.vault.user.balances.mintToken.hasMintBalance, +}) + const useBoostAllowance = (vaultAddress: string | null) => { const { sdk, address, isGnosis, isTestnet } = useConfig() - const [ { permitAddress, isFetching }, setState ] = useObjectState({ + const { hasMintBalance } = useStore(storeSelector) + + const [ state, setState ] = useObjectState({ permitAddress: null, - isFetching: Boolean(address && !isGnosis), + isFetching: Boolean(address && !isGnosis && hasMintBalance), }) - const skip = !permitAddress || isGnosis + const { permitAddress, isFetching } = state + + const skip = !permitAddress || isGnosis || !hasMintBalance const { allowance, isFetching: isAllowanceFetching, checkAllowance } = useAllowance({ tokenAddress: sdk.config.addresses.tokens.mintToken, @@ -31,7 +44,7 @@ const useBoostAllowance = (vaultAddress: string | null) => { }) const fetchPermitAddress = useCallback(async () => { - if (isGnosis) { + if (isGnosis || !hasMintBalance) { setState({ permitAddress: null, isFetching: false }) return @@ -61,7 +74,7 @@ const useBoostAllowance = (vaultAddress: string | null) => { setState({ isFetching: false }) } - }, [ sdk, vaultAddress, address, isGnosis, isTestnet, setState ]) + }, [ sdk, vaultAddress, address, isGnosis, isTestnet, hasMintBalance, setState ]) useEffect(() => { fetchPermitAddress() diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostDisabled.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostDisabled.ts new file mode 100644 index 00000000..7b036643 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostDisabled.ts @@ -0,0 +1,92 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' +import forms from 'modules/forms' +import { useConfig } from 'config' +import { commonMessages } from 'helpers' + +import useBoostSupplyCapsCheck from './useBoostSupplyCapsCheck' + +import messages from './messages' + + +type Input = { + field: Forms.Field + checkSupplyCap: ReturnType['checkSupplyCap'] +} + +const isEnvBoostDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_BOOST) + +const storeSelector = (store: Store) => ({ + vaultApy: store.vault.base.data.apy, + maxBoostApy: store.vault.base.data.allocatorMaxBoostApy, + isCollateralized: store.vault.base.data.isCollateralized, + exitingPercent: store.vault.user.balances.boost.exitingPercent, + mintedShares: store.vault.user.balances.mintToken.mintedShares, +}) + +const useBoostDisabled = (values: Input) => { + const { field, checkSupplyCap } = values + + const { + vaultApy, + maxBoostApy, + mintedShares, + exitingPercent, + isCollateralized, + } = useStore(storeSelector) + + const { value, error } = forms.useFieldValue(field) + const { sdk, address, isReadOnlyMode } = useConfig() + + const isBoostQueued = exitingPercent > 0 + const isBoostProfitable = maxBoostApy > vaultApy + const isValidSupplyCap = error ? true : checkSupplyCap(value || 0n) + + const isBoostDisabled = ( + !address + || isBoostQueued + || !mintedShares + || Boolean(error) + || isReadOnlyMode + || !isCollateralized + || !isValidSupplyCap + || !isBoostProfitable + || isEnvBoostDisabled + ) + + let boostDisabledTooltip: Intl.Message | undefined = undefined + + if (!mintedShares) { + boostDisabledTooltip = { + ...messages.boostTooltips.needMintToken, + values: { mintToken: sdk.config.tokens.mintToken }, + } + } + + if (!isValidSupplyCap) { + boostDisabledTooltip = commonMessages.invalidBoostSupplyCap + } + + if (!isCollateralized) { + boostDisabledTooltip = messages.boostTooltips.needValidators + } + + if (!isBoostProfitable) { + boostDisabledTooltip = messages.boostTooltips.boostNotProfitable + } + + if (isBoostQueued) { + boostDisabledTooltip = messages.boostTooltips.needFinishUnboost + } + + return useMemo(() => ({ + boostDisabledTooltip, + isBoostDisabled, + }), [ + boostDisabledTooltip, + isBoostDisabled, + ]) +} + + +export default useBoostDisabled diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostField.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostField.ts new file mode 100644 index 00000000..11eede97 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostField.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react' +import forms from 'modules/forms' + + +const useBoostField = (balance: bigint) => { + const balanceRef = useRef(balance) + balanceRef.current = balance + + return forms.useField({ + valueType: 'bigint', + validators: [ + forms.validators.numberWithDot, + forms.validators.sufficientBalance(balanceRef), + ], + }) +} + + +export default useBoostField diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/index.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/index.ts new file mode 100644 index 00000000..6d01764b --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/index.ts @@ -0,0 +1,217 @@ +import { useCallback, useMemo, useState } from 'react' +import notifications from 'modules/notifications' +import { useStore, useActions } from 'hooks' +import { BoostStep } from 'helpers/enums' +import { commonMessages } from 'helpers' +import { useConfig } from 'config' + +import type { SetTransaction, SetNextTransactionsFailed } from 'components/Transactions/types' +import { openTransactionsFlowModal } from 'layouts/modals' + +import vaultHooks from '../../../index' + +import useBoostSteps from './useBoostSteps' +import useBoostActions from './useBoostActions' +import useBoostAllowance from '../useBoostAllowance' + + +type OnStartInput = { + amount: bigint + permitAddress?: string + setTransaction?: SetTransaction + setNextTransactionsFailed?: SetNextTransactionsFailed +} + +type Input = { + vaultAddress: string + field: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +type Output = { + allowance: bigint + isSubmitting: boolean + isAllowanceFetching: boolean + submit: () => void +} + +const storeSelector = (store: Store) => ({ + leverageStrategyData: store.vault.user.balances.boost.leverageStrategyData, +}) + +const useBoostSubmit = (values: Input): Output => { + const { vaultAddress, field, fetchAllUserData } = values + + const actions = useActions() + const { address } = useConfig() + + const { leverageStrategyData } = useStore(storeSelector) + const [ isSubmitting, setSubmitting ] = useState(false) + + const { + allowance, + permitAddress, + isFetching, + approve, + checkAllowance, + } = useBoostAllowance(vaultAddress) + + const getStepsData = useBoostSteps({ + allowance, + permitAddress, + }) + + const { + boost, + upgrade, + onSuccess, + refetchData, + approveOrPermit, + } = useBoostActions({ + field, + allowance, + vaultAddress, + permitAddress, + approve, + checkAllowance, + fetchAllUserData, + }) + + const onStart = useCallback(async (values: OnStartInput) => { + const { amount, setTransaction = () => {}, setNextTransactionsFailed = () => {} } = values + + try { + if (!amount || !address || !vaultAddress) { + return + } + + actions.ui.setBottomLoader({ + content: commonMessages.notification.waitingConfirmation, + }) + + setSubmitting(true) + + let hash + let permitParams + let _leverageStrategyData = leverageStrategyData + + const stepsData = getStepsData(amount) + + for (let i = 0; i < stepsData.length; i += 1) { + const step = stepsData[i] + + if (step.id === BoostStep.Upgrade) { + await upgrade({ + userAddress: address, + vaultAddress, + setTransaction, + setNextTransactionsFailed, + }) + + _leverageStrategyData = { + version: 2, + isUpgradeRequired: false, + } + } + if (step.id === BoostStep.Permit) { + permitParams = await approveOrPermit({ + amount, + userAddress: address, + vaultAddress, + setTransaction, + setNextTransactionsFailed, + }) + } + if (step.id === BoostStep.Boost) { + hash = await boost({ + amount, + permitParams, + vaultAddress, + leverageStrategyData: _leverageStrategyData, + userAddress: address, + setTransaction, + }) + } + } + + if (hash) { + await onSuccess({ + hash, + amount, + permitParams, + }) + } + } + catch (error) { + actions.ui.resetBottomLoader() + + console.error('Boost send transaction error', error as Error) + + notifications.open({ + text: commonMessages.notification.failed, + type: 'error', + }) + + return Promise.reject(error) + } + finally { + refetchData() + setSubmitting(false) + } + }, [ + actions, + address, + vaultAddress, + boost, + upgrade, + onSuccess, + refetchData, + getStepsData, + approveOrPermit, + leverageStrategyData, + ]) + + const submit = useCallback(() => { + const amount = field.value || 0n + + if (!amount || !address || !vaultAddress) { + return + } + + const stepsData = getStepsData(amount) + + if (stepsData.length > 1) { + openTransactionsFlowModal({ + flow: 'boost', + stepsData, + onStart: ({ setTransaction, setNextTransactionsFailed }) => { + return onStart({ amount, setTransaction, setNextTransactionsFailed }) + }, + }) + } + else { + onStart({ amount }) + } + }, [ + field, + address, + vaultAddress, + getStepsData, + onStart, + ]) + + return useMemo(() => ({ + allowance, + isSubmitting, + isAllowanceFetching: isFetching, + submit, + }), [ + allowance, + isFetching, + isSubmitting, + submit, + ]) +} + + +export default useBoostSubmit diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostActions.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostActions.ts new file mode 100644 index 00000000..30429cbb --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostActions.ts @@ -0,0 +1,306 @@ +import { useCallback, useMemo } from 'react' +import { useBalances, useSubgraphUpdate } from 'hooks' +import { BoostStep } from 'helpers/enums' +import { useConfig } from 'config' +import { getters } from 'helpers' + +import Transactions from 'components/Transactions/Transactions' +import type { SetTransaction, SetNextTransactionsFailed } from 'components/Transactions/types' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' + +import vaultHooks from '../../../index' + + +type Input = { + allowance: bigint + vaultAddress: string + permitAddress: string | null + field: Forms.Field + approve: () => Promise + fetchAllUserData: ReturnType['fetchAllUserData'] + checkAllowance: (params: { hash?: string, allowance: bigint }) => Promise +} + +type PermitParams = { + vault: string + amount: bigint + deadline: number + v: number + r: string + s: string +} + +type HandleSuccessInput = { + hash: string + amount: bigint + permitParams?: PermitParams +} + +type ApproveOrPermitInput = { + amount: bigint + userAddress: string + vaultAddress: string + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type ApproveInput = { + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type UpgradeInput = { + userAddress: string + vaultAddress: string + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type PermitInput = { + userAddress: string + vaultAddress: string + spenderAddress: string + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type BoostInput = { + amount: bigint + userAddress: string + vaultAddress: string + permitParams?: PermitParams + leverageStrategyData: { + version: number + isUpgradeRequired: boolean + } + setTransaction: SetTransaction +} + +const useBoostActions = (values: Input) => { + const { allowance, permitAddress, vaultAddress, field, fetchAllUserData, approve, checkAllowance } = values + + const { sdk, signSDK, address, chainId, cancelOnChange } = useConfig() + + const subgraphUpdate = useSubgraphUpdate() + const { refetchMintTokenBalance, refetchNativeTokenBalance } = useBalances() + + const handleApprove = useCallback(async (values: ApproveInput) => { + const { setTransaction, setNextTransactionsFailed } = values + + try { + const hash = await approve() + + setTransaction(BoostStep.Permit, Transactions.Status.Processing) + + await checkAllowance({ hash, allowance }) + + setTransaction(BoostStep.Permit, Transactions.Status.Success) + } + catch (error) { + setNextTransactionsFailed(BoostStep.Permit) + + return Promise.reject(error) + } + }, [ allowance, approve, checkAllowance ]) + + const handleGetUserApy = useCallback(async () => { + if (vaultAddress && address) { + const apy = await sdk.vault.getUserApy({ + vaultAddress, + userAddress: address, + }) + + return apy + } + + return 0 + }, [ sdk, address, vaultAddress ]) + + const permit = useCallback(async (values: PermitInput) => { + const { userAddress, vaultAddress, spenderAddress, setTransaction, setNextTransactionsFailed } = values + + try { + setTransaction(BoostStep.Permit, Transactions.Status.Confirm) + + const { amount, deadline, v, r, s } = await signSDK.utils.getPermitSignature({ + contract: signSDK.contracts.tokens.mintToken, + ownerAddress: userAddress, + spenderAddress, + }) + + setTransaction(BoostStep.Permit, Transactions.Status.Success) + + return { + amount, + deadline, + vault: vaultAddress, + v, + r, + s, + } + } + catch (error) { + setNextTransactionsFailed(BoostStep.Permit) + + return Promise.reject(error) + } + }, [ signSDK ]) + + const boost = useCallback(async (values: BoostInput) => { + const { amount, userAddress, vaultAddress, permitParams, leverageStrategyData, setTransaction } = values + + try { + setTransaction(BoostStep.Boost, Transactions.Status.Confirm) + + const referrerAddress = getters.getReferrer() + + const hash = await signSDK.boost.lock({ + amount, + userAddress, + vaultAddress, + referrerAddress, + permitParams, + leverageStrategyData, + }) + + setTransaction(BoostStep.Boost, Transactions.Status.Processing) + + await subgraphUpdate({ hash }) + + setTransaction(BoostStep.Boost, Transactions.Status.Success) + + return hash + } + catch (error) { + setTransaction(BoostStep.Boost, Transactions.Status.Fail) + + return Promise.reject(error) + } + }, [ + signSDK, + subgraphUpdate, + ]) + + const approveOrPermit = useCallback(async (values: ApproveOrPermitInput) => { + const { amount, userAddress, vaultAddress, setTransaction, setNextTransactionsFailed } = values + + const isPermitRequired = amount > allowance + + let permitParams + + if (permitAddress && isPermitRequired) { + const code = await signSDK.provider.getCode(userAddress) + const isMultiSig = code !== '0x' + + if (isMultiSig) { + await handleApprove({ + setTransaction, + setNextTransactionsFailed, + }) + } + else { + permitParams = await permit({ + spenderAddress: permitAddress, + userAddress, + vaultAddress, + setTransaction, + setNextTransactionsFailed, + }) + } + } + + return permitParams + }, [ permitAddress, allowance, signSDK, permit, handleApprove ]) + + const refetchData = useCallback(() => { + cancelOnChange({ + chainId, + address, + logic: () => { + Promise.all([ + fetchAllUserData(), + refetchMintTokenBalance(), + refetchNativeTokenBalance(), + ]) + }, + }) + }, [ + chainId, + address, + cancelOnChange, + fetchAllUserData, + refetchMintTokenBalance, + refetchNativeTokenBalance, + ]) + + const onSuccess = useCallback(async (values: HandleSuccessInput) => { + const { hash, amount, permitParams } = values + + field.reset() + + if (permitParams) { + checkAllowance({ allowance: 0n }) + } + + const userAPY = await handleGetUserApy() + + const tokens = [ + { + apy: userAPY, + value: amount, + action: Action.Boost, + token: signSDK.config.tokens.mintToken, + }, + ] + + openTxCompletedModal({ hash, tokens }) + }, [ field, checkAllowance, signSDK, handleGetUserApy ]) + + const upgrade = useCallback(async (values: UpgradeInput) => { + const { userAddress, vaultAddress, setTransaction } = values + + try { + setTransaction(BoostStep.Upgrade, Transactions.Status.Confirm) + + const hash = await signSDK.boost.upgradeLeverageStrategy({ + userAddress, + vaultAddress, + }) + + setTransaction(BoostStep.Upgrade, Transactions.Status.Processing) + + await subgraphUpdate({ hash }) + + setTransaction(BoostStep.Upgrade, Transactions.Status.Success) + + return hash + } + catch (error) { + setTransaction(BoostStep.Upgrade, Transactions.Status.Fail) + setTransaction(BoostStep.Permit, Transactions.Status.Fail) + setTransaction(BoostStep.Boost, Transactions.Status.Fail) + + return Promise.reject(error) + } + }, [ + signSDK, + subgraphUpdate, + ]) + + return useMemo(() => ({ + boost, + upgrade, + onSuccess, + refetchData, + approveOrPermit, + }), [ + boost, + upgrade, + onSuccess, + refetchData, + approveOrPermit, + ]) +} + + +export default useBoostActions diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostSteps.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostSteps.ts new file mode 100644 index 00000000..e1a0db27 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostSubmit/useBoostSteps.ts @@ -0,0 +1,57 @@ +import { useCallback } from 'react' +import { useStore } from 'hooks' +import { BoostStep } from 'helpers/enums' +import { commonMessages, constants } from 'helpers' + +import type { StepsData } from 'components' + + +type Input = { + allowance: bigint + permitAddress: string | null +} + +const storeSelector = (store: Store) => ({ + leverageStrategyData: store.vault.user.balances.boost.leverageStrategyData, +}) + +const useBoostSteps = (values: Input) => { + const { allowance, permitAddress } = values + + const { leverageStrategyData } = useStore(storeSelector) + + return useCallback((amount: bigint) => { + const result: StepsData = [] + + if (leverageStrategyData.isUpgradeRequired) { + result.push({ + id: BoostStep.Upgrade, + title: commonMessages.upgradeLeverageStrategy, + }) + } + + const isPermitRequired = amount > allowance + + if (permitAddress && isPermitRequired) { + result.push({ + id: BoostStep.Permit, + title: { + ...commonMessages.buttonTitle.approve, + values: { + token: constants.tokens.osETH, + }, + }, + }) + } + + result.push({ + id: BoostStep.Boost, + title: commonMessages.buttonTitle.boost, + }) + + return result + }, [ allowance, permitAddress, leverageStrategyData ]) +} + + +export default useBoostSteps diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostSupplyCapsCheck.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostSupplyCapsCheck.ts new file mode 100644 index 00000000..79adc764 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostSupplyCapsCheck.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { useObjectState, useStore } from 'hooks' +import { constants, requests } from 'helpers' +import { useConfig } from 'config' + + +// TODO We'll be getting from our subgraph later. +const borrowLtv = 930000000000000000n + +// Boost works with Aave, and it's called “reserve”. Reserve has a limit on the number of deposits, +// we should check this limit as transactions will fall if the limit is reached +// One reserve is used for all vaults that have boost logic. + +const storeSelector = (store: Store) => ({ + ltvPercent: store.vault.base.data.osTokenConfig.ltvPercent, + hasMintBalance: store.vault.user.balances.mintToken.hasMintBalance, +}) + +const useBoostSupplyCapsCheck = () => { + const { sdk, isGnosis } = useConfig() + + const { hasMintBalance, ltvPercent } = useStore(storeSelector) + + const skip = isGnosis || !hasMintBalance // ? hasMintBalance + + const [ state, setState ] = useObjectState({ + isFetching: !skip, + hasError: false, + supplyDiff: 0n, + }) + + const fetchSupplyDiff = useCallback(async () => { + try { + const supplyDiff = await requests.fetchBoostSupplyCaps({ + url: sdk.config.api.subgraph, + }) + + if (supplyDiff === null) { + throw new Error('Fetch supply diff error: Failed to request data') + } + + setState({ + supplyDiff, + isFetching: false, + }) + } + catch (error) { + console.error('Fetch supply diff error', error as Error) + + setState({ + isFetching: false, + hasError: true, + }) + } + }, [ sdk, setState ]) + + const checkSupplyCap = useCallback((value: bigint) => { + if (!value) { + return true + } + + if (state.isFetching) { + console.error('checkSupplyCap: Insufficient data for calculations') + + return true + } + + if (state.hasError) { + console.error('checkSupplyCap: Failed to request data from aave') + + return true + } + + const amount1 = constants.blockchain.amount1 + const totalLtv = BigInt(ltvPercent) * borrowLtv / amount1 + const totalSupplied = value * amount1 / (amount1 - totalLtv) + + return state.supplyDiff > totalSupplied + }, [ state, ltvPercent ]) + + useEffect(() => { + if (skip) { + return + } + + fetchSupplyDiff() + }, [ skip, fetchSupplyDiff ]) + + return useMemo(() => ({ + checkSupplyCap, + isFetching: state.isFetching, + }), [ state, checkSupplyCap ]) +} + + +export default useBoostSupplyCapsCheck diff --git a/src/views/SwapView/util/vault/actions/useBoost/useBoostTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useBoost/useBoostTransactionPrice.ts new file mode 100644 index 00000000..026f16a9 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBoost/useBoostTransactionPrice.ts @@ -0,0 +1,57 @@ +import { useState, useCallback } from 'react' +import { useStore, useAutoFetch } from 'hooks' +import { BigDecimal, getters } from 'helpers' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + mintedShares: store.vault.user.balances.mintToken.mintedShares, +}) + +const useBoostTransactionPrice = () => { + const { signSDK, address, isReadOnlyMode } = useConfig() + const { vaultAddress, mintedShares } = useStore(storeSelector) + + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || !mintedShares || !vaultAddress || isReadOnlyMode + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + const referrerAddress = getters.getReferrer() + + const safeAmount = new BigDecimal(mintedShares) + .divide(2) + .decimals(0) + .toString() + + const gas = await signSDK.boost.lock.estimateGas({ + vaultAddress, + referrerAddress, + userAddress: address, + amount: BigInt(safeAmount), + }) + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ signSDK, address, mintedShares, vaultAddress, isSkipFetch ]) + + useAutoFetch({ + action: fetchTransactionPrice, + skip: isSkipFetch, + interval: 30_000, + }) + + return transactionPrice +} + + +export default useBoostTransactionPrice diff --git a/src/views/SwapView/util/vault/actions/useBurn/index.ts b/src/views/SwapView/util/vault/actions/useBurn/index.ts new file mode 100644 index 00000000..3bfec920 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBurn/index.ts @@ -0,0 +1,70 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' + +import vaultHooks from '../../index' + +import useBurnField from './useBurnField' +import useBurnSubmit from './useBurnSubmit' +import useBurnDisabled from './useBurnDisabled' +import useBurnTransactionPrice from './useBurnTransactionPrice' +import useFullUnstakeBurnAmount from './useFullUnstakeBurnAmount' + + +const storeSelector = (store: Store) => ({ + walletMintedShares: store.account.balances.mintToken, + isBalancesFetching: store.vault.user.balances.isFetching, + mintedShares: store.vault.user.balances.mintToken.mintedShares, +}) + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const useBurn = (values: Input) => { + const { fetchAllUserData } = values + + const { mintedShares, walletMintedShares, isBalancesFetching } = useStore(storeSelector) + + const maxBurnShares = mintedShares > walletMintedShares + ? walletMintedShares + : mintedShares + + const field = useBurnField(maxBurnShares) + const isBurnDisabled = useBurnDisabled({ field }) + const transactionPrice = useBurnTransactionPrice() + const fullUnstakeBurnAmount = useFullUnstakeBurnAmount() + const { isSubmitting, submit } = useBurnSubmit({ field, fetchAllUserData }) + + const isBurnLoading = isSubmitting || isBalancesFetching + + return useMemo(() => ({ + field, + isBurnLoading, + isBurnDisabled, + transactionPrice, + fullUnstakeBurnAmount, + maxBurnShares, + submit, + }), [ + field, + maxBurnShares, + isBurnLoading, + isBurnDisabled, + transactionPrice, + fullUnstakeBurnAmount, + submit, + ]) +} + +useBurn.mock = { + maxBurnShares: 0n, + transactionPrice: 0n, + isBurnLoading: false, + isBurnDisabled: false, + fullUnstakeBurnAmount: null, + field: {} as Forms.Field, + submit: () => Promise.resolve(), +} as ReturnType + + +export default useBurn diff --git a/src/views/SwapView/util/vault/actions/useBurn/useBurnDisabled.ts b/src/views/SwapView/util/vault/actions/useBurn/useBurnDisabled.ts new file mode 100644 index 00000000..f1fc30bd --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBurn/useBurnDisabled.ts @@ -0,0 +1,36 @@ +import { useConfig } from 'config' +import forms from 'modules/forms' +import { useStore } from 'hooks' + + +type Input = { + field: Forms.Field +} + +const isEnvBurnDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_BURN) + +const storeSelector = (store: Store) => ({ + hasMintBalance: store.vault.user.balances.mintToken.hasMintBalance, +}) + +const useBurnDisabled = (values: Input) => { + const { field } = values + + const { hasMintBalance } = useStore(storeSelector) + + const { error } = forms.useFieldValue(field) + const { address, isReadOnlyMode } = useConfig() + + const isBurnDisabled = ( + !address + || Boolean(error) + || isReadOnlyMode + || !hasMintBalance + || isEnvBurnDisabled + ) + + return isBurnDisabled +} + + +export default useBurnDisabled diff --git a/src/views/SwapView/util/vault/actions/useBurn/useBurnField.ts b/src/views/SwapView/util/vault/actions/useBurn/useBurnField.ts new file mode 100644 index 00000000..2b4116d3 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBurn/useBurnField.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react' +import forms from 'modules/forms' + + +const useBurnField = (balance: bigint) => { + const balanceRef = useRef(balance) + balanceRef.current = balance + + return forms.useField({ + valueType: 'bigint', + validators: [ + forms.validators.numberWithDot, + forms.validators.sufficientBalance(balanceRef), + ], + }) +} + + +export default useBurnField diff --git a/src/views/HomeView/StakeContext/util/actions/useBurn/useSubmit.ts b/src/views/SwapView/util/vault/actions/useBurn/useBurnSubmit.ts similarity index 68% rename from src/views/HomeView/StakeContext/util/actions/useBurn/useSubmit.ts rename to src/views/SwapView/util/vault/actions/useBurn/useBurnSubmit.ts index 15e5fc50..275ea391 100644 --- a/src/views/HomeView/StakeContext/util/actions/useBurn/useSubmit.ts +++ b/src/views/SwapView/util/vault/actions/useBurn/useBurnSubmit.ts @@ -1,19 +1,26 @@ import { useCallback, useMemo, useState } from 'react' -import { useConfig } from 'config' -import { AllocatorActionType } from 'sdk' -import { commonMessages } from 'helpers' +import { useStore, useActions, useBalances, useSubgraphUpdate } from 'hooks' import notifications from 'modules/notifications' -import { useBalances, useActions, useStore, useSubgraphUpdate } from 'hooks' +import { commonMessages } from 'helpers' +import { useConfig } from 'config' + +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' -import { Action, openTxCompletedModal } from 'layouts/modals' +import vaultHooks from '../../index' +type Input = { + field: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] +} + const storeSelector = (store: Store) => ({ vaultAddress: store.vault.base.data.vaultAddress, }) -const useSubmit = (params: StakePage.Params) => { - const { field, fetch } = params + +const useBurnSubmit = (values: Input) => { + const { field, fetchAllUserData } = values const actions = useActions() const subgraphUpdate = useSubgraphUpdate() @@ -24,19 +31,29 @@ const useSubmit = (params: StakePage.Params) => { const { signSDK, address, chainId, cancelOnChange } = useConfig() const submit = useCallback(async () => { - const shares = field.value || 0n + const shares = field.value if (!address || !shares) { return } - setSubmitting(true) - actions.ui.setBottomLoader({ content: commonMessages.notification.waitingConfirmation, }) try { + setSubmitting(true) + + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchMintTokenBalance() + refetchDepositTokenBalance() + }, + }) + const hash = await signSDK.osToken.burn({ userAddress: address, vaultAddress, @@ -45,29 +62,7 @@ const useSubmit = (params: StakePage.Params) => { if (hash) { await subgraphUpdate({ hash }) - - field.reset() - - cancelOnChange({ - address, - chainId, - logic: () => { - fetch.data() - fetch.balances() - - refetchMintTokenBalance() - refetchDepositTokenBalance() - }, - }) - - const blockExplorerUrl = signSDK.config.network.blockExplorerUrl - - actions.vault.user.allocatorActions.addFirstItem({ - hash, - shares, - actionType: AllocatorActionType.OsTokenBurned, - link: blockExplorerUrl, - }) + await onSuccess() const tokens = [ { @@ -77,6 +72,7 @@ const useSubmit = (params: StakePage.Params) => { }, ] + field.reset() openTxCompletedModal({ tokens, hash }) } @@ -85,6 +81,7 @@ const useSubmit = (params: StakePage.Params) => { catch (error) { setSubmitting(false) actions.ui.resetBottomLoader() + console.error('Burn send transaction error', error as Error) notifications.open({ @@ -94,9 +91,11 @@ const useSubmit = (params: StakePage.Params) => { return Promise.reject(error) } + finally { + setSubmitting(false) + } }, [ field, - fetch, chainId, signSDK, address, @@ -104,6 +103,7 @@ const useSubmit = (params: StakePage.Params) => { vaultAddress, subgraphUpdate, cancelOnChange, + fetchAllUserData, refetchMintTokenBalance, refetchDepositTokenBalance, ]) @@ -118,4 +118,4 @@ const useSubmit = (params: StakePage.Params) => { } -export default useSubmit +export default useBurnSubmit diff --git a/src/views/SwapView/util/vault/actions/useBurn/useBurnTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useBurn/useBurnTransactionPrice.ts new file mode 100644 index 00000000..e3c3494b --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBurn/useBurnTransactionPrice.ts @@ -0,0 +1,49 @@ +import { useCallback, useState } from 'react' +import { useStore, useAutoFetch } from 'hooks' +import { constants } from 'helpers' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + stakedAssets: store.vault.user.balances.stakedAssets, +}) + +const useBurnTransactionPrice = () => { + const { signSDK, address, isReadOnlyMode } = useConfig() + const { vaultAddress, stakedAssets } = useStore(storeSelector) + + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || !vaultAddress || !stakedAssets || isReadOnlyMode + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + const gas = await signSDK.osToken.burn.estimateGas({ + shares: constants.blockchain.minimalAmount, + userAddress: address, + vaultAddress, + }) + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ signSDK, address, vaultAddress, isSkipFetch ]) + + useAutoFetch({ + action: fetchTransactionPrice, + skip: isSkipFetch, + interval: 30_000, + }) + + return transactionPrice +} + + +export default useBurnTransactionPrice diff --git a/src/views/SwapView/util/vault/actions/useBurn/useFullUnstakeBurnAmount.ts b/src/views/SwapView/util/vault/actions/useBurn/useFullUnstakeBurnAmount.ts new file mode 100644 index 00000000..22aae180 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useBurn/useFullUnstakeBurnAmount.ts @@ -0,0 +1,67 @@ +import { useCallback, useState, useEffect } from 'react' +import { parseEther } from 'ethers' +import { useConfig } from 'config' +import { useStore } from 'hooks' + + +const storeSelector = (store: Store) => ({ + isVaultFetching: store.vault.base.isFetching, + vaultAddress: store.vault.base.data.vaultAddress, + isBalancesFetching: store.vault.user.balances.isFetching, + hasMintBalance: store.vault.user.balances.mintToken.hasMintBalance, +}) + +const minBurnAmount = parseEther('0.00001') + +const useFullUnstakeBurnAmount = () => { + const { sdk, address } = useConfig() + + const { + vaultAddress, + hasMintBalance, + isVaultFetching, + isBalancesFetching, + } = useStore(storeSelector) + + const [ fullUnstakeBurnAmount, setFullUnstakeBurnAmount ] = useState(null) + + const isFetching = isBalancesFetching || isVaultFetching + + const calculateBurn = useCallback(async () => { + try { + if (hasMintBalance && !isFetching && address) { + const sharesToBurn = await sdk.osToken.getBurnAmountForUnstake({ + userAddress: address, + vaultAddress, + }) + + if (sharesToBurn > minBurnAmount) { + setFullUnstakeBurnAmount(sharesToBurn) + } + else { + setFullUnstakeBurnAmount(null) + } + } + } + catch (error) { + console.error('calculateBurn error', error as Error) + + return Promise.reject(error) + } + }, [ + sdk, + address, + isFetching, + vaultAddress, + hasMintBalance, + ]) + + useEffect(() => { + calculateBurn() + }, [ calculateBurn ]) + + return fullUnstakeBurnAmount +} + + +export default useFullUnstakeBurnAmount diff --git a/src/views/SwapView/util/vault/actions/useClaimUnboostQueue.ts b/src/views/SwapView/util/vault/actions/useClaimUnboostQueue.ts new file mode 100644 index 00000000..f40cac1b --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useClaimUnboostQueue.ts @@ -0,0 +1,142 @@ +import { useCallback, useMemo, useState } from 'react' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' +import { useStore, useActions, useBalances, useSubgraphUpdate } from 'hooks' +import notifications from 'modules/notifications' +import { commonMessages } from 'helpers' +import { useConfig } from 'config' + +import vaultHooks from '../index' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + unboostQueue: store.vault.user.unboostQueue.data, + isUnboostQueueFetching: store.vault.user.unboostQueue.isFetching, +}) + +const isEnvClaimUnboostQueueDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_UNBOOST_QUEUE) + +const useClaimUnboostQueue = (values: Input) => { + const { fetchAllUserData } = values + + const actions = useActions() + const subgraphUpdate = useSubgraphUpdate() + const { signSDK, chainId, address, isReadOnlyMode, cancelOnChange } = useConfig() + const { unboostQueue, vaultAddress, isUnboostQueueFetching } = useStore(storeSelector) + + const { + refetchDepositTokenBalance, + refetchNativeTokenBalance, + refetchMintTokenBalance, + } = useBalances() + + const [ isSubmitting, setSubmitting ] = useState(false) + + const claim = useCallback(async () => { + if (address && unboostQueue.position && vaultAddress) { + try { + setSubmitting(true) + + actions.ui.setBottomLoader({ + content: commonMessages.notification.waitingConfirmation, + }) + + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchMintTokenBalance() + refetchNativeTokenBalance() + refetchDepositTokenBalance() + }, + }) + + const hash = await signSDK.boost.claimQueue({ + position: unboostQueue.position, + userAddress: address, + vaultAddress, + }) + + if (hash) { + await subgraphUpdate({ hash }) + await onSuccess() + + setSubmitting(false) + + const tokens = [ + { + token: signSDK.config.tokens.mintToken, + action: Action.Receive, + value: unboostQueue.exitingShares, + }, + { + token: signSDK.config.tokens.depositToken, + action: Action.Receive, + value: unboostQueue.exitingAssets, + }, + ] + + openTxCompletedModal({ tokens, hash }) + } + } + catch (error: any) { + actions.ui.resetBottomLoader() + + console.error('Claim unboost queue error', error as Error) + + setSubmitting(false) + + notifications.open({ + type: 'error', + text: commonMessages.notification.failed, + }) + } + } + }, [ + chainId, + signSDK, + actions, + address, + vaultAddress, + unboostQueue, + cancelOnChange, + subgraphUpdate, + fetchAllUserData, + refetchMintTokenBalance, + refetchNativeTokenBalance, + refetchDepositTokenBalance, + ]) + + const isClaimUnboostQueueDisabled = ( + !address + || isReadOnlyMode + || !unboostQueue.isClaimable + || isEnvClaimUnboostQueueDisabled + ) + + const isClaimUnboostQueueLoading = isSubmitting || isUnboostQueueFetching + + return useMemo(() => ({ + isClaimUnboostQueueLoading, + isClaimUnboostQueueDisabled, + claim, + }), [ + isClaimUnboostQueueLoading, + isClaimUnboostQueueDisabled, + claim, + ]) +} + +useClaimUnboostQueue.mock = { + claim: () => Promise.resolve(), + isClaimUnboostQueueLoading: false, + isClaimUnboostQueueDisabled: false, +} as ReturnType + + +export default useClaimUnboostQueue diff --git a/src/views/SwapView/util/vault/actions/useClaimUnstakeQueue.ts b/src/views/SwapView/util/vault/actions/useClaimUnstakeQueue.ts new file mode 100644 index 00000000..83d6fbe1 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useClaimUnstakeQueue.ts @@ -0,0 +1,138 @@ +import { useCallback, useMemo, useState } from 'react' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' +import { useStore, useActions, useBalances, useSubgraphUpdate } from 'hooks' +import notifications from 'modules/notifications' +import { commonMessages } from 'helpers' +import { useConfig } from 'config' + +import vaultHooks from '../index' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + unstakeQueue: store.vault.user.unstakeQueue.data, + vaultAddress: store.vault.base.data.vaultAddress, + isUnstakeQueueFetching: store.vault.user.unstakeQueue.isFetching, +}) + +const isEnvClaimUnstakeQueueDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_UNSTAKE_QUEUE) + +const useClaimUnstakeQueue = (values: Input) => { + const { fetchAllUserData } = values + + const actions = useActions() + const subgraphUpdate = useSubgraphUpdate() + const { signSDK, chainId, address, isReadOnlyMode, cancelOnChange } = useConfig() + const { unstakeQueue, vaultAddress, isUnstakeQueueFetching } = useStore(storeSelector) + + const { + refetchDepositTokenBalance, + refetchNativeTokenBalance, + refetchMintTokenBalance, + } = useBalances() + + const [ isSubmitting, setSubmitting ] = useState(false) + + const claim = useCallback(async () => { + if (address && unstakeQueue.positions.length && vaultAddress) { + try { + setSubmitting(true) + + actions.ui.setBottomLoader({ + content: commonMessages.notification.waitingConfirmation, + }) + + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchMintTokenBalance() + refetchNativeTokenBalance() + refetchDepositTokenBalance() + }, + }) + + actions.vault.user.unstakeQueue.setFetching(true) + + const hash = await signSDK.vault.claimExitQueue({ + positions: unstakeQueue.positions, + userAddress: address, + vaultAddress, + }) + + if (hash) { + await subgraphUpdate({ hash }) + await onSuccess() + + setSubmitting(false) + + const tokens = [ + { + token: signSDK.config.tokens.depositToken, + value: unstakeQueue.withdrawable, + action: Action.Unstake, + }, + ] + + openTxCompletedModal({ tokens, hash }) + } + } + catch (error: any) { + setSubmitting(false) + actions.ui.resetBottomLoader() + actions.vault.user.unstakeQueue.setFetching(false) + console.error('Claim unstake queue error', error as Error) + + notifications.open({ + type: 'error', + text: commonMessages.notification.failed, + }) + } + } + }, [ + chainId, + signSDK, + actions, + address, + vaultAddress, + unstakeQueue, + cancelOnChange, + subgraphUpdate, + fetchAllUserData, + refetchMintTokenBalance, + refetchNativeTokenBalance, + refetchDepositTokenBalance, + ]) + + const isClaimUnstakeQueueDisabled = ( + !address + || isReadOnlyMode + || !unstakeQueue.withdrawable + || isEnvClaimUnstakeQueueDisabled + ) + + const isClaimUnstakeQueueLoading = isSubmitting || isUnstakeQueueFetching + + return useMemo(() => ({ + isClaimUnstakeQueueLoading, + isClaimUnstakeQueueDisabled, + claim, + }), [ + isClaimUnstakeQueueLoading, + isClaimUnstakeQueueDisabled, + claim, + ]) +} + +useClaimUnstakeQueue.mock = { + claim: () => Promise.resolve(), + isClaimUnstakeQueueLoading: false, + isClaimUnstakeQueueDisabled: false, +} as ReturnType + + +export default useClaimUnstakeQueue diff --git a/src/views/SwapView/util/vault/actions/useMint/index.ts b/src/views/SwapView/util/vault/actions/useMint/index.ts new file mode 100644 index 00000000..1f3748b8 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useMint/index.ts @@ -0,0 +1,69 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' + +import vaultHooks from '../../index' + +import useMintField from './useMintField' +import useMintSubmit from './useMintSubmit' +import useMintHealth from './useMintHealth' +import useMintDisabled from './useMintDisabled' +import useMintTransactionPrice from './useMintTransactionPrice' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + isBalancesFetching: store.vault.user.balances.isFetching, + maxMintShares: store.vault.user.balances.mintToken.maxMintShares, +}) + +const useMint = (values: Input) => { + const { fetchAllUserData } = values + + const field = useMintField() + const transactionPrice = useMintTransactionPrice() + const { getStyleByHealth, getHealthFactor } = useMintHealth() + const { isMintDisabled, mintDisabledTooltip } = useMintDisabled({ field }) + const { submit, isSubmitting } = useMintSubmit({ field, fetchAllUserData }) + const { maxMintShares, isBalancesFetching } = useStore(storeSelector) + + const isMintLoading = isBalancesFetching || isSubmitting + + return useMemo(() => ({ + field, + maxMintShares, + isMintLoading, + isMintDisabled, + transactionPrice, + mintDisabledTooltip, + getStyleByHealth, + getHealthFactor, + submit, + }), [ + field, + maxMintShares, + isMintLoading, + isMintDisabled, + transactionPrice, + mintDisabledTooltip, + getStyleByHealth, + getHealthFactor, + submit, + ]) +} + +useMint.mock = { + ...useMintHealth.mock, + maxMintShares: 0n, + isMintLoading: false, + transactionPrice: 0n, + isMintDisabled: false, + mintDisabledTooltip: undefined, + field: {} as Forms.Field, + submit: () => Promise.resolve(), +} as ReturnType + + +export default useMint diff --git a/src/views/SwapView/util/vault/actions/useMint/messages.ts b/src/views/SwapView/util/vault/actions/useMint/messages.ts new file mode 100644 index 00000000..3cc8122a --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useMint/messages.ts @@ -0,0 +1,38 @@ +export default { + tooltipNeedStaking: { + en: 'You can mint {mintToken} only after staking {depositToken} in the Vault.', + ru: 'Вы можете минтить {mintToken} только после стейкинга {depositToken} в Волте.', + fr: 'Vous pouvez minter {mintToken} seulement après avoir staké {depositToken} dans le Vault.', + es: 'Puedes mintear {mintToken} solo después de hacer staking de {depositToken} en el Vault.', + pt: 'Você pode mintar {mintToken} apenas após fazer staking de {depositToken} no Vault.', + de: 'Sie können {mintToken} nur nach dem Staking von {depositToken} im Vault minten.', + zh: '您只能在Vault中质押{depositToken}后铸造{mintToken}。', + }, + tooltipNeedValidators: { + en: 'The Vault must have at least one registered validator to mint {mintToken}.', + ru: 'В Волте должен быть зарегистрирован как минимум один валидатор для Минта {mintToken}.', + fr: 'Le Волт doit avoir au moins un validateur enregistré pour minter {mintToken}.', + es: 'El Волт debe tener al menos un validador registrado para mintear {mintToken}.', + pt: 'O Волт deve ter pelo menos um validador registrado para mintear {mintToken}.', + de: 'Der Волт muss mindestens einen registrierten Validator haben, um {mintToken} zu mintern.', + zh: 'Волт必须有至少一个注册验证者才能铸造{mintToken}。', + }, + capacityError: { + en: 'Value exceeds the capacity of the Vault', + ru: 'Значение превышает емкость Волта', + fr: 'La valeur dépasse la capacité du Vault', + es: 'El valor excede la capacidad del Vault', + pt: 'O valor excede a capacidade do Vault', + de: 'Der Wert überschreitet die Kapazität des Vault', + zh: '数值超出Vault的容量', + }, + maxMintError: { + en: 'Stake more {depositToken} in the Vault to mint more {mintToken}.', + ru: 'Стейкните больше {depositToken} в Волте, чтобы заминтить больше {mintToken}.', + fr: 'Stakez plus de {depositToken} dans le Vault pour minter plus de {mintToken}.', + es: 'Haz stake de más {depositToken} en el Vault para mintear más {mintToken}.', + pt: 'Faça staking de mais {depositToken} no Vault para mintar mais {mintToken}.', + de: 'Staken Sie mehr {depositToken} im Vault, um mehr {mintToken} zu minten.', + zh: '在Vault中质押更多{depositToken}以铸造更多{mintToken}。', + }, +} diff --git a/src/views/SwapView/util/vault/actions/useMint/useMintDisabled.ts b/src/views/SwapView/util/vault/actions/useMint/useMintDisabled.ts new file mode 100644 index 00000000..77af4e13 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useMint/useMintDisabled.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react' +import { useConfig } from 'config' +import forms from 'modules/forms' +import { useStore } from 'hooks' + +import messages from './messages' + + +type Input = { + field: Forms.Field +} + +const isEnvMintDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_MINT) + +const storeSelector = (store: Store) => ({ + stakedAssets: store.vault.user.balances.stakedAssets, + isCollateralized: store.vault.base.data.isCollateralized, +}) + +const useMintDisabled = (values: Input) => { + const { field } = values + + const { stakedAssets, isCollateralized } = useStore(storeSelector) + + const { error } = forms.useFieldValue(field) + const { sdk, address, isReadOnlyMode } = useConfig() + + const isMintDisabled = ( + !address + || isReadOnlyMode + || !stakedAssets + || Boolean(error) + || isEnvMintDisabled + || !isCollateralized + ) + + let mintDisabledTooltip: Intl.Message | undefined = undefined + const { depositToken, mintToken } = sdk.config.tokens + + if (address && !stakedAssets) { + mintDisabledTooltip = { + ...messages.tooltipNeedStaking, + values: { depositToken, mintToken }, + } + } + + if (!isCollateralized) { + mintDisabledTooltip = { + ...messages.tooltipNeedValidators, + values: { mintToken }, + } + } + + return useMemo(() => ({ + mintDisabledTooltip, + isMintDisabled, + }), [ + mintDisabledTooltip, + isMintDisabled, + ]) +} + + +export default useMintDisabled diff --git a/src/views/SwapView/util/vault/actions/useMint/useMintField.ts b/src/views/SwapView/util/vault/actions/useMint/useMintField.ts new file mode 100644 index 00000000..4d02c72a --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useMint/useMintField.ts @@ -0,0 +1,62 @@ +import { useRef } from 'react' +import { parseEther, MaxInt256 } from 'ethers' +import forms from 'modules/forms' +import { StakeWiseSDK } from 'sdk' +import { useConfig } from 'config' +import { useStore } from 'hooks' + +import messages from './messages' + + +const checkCapacity = (capacity: { current: bigint }) => (value?: Forms.FieldValue) => { + if (isNaN(Number(capacity.current))) { + return + } + + if (capacity.current < (value as bigint)) { + return { ...messages.capacityError } + } +} + +const checkMintAvailable = (mintShares: { current: bigint }, sdk: StakeWiseSDK) => (value?: Forms.FieldValue) => { + if (!mintShares.current || mintShares.current < (value as bigint)) { + return { + ...messages.maxMintError, + values: { + depositToken: sdk.config.tokens.depositToken, + mintToken: sdk.config.tokens.mintToken, + }, + } + } +} + +const storeSelector = (store: Store) => ({ + capacity: store.vault.base.data.capacity, + maxMintShares: store.vault.user.balances.mintToken.maxMintShares, +}) + +const useMintField = () => { + const { capacity, maxMintShares } = useStore(storeSelector) + + const { sdk } = useConfig() + + const maxMintSharesRef = useRef(maxMintShares) + maxMintSharesRef.current = maxMintShares + + const capacityRef = useRef(parseEther(capacity === '∞' ? MaxInt256.toString() : capacity)) + capacityRef.current = parseEther(capacity === '∞' ? MaxInt256.toString() : capacity) + + const field = forms.useField({ + valueType: 'bigint', + validators: [ + checkMintAvailable(maxMintSharesRef, sdk), + forms.validators.numberWithDot, + checkCapacity(capacityRef), + ], + }) + + return field +} + + +export default useMintField diff --git a/src/views/HomeView/StakeContext/util/actions/useMint/useHealth.ts b/src/views/SwapView/util/vault/actions/useMint/useMintHealth.ts similarity index 85% rename from src/views/HomeView/StakeContext/util/actions/useMint/useHealth.ts rename to src/views/SwapView/util/vault/actions/useMint/useMintHealth.ts index 67d25183..9631d7eb 100644 --- a/src/views/HomeView/StakeContext/util/actions/useMint/useHealth.ts +++ b/src/views/SwapView/util/vault/actions/useMint/useMintHealth.ts @@ -1,14 +1,14 @@ import { useCallback, useMemo, useRef } from 'react' import { OsTokenPositionHealth } from 'sdk' -import { useStore } from 'hooks' -import { useConfig } from 'config' import { commonMessages } from 'helpers' +import { useConfig } from 'config' +import { useStore } from 'hooks' -import type { TextProps } from 'components' +import type { TextColor } from 'components' type PositionHealthStyle = { - color: TextProps['color'] + color: TextColor text: Intl.Message } @@ -17,16 +17,6 @@ type HealthFactor = { value: number } -type Output = { - getStyleByHealth: (health: OsTokenPositionHealth) => PositionHealthStyle - getHealthFactor: (mintedAssets: bigint, stakedAssets: bigint) => HealthFactor -} - -interface Hook { - (): Output - mock: Output -} - const styles: Record = { [OsTokenPositionHealth.Healthy]: { text: commonMessages.status.healthy, @@ -51,7 +41,7 @@ const storeSelector = (store: Store) => ({ liqThresholdPercent: BigInt(store.vault.base.data.osTokenConfig.liqThresholdPercent), }) -const useHealth: Hook = () => { +const useMintHealth = () => { const { sdk } = useConfig() const { liqThresholdPercent, isFetching } = useStore(storeSelector) @@ -92,10 +82,10 @@ const useHealth: Hook = () => { ]) } -useHealth.mock = { +useMintHealth.mock = { getStyleByHealth: () => styles[OsTokenPositionHealth.Healthy], getHealthFactor: () => ({ health: OsTokenPositionHealth.Healthy, value: 1.2 }), } -export default useHealth +export default useMintHealth diff --git a/src/views/HomeView/StakeContext/util/actions/useMint/useSubmit.ts b/src/views/SwapView/util/vault/actions/useMint/useMintSubmit.ts similarity index 67% rename from src/views/HomeView/StakeContext/util/actions/useMint/useSubmit.ts rename to src/views/SwapView/util/vault/actions/useMint/useMintSubmit.ts index 6bb6c3c5..02a6b06a 100644 --- a/src/views/HomeView/StakeContext/util/actions/useMint/useSubmit.ts +++ b/src/views/SwapView/util/vault/actions/useMint/useMintSubmit.ts @@ -1,37 +1,46 @@ -import { useCallback, useMemo, useState } from 'react' -import { useActions, useBalances, useStore, useSubgraphUpdate } from 'hooks' +import { useCallback, useState, useMemo } from 'react' import { getters, commonMessages } from 'helpers' import notifications from 'modules/notifications' -import { AllocatorActionType } from 'sdk' import { useConfig } from 'config' +import { + useStore, + useActions, + useBalances, + useSubgraphUpdate, +} from 'hooks' -import { Action, openTxCompletedModal } from 'layouts/modals' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' +import vaultHooks from '../../index' + + +type Input = { + field: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] +} const storeSelector = (store: Store) => ({ vaultAddress: store.vault.base.data.vaultAddress, }) -const useSubmit = (params: StakePage.Params) => { - const { field, fetch } = params +const useMintSubmit = (values: Input) => { + const { field, fetchAllUserData } = values const actions = useActions() const { vaultAddress } = useStore(storeSelector) + const [ isSubmitting, setSubmitting ] = useState(false) const { signSDK, address, chainId, cancelOnChange } = useConfig() const subgraphUpdate = useSubgraphUpdate() - const [ isSubmitting, setSubmitting ] = useState(false) const { refetchDepositTokenBalance, refetchMintTokenBalance } = useBalances() const submit = useCallback(async () => { - const shares = field.value || 0n + const shares = field.value if (!address || !shares) { return } - setSubmitting(true) - actions.ui.setBottomLoader({ content: commonMessages.notification.waitingConfirmation, }) @@ -39,6 +48,18 @@ const useSubmit = (params: StakePage.Params) => { try { const referrerAddress = getters.getReferrer() + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchMintTokenBalance() + refetchDepositTokenBalance() + }, + }) + + setSubmitting(true) + const hash = await signSDK.osToken.mint({ userAddress: address, referrerAddress, @@ -48,27 +69,7 @@ const useSubmit = (params: StakePage.Params) => { if (hash) { await subgraphUpdate({ hash }) - - field.reset() - - cancelOnChange({ - address, - chainId, - logic: () => { - fetch.data() - fetch.balances() - - refetchMintTokenBalance() - refetchDepositTokenBalance() - }, - }) - - actions.vault.user.allocatorActions.addFirstItem({ - hash, - shares, - actionType: AllocatorActionType.OsTokenMinted, - link: signSDK.config.network.blockExplorerUrl, - }) + await onSuccess() const tokens = [ { @@ -78,14 +79,13 @@ const useSubmit = (params: StakePage.Params) => { }, ] + field.reset() openTxCompletedModal({ tokens, hash }) } - - setSubmitting(false) } catch (error) { - setSubmitting(false) actions.ui.resetBottomLoader() + console.error('Mint send transaction error', error as Error) notifications.open({ @@ -95,9 +95,11 @@ const useSubmit = (params: StakePage.Params) => { return Promise.reject(error) } + finally { + setSubmitting(false) + } }, [ field, - fetch, chainId, signSDK, address, @@ -105,6 +107,7 @@ const useSubmit = (params: StakePage.Params) => { vaultAddress, subgraphUpdate, cancelOnChange, + fetchAllUserData, refetchMintTokenBalance, refetchDepositTokenBalance, ]) @@ -119,4 +122,4 @@ const useSubmit = (params: StakePage.Params) => { } -export default useSubmit +export default useMintSubmit diff --git a/src/views/SwapView/util/vault/actions/useMint/useMintTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useMint/useMintTransactionPrice.ts new file mode 100644 index 00000000..4dc6d52b --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useMint/useMintTransactionPrice.ts @@ -0,0 +1,52 @@ +import { useCallback, useState } from 'react' +import { useStore, useAutoFetch } from 'hooks' +import { getters, constants } from 'helpers' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + stakedAssets: store.vault.user.balances.stakedAssets, +}) + +const useMintTransactionPrice = () => { + const { signSDK, address, isReadOnlyMode } = useConfig() + const { vaultAddress, stakedAssets } = useStore(storeSelector) + + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || !vaultAddress || !stakedAssets || isReadOnlyMode + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + const referrerAddress = getters.getReferrer() + + const gas = await signSDK.vault.deposit.estimateGas({ + assets: constants.blockchain.minimalAmount, + userAddress: address, + referrerAddress, + vaultAddress, + }) + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ signSDK, address, vaultAddress, isSkipFetch ]) + + useAutoFetch({ + action: fetchTransactionPrice, + skip: isSkipFetch, + interval: 30_000, + }) + + return transactionPrice +} + + +export default useMintTransactionPrice diff --git a/src/views/SwapView/util/vault/actions/useStake/index.ts b/src/views/SwapView/util/vault/actions/useStake/index.ts new file mode 100644 index 00000000..aac416d3 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/index.ts @@ -0,0 +1,100 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' + +import vaultHooks from '../../index' + +import useStakeField from './useStakeField' +import useStakeSubmit from './useStakeSubmit' +import useStakeDisabled from './useStakeDisabled' +import useStakeMaxAmount from './useStakeMaxAmount' +import { useSwapTokens, useSwapQuote, useSwapActions } from './swap' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + isBalancesFetching: store.vault.user.balances.isFetching, +}) + +const useStake = (values: Input) => { + const { fetchAllUserData } = values + + const swapTokens = useSwapTokens() + const { isBalancesFetching } = useStore(storeSelector) + + const { + swapFee, + isSwapQuoteFetching, + fetchQuote, + getSwappedDepositAmount, + } = useSwapQuote({ swapTokens }) + + const field = useStakeField({ + swapFee, + swapTokens, + getSwappedDepositAmount, + }) + + const { swap, cancelSwap } = useSwapActions({ + field, + swapTokens, + fetchQuote, + }) + + const { submit, transactionPrice, isSubmitting, isAllowanceFetching } = useStakeSubmit({ + swapTokens, + field, + swap, + cancelSwap, + fetchAllUserData, + }) + + const isStakeDisabled = useStakeDisabled({ field }) + const maxStakeAmount = useStakeMaxAmount({ swapTokens, transactionPrice }) + + const isStakeLoading = ( + isSubmitting + || isBalancesFetching + || isSwapQuoteFetching + || isAllowanceFetching + ) + + return useMemo(() => ({ + field, + swapTokens, + maxStakeAmount, + isStakeLoading, + isStakeDisabled, + transactionPrice, + submit, + fetchQuote, + getSwappedDepositAmount, + }), [ + field, + swapTokens, + maxStakeAmount, + isStakeLoading, + isStakeDisabled, + transactionPrice, + submit, + fetchQuote, + getSwappedDepositAmount, + ]) +} + +useStake.mock = { + maxStakeAmount: 0n, + transactionPrice: 0n, + isStakeLoading: false, + isStakeDisabled: false, + swapTokens: useSwapTokens.mock, + field: {} as Forms.Field, + fetchQuote: () => ({}) as any, + submit: () => Promise.resolve(), + getSwappedDepositAmount: () => 0n, +} as ReturnType + + +export default useStake diff --git a/src/hooks/stake/messages.ts b/src/views/SwapView/util/vault/actions/useStake/messages.ts similarity index 100% rename from src/hooks/stake/messages.ts rename to src/views/SwapView/util/vault/actions/useStake/messages.ts diff --git a/src/views/SwapView/util/vault/actions/useStake/swap/index.ts b/src/views/SwapView/util/vault/actions/useStake/swap/index.ts new file mode 100644 index 00000000..cffbad5d --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/swap/index.ts @@ -0,0 +1,3 @@ +export { default as useSwapActions } from './useSwapActions' +export { default as useSwapTokens } from './useSwapTokens' +export { default as useSwapQuote } from './useSwapQuote' diff --git a/src/hooks/stake/useSwap.ts b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapActions.ts similarity index 69% rename from src/hooks/stake/useSwap.ts rename to src/views/SwapView/util/vault/actions/useStake/swap/useSwapActions.ts index d624096a..581269e8 100644 --- a/src/hooks/stake/useSwap.ts +++ b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapActions.ts @@ -1,23 +1,29 @@ import { useCallback, useMemo, useRef } from 'react' -import { useConfig } from 'config' -import { ZeroAddress } from 'ethers' import type { OrderCreation, OrderStatus, SupportedChainId } from '@cowprotocol/cow-sdk' +import { useActions, useBalances } from 'hooks' import { StakeStep } from 'helpers/enums' +import { useConfig } from 'config' -import type { TransactionStatus } from '../../components/Transactions/util' -import type { SetTransaction } from '../../components/Transactions/types' -import Transactions from '../../components/Transactions/Transactions' +import { Transactions } from 'components' +import type { SetTransaction, TransactionStatus } from 'components' -import useActions from '../data/useActions' -import useBalances from '../data/useBalances' +import useSwapSDK from './useSwapSDK' +import useSwapQuote from './useSwapQuote' +import useSwapTokens from './useSwapTokens' +type Input = { + field: Forms.Field + swapTokens: ReturnType + fetchQuote: ReturnType['fetchQuote'] +} + type FetchQuoteInput = { amount: bigint fromToken: string } -type SwapInput = FetchQuoteInput & { +type SwapInput = { setTransaction: SetTransaction } @@ -39,69 +45,24 @@ type WaitForTradeOutput = { sellAmount: string } -const useSwap = () => { +const useSwapActions = (values: Input) => { + const { field, swapTokens, fetchQuote } = values + const { signSDK, address, chainId, isMainnet } = useConfig() const { refetchDepositTokenBalance, refetchSwapTokenBalances } = useBalances() const actions = useActions() const orderIdRef = useRef('') - - const depositTokenAddress = isMainnet - ? '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' // this is the address of ETH in cow protocol - : '0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb' - - const getCowSdk = useCallback(async () => { - const { OrderBookApi, OrderQuoteSideKindSell, OrderSigningUtils } = await import('@cowprotocol/cow-sdk') - const { signOrder, signOrderCancellations } = OrderSigningUtils - - return { - kind: OrderQuoteSideKindSell.SELL, - signOrder, - signOrderCancellations, - orderBookApi: new OrderBookApi({ - chainId: chainId as SupportedChainId, - }), - } - }, [ chainId ]) - - const fetchQuote = useCallback(async (values: FetchQuoteInput) => { - const { amount, fromToken } = values - - const { orderBookApi, kind } = await getCowSdk() - - const quoteRequest = { - from: address || ZeroAddress, - receiver: address || ZeroAddress, - buyToken: depositTokenAddress, - sellToken: fromToken, - sellAmountBeforeFee: amount.toString(), - kind, - } - - try { - const { quote } = await orderBookApi.getQuote(quoteRequest) - - return quote - } - catch (error: any) { - if (error?.body?.data?.fee_amount) { - return Promise.reject({ - feeAmount: error?.body?.data?.fee_amount, - }) - } - - return Promise.reject(error) - } - }, [ address, depositTokenAddress, getCowSdk ]) + const getSwapSDK = useSwapSDK() const getSigner = useCallback(async (address: string) => { const signer = await signSDK.provider.getSigner(address) - // Fix for error caused by different ethers versions: signer (v6) and cow sdk (v5) + // @ts-ignore: Fix for error caused by different ethers versions: signer (v6) and cow sdk (v5) signer._signTypedData = signer.signTypedData return signer - }, [ signSDK, address ]) + }, [ signSDK ]) const sendOrder = useCallback(async (values: FetchQuoteInput) => { const { amount, fromToken } = values @@ -110,7 +71,7 @@ const useSwap = () => { return {} } - const { orderBookApi, signOrder } = await getCowSdk() + const { orderBookApi, signOrder } = await getSwapSDK() const quote = await fetchQuote({ amount, fromToken }) const signer = await getSigner(address) @@ -127,7 +88,7 @@ const useSwap = () => { ...orderParams, } - const orderSigningResult = await signOrder(unsignedOrder, chainId as SupportedChainId, signer) + const orderSigningResult = await signOrder(unsignedOrder, chainId as SupportedChainId, signer as any) const sendOrderInput = { ...quote, @@ -141,16 +102,23 @@ const useSwap = () => { orderId, buyAmount: BigInt(quote.buyAmount), } - }, [ chainId, getSigner, address, getCowSdk, fetchQuote ]) + }, [ chainId, getSigner, address, getSwapSDK, fetchQuote ]) const waitForTrade = useCallback(async (orderId: string): Promise => { try { - const { orderBookApi } = await getCowSdk() + const { orderBookApi } = await getSwapSDK() const { status, buyAmount, sellAmount, sellToken, buyToken } = await orderBookApi.getOrder(orderId) const isFailed = [ 'expired', 'cancelled' ].includes(status) if (isFailed) { + console.error('Swap failed', { + orderId, + status, + sellToken, + sellAmount, + }) + return { status, buyToken, @@ -179,7 +147,7 @@ const useSwap = () => { catch (error) { throw new Error(error as string) } - }, [ getCowSdk ]) + }, [ getSwapSDK ]) const setNextSteps = useCallback(({ status, setTransaction }: SetNextStepsInput) => { const nextSteps = [ StakeStep.Swap, StakeStep.Approve, StakeStep.Stake ] @@ -189,15 +157,24 @@ const useSwap = () => { nextSteps.forEach((step) => { setTransaction(step, status) }) - }, []) + }, [ actions ]) const swap = useCallback(async (values: SwapInput) => { - const { amount, fromToken, setTransaction } = values + const { setTransaction } = values + + const assets = field.value + + if (!swapTokens.selected.address || !assets) { + return 0n + } try { setTransaction(StakeStep.Swap, Transactions.Status.Confirm) - const { orderId } = await sendOrder({ amount, fromToken }) + const { orderId } = await sendOrder({ + fromToken: swapTokens.selected.address, + amount: assets, + }) if (!orderId) { setNextSteps({ status: Transactions.Status.Fail, setTransaction }) @@ -225,12 +202,19 @@ const useSwap = () => { return Promise.reject('Order was cancelled') } else if (hash) { + const resultAmount = BigInt(buyAmount) + setTransaction(StakeStep.Swap, Transactions.Status.Success) refetchSwapTokenBalances() refetchDepositTokenBalance() actions.ui.resetBottomLoader() - return BigInt(buyAmount) + if (assets !== resultAmount) { + swapTokens.setSelected('') + field.setValue(assets) + } + + return resultAmount } else { setNextSteps({ status: Transactions.Status.Fail, setTransaction }) @@ -245,17 +229,20 @@ const useSwap = () => { return Promise.reject(error as string) } }, [ + field, actions, isMainnet, sendOrder, + swapTokens, waitForTrade, + setNextSteps, refetchSwapTokenBalances, refetchDepositTokenBalance, ]) const cancelSwap = useCallback(async ({ setTransaction }: CancelSwapInput) => { if (address && orderIdRef.current) { - const { orderBookApi, signOrderCancellations } = await getCowSdk() + const { orderBookApi, signOrderCancellations } = await getSwapSDK() setTransaction(StakeStep.Swap, Transactions.Status.Canceling) @@ -266,7 +253,7 @@ const useSwap = () => { const orderCancellationsSigningResult = await signOrderCancellations( orderUids, chainId as SupportedChainId, - signer + signer as any ) try { @@ -282,18 +269,16 @@ const useSwap = () => { setTransaction(StakeStep.Swap, Transactions.Status.Success) } } - }, [ address, getSigner, getCowSdk ]) + }, [ address, chainId, getSigner, getSwapSDK ]) return useMemo(() => ({ swap, cancelSwap, - fetchQuote, }), [ swap, cancelSwap, - fetchQuote, ]) } -export default useSwap +export default useSwapActions diff --git a/src/views/SwapView/util/vault/actions/useStake/swap/useSwapQuote.ts b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapQuote.ts new file mode 100644 index 00000000..df28ddb5 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapQuote.ts @@ -0,0 +1,151 @@ +import { useCallback, useMemo, useEffect } from 'react' +import { ZeroAddress, parseUnits } from 'ethers' +import { useObjectState } from 'hooks' +import { useConfig } from 'config' +import { BigDecimal } from 'sdk' + +import useSwapSDK from './useSwapSDK' +import useSwapTokens from './useSwapTokens' + + +type Input = { + swapTokens: ReturnType +} + +type FetchQuoteInput = { + amount: bigint + fromToken: string +} + +const initialState = { + swapFee: 0n, + swappedDepositAmount: 0n, + isSwapQuoteFetching: false, +} + +const useSwap = (values: Input) => { + const { swapTokens } = values + + const getSwapSDK = useSwapSDK() + const { address, isMainnet } = useConfig() + + const balance = address + ? swapTokens.selected.balance + : parseUnits('1', swapTokens.selected.units) + + const tokenAddress = swapTokens.selected.address + + const skip = !tokenAddress || !balance + + const [ state, setState ] = useObjectState({ + ...initialState, + isSwapQuoteFetching: !skip, + }) + + const depositTokenAddress = isMainnet + ? '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' // this is the address of ETH in cow protocol + : '0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb' + + const fetchQuote = useCallback(async (values: FetchQuoteInput) => { + const { amount, fromToken } = values + + const { orderBookApi, kind } = await getSwapSDK() + + const quoteRequest = { + from: address || ZeroAddress, + receiver: address || ZeroAddress, + buyToken: depositTokenAddress, + sellToken: fromToken, + sellAmountBeforeFee: amount.toString(), + kind, + } + + try { + const { quote } = await orderBookApi.getQuote(quoteRequest) + + return quote + } + catch (error: any) { + if (error?.body?.data?.fee_amount) { + return Promise.reject({ + feeAmount: error?.body?.data?.fee_amount, + }) + } + + return Promise.reject(error) + } + }, [ address, depositTokenAddress, getSwapSDK ]) + + const fetchBalanceQuote = useCallback(async () => { + setState({ ...initialState, isSwapQuoteFetching: true }) + + let fee = '0' + let buyAmount = '0' + + try { + if (tokenAddress) { + const quote = await fetchQuote({ + fromToken: tokenAddress, + amount: balance, + }) + + fee = quote.feeAmount + buyAmount = quote.buyAmount + } + } + catch (error: any) { + if (error?.feeAmount) { + fee = error.feeAmount as string + } + } + + setState({ + swapFee: BigInt(fee), + swappedDepositAmount: BigInt(buyAmount), + isSwapQuoteFetching: false, + }) + }, [ balance, tokenAddress, fetchQuote, setState ]) + + const getSwappedDepositAmount = useCallback((value: bigint) => { + if (!tokenAddress) { + return value + } + + if (balance && state.swappedDepositAmount) { + const balancePercent = new BigDecimal(balance).divide(100) + const percent = new BigDecimal(value).divide(balancePercent) + + const result = new BigDecimal(state.swappedDepositAmount) + .divide(100) + .multiply(percent) + .decimals(0) + .toNumber() + + return BigInt(result) + } + + return 0n + }, [ balance, tokenAddress, state.swappedDepositAmount ]) + + useEffect(() => { + if (skip) { + setState(initialState) + } + else { + fetchBalanceQuote() + } + }, [ skip, fetchBalanceQuote, setState ]) + + return useMemo(() => ({ + ...state, + fetchQuote, + getSwappedDepositAmount, + }), [ + state, + fetchQuote, + getSwappedDepositAmount, + ]) +} + + +export default useSwap diff --git a/src/views/SwapView/util/vault/actions/useStake/swap/useSwapSDK.ts b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapSDK.ts new file mode 100644 index 00000000..4abe03b4 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapSDK.ts @@ -0,0 +1,25 @@ +import { useCallback } from 'react' +import { useConfig } from 'config' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' + + +const useSwapSDK = () => { + const { chainId } = useConfig() + + return useCallback(async () => { + const { OrderBookApi, OrderQuoteSideKindSell, OrderSigningUtils } = await import('@cowprotocol/cow-sdk') + const { signOrder, signOrderCancellations } = OrderSigningUtils + + return { + kind: OrderQuoteSideKindSell.SELL, + signOrder, + signOrderCancellations, + orderBookApi: new OrderBookApi({ + chainId: chainId as SupportedChainId, + }), + } + }, [ chainId ]) +} + + +export default useSwapSDK diff --git a/src/hooks/stake/useSwapTokens.ts b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapTokens.ts similarity index 69% rename from src/hooks/stake/useSwapTokens.ts rename to src/views/SwapView/util/vault/actions/useStake/swap/useSwapTokens.ts index e7e8224e..42e37e65 100644 --- a/src/hooks/stake/useSwapTokens.ts +++ b/src/views/SwapView/util/vault/actions/useStake/swap/useSwapTokens.ts @@ -1,7 +1,8 @@ import { useEffect, useMemo, useState } from 'react' +import { swapTokens, swapTokenTitles, constants, getters } from 'helpers' +import { ZeroAddress } from 'ethers' import { useConfig } from 'config' -import { swapTokens, swapTokenTitles, constants } from 'helpers' -import useStore from '../data/useStore' +import { useStore } from 'hooks' import { LogoName } from 'components' @@ -9,39 +10,33 @@ import { LogoName } from 'components' const storeSelector = (store: Store) => ({ swapTokenRates: store.swapTokenRates.data, swapTokenBalances: store.account.swapTokenBalances.data, - depositTokenBalance: store.account.balances.data.depositTokenBalance, + depositTokenBalance: store.account.balances.depositToken, + isSwapTokenRatesFetching: store.swapTokenRates.isFetching, + isSwapTokenBalancesFetching: store.account.swapTokenBalances.isFetching, }) -type Output = { - list: SwapToken[] - selected: SwapToken - setSelected: (address: string | null) => void -} - -interface Hook { - (): Output - mock: Output -} - -const useSwapTokens: Hook = () => { +const useSwapTokens = () => { const { sdk, chainId, isMainnet } = useConfig() const [ selected, setSelected ] = useState('') - const { swapTokenRates, swapTokenBalances, depositTokenBalance } = useStore(storeSelector) - const chainTokens = swapTokens[chainId as keyof typeof swapTokens] + const { + swapTokenRates, + swapTokenBalances, + depositTokenBalance, + isSwapTokenRatesFetching, + isSwapTokenBalancesFetching, + } = useStore(storeSelector) - useEffect(() => { - setSelected('') - }, [ chainTokens ]) + const chainTokens = swapTokens[chainId as keyof typeof swapTokens] const depositToken = useMemo(() => ({ - title: isMainnet ? 'Ether' : 'Gnosis', - name: sdk.config.tokens.depositToken as string, - address: '', logo: `token/${sdk.config.tokens.depositToken}` as LogoName, + emptyBalance: constants.blockchain.emptyBalance, + name: sdk.config.tokens.depositToken as string, + title: isMainnet ? 'Ether' : 'Gnosis', balance: depositTokenBalance, + address: null as string | null, units: 18, - emptyBalance: constants.blockchain.emptyBalance, }), [ sdk, isMainnet, depositTokenBalance ]) const list = useMemo(() => { @@ -55,6 +50,7 @@ const useSwapTokens: Hook = () => { .map((name) => { const address = chainTokens[name as keyof typeof chainTokens] const balance = swapTokenBalances[name] || 0n + let units = 18 let emptyBalance = constants.blockchain.emptyBalance @@ -93,17 +89,30 @@ const useSwapTokens: Hook = () => { return result.concat(swapTokensList) }, [ depositToken, chainTokens, swapTokenRates, swapTokenBalances ]) - const selectedToken = useMemo(() => ( - list.find(({ address }) => address === selected) || depositToken - ), [ list, selected, depositToken ]) + const selectedToken = useMemo(() => { + if (!selected) { + return null + } + + return list.find(({ address }) => address && getters.isEqualAddresses(address, selected)) + }, [ list, selected ]) + + useEffect(() => { + setSelected('') + }, [ chainTokens ]) + + const isSwapTokensFetching = isSwapTokenRatesFetching || isSwapTokenBalancesFetching return useMemo(() => ({ list, - selected: selectedToken, - setSelected: setSelected as Output['setSelected'], + isSwapTokensFetching, + selected: selectedToken || depositToken, + setSelected: setSelected, }), [ list, + depositToken, selectedToken, + isSwapTokensFetching, setSelected, ]) } @@ -111,16 +120,17 @@ const useSwapTokens: Hook = () => { useSwapTokens.mock = { list: [], selected: { + units: 18, + balance: 0n, name: 'Ether', + address: ZeroAddress, title: constants.tokens.eth, - address: '', - logo: `token/${constants.tokens.eth}` as LogoName, - balance: 0n, - units: 18, emptyBalance: constants.blockchain.emptyBalance, + logo: `token/${constants.tokens.eth}` as LogoName, }, + isSwapTokensFetching: false, setSelected: () => {}, -} +} as ReturnType export default useSwapTokens diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeDisabled.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeDisabled.ts new file mode 100644 index 00000000..62d42abe --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeDisabled.ts @@ -0,0 +1,28 @@ +import { useConfig } from 'config' +import forms from 'modules/forms' + + +type Input = { + field: Forms.Field +} + +const isEnvStakeDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_STAKE) + +const useStakeDisabled = (values: Input) => { + const { field } = values + + const { error } = forms.useFieldValue(field) + const { address, isReadOnlyMode } = useConfig() + + const isStakeDisabled = ( + !address + || Boolean(error) + || isReadOnlyMode + || isEnvStakeDisabled + ) + + return isStakeDisabled +} + + +export default useStakeDisabled diff --git a/src/hooks/stake/useStakeField.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeField.ts similarity index 56% rename from src/hooks/stake/useStakeField.ts rename to src/views/SwapView/util/vault/actions/useStake/useStakeField.ts index 381e8181..1b4dcf0b 100644 --- a/src/hooks/stake/useStakeField.ts +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeField.ts @@ -1,34 +1,34 @@ -import { useRef, useMemo, RefObject, useEffect } from 'react' +import { useRef, RefObject } from 'react' +import { parseEther } from 'ethers' import { useConfig } from 'config' import forms from 'modules/forms' -import { parseEther } from 'ethers' +import { useStore } from 'hooks' -import useStore from '../data/useStore' +import { useSwapTokens, useSwapQuote } from './swap' import messages from './messages' type Input = { - minBalance: bigint - maxBalance: bigint - getDepositAmount?: (value: bigint) => bigint - withCapacityCheck?: boolean + swapFee: bigint + swapTokens: ReturnType + getSwappedDepositAmount: ReturnType['getSwappedDepositAmount'] } type CheckCapacityInput = RefObject<{ capacity: string totalAssets: string depositToken: string - withCapacityCheck?: boolean - getDepositAmount?: Input['getDepositAmount'] + getSwappedDepositAmount: ReturnType['getSwappedDepositAmount'] }> const checkCapacity = (dataRef: CheckCapacityInput) => (value?: Forms.FieldValue) => { - const { capacity, totalAssets, depositToken, withCapacityCheck, getDepositAmount } = dataRef.current + const { capacity, totalAssets, depositToken, getSwappedDepositAmount } = dataRef.current - if (Number(value) && Number(capacity) && withCapacityCheck) { + if (Number(value) && Number(capacity)) { const valueBI = BigInt(value as string) - const amount = typeof getDepositAmount === 'function' ? getDepositAmount(valueBI) : valueBI + + const amount = getSwappedDepositAmount(valueBI) const capacityBI = parseEther(capacity) const totalAssetsBI = parseEther(totalAssets) const newCapacity = amount + totalAssetsBI @@ -55,13 +55,17 @@ const checkMinBalance = (minBalanceRef: RefObject) => (value?: Forms.Fie const storeSelector = (store: Store) => ({ capacity: store.vault.base.data.capacity, totalAssets: store.vault.base.data.totalAssets, - isWhitelisted: store.vault.user.roles.data.isWhitelisted, }) -const useStakeField = ({ minBalance, maxBalance, withCapacityCheck, getDepositAmount }: Input) => { - const { sdk } = useConfig() +const useStakeField = (values: Input) => { + const { swapTokens, swapFee, getSwappedDepositAmount } = values - const { capacity, totalAssets, isWhitelisted } = useStore(storeSelector) + const { sdk, address } = useConfig() + const { capacity, totalAssets } = useStore(storeSelector) + + const depositToken = sdk.config.tokens.depositToken + const maxBalance = address ? swapTokens.selected.balance : swapTokens.selected.emptyBalance + const minBalance = swapFee / 100n * 120n const balanceRef = useRef(maxBalance) balanceRef.current = maxBalance @@ -69,9 +73,8 @@ const useStakeField = ({ minBalance, maxBalance, withCapacityCheck, getDepositAm const minBalanceRef = useRef(minBalance) minBalanceRef.current = minBalance - const depositToken = sdk.config.tokens.depositToken - const capacityDataRef = useRef({ capacity, totalAssets, depositToken, withCapacityCheck, getDepositAmount }) - capacityDataRef.current = { capacity, totalAssets, depositToken, withCapacityCheck, getDepositAmount } + const capacityDataRef = useRef({ capacity, totalAssets, depositToken, getSwappedDepositAmount }) + capacityDataRef.current = { capacity, totalAssets, depositToken, getSwappedDepositAmount } const field = forms.useField({ valueType: 'bigint', @@ -83,27 +86,7 @@ const useStakeField = ({ minBalance, maxBalance, withCapacityCheck, getDepositAm ], }) - const { value, error } = forms.useFieldValue(field) - - const isDisabled = Boolean(error) || !value || !isWhitelisted - - useEffect(() => { - if (field.value) { - field.validate(field.value) - } - }, [ field, capacity, totalAssets, depositToken, minBalance, maxBalance ]) - - return useMemo(() => ({ - field, - value, - error, - isDisabled, - }), [ - field, - value, - error, - isDisabled, - ]) + return field } diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeMaxAmount.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeMaxAmount.ts new file mode 100644 index 00000000..dd67e46a --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeMaxAmount.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' +import { useConfig, wallets } from 'config' + +import { useSwapTokens } from './swap' + + +type Input = { + swapTokens: ReturnType + transactionPrice: bigint +} + +const storeSelector = (store: Store) => ({ + depositTokenBalance : store.account.balances.depositToken, +}) + +const useStakeMaxAmount = (values: Input) => { + const { swapTokens, transactionPrice } = values + + const { activeWallet, isGnosis } = useConfig() + const { depositTokenBalance } = useStore(storeSelector) + + const maxStakeAmount = useMemo(() => { + if (swapTokens.selected.address) { + return swapTokens.selected.balance + } + + const isGnosisSafeWallet = activeWallet === wallets.gnosisSafe.id + + if (isGnosis || isGnosisSafeWallet) { + return depositTokenBalance + } + + const maxAmount = depositTokenBalance - (transactionPrice * 2n) + + return maxAmount > 0n ? maxAmount : 0n + }, [ swapTokens, activeWallet, transactionPrice, depositTokenBalance, isGnosis ]) + + return maxStakeAmount +} + + +export default useStakeMaxAmount diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/index.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/index.ts new file mode 100644 index 00000000..388cc9b4 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/index.ts @@ -0,0 +1,157 @@ +import { useCallback, useMemo, useState } from 'react' +import addresses from 'helpers/contracts/addresses' +import notifications from 'modules/notifications' +import { useStore, useActions } from 'hooks' +import { StakeStep } from 'helpers/enums' +import { commonMessages } from 'helpers' +import { useConfig } from 'config' + +import { vaultHooks } from 'views/SwapView/util' +import type { SetTransaction, SetNextTransactionsFailed } from 'components/Transactions/types' +import { openTransactionsFlowModal } from 'layouts/modals/TransactionsFlowModal/TransactionsFlowModal' + +import { useSwapActions, useSwapTokens } from '../swap' + +import useStakeSteps from './useStakeSteps' +import useStakeActions from './useStakeActions' +import useStakeApprove from './useStakeApprove' +import useStakeTransactionPrice from './useStakeTransactionPrice' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, +}) + +type OnStartInput = { + setTransaction?: SetTransaction + setNextTransactionsFailed?: SetNextTransactionsFailed +} + +type SwapActions = Pick, 'swap' | 'cancelSwap'> + +type Input = SwapActions & { + field: Forms.Field + swapTokens: ReturnType + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const useStakeSubmit = (values: Input) => { + const { field, swapTokens, swap, cancelSwap, fetchAllUserData } = values + + const actions = useActions() + const { vaultAddress } = useStore(storeSelector) + const { sdk, address, chainId, isGnosis } = useConfig() + const [ isSubmitting, setSubmitting ] = useState(false) + + const { stake } = useStakeActions({ field, fetchAllUserData }) + + const swapApprove = useStakeApprove({ + field, + step: StakeStep.SwapApprove, + recipient: addresses[chainId].cow.vaultRelayer, + tokenAddress: swapTokens.selected.address, + skip: !swapTokens.selected.address || !vaultAddress, + }) + + const stakeApprove = useStakeApprove({ + field, + step: StakeStep.Approve, + recipient: vaultAddress as string, + tokenAddress: sdk.config.addresses.tokens.depositToken, + skip: !isGnosis || !vaultAddress, + }) + + const stepsData = useStakeSteps({ + isStakeApproveRequired: stakeApprove.isApproveRequired, + isSwapApproveRequired: swapApprove.isApproveRequired, + swapTokens, + cancelSwap, + }) + + const onStart = useCallback(async (values?: OnStartInput) => { + const { setTransaction = () => {}, setNextTransactionsFailed = () => {} } = values || {} + + let assets = field.value || 0n + + if (!address || !assets || !vaultAddress) { + return + } + + setSubmitting(true) + + const calls = { + [StakeStep.Swap]: async () => assets = await swap({ setTransaction }), + [StakeStep.Stake]: (assets: bigint) => stake({ assets, setTransaction }), + [StakeStep.Approve]: () => stakeApprove.approve({ setTransaction, setNextTransactionsFailed }), + [StakeStep.SwapApprove]: () => swapApprove.approve({ setTransaction, setNextTransactionsFailed }), + } as const + + try { + for (let i = 0; i < stepsData.length; i += 1) { + const step = stepsData[i] + + const call = calls[step.id as keyof typeof calls] + + await call(assets) + } + } + catch (error) { + actions.ui.resetBottomLoader() + + console.error('Deposit send transaction error', error) + + notifications.open({ + type: 'error', + text: commonMessages.notification.failed, + }) + } + finally { + field.reset() + swapTokens.setSelected('') + setSubmitting(false) + } + }, [ + field, + actions, + address, + stepsData, + swapTokens, + swapApprove, + stakeApprove, + vaultAddress, + swap, + stake, + ]) + + const submit = useCallback(() => { + if (stepsData.length > 1) { + openTransactionsFlowModal({ + flow: 'stake', + stepsData, + onStart, + }) + } + else { + onStart() + } + }, [ stepsData, onStart ]) + + const transactionPrice = useStakeTransactionPrice({ swapApprove, stakeApprove, stepsData }) + + const isAllowanceFetching = swapApprove.isAllowanceFetching || stakeApprove.isAllowanceFetching + + return useMemo(() => ({ + isSubmitting, + transactionPrice, + isAllowanceFetching, + submit, + }), [ + isSubmitting, + transactionPrice, + isAllowanceFetching, + submit, + ]) +} + + +export default useStakeSubmit diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeActions.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeActions.ts new file mode 100644 index 00000000..d68e1013 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeActions.ts @@ -0,0 +1,115 @@ +import { useCallback, useMemo } from 'react' +import { useStore, useBalances, useSubgraphUpdate } from 'hooks' +import { StakeStep } from 'helpers/enums' +import { useConfig } from 'config' +import { getters } from 'helpers' + +import Transactions from 'components/Transactions/Transactions' +import type { SetTransaction } from 'components/Transactions/types' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' + +import vaultHooks from '../../../index' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, +}) + +type ActionInput = { + assets: bigint + setTransaction: SetTransaction +} + +type Input = { + field: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const useStakeActions = (values: Input) => { + const { fetchAllUserData } = values + + const { vaultAddress } = useStore(storeSelector) + const { signSDK, address, chainId, cancelOnChange } = useConfig() + + const subgraphUpdate = useSubgraphUpdate() + const { refetchNativeTokenBalance, refetchDepositTokenBalance } = useBalances() + + const stake = useCallback(async (values: ActionInput) => { + const { assets, setTransaction } = values + + if (!assets || !address) { + return + } + + try { + setTransaction(StakeStep.Stake, Transactions.Status.Confirm) + + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchNativeTokenBalance() + refetchDepositTokenBalance() + + const tokens = [ + { + token: signSDK.config.tokens.depositToken, + action: Action.Stake, + value: assets, + }, + ] + + openTxCompletedModal({ tokens, hash }) + }, + }) + + const referrerAddress = getters.getReferrer() + + const hash = await signSDK.vault.deposit({ + userAddress: address as string, + assets, + vaultAddress, + referrerAddress, + }) + + setTransaction(StakeStep.Stake, Transactions.Status.Processing) + + if (hash) { + await subgraphUpdate({ hash }) + await onSuccess() + + setTransaction(StakeStep.Stake, Transactions.Status.Success) + } + else { + setTransaction(StakeStep.Stake, Transactions.Status.Fail) + + return Promise.reject('TxHash is not defined') + } + } + catch (error) { + setTransaction(StakeStep.Stake, Transactions.Status.Fail) + + return Promise.reject(error) + } + }, [ + signSDK, + chainId, + address, + vaultAddress, + subgraphUpdate, + cancelOnChange, + fetchAllUserData, + refetchNativeTokenBalance, + refetchDepositTokenBalance, + ]) + + return useMemo(() => ({ + stake, + }), [ + stake, + ]) +} + + +export default useStakeActions diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeApprove.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeApprove.ts new file mode 100644 index 00000000..a3fad259 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeApprove.ts @@ -0,0 +1,90 @@ +import { useCallback, useMemo, useState, useEffect } from 'react' +import { useFieldListener, useApprove } from 'hooks' +import { StakeStep } from 'helpers/enums' + +import { Transactions } from 'components' +import type { SetNextTransactionsFailed, SetTransaction } from 'components' + + +type SubmitInput = { + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type Input = { + step: StakeStep + field: Forms.Field + tokenAddress: string | null + recipient: string + skip?: boolean +} + +const useStakeApprove = (values: Input) => { + const { step, field, tokenAddress, recipient, skip } = values + + const [ isApproveRequired, setApproveRequired ] = useState(false) + + const { allowance, isFetching, getGas, approve, checkAllowance } = useApprove({ + tokenAddress: tokenAddress || '', + recipient, + skip, + }) + + const handleApproveRequired = useCallback((amountField: Forms.Field) => { + if (skip) { + return + } + + const amount = amountField.value || 0n + + setApproveRequired(amount > allowance) + }, [ allowance, skip, setApproveRequired ]) + + const handleApprove = useCallback(async (values: SubmitInput) => { + const { setTransaction, setNextTransactionsFailed } = values + + try { + setTransaction(step, Transactions.Status.Confirm) + + const hash = await approve() + + setTransaction(step, Transactions.Status.Processing) + + await checkAllowance({ hash, allowance }) + + setTransaction(step, Transactions.Status.Success) + } + catch (error) { + setNextTransactionsFailed(step) + + return Promise.reject(error) + } + }, [ step, allowance, approve, checkAllowance ]) + + useFieldListener(field, handleApproveRequired) + + useEffect(() => { + if (skip) { + setApproveRequired(false) + } + }, [ skip ]) + + useEffect(() => { + handleApproveRequired(field) + }, [ field, handleApproveRequired ]) + + return useMemo(() => ({ + isApproveRequired, + isAllowanceFetching: isFetching, + getApproveGas: getGas, + approve: handleApprove, + }), [ + isFetching, + isApproveRequired, + getGas, + handleApprove, + ]) +} + + +export default useStakeApprove diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeSteps.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeSteps.ts new file mode 100644 index 00000000..885873c7 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeSteps.ts @@ -0,0 +1,78 @@ +import { useMemo } from 'react' +import { useConfig } from 'config' +import { commonMessages } from 'helpers' +import { StakeStep } from 'helpers/enums' + +import { StepsData } from 'components' + +import { useSwapActions, useSwapTokens } from '../swap' + + +type Input = { + isSwapApproveRequired: boolean + isStakeApproveRequired: boolean + swapTokens: ReturnType + cancelSwap: ReturnType['cancelSwap'] +} + +const useStakeSteps = (values: Input) => { + const { + swapTokens, + isSwapApproveRequired, + isStakeApproveRequired, + cancelSwap, + } = values + + const { sdk } = useConfig() + + const steps = useMemo>(() => ({ + stake: { + id: StakeStep.Stake, + }, + swap: { + id: StakeStep.Swap, + onCancel: cancelSwap, + }, + swapApprove: { + id: StakeStep.SwapApprove, + title: { + ...commonMessages.buttonTitle.approve, + values: { + token: swapTokens.selected.name, + }, + }, + }, + stakeApprove: { + id: StakeStep.Approve, + title: { + ...commonMessages.buttonTitle.approve, + values: { + token: sdk.config.tokens.depositToken, + }, + }, + }, + }), [ sdk, swapTokens, cancelSwap ]) + + return useMemo(() => { + const result: StepsData = [] + + if (isSwapApproveRequired) { + result.push(steps.swapApprove) + } + + if (swapTokens.selected.address) { + result.push(steps.swap) + } + + if (isStakeApproveRequired) { + result.push(steps.stakeApprove) + } + + result.push(steps.stake) + + return result + }, [ steps, swapTokens, isSwapApproveRequired, isStakeApproveRequired ]) +} + + +export default useStakeSteps diff --git a/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeTransactionPrice.ts new file mode 100644 index 00000000..bb0bc010 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useStake/useStakeSubmit/useStakeTransactionPrice.ts @@ -0,0 +1,84 @@ +import { useState, useCallback, useEffect } from 'react' +import { StakeStep } from 'helpers/enums' +import { constants } from 'helpers' +import { useConfig } from 'config' +import { useStore } from 'hooks' + +import useStakeSteps from './useStakeSteps' +import useStakeApprove from './useStakeApprove' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, +}) + +type Input = { + stakeApprove: ReturnType + swapApprove: ReturnType + stepsData: ReturnType +} + +const useStakeTransactionPrice = (values: Input) => { + const { stakeApprove, swapApprove, stepsData } = values + + const { vaultAddress } = useStore(storeSelector) + const { signSDK, address, isReadOnlyMode } = useConfig() + + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || isReadOnlyMode || !vaultAddress + + const fetchStakeGas = useCallback(() => { + return signSDK.vault.deposit.estimateGas({ + assets: constants.blockchain.minimalAmount, + userAddress: address as string, + vaultAddress, + }) + }, [ signSDK, address, vaultAddress ]) + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + let gas = 0n + + const calls = { + [StakeStep.Stake]: fetchStakeGas, + [StakeStep.Approve]: stakeApprove.getApproveGas, + [StakeStep.SwapApprove]: swapApprove.getApproveGas, + } as const + + for (let i = 0; i < stepsData.length; i += 1) { + const step = stepsData[i] + + const call = calls[step.id as keyof typeof calls] + + gas += await call?.() + } + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ + stepsData, + stakeApprove, + swapApprove, + isSkipFetch, + fetchStakeGas, + ]) + + useEffect(() => { + if (!isSkipFetch) { + fetchTransactionPrice() + } + }, [ isSkipFetch, fetchTransactionPrice ]) + + return transactionPrice +} + + +export default useStakeTransactionPrice diff --git a/src/views/SwapView/util/vault/actions/useUnboost/index.ts b/src/views/SwapView/util/vault/actions/useUnboost/index.ts new file mode 100644 index 00000000..a9f980a6 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnboost/index.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react' +import forms from 'modules/forms' + +import vaultHooks from '../../index' + +import useUnboostSubmit from './useUnboostSubmit' +import useUnboostDisabled from './useUnboostDisabled' +import useUnboostTransactionPrice from './useUnboostTransactionPrice' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const useUnboost = (values: Input) => { + const { fetchAllUserData } = values + + const percentField = forms.useField({ + valueType: 'string', + initialValue: '', + }) + + const transactionPrice = useUnboostTransactionPrice() + + const { submit, isSubmitting } = useUnboostSubmit({ + percentField, + fetchAllUserData, + }) + + const { unboostTooltip, isUnboostDisabled } = useUnboostDisabled() + + return useMemo(() => ({ + percentField, + unboostTooltip, + transactionPrice, + isUnboostDisabled, + isUnboostLoading: isSubmitting, + submit, + }), [ + percentField, + isSubmitting, + unboostTooltip, + transactionPrice, + isUnboostDisabled, + submit, + ]) +} + +useUnboost.mock = { + transactionPrice: 0n, + isUnboostLoading: false, + isUnboostDisabled: false, + percentField: {} as Forms.Field, + submit: () => {}, +} as ReturnType + + +export default useUnboost diff --git a/src/views/SwapView/util/vault/actions/useUnboost/messages.ts b/src/views/SwapView/util/vault/actions/useUnboost/messages.ts new file mode 100644 index 00000000..749f1c45 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnboost/messages.ts @@ -0,0 +1,11 @@ +export default { + unboostDisabled: { + en: 'The current unboost request must be claimed before unboosting again', + ru: 'Текущий запрос на анбуст должен быть выполнен перед повторным анбустом', + fr: 'La demande de déboost actuelle doit être réclamée avant de débooster à nouveau', + es: 'La solicitud de despotenciación actual debe ser reclamada antes de volver a despotenciar', + pt: 'O pedido atual de desaceleração deve ser reivindicado antes de desacelerar novamente', + de: 'Der aktuelle Antrag auf Ent-Boost muss beansprucht werden, bevor erneut ein Ent-Boost durchgeführt werden kann', + zh: '当前的解除加速请求必须在再次解除加速之前领取', + }, +} diff --git a/src/views/SwapView/util/vault/actions/useUnboost/useUnboostDisabled.ts b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostDisabled.ts new file mode 100644 index 00000000..8a3249ff --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostDisabled.ts @@ -0,0 +1,52 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' +import { useConfig } from 'config' + +import messages from './messages' + + +const isEnvUnboostDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_UNBOOST) + +const storeSelector = (store: Store) => ({ + boostedShares: store.vault.user.balances.boost.shares, + isBalancesFetching: store.vault.user.balances.isFetching, + exitingPercent: store.vault.user.balances.boost.exitingPercent, +}) + +const useUnboostDisabled = () => { + const { address, isReadOnlyMode } = useConfig() + + const { + boostedShares, + exitingPercent, + isBalancesFetching, + } = useStore(storeSelector) + + const isBoostQueued = exitingPercent > 0 + + const isUnboostDisabled = ( + !address + || isBoostQueued + || isReadOnlyMode + || !boostedShares + || isEnvUnboostDisabled + || isBalancesFetching + ) + + let unboostTooltip: Intl.Message | undefined = undefined + + if (isBoostQueued && boostedShares) { + unboostTooltip = messages.unboostDisabled + } + + return useMemo(() => ({ + unboostTooltip, + isUnboostDisabled, + }), [ + unboostTooltip, + isUnboostDisabled, + ]) +} + + +export default useUnboostDisabled diff --git a/src/views/SwapView/util/vault/actions/useUnboost/useUnboostSubmit.ts b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostSubmit.ts new file mode 100644 index 00000000..0b3c267d --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostSubmit.ts @@ -0,0 +1,318 @@ +import { useCallback, useMemo, useState } from 'react' +import { useConfig } from 'config' +import { UnboostStep } from 'helpers/enums' +import notifications from 'modules/notifications' +import { commonMessages, modifiers } from 'helpers' +import { useStore, useActions, useBalances, useSubgraphUpdate } from 'hooks' + +import { Transactions } from 'components' +import type { StepsData, SetNextTransactionsFailed, SetTransaction } from 'components' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' +import { openTransactionsFlowModal } from 'layouts/modals/TransactionsFlowModal/TransactionsFlowModal' + +import vaultHooks from '../../index' + + +type TokenData = { + token: Tokens + value: bigint + action: Action +} + +type HandleSuccessInput = { + hash: string + percent: number +} + +type UpgradeInput = { + userAddress: string + vaultAddress: string + setTransaction: SetTransaction + setNextTransactionsFailed: SetNextTransactionsFailed +} + +type UnboostInput = { + percent: number + userAddress: string + vaultAddress: string + leverageStrategyData: { + version: number + isUpgradeRequired: boolean + } + setTransaction: SetTransaction +} + +type OnStartInput = { + percent: number + setTransaction?: SetTransaction + setNextTransactionsFailed?: SetNextTransactionsFailed +} + +type Input = { + percentField: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, + boostedShares: store.vault.user.balances.boost.shares, + boostedRewardAssets: store.vault.user.balances.boost.rewardAssets, + leverageStrategyData: store.vault.user.balances.boost.leverageStrategyData, +}) + +const useUnboostSubmit = (values: Input) => { + const { percentField, fetchAllUserData } = values + + const actions = useActions() + const { signSDK, address, chainId, cancelOnChange } = useConfig() + const { refetchMintTokenBalance, refetchNativeTokenBalance } = useBalances() + + const { + vaultAddress, + boostedShares, + boostedRewardAssets, + leverageStrategyData, + } = useStore(storeSelector) + + const subgraphUpdate = useSubgraphUpdate() + const [ isSubmitting, setSubmitting ] = useState(false) + + const stepsData = useMemo(() => { + const result: StepsData = [] + + if (leverageStrategyData.isUpgradeRequired) { + result.push({ + id: UnboostStep.Upgrade, + title: commonMessages.upgradeLeverageStrategy, + }) + } + + result.push({ + id: UnboostStep.Unboost, + title: commonMessages.buttonTitle.unboost, + }) + + return result + }, [ leverageStrategyData ]) + + const refetchData = useCallback(() => { + cancelOnChange({ + address, + chainId, + logic: () => { + Promise.all([ + fetchAllUserData(), + refetchMintTokenBalance(), + refetchNativeTokenBalance(), + ]) + }, + }) + }, [ + address, + chainId, + cancelOnChange, + fetchAllUserData, + refetchMintTokenBalance, + refetchNativeTokenBalance, + ]) + + const upgrade = useCallback(async (values: UpgradeInput) => { + const { userAddress, vaultAddress, setTransaction, setNextTransactionsFailed } = values + + try { + setTransaction(UnboostStep.Upgrade, Transactions.Status.Confirm) + + const hash = await signSDK.boost.upgradeLeverageStrategy({ + userAddress, + vaultAddress, + }) + + setTransaction(UnboostStep.Upgrade, Transactions.Status.Processing) + + await subgraphUpdate({ hash }) + + setTransaction(UnboostStep.Upgrade, Transactions.Status.Success) + + return hash + } + catch (error) { + setNextTransactionsFailed(UnboostStep.Upgrade) + + return Promise.reject(error) + } + }, [ + signSDK, + subgraphUpdate, + ]) + + const unboost = useCallback(async (values: UnboostInput) => { + const { percent, userAddress, vaultAddress, leverageStrategyData, setTransaction } = values + + try { + setTransaction(UnboostStep.Unboost, Transactions.Status.Confirm) + + const hash = await signSDK.boost.unlock({ + percent, + userAddress, + vaultAddress, + leverageStrategyData, + }) + + setTransaction(UnboostStep.Unboost, Transactions.Status.Processing) + + await subgraphUpdate({ hash }) + + setTransaction(UnboostStep.Unboost, Transactions.Status.Success) + + return hash + } + catch (error) { + setTransaction(UnboostStep.Unboost, Transactions.Status.Fail) + + return Promise.reject(error) + } + }, [ + signSDK, + subgraphUpdate, + ]) + + const handleSuccess = useCallback(async (values: HandleSuccessInput) => { + const { hash, percent } = values + + percentField.reset() + + const [ exitShares ] = modifiers.splitPercent(boostedShares, percent) + const [ exitAssets ] = modifiers.splitPercent(boostedRewardAssets, percent) + + const tokens: TokenData[] = [ + { + token: signSDK.config.tokens.mintToken, + value: exitShares, + action: Action.Exiting, + }, + ] + + if (exitAssets) { + tokens.push({ + token: signSDK.config.tokens.depositToken, + value: exitAssets, + action: Action.Exiting, + }) + } + + openTxCompletedModal({ tokens, hash }) + }, [ percentField, boostedShares, boostedRewardAssets, signSDK ]) + + const onStart = useCallback(async (values: OnStartInput) => { + const { percent, setTransaction = () => {}, setNextTransactionsFailed = () => {} } = values + + if (!percent || !address || !vaultAddress) { + return + } + + try { + actions.ui.setBottomLoader({ + content: commonMessages.notification.waitingConfirmation, + }) + + console.log({ + category: 'action', + message: 'Submit unboost click', + }) + + setSubmitting(true) + + let hash + let _leverageStrategyData = leverageStrategyData + + for (let i = 0; i < stepsData.length; i += 1) { + const step = stepsData[i] + + if (step.id === UnboostStep.Upgrade) { + await upgrade({ + userAddress: address, + vaultAddress, + setTransaction, + setNextTransactionsFailed, + }) + + _leverageStrategyData = { + version: 2, + isUpgradeRequired: false, + } + } + if (step.id === UnboostStep.Unboost) { + hash = await unboost({ + percent, + vaultAddress, + userAddress: address, + leverageStrategyData: _leverageStrategyData, + setTransaction, + }) + } + } + + if (hash) { + await handleSuccess({ hash, percent }) + } + } + catch (error) { + actions.ui.resetBottomLoader() + + console.error('Unboost: submit failed', error as Error) + + notifications.open({ + text: commonMessages.notification.failed, + type: 'error', + }) + + return Promise.reject(error) + } + finally { + refetchData() + setSubmitting(false) + } + }, [ + actions, + address, + stepsData, + vaultAddress, + leverageStrategyData, + upgrade, + unboost, + refetchData, + handleSuccess, + ]) + + const submit = useCallback(() => { + const percent = Number(percentField.value || 0) + + if (!percent || !address || !vaultAddress) { + return + } + + if (stepsData.length > 1) { + openTransactionsFlowModal({ + flow: 'unboost', + stepsData, + onStart: ({ setTransaction, setNextTransactionsFailed }) => { + return onStart({ percent, setTransaction, setNextTransactionsFailed }) + }, + }) + } + else { + onStart({ percent }) + } + }, [ address, stepsData, vaultAddress, percentField, onStart ]) + + return useMemo(() => ({ + submit, + isSubmitting, + }), [ + submit, + isSubmitting, + ]) +} + + +export default useUnboostSubmit diff --git a/src/views/SwapView/util/vault/actions/useUnboost/useUnboostTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostTransactionPrice.ts new file mode 100644 index 00000000..3406a62a --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnboost/useUnboostTransactionPrice.ts @@ -0,0 +1,49 @@ +import { useState, useCallback } from 'react' +import { useStore, useAutoFetch } from 'hooks' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, +}) + +const useUnboostTransactionPrice = () => { + const { vaultAddress } = useStore(storeSelector) + + const { signSDK, address, isReadOnlyMode } = useConfig() + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || !vaultAddress || isReadOnlyMode + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + const params = { + vaultAddress, + percent: 100, + userAddress: address, + } + + const gas = await signSDK.boost.unlock.estimateGas({ ...params }) + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ signSDK, address, vaultAddress, isSkipFetch ]) + + useAutoFetch({ + action: fetchTransactionPrice, + skip: isSkipFetch, + interval: 30_000, + }) + + return transactionPrice +} + + +export default useUnboostTransactionPrice diff --git a/src/views/SwapView/util/vault/actions/useUnstake/index.ts b/src/views/SwapView/util/vault/actions/useUnstake/index.ts new file mode 100644 index 00000000..3eb5d8bb --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnstake/index.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react' +import { useStore } from 'hooks' + +import vaultHooks from '../../index' + +import useUnstakeField from './useUnstakeField' +import useUnstakeSubmit from './useUnstakeSubmit' +import useUnstakeDisabled from './useUnstakeDisabled' +import useUnstakeTransactionPrice from './useUnstakeTransactionPrice' + + +type Input = { + fetchAllUserData: ReturnType['fetchAllUserData'] +} + +const storeSelector = (store: Store) => ({ + isBalancesFetching: store.vault.user.balances.isFetching, + maxWithdrawAssets: store.vault.user.balances.maxWithdrawAssets, +}) + +const useUnstake = (values: Input) => { + const { fetchAllUserData } = values + + const { maxWithdrawAssets, isBalancesFetching } = useStore(storeSelector) + + const field = useUnstakeField(maxWithdrawAssets) + const isUnstakeDisabled = useUnstakeDisabled(field) + const transactionPrice = useUnstakeTransactionPrice() + + const { submit, isSubmitting } = useUnstakeSubmit({ + field, + fetchAllUserData, + }) + + const isUnstakeLoading = isSubmitting || isBalancesFetching + + return useMemo(() => ({ + field, + transactionPrice, + isUnstakeLoading, + isUnstakeDisabled, + maxUnstakeAmount: maxWithdrawAssets, + submit, + }), [ + field, + transactionPrice, + isUnstakeLoading, + isUnstakeDisabled, + maxWithdrawAssets, + submit, + ]) +} + +useUnstake.mock = { + transactionPrice: 0n, + isUnstakeLoading: false, + isUnstakeDisabled: false, + field: {} as Forms.Field, + submit: () => {}, +} as ReturnType + + +export default useUnstake diff --git a/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeDisabled.ts b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeDisabled.ts new file mode 100644 index 00000000..7c84b893 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeDisabled.ts @@ -0,0 +1,24 @@ +import { constants } from 'helpers' +import { useConfig } from 'config' +import forms from 'modules/forms' +import { useStore } from 'hooks' + + +const isEnvUnstakeDisabled = Boolean(process.env.NEXT_PUBLIC_DISABLE_UNSTAKE) + +const storeSelector = (store: Store) => ({ + stakedAssets: store.vault.user.balances.stakedAssets, +}) + +const useUnstakeDisabled = (field: Forms.Field) => { + const { stakedAssets } = useStore(storeSelector) + + const { isReadOnlyMode } = useConfig() + const { error } = forms.useFieldValue(field) + const hasStake = stakedAssets > constants.blockchain.minimalAmount + + return !hasStake || isReadOnlyMode || isEnvUnstakeDisabled || Boolean(error) +} + + +export default useUnstakeDisabled diff --git a/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeField.ts b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeField.ts new file mode 100644 index 00000000..5896a22f --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeField.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react' +import forms from 'modules/forms' + + +const useUnstakeField = (balance: bigint) => { + const balanceRef = useRef(balance) + balanceRef.current = balance + + return forms.useField({ + valueType: 'bigint', + validators: [ + forms.validators.numberWithDot, + forms.validators.sufficientBalance(balanceRef), + ], + }) +} + + +export default useUnstakeField diff --git a/src/views/HomeView/StakeContext/util/actions/useUnstake/useSubmit.ts b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeSubmit.ts similarity index 53% rename from src/views/HomeView/StakeContext/util/actions/useUnstake/useSubmit.ts rename to src/views/SwapView/util/vault/actions/useUnstake/useUnstakeSubmit.ts index aa2b2dba..85225257 100644 --- a/src/views/HomeView/StakeContext/util/actions/useUnstake/useSubmit.ts +++ b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeSubmit.ts @@ -1,47 +1,58 @@ import { useCallback, useMemo, useState } from 'react' import { useConfig } from 'config' -import { AllocatorActionType } from 'sdk' import { commonMessages } from 'helpers' import notifications from 'modules/notifications' -import { useActions, useBalances, useStore, useSubgraphUpdate } from 'hooks' +import { useStore, useActions, useBalances, useSubgraphUpdate } from 'hooks' -import { Action, openTxCompletedModal } from 'layouts/modals' +import { Action, openTxCompletedModal } from 'layouts/modals/TxCompletedModal/TxCompletedModal' +import vaultHooks from '../../index' -type Output = { - isSubmitting: boolean - submit: () => Promise + +type Input = { + field: Forms.Field + fetchAllUserData: ReturnType['fetchAllUserData'] } const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, isCollateralized: store.vault.base.data.isCollateralized, }) -const useSubmit = (params: StakePage.Params): Output => { - const { field, vaultAddress, fetch } = params +const useUnstakeSubmit = (values: Input) => { + const { field, fetchAllUserData } = values const actions = useActions() - const { signSDK, address, chainId, cancelOnChange } = useConfig() - const { isCollateralized } = useStore(storeSelector) const [ isSubmitting, setSubmitting ] = useState(false) + const { signSDK, address, chainId, cancelOnChange } = useConfig() + const { vaultAddress, isCollateralized } = useStore(storeSelector) const subgraphUpdate = useSubgraphUpdate() - const { refetchNativeTokenBalance, refetchDepositTokenBalance } = useBalances() + const { refetchDepositTokenBalance } = useBalances() const submit = useCallback(async () => { const assets = field.value || 0n - if (!address) { + if (!address || !assets) { return } - setSubmitting(true) + try { + setSubmitting(true) - actions.ui.setBottomLoader({ - content: commonMessages.notification.waitingConfirmation, - }) + actions.ui.setBottomLoader({ + content: commonMessages.notification.waitingConfirmation, + }) + + const onSuccess = () => cancelOnChange({ + address, + chainId, + logic: () => { + fetchAllUserData() + refetchDepositTokenBalance() + }, + }) - try { const hash = await signSDK.vault.withdraw({ userAddress: address, vaultAddress, @@ -50,32 +61,7 @@ const useSubmit = (params: StakePage.Params): Output => { if (hash) { await subgraphUpdate({ hash }) - - field.reset() - - cancelOnChange({ - address, - chainId, - logic: () => { - fetch.data() - fetch.balances() - fetch.unstakeQueue() - - refetchNativeTokenBalance() - refetchDepositTokenBalance() - }, - }) - - const blockExplorerUrl = signSDK.config.network.blockExplorerUrl - - if (!isCollateralized) { - actions.vault.user.allocatorActions.addFirstItem({ - hash, - assets, - actionType: AllocatorActionType.Redeemed, - link: blockExplorerUrl, - }) - } + await onSuccess() const tokens = [ { @@ -85,23 +71,25 @@ const useSubmit = (params: StakePage.Params): Output => { }, ] + field.reset() openTxCompletedModal({ tokens, hash }) } } catch (error) { actions.ui.resetBottomLoader() - console.error('Unstake send transaction error', error as Error) + + console.error('Unstake send transaction error', error) notifications.open({ type: 'error', text: commonMessages.notification.failed, }) } - - setSubmitting(false) + finally { + setSubmitting(false) + } }, [ field, - fetch, chainId, signSDK, actions, @@ -110,18 +98,18 @@ const useSubmit = (params: StakePage.Params): Output => { isCollateralized, subgraphUpdate, cancelOnChange, - refetchNativeTokenBalance, + fetchAllUserData, refetchDepositTokenBalance, ]) return useMemo(() => ({ - submit, isSubmitting, - }), [ submit, + }), [ isSubmitting, + submit, ]) } -export default useSubmit +export default useUnstakeSubmit diff --git a/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeTransactionPrice.ts b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeTransactionPrice.ts new file mode 100644 index 00000000..d86568d6 --- /dev/null +++ b/src/views/SwapView/util/vault/actions/useUnstake/useUnstakeTransactionPrice.ts @@ -0,0 +1,48 @@ +import { useState, useCallback } from 'react' +import { useStore, useAutoFetch } from 'hooks' +import { constants } from 'helpers' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + vaultAddress: store.vault.base.data.vaultAddress, +}) + +const useUnstakeTransactionPrice = () => { + const { vaultAddress } = useStore(storeSelector) + + const { signSDK, address, isReadOnlyMode } = useConfig() + const [ transactionPrice, setTransactionPrice ] = useState(0n) + + const isSkipFetch = !address || !vaultAddress || isReadOnlyMode + + const fetchTransactionPrice = useCallback(async () => { + if (isSkipFetch) { + return + } + + try { + const gas = await signSDK.vault.withdraw.estimateGas({ + assets: constants.blockchain.minimalAmount, + userAddress: address, + vaultAddress, + }) + + setTransactionPrice(gas) + } + catch { + setTransactionPrice(0n) + } + }, [ signSDK, address, vaultAddress, isSkipFetch ]) + + useAutoFetch({ + action: fetchTransactionPrice, + skip: isSkipFetch, + interval: 30_000, + }) + + return transactionPrice +} + + +export default useUnstakeTransactionPrice diff --git a/src/views/SwapView/util/vault/helpers/index.ts b/src/views/SwapView/util/vault/helpers/index.ts new file mode 100644 index 00000000..510f901d --- /dev/null +++ b/src/views/SwapView/util/vault/helpers/index.ts @@ -0,0 +1,14 @@ +import useAPY from './useAPY' +import useAssets from './useAssets' +import useShares from './useShares' +import useStakeReceive from './useStakeReceive' +import useUnboostReceive from './useUnboostReceive' + + +export default { + useAPY, + useAssets, + useShares, + useStakeReceive, + useUnboostReceive, +} diff --git a/src/views/SwapView/util/vault/helpers/types.d.ts b/src/views/SwapView/util/vault/helpers/types.d.ts new file mode 100644 index 00000000..6801d14c --- /dev/null +++ b/src/views/SwapView/util/vault/helpers/types.d.ts @@ -0,0 +1,8 @@ +export type ActionType = 'stake' | 'unstake' | 'mint' | 'burn' | 'boost' | 'unboost' + +export type Input = { + field: Forms.Field + type: ActionType + depositAmount?: bigint + modifier?: (bigint) => bigint +} diff --git a/src/views/HomeView/content/util/useGetApy.ts b/src/views/SwapView/util/vault/helpers/useAPY.ts similarity index 66% rename from src/views/HomeView/content/util/useGetApy.ts rename to src/views/SwapView/util/vault/helpers/useAPY.ts index 60de608d..f1c8c51b 100644 --- a/src/views/HomeView/content/util/useGetApy.ts +++ b/src/views/SwapView/util/vault/helpers/useAPY.ts @@ -1,5 +1,5 @@ -import { useCallback } from 'react' -import { useStore } from 'hooks' +import { useCallback, useMemo, useRef } from 'react' +import { useStore, useObjectState, useFieldListener } from 'hooks' import { constants } from 'helpers' import { useConfig } from 'config' import { BigDecimal } from 'sdk' @@ -12,12 +12,14 @@ const storeSelector = (store: Store) => ({ vaultAPY: store.vault.base.data.apy, feePercent: store.mintToken.feePercent, userAPY: store.vault.user.balances.userAPY, - stakedAssets: store.vault.user.balances.stake.assets, + vaultAddress: store.vault.base.data.vaultAddress, + stakedAssets: store.vault.user.balances.stakedAssets, boostedShares: store.vault.user.balances.boost.shares, isV2Version: store.vault.base.data.versions.isV2Version, + maxBoostApy: store.vault.base.data.allocatorMaxBoostApy, ltvPercent: store.vault.base.data.osTokenConfig.ltvPercent, - mintedShares: store.vault.user.balances.mintToken.minted.shares, - maxBoostApy: store.vault.user.balances.boost.osTokenHolderMaxBoostApy, + mintedShares: store.vault.user.balances.mintToken.mintedShares, + boostExitingPercent: store.vault.user.balances.boost.exitingPercent, unboostExitingShares: store.vault.user.unboostQueue.data.exitingShares, }) @@ -29,8 +31,8 @@ const getAnnualReward = (assets: bigint, apy: number) => BigInt( .toString() ) -const useGetApy = ({ type }: Pick) => { - const { sdk } = useConfig() +const useAPY = (values: Input) => { + const { type, field, modifier } = values const { userAPY, @@ -43,14 +45,24 @@ const useGetApy = ({ type }: Pick) => { mintTokenAPY, stakedAssets, boostedShares, + boostExitingPercent, unboostExitingShares, } = useStore(storeSelector) + const initialStateRef = useRef({ + newAPY: userAPY, + isHidden: false, + isFetching: false, + }) + + const { signSDK } = useConfig() + const [ state, setState ] = useObjectState(initialStateRef.current) + const convertToAssets = useCallback((shares: bigint) => ( - sdk.contracts.base.mintTokenController.convertToAssets(shares) - ), [ sdk ]) + signSDK.contracts.base.mintTokenController.convertToAssets(shares) + ), [ signSDK ]) - return useCallback(async (value: bigint) => { + const calculateAPY = useCallback(async (value: bigint) => { // Do not subtract boostedShares since we continue to earn rewards // while the position is in the queue, so the api should not change const isUnboost = type === 'unboost' @@ -147,10 +159,48 @@ const useGetApy = ({ type }: Pick) => { stakedAssets, mintTokenAPY, boostedShares, - unboostExitingShares, convertToAssets, + unboostExitingShares, + ]) + + const handleGetAPY = useCallback(async () => { + const inputValue = field.value || 0n + const isValid = Number(inputValue) && !field.error + + if (!isValid) { + setState(initialStateRef.current) + + return + } + + setState({ isFetching: true }) + + let value = inputValue + + if (typeof modifier === 'function') { + value = modifier(value) + } + + const newAPY = await calculateAPY(value || 0n) + + if (inputValue === inputValue) { + setState({ newAPY, isFetching: false }) + } + }, [ field, modifier, calculateAPY, setState ]) + + useFieldListener(field, handleGetAPY, 0) + + const diff = userAPY ? userAPY - state.newAPY : state.newAPY + const isApyHidden = state.isFetching || Boolean(boostedShares || boostExitingPercent) || Math.abs(diff) < 0.01 + + return useMemo(() => ({ + ...state, + isApyHidden, + }), [ + state, + isApyHidden, ]) } -export default useGetApy +export default useAPY diff --git a/src/views/HomeView/content/util/useAssets.ts b/src/views/SwapView/util/vault/helpers/useAssets.ts similarity index 54% rename from src/views/HomeView/content/util/useAssets.ts rename to src/views/SwapView/util/vault/helpers/useAssets.ts index 3dc8fbfe..e15f12d6 100644 --- a/src/views/HomeView/content/util/useAssets.ts +++ b/src/views/SwapView/util/vault/helpers/useAssets.ts @@ -1,59 +1,54 @@ import { useMemo } from 'react' import { useStore } from 'hooks' -import { useConfig } from 'config' -import { commonMessages } from 'helpers' import forms from 'modules/forms' +import { useConfig } from 'config' +import { commonMessages, methods } from 'helpers' -import type { Input, Position } from './types' +import type { LogoName } from 'components' +import type { Input } from './types' -type Item = Position const storeSelector = (store: Store) => ({ - stakedAssets: store.vault.user.balances.stake.assets, + stakedAssets: store.vault.user.balances.stakedAssets, }) -const useAssets = ({ field, type }: Input) => { +const useAssets = ({ field, type, depositAmount }: Input) => { const { sdk } = useConfig() - const { value, error } = forms.useFieldValue(field) + const { value: fieldValue, error } = forms.useFieldValue(field) const { stakedAssets } = useStore(storeSelector) + const value = depositAmount || fieldValue const depositToken = sdk.config.tokens.depositToken return useMemo(() => { if (type === 'stake' || type === 'unstake') { const isValid = Number(value) && typeof value === 'bigint' && !error - const prev: NonNullable['prev'] = { - value: stakedAssets, - dataTestId: 'assets', - } + const prev = methods.formatTokenValue(stakedAssets) - const next: NonNullable['next'] = { - value: null, - dataTestId: 'assets', - } + let next = '' if (isValid) { if (type === 'stake') { - next.value = stakedAssets + value + next = methods.formatTokenValue(stakedAssets + value) } if (type === 'unstake') { - next.value = stakedAssets - value + next = methods.formatTokenValue(stakedAssets - value) } } - const result: Item = { - title: { + const result = { + text: { ...commonMessages.staked, values: { depositToken }, }, - tokenValue: { + values: { prev, next, - token: depositToken, }, + logo: `token/${depositToken}` as LogoName, } return result diff --git a/src/views/HomeView/content/util/useShares.ts b/src/views/SwapView/util/vault/helpers/useShares.ts similarity index 54% rename from src/views/HomeView/content/util/useShares.ts rename to src/views/SwapView/util/vault/helpers/useShares.ts index caeb16a0..308f3d71 100644 --- a/src/views/HomeView/content/util/useShares.ts +++ b/src/views/SwapView/util/vault/helpers/useShares.ts @@ -1,19 +1,21 @@ import { useMemo } from 'react' import { useStore } from 'hooks' -import { useConfig } from 'config' import forms from 'modules/forms' -import { commonMessages } from 'helpers' +import { useConfig } from 'config' +import { commonMessages, methods } from 'helpers' -import type { Input, Position } from './types' +import type { LogoName } from 'components' +import type { Input } from './types' -type Item = Position const storeSelector = (store: Store) => ({ - mintedShares: store.vault.user.balances.mintToken.minted.shares, + mintedShares: store.vault.user.balances.mintToken.mintedShares, }) -const useShares = ({ field, type }: Input) => { +const useShares = (values: Input) => { + const { type, field } = values + const { sdk } = useConfig() const { mintedShares } = useStore(storeSelector) @@ -24,43 +26,37 @@ const useShares = ({ field, type }: Input) => { return useMemo(() => { if (type === 'mint' || type === 'burn') { - const prev: NonNullable['prev'] = { - value: mintedShares, - dataTestId: 'shares', - } + const prev = methods.formatTokenValue(mintedShares) - const next: NonNullable['next'] = { - value: null, - dataTestId: 'shares', - } + let next = '' if (isValid && value && typeof value === 'bigint') { if (type === 'mint') { - next.value = mintedShares + value + next = methods.formatTokenValue(mintedShares + value) } if (type === 'burn') { - next.value = mintedShares - value + next = methods.formatTokenValue(mintedShares - value) } } - const result: Item = { - title: { + const result = { + text: { ...commonMessages.minted, values: { mintToken }, }, - tokenValue: { + values: { prev, next, - token: mintToken, }, + logo: `token/${mintToken}` as LogoName, } return result } return null - }, [ value, type, mintToken, mintedShares, isValid ]) + }, [ type, mintedShares, isValid, value, mintToken ]) } diff --git a/src/views/SwapView/util/vault/helpers/useStakeReceive.ts b/src/views/SwapView/util/vault/helpers/useStakeReceive.ts new file mode 100644 index 00000000..75214aec --- /dev/null +++ b/src/views/SwapView/util/vault/helpers/useStakeReceive.ts @@ -0,0 +1,139 @@ +import { useCallback, useEffect, useRef } from 'react' +import { useObjectState, useFieldListener, useStore } from 'hooks' +import { requests, constants } from 'helpers' +import { useConfig } from 'config' + +import actions from '../actions' + + +type Input = Pick, 'swapTokens' | 'fetchQuote' | 'field'> + +type State = { + exchangeRate: bigint + receiveShares: bigint + validTo: number | null + isFetching: boolean +} + +const storeSelector = (store: Store) => ({ + vaultAddress : store.vault.base.data.vaultAddress, +}) + +const initialState = { + validTo: null, + exchangeRate: 0n, + receiveShares: 0n, + isFetching: true, +} + +const useStakeReceive = (values: Input) => { + const { field, swapTokens, fetchQuote } = values + + const { sdk, address } = useConfig() + const { vaultAddress } = useStore(storeSelector) + const [ state, setState ] = useObjectState(initialState) + + const getReceiveData = useCallback(async () => { + const value = field.value || 0n + + let amount = value + let validTo = null + + if (!amount || !vaultAddress) { + return { + receiveShares: 0n, + exchangeRate: 0n, + validTo, + } + } + + try { + if (swapTokens.selected.address) { + const quote = await fetchQuote({ + amount, + fromToken: swapTokens.selected.address, + }) + + validTo = quote.validTo * 1000 + amount = BigInt(quote.buyAmount) + } + + const { receiveShares, exchangeRate } = await requests.fetchStakeSwapData({ + userAddress: address, + vaultAddress, + amount, + sdk, + }) + + let formattedRate = exchangeRate + + if (swapTokens.selected.address) { + let divider = value + + if (swapTokens.selected.units !== 18) { + const unitsDiff = BigInt(18 - swapTokens.selected.units) + + divider *= 10n ** unitsDiff + } + + formattedRate = receiveShares * constants.blockchain.amount1 / divider + } + + setState({ + validTo, + receiveShares, + exchangeRate: formattedRate, + isFetching: false, + }) + } + catch (error) { + console.error('error', error) + + setState({ + ...initialState, + isFetching: false, + }) + } + }, [ sdk, field, address, vaultAddress, swapTokens, fetchQuote, setState ]) + + const getReceiveDataRef = useRef(getReceiveData) + getReceiveDataRef.current = getReceiveData + + const fetchingRef = useRef(state.isFetching) + fetchingRef.current = state.isFetching + + const handleFetching = useCallback(() => { + if (field.value) { + if (!fetchingRef.current) { + setState({ isFetching: true }) + } + } + }, [ field, setState ]) + + useEffect(() => { + if (state.validTo) { + const diff = state.validTo - Date.now() + + // When cow quote is expired, we need to request the quote again + const timer = setTimeout(getReceiveDataRef.current, diff) + + return () => { + clearTimeout(timer) + } + } + }, [ state.validTo, getReceiveDataRef ]) + + useEffect(() => { + if (field.value) { + getReceiveData() + } + }, []) + + useFieldListener(field, handleFetching, 0) + useFieldListener(field, getReceiveData, 350) + + return state +} + + +export default useStakeReceive diff --git a/src/views/HomeView/content/Unboost/UnboostContent/UnboostInfo/util/useReceive.ts b/src/views/SwapView/util/vault/helpers/useUnboostReceive.ts similarity index 78% rename from src/views/HomeView/content/Unboost/UnboostContent/UnboostInfo/util/useReceive.ts rename to src/views/SwapView/util/vault/helpers/useUnboostReceive.ts index a5093886..b0b6d3b8 100644 --- a/src/views/HomeView/content/Unboost/UnboostContent/UnboostInfo/util/useReceive.ts +++ b/src/views/SwapView/util/vault/helpers/useUnboostReceive.ts @@ -1,22 +1,16 @@ import { useMemo } from 'react' import { useStore } from 'hooks' -import { modifiers } from 'helpers' import forms from 'modules/forms' +import { modifiers } from 'helpers' -type Input = { - field: Forms.Field -} - const storeSelector = (store: Store) => ({ boostedShares: store.vault.user.balances.boost.shares, rewardAssets: store.vault.user.balances.boost.rewardAssets, }) -const useReceive = (values: Input) => { - const { field } = values - - const { value: percent } = forms.useFieldValue(field) +const useUnboostReceive = (percentField: Forms.Field) => { + const { value: percent } = forms.useFieldValue(percentField) const { boostedShares, rewardAssets } = useStore(storeSelector) return useMemo(() => { @@ -38,4 +32,4 @@ const useReceive = (values: Input) => { } -export default useReceive +export default useUnboostReceive diff --git a/src/views/SwapView/util/vault/index.ts b/src/views/SwapView/util/vault/index.ts new file mode 100644 index 00000000..eae218a2 --- /dev/null +++ b/src/views/SwapView/util/vault/index.ts @@ -0,0 +1,14 @@ +import actions from './actions' +import helpers from './helpers' + +import useUser from './useUser' +import useVault from './useVault' + + +export default { + actions, + helpers, + + useUser, + useVault, +} diff --git a/src/views/SwapView/util/vault/useUser/index.ts b/src/views/SwapView/util/vault/useUser/index.ts new file mode 100644 index 00000000..a19792a1 --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/index.ts @@ -0,0 +1,117 @@ +import { useCallback, useMemo, useEffect } from 'react' +import { useConfig } from 'config' +import { useStore } from 'hooks' + +import useBalances from './useBalances' +import useUnstakeQueue from './useUnstakeQueue' +import useUnboostQueue from './useUnboostQueue' +import useUserChartStats from './useUserChartStats' + + +type Input = { + withBalances?: boolean + withUnstakeQueue?: boolean + withUnboostQueue?: boolean + withUserChartStats?: boolean +} + +const storeSelector = (store: Store) => ({ + isVaultFetching: store.vault.base.isFetching, + vaultAddress: store.vault.base.data.vaultAddress, +}) + +const useUser = (values: Input) => { + const { + withBalances = true, + withUnstakeQueue = true, + withUnboostQueue = true, + withUserChartStats = true, + } = values + + const { address, autoConnectChecked } = useConfig() + const { vaultAddress, isVaultFetching } = useStore(storeSelector) + + const { fetchBalances, resetBalances } = useBalances(vaultAddress) + const { fetchUnstakeQueue, resetUnstakeQueue } = useUnstakeQueue(vaultAddress) + const { fetchUnboostQueue, resetUnboostQueue } = useUnboostQueue(vaultAddress) + const { fetchUserChartStats, resetUserChartStats } = useUserChartStats(vaultAddress) + + const fetchAllUserData = useCallback(async () => { + const requests: Promise[] = [] + + if (!address || !autoConnectChecked || isVaultFetching || !vaultAddress) { + return + } + + if (withBalances) { + requests.push(fetchBalances()) + } + + if (withUnstakeQueue) { + requests.push(fetchUnstakeQueue()) + } + + if (withUnboostQueue) { + requests.push(fetchUnboostQueue()) + } + + if (withUserChartStats) { + requests.push(fetchUserChartStats(30)) + } + + return Promise.all(requests) + }, [ + address, + vaultAddress, + withBalances, + isVaultFetching, + withUnstakeQueue, + withUnboostQueue, + withUserChartStats, + autoConnectChecked, + fetchBalances, + fetchUnstakeQueue, + fetchUnboostQueue, + fetchUserChartStats, + ]) + + const resetAllUserData = useCallback(() => { + resetBalances() + resetUnstakeQueue() + resetUnboostQueue() + resetUserChartStats() + }, [ + resetBalances, + resetUnstakeQueue, + resetUnboostQueue, + resetUserChartStats, + ]) + + useEffect(() => { + if (!address && autoConnectChecked) { + resetAllUserData() + } + }, [ address, autoConnectChecked, resetAllUserData ]) + + return useMemo(() => ({ + fetchAllUserData, + resetAllUserData, + + fetchBalances, + fetchUnstakeQueue, + fetchUnboostQueue, + + fetchUserChartStats, + }), [ + fetchAllUserData, + resetAllUserData, + + fetchBalances, + fetchUnstakeQueue, + fetchUnboostQueue, + fetchUserChartStats, + ]) +} + + +export default useUser diff --git a/src/views/SwapView/util/vault/useUser/useBalances/index.ts b/src/views/SwapView/util/vault/useUser/useBalances/index.ts new file mode 100644 index 00000000..bc91fdb2 --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/useBalances/index.ts @@ -0,0 +1,125 @@ +import { useCallback, useMemo } from 'react' +import { useStore, useActions, useMountedRef } from 'hooks' +import notifications from 'modules/notifications' +import { useConfig } from 'config' + +import useStake from './useStake' +import useBoost from './useBoost' +import useUserApy from './useUserApy' +import useMintToken from './useMintToken' +import useMaxWithdrawAssets from './useMaxWithdrawAssets' + +import messages from './messages' + + +const storeSelector = (store: Store) => ({ + isVaultFetching: store.vault.base.isFetching, +}) + +const useBalances = (vaultAddress: string) => { + const actions = useActions() + const mountedRef = useMountedRef() + const { sdk, address, autoConnectChecked } = useConfig() + + const fetchStake = useStake() + const fetchBoost = useBoost() + const fetchUserApy = useUserApy() + const fetchMintToken = useMintToken() + const fetchWithdraw = useMaxWithdrawAssets() + + const { isVaultFetching } = useStore(storeSelector) + + const fetchBalances = useCallback(async () => { + if ((!address && autoConnectChecked) || isVaultFetching) { + actions.vault.user.balances.setFetching(false) + + return + } + + if (address && vaultAddress) { + + try { + actions.vault.user.balances.setFetching(true) + + const params = { + userAddress: address, + vaultAddress, + } + + const [ stake, boost, userAPY, maxWithdrawAssets, mintToken ] = await Promise.all([ + fetchStake(params), + fetchBoost(params), + fetchUserApy(params), + fetchWithdraw(params), + fetchMintToken(params), + ]) + + const { + stakedAssets, + totalEarnedAssets, + totalBoostEarnedAssets, + totalStakeEarnedAssets, + } = stake + + const boostedAssets = await sdk.contracts.base.mintTokenController.convertToShares(boost.shares) + const mintedAssets = mintToken.mintedAssets + + const mintTokenAssets = boostedAssets > mintedAssets ? boostedAssets : mintedAssets + const totalRewardingAssets = stakedAssets + boost.rewardAssets + mintTokenAssets - mintedAssets + + const result: Omit = { + boost, + userAPY, + mintToken, + stakedAssets, + totalEarnedAssets, + maxWithdrawAssets, + totalRewardingAssets, + totalBoostEarnedAssets, + totalStakeEarnedAssets, + } + + if (mountedRef.current) { + actions.vault.user.balances.setData(result) + } + } + catch (error) { + console.error(error) + actions.vault.user.balances.setFetching(false) + + notifications.open({ + type: 'error', + text: messages.error, + }) + } + } + }, [ + sdk, + address, + actions, + mountedRef, + vaultAddress, + isVaultFetching, + autoConnectChecked, + fetchStake, + fetchBoost, + fetchUserApy, + fetchWithdraw, + fetchMintToken, + ]) + + const resetBalances = useCallback(() => { + actions.vault.user.balances.resetData() + }, [ actions ]) + + return useMemo(() => ({ + fetchBalances, + resetBalances, + }), [ + fetchBalances, + resetBalances, + ]) +} + + +export default useBalances diff --git a/src/views/HomeView/StakeContext/util/useBalances/messages.ts b/src/views/SwapView/util/vault/useUser/useBalances/messages.ts similarity index 100% rename from src/views/HomeView/StakeContext/util/useBalances/messages.ts rename to src/views/SwapView/util/vault/useUser/useBalances/messages.ts diff --git a/src/views/HomeView/StakeContext/util/useBalances/useBoost.ts b/src/views/SwapView/util/vault/useUser/useBalances/useBoost.ts similarity index 100% rename from src/views/HomeView/StakeContext/util/useBalances/useBoost.ts rename to src/views/SwapView/util/vault/useUser/useBalances/useBoost.ts diff --git a/src/views/HomeView/StakeContext/util/useBalances/useWithdraw.ts b/src/views/SwapView/util/vault/useUser/useBalances/useMaxWithdrawAssets.ts similarity index 55% rename from src/views/HomeView/StakeContext/util/useBalances/useWithdraw.ts rename to src/views/SwapView/util/vault/useUser/useBalances/useMaxWithdrawAssets.ts index 84aa073e..34c08539 100644 --- a/src/views/HomeView/StakeContext/util/useBalances/useWithdraw.ts +++ b/src/views/SwapView/util/vault/useUser/useBalances/useMaxWithdrawAssets.ts @@ -4,30 +4,28 @@ import { useConfig } from 'config' type Input = { - ltvPercent: bigint - mintedAssets: bigint - stakedAssets: bigint + userAddress: string vaultAddress: string } -const useWithdraw = () => { +const useMaxWithdrawAssets = () => { const { sdk } = useConfig() return useCallback(async (values: Input) => { try { - const maxAssets = await sdk.vault.getMaxWithdraw(values) + const maxAssets = await sdk.vault.getMaxWithdrawAmount(values) - const result: Store['vault']['user']['balances']['withdraw'] = { maxAssets } + const result: Store['vault']['user']['balances']['maxWithdrawAssets'] = maxAssets return result } catch (error) { console.error('fetch vault withdraw user data error', error as Error) - return initialState.user.balances.withdraw + return initialState.user.balances.maxWithdrawAssets } }, [ sdk ]) } -export default useWithdraw +export default useMaxWithdrawAssets diff --git a/src/views/HomeView/StakeContext/util/useBalances/useMintToken.ts b/src/views/SwapView/util/vault/useUser/useBalances/useMintToken.ts similarity index 79% rename from src/views/HomeView/StakeContext/util/useBalances/useMintToken.ts rename to src/views/SwapView/util/vault/useUser/useBalances/useMintToken.ts index 60cbabe0..f37a24c0 100644 --- a/src/views/HomeView/StakeContext/util/useBalances/useMintToken.ts +++ b/src/views/SwapView/util/vault/useUser/useBalances/useMintToken.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react' import { initialState } from 'store/store/vault' +import * as methods from 'helpers/methods' import { constants } from 'helpers' import { useConfig } from 'config' -import methods from 'helpers/methods' type OsTokenEnabledQueryPayload = { @@ -12,11 +12,8 @@ type OsTokenEnabledQueryPayload = { } type Input = { - ltvPercent: bigint userAddress: string - stakedAssets: bigint vaultAddress: string - liqThresholdPercent: bigint } type Output = Store['vault']['user']['balances']['mintToken'] @@ -51,22 +48,21 @@ const useMintToken = () => { } } - const baseData = await sdk.osToken.getPosition(values) - - const maxMintShares = await sdk.osToken.getMaxMint({ - mintedAssets: baseData.minted.assets, - ...values, - }) + const [ minted, maxMintShares ] = await Promise.all([ + sdk.osToken.getBalance(values), + sdk.osToken.getMaxMintAmount(values), + ]) // We can never withdraw all osETH tokens since they are accrued every second. // So we have to look at the dust and assume that osETH just isn't there - const hasMintBalance = baseData.minted.assets > constants.blockchain.minimalAmount + const hasMintBalance = minted.assets > constants.blockchain.minimalAmount const mintToken: Output = { - ...baseData, maxMintShares, hasMintBalance, isDisabled: false, + mintedShares: minted.shares, + mintedAssets: minted.assets, } return mintToken diff --git a/src/views/SwapView/util/vault/useUser/useBalances/useStake.ts b/src/views/SwapView/util/vault/useUser/useBalances/useStake.ts new file mode 100644 index 00000000..bf2f20d7 --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/useBalances/useStake.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { constants } from 'helpers' +import { useConfig } from 'config' + + +type Input = { + vaultAddress: string + userAddress: string +} + +const useStake = () => { + const { sdk } = useConfig() + + return useCallback(async (values: Input) => { + try { + const { + assets, + totalEarnedAssets, + totalStakeEarnedAssets, + totalBoostEarnedAssets, + } = await sdk.vault.getStakeBalance(values) + + return { + stakedAssets: assets > constants.blockchain.minimalAmount ? assets : 0n, + totalEarnedAssets, + totalStakeEarnedAssets, + totalBoostEarnedAssets, + } + } + catch (error) { + console.error('fetch vault stake user data error', error as Error) + + return { + stakedAssets: 0n, + totalEarnedAssets: 0n, + totalStakeEarnedAssets: 0n, + totalBoostEarnedAssets: 0n, + } + } + }, [ sdk ]) +} + + +export default useStake diff --git a/src/views/HomeView/StakeContext/util/useBalances/useUserApy.ts b/src/views/SwapView/util/vault/useUser/useBalances/useUserApy.ts similarity index 100% rename from src/views/HomeView/StakeContext/util/useBalances/useUserApy.ts rename to src/views/SwapView/util/vault/useUser/useBalances/useUserApy.ts diff --git a/src/views/SwapView/util/vault/useUser/useUnboostQueue.ts b/src/views/SwapView/util/vault/useUser/useUnboostQueue.ts new file mode 100644 index 00000000..26fef1ce --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/useUnboostQueue.ts @@ -0,0 +1,46 @@ +import { useCallback, useMemo } from 'react' +import { useMountedRef, useActions } from 'hooks' +import { useConfig } from 'config' + + +const useUnboostQueue = (vaultAddress: string) => { + const actions = useActions() + const mountedRef = useMountedRef() + const { sdk, address, isEthereum } = useConfig() + + const fetchUnboostQueue = useCallback(async () => { + if (address && vaultAddress && isEthereum) { + try { + actions.vault.user.unboostQueue.setFetching(true) + + const unboostQueue = await sdk.boost.getQueuePosition({ + userAddress: address, + vaultAddress, + }) + + if (mountedRef.current) { + actions.vault.user.unboostQueue.setData(unboostQueue) + } + } + catch (error: any) { + console.error('Fetch UnboostQueue error', error) + actions.vault.user.unboostQueue.setFetching(false) + } + } + }, [ sdk, actions, address, mountedRef, vaultAddress, isEthereum ]) + + const resetUnboostQueue = useCallback(() => { + actions.vault.user.unboostQueue.resetData() + }, [ actions ]) + + return useMemo(() => ({ + fetchUnboostQueue, + resetUnboostQueue, + }), [ + fetchUnboostQueue, + resetUnboostQueue, + ]) +} + + +export default useUnboostQueue diff --git a/src/views/SwapView/util/vault/useUser/useUnstakeQueue.ts b/src/views/SwapView/util/vault/useUser/useUnstakeQueue.ts new file mode 100644 index 00000000..da1cf8f3 --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/useUnstakeQueue.ts @@ -0,0 +1,54 @@ +import { useCallback, useMemo } from 'react' +import { useMountedRef, useActions } from 'hooks' +import { useConfig } from 'config' + + +const useUnstakeQueue = (vaultAddress: string) => { + const actions = useActions() + const mountedRef = useMountedRef() + const { sdk, address } = useConfig() + + const fetchUnstakeQueue = useCallback(async () => { + if (address && vaultAddress) { + try { + actions.vault.user.unstakeQueue.setFetching(true) + + const exitQueue = await sdk.vault.getExitQueuePositions({ + userAddress: address, + isClaimed: false, + vaultAddress, + }) + + if (mountedRef.current) { + actions.vault.user.unstakeQueue.setData({ + withdrawable: exitQueue.withdrawable, + positions: exitQueue.positions, + duration: exitQueue.duration, + requests: exitQueue.requests, + total: exitQueue.total, + }) + } + } + catch (error: any) { + console.error('Fetch ExitQueue error', error) + + actions.vault.user.unstakeQueue.setFetching(false) + } + } + }, [ sdk, actions, address, mountedRef, vaultAddress ]) + + const resetUnstakeQueue = useCallback(() => { + actions.vault.user.unstakeQueue.resetData() + }, [ actions ]) + + return useMemo(() => ({ + fetchUnstakeQueue, + resetUnstakeQueue, + }), [ + fetchUnstakeQueue, + resetUnstakeQueue, + ]) +} + + +export default useUnstakeQueue diff --git a/src/views/SwapView/util/vault/useUser/useUserChartStats.ts b/src/views/SwapView/util/vault/useUser/useUserChartStats.ts new file mode 100644 index 00000000..0a511443 --- /dev/null +++ b/src/views/SwapView/util/vault/useUser/useUserChartStats.ts @@ -0,0 +1,47 @@ +import { useCallback, useMemo } from 'react' +import { useActions, useMountedRef } from 'hooks' +import { useConfig } from 'config' + + +const useUserChartStats = (vaultAddress: string) => { + const actions = useActions() + const mountedRef = useMountedRef() + const { sdk, address } = useConfig() + + const fetchUserChartStats = useCallback(async (daysCount: number) => { + try { + if (address) { + actions.vault.user.rewards.setFetching(true) + + const data = await sdk.vault.getUserStats({ + daysCount, + vaultAddress, + userAddress: address, + }) + + if (mountedRef.current) { + actions.vault.user.rewards.setData(data) + } + } + } catch (error: any) { + console.error('Fetch user chart stats fail', error) + + actions.vault.user.rewards.setFetching(false) + } + }, [ actions, address, sdk, vaultAddress, mountedRef ]) + + const resetUserChartStats = useCallback(() => { + actions.vault.user.rewards.resetData() + }, [ actions ]) + + return useMemo(() => ({ + fetchUserChartStats, + resetUserChartStats, + }), [ + fetchUserChartStats, + resetUserChartStats, + ]) +} + + +export default useUserChartStats diff --git a/src/views/SwapView/util/vault/useVault/index.ts b/src/views/SwapView/util/vault/useVault/index.ts new file mode 100644 index 00000000..8f49187f --- /dev/null +++ b/src/views/SwapView/util/vault/useVault/index.ts @@ -0,0 +1,66 @@ +import { useCallback, useMemo } from 'react' + +import useVaultData from './useVaultData' +import useVaultChartStats from './useVaultChartStats' + + +type Input = { + vaultAddress: string + withVaultData?: boolean + withVaultChartStats?: boolean +} + +const useVault = (values: Input) => { + const { + vaultAddress, + withVaultData = true, + withVaultChartStats = true, + } = values + + const { fetchVault, resetVault } = useVaultData(vaultAddress) + const { fetchVaultChartStats, resetVaultChartStats } = useVaultChartStats(vaultAddress) + + const fetchAllVaultData = useCallback(async () => { + const promises: Promise[] = [] + + if (withVaultData) { + promises.push(fetchVault()) + } + + if (withVaultChartStats) { + promises.push(fetchVaultChartStats(30)) + } + + return Promise.all(promises) + }, [ + withVaultData, + withVaultChartStats, + fetchVault, + fetchVaultChartStats, + ]) + + const resetAllVaultData = useCallback(() => { + resetVault() + resetVaultChartStats() + }, [ + resetVault, + resetVaultChartStats, + ]) + + return useMemo(() => ({ + fetchAllVaultData, + resetAllVaultData, + + fetchVault, + fetchVaultChartStats, + }), [ + fetchAllVaultData, + resetAllVaultData, + + fetchVault, + fetchVaultChartStats, + ]) +} + + +export default useVault diff --git a/src/views/SwapView/util/vault/useVault/useVaultChartStats.ts b/src/views/SwapView/util/vault/useVault/useVaultChartStats.ts new file mode 100644 index 00000000..1a5fe3fc --- /dev/null +++ b/src/views/SwapView/util/vault/useVault/useVaultChartStats.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo } from 'react' +import { useActions, useMountedRef } from 'hooks' +import { useConfig } from 'config' + + +const useVaultChartStats = (vaultAddress: string) => { + const { sdk } = useConfig() + const actions = useActions() + const mountedRef = useMountedRef() + + const fetchVaultChartStats = useCallback(async (daysCount: number) => { + try { + actions.vault.chart.setFetching(true) + + const data = await sdk.vault.getVaultStats({ + vaultAddress, + daysCount, + }) + + if (mountedRef.current) { + actions.vault.chart.setData(data) + } + } + catch (error: any) { + console.error('Fetch vault stats collection fail', error) + + actions.vault.chart.setFetching(false) + } + }, [ vaultAddress, sdk, actions, mountedRef ]) + + const resetVaultChartStats = useCallback(() => { + actions.vault.chart.resetData() + }, [ actions ]) + + return useMemo(() => ({ + fetchVaultChartStats, + resetVaultChartStats, + }), [ + fetchVaultChartStats, + resetVaultChartStats, + ]) +} + + +export default useVaultChartStats diff --git a/src/views/SwapView/util/vault/useVault/useVaultData.ts b/src/views/SwapView/util/vault/useVault/useVaultData.ts new file mode 100644 index 00000000..a478ed13 --- /dev/null +++ b/src/views/SwapView/util/vault/useVault/useVaultData.ts @@ -0,0 +1,69 @@ +import { useCallback, useMemo, useRef } from 'react' +import { useStore, useActions, useMountedRef } from 'hooks' +import { useConfig } from 'config' + + +const storeSelector = (store: Store) => ({ + isSSR: store.vault.base.isSSR, +}) + +const useVaultData = (vaultAddress: string) => { + const actions = useActions() + const mountedRef = useMountedRef() + const { isSSR } = useStore(storeSelector) + const { sdk, isGnosis, isEthereum } = useConfig() + + const isSsrRef = useRef(isSSR) + isSsrRef.current = isSSR + + const fetchVault = useCallback(async () => { + const isSSR = isSsrRef.current + + if (isSSR) { + // We need this property only for the first render, after that it will prevent us from refetching. + actions.vault.base.resetSSR() + + return + } + + try { + actions.vault.base.setFetching(true) + + const vault = await sdk.vault.getVault({ vaultAddress }) + const versions = await sdk.getVaultVersion(vaultAddress) + const feePercent = await sdk.contracts.base.mintTokenController.feePercent() + + const isEditableInGnosis = isGnosis && versions.version >= 3 + const isEditableInEthereum = isEthereum && versions.version >= 5 + const isPostPectra = isEditableInGnosis || isEditableInEthereum + + if (mountedRef.current) { + actions.vault.base.setData({ + ...vault, + versions, + isPostPectra, + protocolFeePercent: String(feePercent / 100n), + }) + } + } + catch (error: any) { + console.error('Fetch vault base data fail', error) + actions.vault.base.setFetching(false) + } + }, [ actions, mountedRef, sdk, vaultAddress, isEthereum, isGnosis ]) + + const resetVault = useCallback(() => { + actions.vault.base.resetData() + }, [ actions ]) + + return useMemo(() => ({ + resetVault, + fetchVault, + }), [ + resetVault, + fetchVault, + ]) +} + + +export default useVaultData diff --git a/tsconfig.json b/tsconfig.json index 6d34cac3..a904661f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -54,7 +54,5 @@ "exclude": [ "node_modules", ".next", - "../web", - "../../packages" ] }