diff --git a/.env.sample b/.env.sample index 0db8a520b..425187239 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,10 @@ +REACT_APP_DEBUG_MODE= REACT_APP_ALCHEMY_MAINNET= REACT_APP_ALCHEMY_ROPSTEN= REACT_APP_PORTIS_DAPP_ID= REACT_APP_FORTMATIC_API_KEY= REACT_APP_APY_VISION_TOKEN= REACT_APP_INTOTHEBLOCK_KEY= +REACT_APP_SENTRY_DSN= REACT_APP_BANCOR_DEPLOYER= GENERATE_SOURCEMAP=false diff --git a/README.md b/README.md index dc61672b7..420faaba3 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Builds the app for production to the `build` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +Your app is ready to be deployed. See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. diff --git a/package.json b/package.json index be3c0f8b0..2346f38c4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "@headlessui/react": "^1.3.0", "@popperjs/core": "^2.9.3", "@reduxjs/toolkit": "^1.6.2", + "@sentry/react": "^7.7.0", + "@sentry/tracing": "^7.7.0", + "@tanstack/react-query": "^4.0.5", + "@tanstack/react-query-devtools": "^4.0.5", "@types/jest": "^26.0.15", "@types/lodash": "^4.14.170", "@types/node": "^12.0.0", @@ -56,10 +60,10 @@ "@storybook/addon-actions": "6.5.9", "@storybook/addon-essentials": "6.5.9", "@storybook/addon-links": "6.5.9", - "@storybook/node-logger": "6.5.9", - "@storybook/preset-create-react-app": "4.1.2", "@storybook/builder-webpack5": "6.5.9", "@storybook/manager-webpack5": "6.5.9", + "@storybook/node-logger": "6.5.9", + "@storybook/preset-create-react-app": "4.1.2", "@storybook/react": "6.5.9", "@typechain/ethers-v5": "^8.0.2", "@types/json-bigint": "^1.0.0", diff --git a/src/App.tsx b/src/App.tsx index 80ca9b1b2..9b0d3b2dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,10 +10,7 @@ import { setSlippageTolerance, setUsdToggle, } from 'store/user/user'; -import { - Notification, - setNotifications, -} from 'store/notification/notification'; +import { setNotifications } from 'store/notification/notification'; import { store, useAppSelector } from 'store'; import { googleTagManager } from 'services/api/googleTagManager'; import { @@ -23,7 +20,6 @@ import { getUsdToggleLS, setNotificationsLS, } from 'utils/localStorage'; -import { subscribeToObservables } from 'services/observables/triggers'; import { isUnsupportedNetwork } from 'utils/helperFunctions'; import { MobileBottomNav } from 'elements/layoutHeader/MobileBottomNav'; import { useWeb3React } from '@web3-react/core'; @@ -41,7 +37,7 @@ export const App = () => { const { chainId, account } = useWeb3React(); useAutoConnect(); const unsupportedNetwork = isUnsupportedNetwork(chainId); - const notifications = useAppSelector( + const notifications = useAppSelector( (state) => state.notification.notifications ); @@ -75,8 +71,6 @@ export const App = () => { const slippage = getSlippageToleranceLS(); if (slippage) dispatch(setSlippageTolerance(slippage)); - subscribeToObservables(dispatch); - const dark = getDarkModeLS(); dispatch(setDarkMode(dark)); }, [dispatch]); diff --git a/src/components/tokenInput/TokenInputV3.tsx b/src/components/tokenInput/TokenInputV3.tsx index 51a3c0d72..4f5fa198a 100644 --- a/src/components/tokenInput/TokenInputV3.tsx +++ b/src/components/tokenInput/TokenInputV3.tsx @@ -2,12 +2,12 @@ import { memo, useMemo } from 'react'; import { prettifyNumber } from 'utils/helperFunctions'; import { useResizeTokenInput } from 'components/tokenInput/useResizeTokenInput'; import { useTokenInputV3 } from 'components/tokenInput/useTokenInputV3'; -import { Token } from 'services/observables/tokens'; import useDimensions from 'hooks/useDimensions'; import { Image } from 'components/image/Image'; +import { getBancorLogoUrl } from 'utils/pureFunctions'; export interface TokenInputV3Props { - token: Token; + dltId: string; inputTkn: string; setInputTkn: (amount: string) => void; inputFiat: string; @@ -17,7 +17,7 @@ export interface TokenInputV3Props { } const TokenInputV3 = ({ - token, + dltId, inputTkn, setInputTkn, inputFiat, @@ -27,7 +27,7 @@ const TokenInputV3 = ({ }: TokenInputV3Props) => { const { handleChange, inputUnit, oppositeUnit, isFocused, setIsFocused } = useTokenInputV3({ - token, + dltId, setInputTkn, setInputFiat, isFiat, @@ -55,7 +55,7 @@ const TokenInputV3 = ({ } ${isError ? 'border-error text-error' : ''}`} > {'Token diff --git a/src/components/tokenInput/useTokenInputV3.tsx b/src/components/tokenInput/useTokenInputV3.tsx index e467865a7..14a725499 100644 --- a/src/components/tokenInput/useTokenInputV3.tsx +++ b/src/components/tokenInput/useTokenInputV3.tsx @@ -1,7 +1,7 @@ -import { ChangeEvent, useCallback, useMemo, useState } from 'react'; +import { ChangeEvent, useCallback, useState } from 'react'; import { calcFiatValue, calcTknValue } from 'utils/helperFunctions'; -import { Token } from 'services/observables/tokens'; import { sanitizeNumberInput } from 'utils/pureFunctions'; +import { usePoolPick } from 'queries/index'; export const calcOppositeValue = ( isFiat: boolean, @@ -17,20 +17,24 @@ export const calcOppositeValue = ( }; export interface useTokenInputV3Props { - token: Token; + dltId: string; setInputTkn: (amount: string) => void; setInputFiat: (amount: string) => void; isFiat: boolean; } export const useTokenInputV3 = ({ - token, + dltId, setInputTkn, setInputFiat, isFiat, }: useTokenInputV3Props) => { + const { getOne } = usePoolPick(['symbol', 'decimals', 'rate']); + const { data } = getOne(dltId); + const symbol = data?.symbol; + const usdPrice = data?.rate?.usd; + const decimals = data?.decimals; const [isFocused, setIsFocused] = useState(false); - const { symbol, usdPrice, decimals } = useMemo(() => token, [token]); const inputUnit = isFiat ? 'USD' : symbol; const oppositeUnit = isFiat ? symbol : 'USD'; @@ -38,6 +42,10 @@ export const useTokenInputV3 = ({ const handleChange = useCallback( (e: ChangeEvent) => { const value = sanitizeNumberInput(e.target.value); + if (!usdPrice || !decimals) { + console.error('Missing or still loading data for calculation'); + return; + } if (isFiat) { const oppositeValue = value diff --git a/src/components/tokenInputPercentage/TokenInputPercentageV3.tsx b/src/components/tokenInputPercentage/TokenInputPercentageV3New.tsx similarity index 73% rename from src/components/tokenInputPercentage/TokenInputPercentageV3.tsx rename to src/components/tokenInputPercentage/TokenInputPercentageV3New.tsx index d80fc0b70..d061d3758 100644 --- a/src/components/tokenInputPercentage/TokenInputPercentageV3.tsx +++ b/src/components/tokenInputPercentage/TokenInputPercentageV3New.tsx @@ -5,15 +5,19 @@ import TokenInputV3, { } from 'components/tokenInput/TokenInputV3'; import { useEffect, useState } from 'react'; import { calcFiatValue, prettifyNumber } from 'utils/helperFunctions'; +import { usePoolPick } from 'queries/index'; -interface TokenInputPercentageV3Props extends TokenInputV3Props { +interface TokenInputPercentageV3Props extends Omit { + dltId: string; label: string; + balance?: string; balanceLabel?: string; } const percentages = [25, 50, 75, 100]; -export const TokenInputPercentageV3 = ({ - token, +export const TokenInputPercentageV3New = ({ + dltId, + balance, inputTkn, setInputTkn, inputFiat, @@ -23,27 +27,23 @@ export const TokenInputPercentageV3 = ({ label, balanceLabel = 'Balance', }: TokenInputPercentageV3Props) => { - const fieldBalance = token.balance - ? token.balance - : token && token.balance - ? token.balance - : undefined; - + const { getOne } = usePoolPick(['decimals', 'rate']); + const { data: token } = getOne(dltId); const [selPercentage, setSelPercentage] = useState(-1); const handleSetPercentage = (percent: number) => { setSelPercentage(percentages.indexOf(percent)); - if (fieldBalance !== undefined) { - const amount = new BigNumber(fieldBalance).times(percent / 100); + if (balance !== undefined && !!token) { + const amount = new BigNumber(balance).times(percent / 100); setInputTkn(amount.toString()); - setInputFiat(calcFiatValue(amount, token.usdPrice)); + setInputFiat(calcFiatValue(amount, token.rate?.usd ?? 0)); } }; useEffect(() => { - if (fieldBalance !== undefined) { + if (balance !== undefined) { const percentage = new BigNumber(inputTkn) - .div(fieldBalance) + .div(balance) .times(100) .toNumber() .toFixed(10); @@ -51,22 +51,22 @@ export const TokenInputPercentageV3 = ({ percentages.findIndex((x) => percentage === x.toFixed(10)) ); } - }, [fieldBalance, inputTkn]); + }, [balance, inputTkn]); return (
{label} - {fieldBalance && ( + {balance && (
{token && ( { - const stats = useAppSelector((state) => state.bancor.statistics); - const first = stats[0]; + const { data: stats } = useApiStatistics(); - return ( -
- {!stats.length ? ( - [...Array(4)].map((_, i) => ( + if (!stats) { + return ( + <> + {[...Array(4)].map((_, i) => (
- )) - ) : ( + ))} + + ); + } + + const first = stats[0]; + + return ( + <> +
-
-
-
{first.label}
-
- {first.value} -
-
+
{first.label}
+
+ {first.value}
-
-
- {stats.slice(1).map((item, i) => ( -
-
-
-
{item.label}
-
- {item.value} -
-
+
+
+
+
+ {stats.slice(1).map((item, i) => ( +
+
+
+
{item.label}
+
+ {item.value}
- ))} +
-
- )} -
+ ))} +
+ ); }; diff --git a/src/elements/earn/pools/TopPools.tsx b/src/elements/earn/pools/TopPools.tsx index b97fc66bc..c2862b82d 100644 --- a/src/elements/earn/pools/TopPools.tsx +++ b/src/elements/earn/pools/TopPools.tsx @@ -1,23 +1,39 @@ import { Ticker } from 'components/ticker/Ticker'; -import { useAppSelector } from 'store'; -import { getTopPoolsV3 } from 'store/bancor/pool'; import { ReactComponent as IconGift } from 'assets/icons/gift.svg'; import { Image } from 'components/image/Image'; +import { usePoolPick } from 'queries'; +import { useMemo } from 'react'; +import { orderBy } from 'lodash'; import { DepositV3Modal } from './poolsTable/v3/DepositV3Modal'; export const TopPools = () => { - const pools = useAppSelector(getTopPoolsV3); + const { getMany } = usePoolPick([ + 'poolDltId', + 'symbol', + 'apr', + 'latestProgram', + ]); + + const { data, isLoading } = getMany(); + + const pools = useMemo(() => { + return orderBy( + data?.filter((p) => p.apr && p.apr.apr7d.total > 0), + 'apr.apr7d.total', + 'desc' + ).slice(0, 20); + }, [data]); return (

Top Performing

- {pools.length + {!isLoading ? pools.map((pool, index) => { return ( (
- + data={data} columns={columns} defaultSort={defaultSort} - isLoading={!pools.length} + isLoading={isLoading} search={search} />
- +
+ +
diff --git a/src/elements/earn/pools/poolsTable/v3/DepositV3Modal.tsx b/src/elements/earn/pools/poolsTable/v3/DepositV3Modal.tsx index 1586d1ae1..9566c8c72 100644 --- a/src/elements/earn/pools/poolsTable/v3/DepositV3Modal.tsx +++ b/src/elements/earn/pools/poolsTable/v3/DepositV3Modal.tsx @@ -1,6 +1,5 @@ import { Button, ButtonSize } from 'components/button/Button'; -import { PoolV3 } from 'services/observables/pools'; -import { useCallback, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import { ContractsApi } from 'services/web3/v3/contractsApi'; import { useDispatch } from 'react-redux'; import { updatePortfolioData } from 'services/web3/v3/portfolio/helpers'; @@ -8,7 +7,6 @@ import { useAppSelector } from 'store'; import { useApproveModal } from 'hooks/useApproveModal'; import { ModalV3 } from 'components/modal/ModalV3'; import { SwapSwitch } from 'elements/swapSwitch/SwapSwitch'; -import { TokenInputPercentageV3 } from 'components/tokenInputPercentage/TokenInputPercentageV3'; import { ethToken } from 'services/web3/config'; import { Switch } from 'components/switch/Switch'; import { getTokenById } from 'store/bancor/bancor'; @@ -30,6 +28,7 @@ import { ExpandableSection } from 'components/expandableSection/ExpandableSectio import { ReactComponent as IconChevron } from 'assets/icons/chevronDown.svg'; import { getPoolsV3Map } from 'store/bancor/pool'; import { useWalletConnect } from 'elements/walletConnect/useWalletConnect'; +import { TokenInputPercentageV3New } from 'components/tokenInputPercentage/TokenInputPercentageV3New'; import { DepositEvent, sendDepositEvent, @@ -43,17 +42,24 @@ import { getOnOff, } from 'services/api/googleTagManager'; import { DepositDisabledModal } from './DepositDisabledModal'; +import { usePoolPick } from 'queries'; interface Props { - pool: PoolV3; - renderButton: ( - onClick: (pool_click_location?: string) => void - ) => React.ReactNode; + poolId: string; + renderButton: (onClick: (pool_click_location?: string) => void) => ReactNode; } const REWARDS_EXTRA_GAS = 130_000; -export const DepositV3Modal = ({ pool, renderButton }: Props) => { +export const DepositV3Modal = ({ poolId, renderButton }: Props) => { + const { getOne } = usePoolPick([ + 'balance', + 'symbol', + 'latestProgram', + 'decimals', + 'apr', + ]); + const { data: pool } = getOne(poolId); const enableDeposit = useAppSelector((state) => state.user.enableDeposit); const account = useAppSelector((state) => state.user.account); const [isOpen, setIsOpen] = useState(false); @@ -77,15 +83,15 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { }; const isInputError = useMemo( - () => !!account && new BigNumber(pool.reserveToken.balance || 0).lt(amount), - [account, amount, pool.reserveToken.balance] + () => !!account && new BigNumber(pool?.balance?.tkn || 0).lt(amount), + [account, amount, pool?.balance?.tkn] ); const dispatch = useDispatch(); const { goToPage } = useNavigation(); const deposit = async (approvalHash?: string) => { - if (!pool.reserveToken.balance || !account) { + if (!pool || !pool.balance?.tkn || !account) { return; } @@ -98,8 +104,8 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { ); sendDepositEvent(DepositEvent.DepositWalletRequest); - const amountWei = expandToken(amount, pool.reserveToken.decimals); - const isETH = pool.reserveToken.address === ethToken; + const amountWei = expandToken(amount, pool.decimals); + const isETH = poolId === ethToken; try { setTxBusy(true); @@ -110,23 +116,16 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { amountWei, { value: isETH ? amountWei : undefined } ) - : await ContractsApi.BancorNetwork.write.deposit( - pool.reserveToken.address, - amountWei, - { value: isETH ? amountWei : undefined } - ); + : await ContractsApi.BancorNetwork.write.deposit(poolId, amountWei, { + value: isETH ? amountWei : undefined, + }); sendDepositEvent( DepositEvent.DepositWalletConfirm, undefined, undefined, tx.hash ); - confirmDepositNotification( - dispatch, - tx.hash, - amount, - pool.reserveToken.symbol - ); + confirmDepositNotification(dispatch, tx.hash, amount, pool.symbol); setTxBusy(false); onClose(); goToPage.portfolio(); @@ -152,9 +151,18 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { }; const [onStart, ApproveModal] = useApproveModal( - [{ amount: amount || '0', token: pool.reserveToken }], + [ + { + amount: amount || '0', + token: { + address: poolId, + symbol: pool?.symbol ?? 'N/A', + decimals: pool?.decimals ?? 18, + }, + }, + ], (approvalHash?: string) => deposit(approvalHash), - accessFullEarnings && pool.latestProgram?.isActive + accessFullEarnings && pool?.latestProgram?.isActive ? ContractsApi.StandardRewards.contractAddress : ContractsApi.BancorNetwork.contractAddress, () => sendDepositEvent(DepositEvent.DepositUnlimitedPopupRequest), @@ -172,13 +180,10 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { ); const handleClick = useCallback(() => { - if (canDeposit) { + if (canDeposit && pool) { const portion = - pool.reserveToken.balance && - new BigNumber(amount) - .div(pool.reserveToken.balance) - .times(100) - .toFixed(0); + pool?.balance && + new BigNumber(amount).div(pool?.balance.tkn).times(100).toFixed(0); const deposit_portion = portion && (portion === '25' || @@ -188,11 +193,11 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { ? portion : '(no value)'; setCurrentDeposit({ - deposit_pool: pool.name, + deposit_pool: pool.symbol, deposit_blockchain: getBlockchain(), deposit_blockchain_network: getBlockchainNetwork(), deposit_input_type: getFiat(isFiat), - deposit_token: pool.name, + deposit_token: pool.symbol, deposit_token_amount: amount, deposit_token_amount_usd: inputFiat, deposit_portion, @@ -213,8 +218,7 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { amount, inputFiat, isFiat, - pool.name, - pool.reserveToken.balance, + pool, ]); const shouldPollForGasPrice = useMemo(() => { @@ -241,15 +245,17 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { if (!enableDeposit) return ; + if (!pool) return null; + return ( <> {renderButton((pool_click_location) => { setCurrentDeposit({ - deposit_pool: pool.name, + deposit_pool: pool.symbol, deposit_blockchain: getBlockchain(), deposit_blockchain_network: getBlockchainNetwork(), deposit_input_type: getFiat(isFiat), - deposit_token: pool.name, + deposit_token: pool.symbol, deposit_token_amount: undefined, deposit_token_amount_usd: undefined, deposit_portion: undefined, @@ -275,10 +281,10 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { large >
- {
{accessFullEarnings - ? pool.apr7d.total.toFixed(2) - : pool.apr7d.tradingFees.toFixed(2)} + ? pool.apr?.apr7d.total.toFixed(2) + : pool.apr?.apr7d.tradingFees.toFixed(2)} % {
Compounding rewards{' '} - - {pool.reserveToken.symbol} - + {pool.symbol} - {pool.apr7d.tradingFees.toFixed(2)}% + {pool.apr?.apr7d.tradingFees.toFixed(2)}%
@@ -347,7 +351,7 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { {accessFullEarnings - ? pool.apr7d.standardRewards.toFixed(2) + ? pool.apr?.apr7d.standardRewards.toFixed(2) : 0} % @@ -358,11 +362,9 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => {
Compounding rewards{' '} - - {pool.reserveToken.symbol} - + {pool.symbol} - {pool.apr7d.tradingFees.toFixed(2)}% + {pool.apr?.apr7d.tradingFees.toFixed(2)}%
)} @@ -377,7 +379,7 @@ export const DepositV3Modal = ({ pool, renderButton }: Props) => { : shouldConnect ? 'Connect your wallet' : canDeposit - ? `Deposit ${pool.name}` + ? `Deposit ${pool.symbol}` : 'Enter amount'} diff --git a/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx b/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx index 045ff7ed6..a213a9e23 100644 --- a/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx +++ b/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx @@ -13,7 +13,7 @@ import { DepositV3Modal } from 'elements/earn/pools/poolsTable/v3/DepositV3Modal const AvailableItem = ({ token, pool }: { token: Token; pool: PoolV3 }) => { return ( ( + +
+ +
{children}
+
+ ); +}; diff --git a/src/elements/vote/VoteCardStep1.tsx b/src/elements/vote/VoteCardStep1.tsx new file mode 100644 index 000000000..ee8b72394 --- /dev/null +++ b/src/elements/vote/VoteCardStep1.tsx @@ -0,0 +1,71 @@ +import { usePoolPick } from 'queries'; +import { useState } from 'react'; +import { vBntToken } from 'services/web3/config'; +import { useAppSelector } from 'store'; +import { prettifyNumber } from 'utils/helperFunctions'; +import { useVoteStakedBalance } from './useVoteStakedBalance'; +import { VoteCard } from './VoteCard'; +import { VoteStakeModal } from './VoteStakeModal'; + +const loadingElement =
; + +const VoteCardStep1Children = () => { + const account = useAppSelector((state) => state.user.account); + + const stakedBalanceQuery = useVoteStakedBalance(); + const stakedBalance = stakedBalanceQuery.data + ? prettifyNumber(stakedBalanceQuery.data) + ' vBNT' + : '--'; + + const { getOne } = usePoolPick(['balance']); + const vBntQuery = getOne(vBntToken); + + const unstakedBalance = vBntQuery.data?.balance + ? prettifyNumber(vBntQuery.data.balance.tkn) + ' vBNT' + : '--'; + + return ( +
+
+
+ {vBntQuery.isLoading && account ? loadingElement : unstakedBalance} +
+
Unstaked Balance
+
+
+
+ {stakedBalanceQuery.isLoading && account + ? loadingElement + : stakedBalance} +
+
Staked Balance
+
+
+ ); +}; + +const step = 1; +const title = 'Stake your vBNT'; +const buttonText = 'Stake Tokens'; +const description = + 'In order to participate in Bancor governance activities, you should first stake your vBNT tokens. Staked vBNT will be locked for the initial 3 days.'; + +export const VoteCardStep1 = () => { + const [isOpen, setIsOpen] = useState(false); + const onButtonClick = () => setIsOpen(true); + + return ( + <> + + + + + + ); +}; diff --git a/src/elements/vote/VoteCardStep2.tsx b/src/elements/vote/VoteCardStep2.tsx new file mode 100644 index 000000000..e89cf41ab --- /dev/null +++ b/src/elements/vote/VoteCardStep2.tsx @@ -0,0 +1,20 @@ +import { VoteCard } from './VoteCard'; + +const step = 2; +const title = 'Make a Difference'; +const buttonText = 'Vote on Snapshot'; +const onButtonClick = () => {}; +const description = + 'Voting on Bancor DAO is free as it is using the Snapshot off-chain infrastructure. Every user can vote on every available proposal and help shape the future of the Bancor Protocol.'; + +export const VoteCardStep2 = () => { + return ( + + ); +}; diff --git a/src/elements/vote/VoteCardStep3.tsx b/src/elements/vote/VoteCardStep3.tsx new file mode 100644 index 000000000..0317addc9 --- /dev/null +++ b/src/elements/vote/VoteCardStep3.tsx @@ -0,0 +1,26 @@ +import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; +import { useState } from 'react'; +import { VoteStakeModal } from './VoteStakeModal'; + +const title = 'Unstake from Governance'; +const buttonText = 'Stake Tokens'; +const description = + 'In order to remove vBNT from governance you would need to unstake them first.'; + +export const VoteCardStep3 = () => { + const [isOpen, setIsOpen] = useState(false); + const onButtonClick = () => setIsOpen(true); + + return ( + <> +
+

{title}

+

{description}

+ +
+ + + ); +}; diff --git a/src/elements/vote/VoteStakeModal.tsx b/src/elements/vote/VoteStakeModal.tsx new file mode 100644 index 000000000..c8c942370 --- /dev/null +++ b/src/elements/vote/VoteStakeModal.tsx @@ -0,0 +1,174 @@ +import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; +import { ModalV3 } from 'components/modal/ModalV3'; +import { usePoolPick } from 'queries'; +import { useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { getApproval, setApproval } from 'services/web3/approval'; +import { vBntToken } from 'services/web3/config'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { useAppSelector } from 'store'; +import useAsyncEffect from 'use-async-effect'; +import { expandToken } from 'utils/formulas'; +import { TokenInputPercentageV3New } from 'components/tokenInputPercentage/TokenInputPercentageV3New'; +import { toBigNumber } from 'utils/helperFunctions'; + +interface Props { + isOpen: boolean; + setIsOpen: (state: boolean) => void; +} + +const useApproval = (id: string, amount: string, spender: string) => { + const account = useAppSelector((state) => state.user.account); + const { getOne } = usePoolPick(['decimals']); + const query = getOne(id); + const [approvalRequired, setApprovalRequired] = useState(false); + const [isLoadingAllowance, setIsLoadingAllowance] = useState(false); + + const isLoading = query.isLoading || isLoadingAllowance; + + const getAllowance = async () => { + if (!account || !query.data || query.isLoading) { + throw new Error('Error getAllowance'); + } + setIsLoadingAllowance(true); + const amountWei = expandToken(amount, query.data.decimals); + + const { isApprovalRequired } = await getApproval( + id, + account, + spender, + amountWei + ); + setApprovalRequired(isApprovalRequired); + setIsLoadingAllowance(false); + }; + + const setAllowance = async (unlimited: boolean = true) => { + if (!account || !query.data || query.isLoading) { + throw new Error('Error'); + } + + const amountWei = expandToken(amount, query.data.decimals); + await setApproval( + id, + account, + spender, + unlimited ? undefined : amountWei, + false + ); + await getAllowance(); + }; + + useAsyncEffect(async () => { + if (account && query.data && !query.isLoading) { + await getAllowance(); + } + }, [id, amount, account]); + + return { approvalRequired, isLoading, setAllowance }; +}; + +export const VoteStakeModal = ({ isOpen, setIsOpen }: Props) => { + const queryClient = useQueryClient(); + const { getOne } = usePoolPick(['balance', 'decimals']); + const vBntQuery = getOne(vBntToken); + const balance = vBntQuery.data?.balance ? vBntQuery.data.balance?.tkn : '0'; + const decimals = vBntQuery.data?.decimals; + const account = useAppSelector((state) => state.user.account); + + const handleStake = async () => { + try { + if (!decimals) { + throw new Error('No decimals found'); + } + const inputWei = expandToken(input, decimals); + const tx = await ContractsApi.Governance.write.stake(inputWei); + await tx.wait(); + await queryClient.invalidateQueries(['chain']); + setInput(''); + setIsOpen(false); + + console.log('muh'); + } catch (e: any) { + console.error(e.message); + } + }; + + const [input, setInput] = useState(''); + const [inputFiat, setInputFiat] = useState(''); + + const isInsufficientBalance = toBigNumber(balance).lt(input); + + const { isLoading, approvalRequired, setAllowance } = useApproval( + vBntToken, + input, + ContractsApi.Governance.contractAddress + ); + + return ( + +
+ +
+ +
+

Approval required

+
+ Before you can proceed, please set the allowance for the Bancor + Governance Contract. +
+
+ + +
+
+ +
+ +
+
+ + ); +}; diff --git a/src/elements/vote/useVoteStakedBalance.ts b/src/elements/vote/useVoteStakedBalance.ts new file mode 100644 index 000000000..f344f5339 --- /dev/null +++ b/src/elements/vote/useVoteStakedBalance.ts @@ -0,0 +1,27 @@ +import { usePoolPick } from 'queries'; +import { useQuery } from '@tanstack/react-query'; +import { vBntToken } from 'services/web3/config'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { useAppSelector } from 'store'; +import { shrinkToken } from 'utils/formulas'; + +export const useVoteStakedBalance = () => { + const { getOne } = usePoolPick(['decimals']); + const vBNT = getOne(vBntToken); + const account = useAppSelector((state) => state.user.account); + + return useQuery( + ['chain', 'vote', 'stakedBalance', account], + async () => { + if (!account) { + throw new Error('Not logged in.'); + } + if (!vBNT.data) { + throw new Error('Data not fetched.'); + } + const staked = await ContractsApi.Governance.read.votesOf(account); + return shrinkToken(staked.toString(), vBNT.data.decimals); + }, + { enabled: !!account && !!vBNT.data } + ); +}; diff --git a/src/hooks/useApproveModal.tsx b/src/hooks/useApproveModal.tsx index 628827547..d28b11c5a 100644 --- a/src/hooks/useApproveModal.tsx +++ b/src/hooks/useApproveModal.tsx @@ -1,9 +1,9 @@ -import { Token } from 'services/observables/tokens'; import { useCallback, useRef, useState } from 'react'; import { ApprovalContract, - getNetworkContractApproval, - setNetworkContractApproval, + getApproval, + setApproval as setUserApproval, + getApprovalAddress, } from 'services/web3/approval'; import { ModalApproveNew } from 'elements/modalApprove/modalApproveNew'; import { @@ -15,9 +15,15 @@ import { ErrorCode } from 'services/web3/types'; import { wait } from 'utils/pureFunctions'; import { web3 } from 'services/web3'; import { Events } from 'services/api/googleTagManager'; +import { useAppSelector } from 'store/index'; +import { expandToken } from 'utils/formulas'; interface Tokens { - token: Token; + token: { + address: string; + symbol: string; + decimals: number; + }; amount: string; } @@ -29,6 +35,7 @@ export const useApproveModal = ( gtmSelectEvent?: (isUnlimited: boolean) => void, onClose?: Function ) => { + const account = useAppSelector((state) => state.user.account); const [isOpen, setIsOpen] = useState(false); const [tokenIndex, setTokenIndex] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -80,11 +87,17 @@ export const useApproveModal = ( }; const checkApprovalRequired = async (tokenIndex: number = 0) => { + if (!account) { + return; + } + const { token, amount } = tokens[tokenIndex]; - const isApprovalRequired = await getNetworkContractApproval( - token, - contract, - amount + const amountWei = expandToken(amount, token.decimals); + const { isApprovalRequired } = await getApproval( + token.address, + account, + await getApprovalAddress(contract), + amountWei ); if (!isApprovalRequired) { @@ -97,18 +110,23 @@ export const useApproveModal = ( }; const setApproval = async (amount?: string) => { + if (!account) { + return; + } if (gtmSelectEvent) { const isUnlimited = amount === undefined; gtmSelectEvent(isUnlimited); } const { token } = tokens[tokenIndex]; + const amountWei = amount ? expandToken(amount, token.decimals) : undefined; + try { setIsLoading(true); - const txHash = await setNetworkContractApproval( - token, - contract, - amount, - true + const txHash = await setUserApproval( + token.address, + account, + await getApprovalAddress(contract), + amountWei ); ref.current = [...ref.current, txHash]; diff --git a/src/index.tsx b/src/index.tsx index d49453ff3..8a6e1ba73 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,17 +8,23 @@ import { I18nProvider } from 'i18n/i18nProvider'; import { getLibrary } from 'services/web3/wallet/utils'; import { Web3ReactProvider } from '@web3-react/core'; import 'styles/index.css'; +import { QueryClientProvider } from 'queries'; +import { SentryErrorBoundary } from 'sentry/ErrorBoundary'; ReactDOM.render( - - - - - - - - - , + + + + + + + + + + + + + , document.getElementById('root') ); diff --git a/src/pages/Vote2.tsx b/src/pages/Vote2.tsx new file mode 100644 index 000000000..1f46bec60 --- /dev/null +++ b/src/pages/Vote2.tsx @@ -0,0 +1,21 @@ +import { Page } from 'components/Page'; +import { VoteCardStep1 } from 'elements/vote/VoteCardStep1'; +import { VoteCardStep2 } from 'elements/vote/VoteCardStep2'; +import { VoteCardStep3 } from 'elements/vote/VoteCardStep3'; + +const title = 'Vote'; +const subtitle = + 'Bancor is a DAO managed by vBNT stakers who determine the future of the protocol with their proposals.'; + +export const Vote2 = () => { + return ( + +
+ + + + +
+
+ ); +}; diff --git a/src/pages/earn/portfolio/MigrateProtect.tsx b/src/pages/earn/portfolio/MigrateProtect.tsx index f713800d5..b2f369280 100644 --- a/src/pages/earn/portfolio/MigrateProtect.tsx +++ b/src/pages/earn/portfolio/MigrateProtect.tsx @@ -274,7 +274,7 @@ const Protect = () => {
$??,??? balance
{ + const poolIds = useChainPoolIds(); + const apiPools = useApiPools({ enabled }); + const programsMap = useChainPrograms({ enabled }); + + const data = new Map( + poolIds.data?.map((id) => { + const apiPool = apiPools.getByID(id); + const programs = programsMap.data?.get(id); + + if (!apiPool) { + return [id, undefined]; + } + // FIXES STAKED BALANCE = 0 WHEN TRADING ENABLED = FALSE + const stakedBalance = { ...apiPool.stakedBalance }; + if ( + apiPool.tradingEnabled === false && + toBigNumber(stakedBalance.usd).isZero() + ) { + stakedBalance.usd = toBigNumber(apiPool.stakedBalance.tkn) + // TODO - add apiToken.rate + .times('0') + .toString(); + } + + // Calculate APR + const standardRewardsApr24H = standardsRewardsAPR(apiPool, programs); + const standardRewardsApr7d = standardsRewardsAPR(apiPool, programs); + + const tradingFeesApr24h = calcApr(apiPool.fees24h.usd, stakedBalance.usd); + const tradingFeesApr7d = calcApr( + apiPool.fees7d.usd, + stakedBalance.usd, + true + ); + + // TODO - add values once available + const autoCompoundingApr24H = 0; + const autoCompoundingApr7d = 0; + + const totalApr24H = toBigNumber(tradingFeesApr24h) + .plus(standardRewardsApr24H) + .plus(autoCompoundingApr24H) + .toNumber(); + + const totalApr7d = toBigNumber(tradingFeesApr7d) + .plus(autoCompoundingApr7d) + .plus(standardRewardsApr7d) + .toNumber(); + + const apr = { + apr24h: { + tradingFees: tradingFeesApr24h, + standardRewards: standardRewardsApr24H, + autoCompounding: autoCompoundingApr24H, + total: totalApr24H, + }, + apr7d: { + tradingFees: tradingFeesApr7d, + standardRewards: standardRewardsApr7d, + autoCompounding: autoCompoundingApr7d, + total: totalApr7d, + }, + }; + return [id, { ...apr }]; + }) + ); + + const getByID = (id: string) => data.get(id); + + return { + data, + getByID, + isLoading: poolIds.isLoading || apiPools.isLoading || programsMap.isLoading, + isFetching: + poolIds.isFetching || apiPools.isFetching || programsMap.isFetching, + isError: poolIds.isError || apiPools.isError || programsMap.isError, + }; +}; diff --git a/src/queries/api/useApiFees.ts b/src/queries/api/useApiFees.ts new file mode 100644 index 000000000..45f8b4a4e --- /dev/null +++ b/src/queries/api/useApiFees.ts @@ -0,0 +1,32 @@ +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useApiPools } from 'queries/api/useApiPools'; + +interface Props { + enabled?: boolean; +} + +export const useApiFees = ({ enabled = true }: Props = {}) => { + const poolIds = useChainPoolIds(); + const apiPools = useApiPools({ enabled }); + + const data = new Map( + poolIds.data?.map((id) => { + const apiPool = apiPools.getByID(id); + + if (!apiPool) { + return [id, undefined]; + } + return [id, { fees7d: apiPool.fees7d, fees24h: apiPool.fees24h }]; + }) + ); + + const getByID = (id: string) => data.get(id); + + return { + data, + getByID, + isLoading: apiPools.isLoading || poolIds.isLoading, + isFetching: apiPools.isFetching || poolIds.isFetching, + isError: apiPools.isError || poolIds.isError, + }; +}; diff --git a/src/queries/api/useApiPools.ts b/src/queries/api/useApiPools.ts new file mode 100644 index 000000000..845c7fa74 --- /dev/null +++ b/src/queries/api/useApiPools.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { BancorApi } from 'services/api/bancorApi/bancorApi'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { queryOptionsStaleTime15s } from 'queries/queryOptions'; + +interface Props { + enabled?: boolean; +} + +export const useApiPools = ({ enabled = true }: Props = {}) => { + const queryKey = QueryKey.apiPools(); + + const query = useQuery( + queryKey, + async () => { + try { + const pools = await BancorApi.v3.getPoolsWithBNT(); + return new Map(pools.map((p) => [p.poolDltId, p])); + } catch (e: any) { + throw { + ...e, + message: + 'useQuery failed: ' + queryKey.join('-') + ' MSG: ' + e.message, + }; + } + }, + queryOptionsStaleTime15s(enabled) + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/api/useApiRate.ts b/src/queries/api/useApiRate.ts new file mode 100644 index 000000000..1031aa4b4 --- /dev/null +++ b/src/queries/api/useApiRate.ts @@ -0,0 +1,32 @@ +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useApiTokens } from 'queries/api/useApiTokens'; + +interface Props { + enabled?: boolean; +} + +export const useApiRate = ({ enabled = true }: Props = {}) => { + const poolIds = useChainPoolIds(); + const apiTokens = useApiTokens({ enabled }); + + const data = new Map( + poolIds.data?.map((id) => { + const apiToken = apiTokens.getApiPoolByID(id); + + if (!apiToken) { + return [id, undefined]; + } + return [id, apiToken.rate]; + }) + ); + + const getByID = (id: string) => data.get(id); + + return { + data, + getByID, + isLoading: apiTokens.isLoading || poolIds.isLoading, + isFetching: apiTokens.isFetching || poolIds.isFetching, + isError: apiTokens.isError || poolIds.isError, + }; +}; diff --git a/src/queries/api/useApiStakedBalance.ts b/src/queries/api/useApiStakedBalance.ts new file mode 100644 index 000000000..5d9132758 --- /dev/null +++ b/src/queries/api/useApiStakedBalance.ts @@ -0,0 +1,32 @@ +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useApiPools } from 'queries/api/useApiPools'; + +interface Props { + enabled?: boolean; +} + +export const useApiStakedBalance = ({ enabled = true }: Props = {}) => { + const poolIds = useChainPoolIds(); + const apiPools = useApiPools({ enabled }); + + const data = new Map( + poolIds.data?.map((id) => { + const apiPool = apiPools.getByID(id); + + if (!apiPool) { + return [id, undefined]; + } + return [id, { ...apiPool.stakedBalance }]; + }) + ); + + const getByID = (id: string) => data.get(id); + + return { + data, + getByID, + isLoading: apiPools.isLoading || poolIds.isLoading, + isFetching: apiPools.isFetching || poolIds.isFetching, + isError: apiPools.isError || poolIds.isError, + }; +}; diff --git a/src/queries/api/useApiStatistics.ts b/src/queries/api/useApiStatistics.ts new file mode 100644 index 000000000..194dbdb35 --- /dev/null +++ b/src/queries/api/useApiStatistics.ts @@ -0,0 +1,119 @@ +import { useQuery } from '@tanstack/react-query'; +import { BancorApi } from 'services/api/bancorApi/bancorApi'; +import BigNumber from 'bignumber.js'; +import { bntToken } from 'services/web3/config'; +import { toBigNumber } from 'utils/helperFunctions'; +import numbro from 'numbro'; +import { useApiV2Welcome } from 'queries/api/useApiV2Welcome'; +import { WelcomeData } from 'services/api/bancorApi/bancorApi.types'; +import { genericFailedNotification } from 'services/notifications/notifications'; +import { useDispatch } from 'react-redux'; +import { queryOptionsStaleTime15s } from 'queries/queryOptions'; +import { QueryKey } from 'queries/queryKeyFactory'; + +export interface Statistic { + label: string; + value: string; + change24h?: number; +} + +const averageFormat = { + average: true, + mantissa: 2, + optionalMantissa: true, + spaceSeparated: true, + lowPrecision: false, +}; + +const fetchStatistics = async ( + apiDataV2: WelcomeData +): Promise => { + const stats = await BancorApi.v3.getStatistics(); + + const bnt24hChange = new BigNumber(stats.bntRate) + .div(stats.bntRate24hAgo) + .times(100) + .minus(100) + .toNumber(); + + const bntSupply = apiDataV2.bnt_supply; + + const totalBntStakedV2: number = apiDataV2.pools.reduce((acc, item) => { + const bntReserve = item.reserves.find( + (reserve) => reserve.address === bntToken + ); + if (!bntReserve) return acc; + return Number(bntReserve.balance) + acc; + }, 0); + + const stakedBntPercentV2 = new BigNumber(totalBntStakedV2) + .div(toBigNumber(bntSupply).toExponential(18)) + .times(100); + + const stakedBntPercentV3 = new BigNumber(stats.stakedBalanceBNT.bnt) + .div(toBigNumber(bntSupply).toExponential(18)) + .times(100); + + const totalBNTStaked = new BigNumber(stakedBntPercentV2).plus( + stakedBntPercentV3 + ); + + const totalLiquidity = new BigNumber(stats.tradingLiquidityBNT.usd) + .plus(stats.tradingLiquidityTKN.usd) + .plus(apiDataV2.total_liquidity.usd); + + const totalVolume = new BigNumber(apiDataV2.total_volume_24h.usd).plus( + stats.totalVolume24h.usd + ); + const totalFees = new BigNumber(apiDataV2.total_fees_24h.usd).plus( + stats.totalFees24h.usd + ); + + return [ + { + label: 'Total Liquidity', + value: '$' + numbro(totalLiquidity).format(averageFormat), + change24h: 0, + }, + { + label: 'Volume', + value: '$' + numbro(totalVolume).format(averageFormat), + change24h: 0, + }, + { + label: 'Fees (24h)', + value: '$' + numbro(totalFees).format(averageFormat), + change24h: 0, + }, + { + label: 'BNT Price', + value: '$' + numbro(stats.bntRate).format({ mantissa: 2 }), + change24h: bnt24hChange, + }, + { + label: 'BNT Staked', + value: numbro(totalBNTStaked).format({ mantissa: 2 }) + '%', + }, + ]; +}; + +export const useApiStatistics = () => { + const dispatch = useDispatch(); + const { data: apiDataV2 } = useApiV2Welcome(); + + return useQuery( + QueryKey.apiStatistics(), + () => fetchStatistics(apiDataV2!), + { + ...queryOptionsStaleTime15s(!!apiDataV2), + useErrorBoundary: false, + onError: (err: any) => { + genericFailedNotification( + dispatch, + `${err.message}`, + `Server Error: ${QueryKey.apiStatistics().join('->')}` + ); + }, + } + ); +}; diff --git a/src/queries/api/useApiTokens.ts b/src/queries/api/useApiTokens.ts new file mode 100644 index 000000000..e6e6e9a98 --- /dev/null +++ b/src/queries/api/useApiTokens.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; +import { BancorApi } from 'services/api/bancorApi/bancorApi'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { queryOptionsStaleTime15s } from 'queries/queryOptions'; + +interface Props { + enabled?: boolean; +} + +export const useApiTokens = ({ enabled = true }: Props = {}) => { + const queryKey = QueryKey.apiTokens(); + + const query = useQuery( + queryKey, + async () => { + try { + const pools = await BancorApi.v3.getTokens(); + return new Map(pools.map((t) => [t.dltId, t])); + } catch (e: any) { + throw new Error( + 'useQuery failed: ' + queryKey.join('-') + ' MSG: ' + e.message + ); + } + }, + queryOptionsStaleTime15s(enabled) + ); + + const getApiPoolByID = (id: string) => query.data?.get(id); + + return { ...query, getApiPoolByID }; +}; diff --git a/src/queries/api/useApiTradingLiquidity.ts b/src/queries/api/useApiTradingLiquidity.ts new file mode 100644 index 000000000..ed4a11d62 --- /dev/null +++ b/src/queries/api/useApiTradingLiquidity.ts @@ -0,0 +1,33 @@ +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; + +import { useApiPools } from 'queries/api/useApiPools'; + +interface Props { + enabled?: boolean; +} + +export const useApiTradingLiquidity = ({ enabled = true }: Props = {}) => { + const poolIds = useChainPoolIds(); + const apiPools = useApiPools({ enabled }); + + const data = new Map( + poolIds?.data?.map((id) => [ + id, + apiPools.getByID(id) + ? { + BNT: apiPools.getByID(id)!.tradingLiquidityBNT, + TKN: apiPools.getByID(id)!.tradingLiquidityTKN, + } + : undefined, + ]) + ); + + const getByID = (id: string) => data.get(id); + + return { + getByID, + isLoading: poolIds.isLoading || apiPools.isLoading, + isError: poolIds.isError || apiPools.isError, + isFetching: poolIds.isFetching || apiPools.isFetching, + }; +}; diff --git a/src/queries/api/useApiV2Welcome.ts b/src/queries/api/useApiV2Welcome.ts new file mode 100644 index 000000000..b8172c413 --- /dev/null +++ b/src/queries/api/useApiV2Welcome.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { BancorApi } from 'services/api/bancorApi/bancorApi'; +import { WelcomeData } from 'services/api/bancorApi/bancorApi.types'; +import { queryOptionsStaleTime15s } from 'queries/queryOptions'; +import { QueryKey } from 'queries/queryKeyFactory'; + +export const useApiV2Welcome = () => { + return useQuery( + QueryKey.v2ApiWelcome(), + BancorApi.v2.getWelcome, + queryOptionsStaleTime15s() + ); +}; diff --git a/src/queries/api/useApiVolume.ts b/src/queries/api/useApiVolume.ts new file mode 100644 index 000000000..31f40e0c8 --- /dev/null +++ b/src/queries/api/useApiVolume.ts @@ -0,0 +1,32 @@ +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useApiPools } from 'queries/api/useApiPools'; + +interface Props { + enabled?: boolean; +} + +export const useApiVolume = ({ enabled = true }: Props = {}) => { + const poolIds = useChainPoolIds(); + const apiPools = useApiPools({ enabled }); + + const data = new Map( + poolIds.data?.map((id) => { + const apiPool = apiPools.getByID(id); + + if (!apiPool) { + return [id, undefined]; + } + return [id, { volume7d: apiPool.volume7d, volume24h: apiPool.volume24h }]; + }) + ); + + const getByID = (id: string) => data.get(id); + + return { + data, + getByID, + isLoading: apiPools.isLoading || poolIds.isLoading, + isFetching: apiPools.isFetching || poolIds.isFetching, + isError: apiPools.isError || poolIds.isError, + }; +}; diff --git a/src/queries/chain/useChainBalances.ts b/src/queries/chain/useChainBalances.ts new file mode 100644 index 000000000..d9e7e4044 --- /dev/null +++ b/src/queries/chain/useChainBalances.ts @@ -0,0 +1,61 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchTokenBalanceMulticall } from 'services/web3/token/token'; +import { useAppSelector } from 'store/index'; +import { ethToken } from 'services/web3/config'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useChainPoolTokenIds } from 'queries/chain/useChainPoolTokenIds'; +import { useChainDecimals } from 'queries/chain/useChainDecimals'; +import { shrinkToken } from 'utils/formulas'; +import { QueryKey } from 'queries/queryKeyFactory'; + +interface Props { + enabled?: boolean; +} + +export const useChainBalances = ({ enabled = true }: Props = {}) => { + const user = useAppSelector((state) => state.user.account); + const poolIds = useChainPoolIds(); + const poolTokenIds = useChainPoolTokenIds({ enabled }); + const decimals = useChainDecimals({ enabled }); + + const tknIds = poolIds.data ?? []; + const bnTknIds = poolTokenIds.data + ? Array.from(poolTokenIds.data.values()) + : []; + + const query = useQuery( + QueryKey.chainBalances(user), + () => + fetchTokenBalanceMulticall( + [...tknIds, ...bnTknIds].filter((id) => id !== ethToken), + user! + ), + { + enabled: + !!user && + !!poolIds.data && + !!poolTokenIds.data && + !!decimals.data && + enabled, + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => { + if (!user) return undefined; + const poolTokenId = poolTokenIds.data?.get(id) ?? ''; + const dec = decimals.data?.get(id) ?? 0; + return { + tkn: shrinkToken(query.data?.get(id) ?? '0', dec), + bnTkn: shrinkToken(query.data?.get(poolTokenId) ?? '0', dec), + }; + }; + + const queries = [poolIds, poolTokenIds, decimals, query]; + + const isLoading = queries.some((q) => q.isLoading); + const isFetching = queries.some((q) => q.isFetching); + const isError = queries.some((q) => q.isError); + + return { ...query, getByID, isLoading, isFetching, isError }; +}; diff --git a/src/queries/chain/useChainDecimals.ts b/src/queries/chain/useChainDecimals.ts new file mode 100644 index 000000000..59b195f59 --- /dev/null +++ b/src/queries/chain/useChainDecimals.ts @@ -0,0 +1,36 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { + buildMulticallDecimal, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { ethToken } from 'services/web3/config'; + +interface Props { + enabled?: boolean; +} + +export const useChainDecimals = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const query = useQuery( + QueryKey.chainDecimals(poolIds?.length), + async () => { + const decimals = await fetchMulticallHelper( + poolIds!.filter((id) => id !== ethToken), + buildMulticallDecimal + ); + decimals.set(ethToken, 18); + return decimals; + }, + { + ...queryOptionsNoInterval(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainDepositingEnabled.ts b/src/queries/chain/useChainDepositingEnabled.ts new file mode 100644 index 000000000..ebcbf14b7 --- /dev/null +++ b/src/queries/chain/useChainDepositingEnabled.ts @@ -0,0 +1,30 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { + buildMulticallDepositingEnabled, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; + +interface Props { + enabled?: boolean; +} + +export const useChainDepositingEnabled = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + + const query = useQuery( + QueryKey.chainDepositingEnabled(poolIds?.length), + () => + fetchMulticallHelper(poolIds!, buildMulticallDepositingEnabled), + { + ...queryOptionsNoInterval(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainLatestProgram.ts b/src/queries/chain/useChainLatestProgram.ts new file mode 100644 index 000000000..191e577cf --- /dev/null +++ b/src/queries/chain/useChainLatestProgram.ts @@ -0,0 +1,45 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { + buildMulticallLatestProgramId, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; +import { useChainPrograms } from 'queries/chain/useChainPrograms'; +import { BigNumberish } from 'ethers'; + +interface Props { + enabled?: boolean; +} + +export const useChainLatestProgram = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const { data: programsMap } = useChainPrograms({ enabled }); + + const query = useQuery( + QueryKey.chainLatestProgram(poolIds?.length), + async () => { + const ids = await fetchMulticallHelper( + poolIds!, + buildMulticallLatestProgramId + ); + return new Map( + poolIds?.map((id) => { + const programs = programsMap + ?.get(id) + ?.find((p) => p.id === ids.get(id)?.toString()); + return [id, programs]; + }) + ); + }, + { + ...queryOptionsNoInterval(!!poolIds && !!programsMap && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainPoolIds.ts b/src/queries/chain/useChainPoolIds.ts new file mode 100644 index 000000000..96879eaa1 --- /dev/null +++ b/src/queries/chain/useChainPoolIds.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { queryOptionsStaleTime2m } from 'queries/queryOptions'; +import { bntToken } from 'services/web3/config'; + +export const useChainPoolIds = () => { + return useQuery( + QueryKey.chainPoolIds(), + async () => { + const pools = await ContractsApi.BancorNetwork.read.liquidityPools(); + return [bntToken, ...pools]; + }, + { + ...queryOptionsStaleTime2m(), + useErrorBoundary: true, + } + ); +}; diff --git a/src/queries/chain/useChainPoolTokenIds.ts b/src/queries/chain/useChainPoolTokenIds.ts new file mode 100644 index 000000000..4bfc08089 --- /dev/null +++ b/src/queries/chain/useChainPoolTokenIds.ts @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { + buildMulticallPoolToken, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; + +interface Props { + enabled?: boolean; +} + +export const useChainPoolTokenIds = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const query = useQuery( + QueryKey.chainPoolTokenIds(poolIds?.length), + () => fetchMulticallHelper(poolIds!, buildMulticallPoolToken), + { + ...queryOptionsNoInterval(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainPrograms.ts b/src/queries/chain/useChainPrograms.ts new file mode 100644 index 000000000..4a174f485 --- /dev/null +++ b/src/queries/chain/useChainPrograms.ts @@ -0,0 +1,30 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { fetchAllStandardRewards } from 'services/web3/v3/portfolio/standardStaking'; + +interface Props { + enabled?: boolean; +} + +export const useChainPrograms = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const query = useQuery( + QueryKey.chainPrograms(poolIds?.length), + async () => { + const programs = await fetchAllStandardRewards(); + return new Map( + poolIds?.map((id) => [id, programs.filter((p) => p.pool === id)]) + ); + }, + { + ...queryOptionsNoInterval(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainSymbol.ts b/src/queries/chain/useChainSymbol.ts new file mode 100644 index 000000000..6ff75e3ba --- /dev/null +++ b/src/queries/chain/useChainSymbol.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsNoInterval } from 'queries/queryOptions'; +import { + buildMulticallSymbol, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; +import { ethToken } from 'services/web3/config'; + +interface Props { + enabled?: boolean; +} + +export const useChainSymbol = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const query = useQuery( + QueryKey.chainSymbols(poolIds?.length), + async () => { + const symbols = await fetchMulticallHelper( + poolIds!, + buildMulticallSymbol, + true + ); + symbols.set(ethToken, 'ETH'); + return symbols; + }, + { + ...queryOptionsNoInterval(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainTradingEnabled.ts b/src/queries/chain/useChainTradingEnabled.ts new file mode 100644 index 000000000..8bb8fa113 --- /dev/null +++ b/src/queries/chain/useChainTradingEnabled.ts @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsStaleTime2m } from 'queries/queryOptions'; +import { + buildMulticallTradingEnabled, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; + +interface Props { + enabled?: boolean; +} + +export const useChainTradingEnabled = ({ enabled = true }: Props = {}) => { + const { data: poolIds } = useChainPoolIds(); + const query = useQuery( + QueryKey.chainTradingEnabled(poolIds?.length), + () => fetchMulticallHelper(poolIds!, buildMulticallTradingEnabled), + { + ...queryOptionsStaleTime2m(!!poolIds && enabled), + useErrorBoundary: true, + } + ); + + const getByID = (id: string) => query.data?.get(id); + + return { ...query, getByID }; +}; diff --git a/src/queries/chain/useChainTradingFee.ts b/src/queries/chain/useChainTradingFee.ts new file mode 100644 index 000000000..abca3f42f --- /dev/null +++ b/src/queries/chain/useChainTradingFee.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from 'queries/queryKeyFactory'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { queryOptionsStaleTime2m } from 'queries/queryOptions'; +import { + buildMulticallTradingFee, + fetchMulticallHelper, +} from 'services/web3/multicall/multicallFunctions'; + +export const useChainTradingFee = () => { + const { data: poolIds } = useChainPoolIds(); + return useQuery( + QueryKey.chainTradingFee(poolIds?.length), + () => fetchMulticallHelper(poolIds!, buildMulticallTradingFee), + { + ...queryOptionsStaleTime2m(!!poolIds), + useErrorBoundary: true, + } + ); +}; diff --git a/src/queries/index.ts b/src/queries/index.ts new file mode 100644 index 000000000..be4f6b656 --- /dev/null +++ b/src/queries/index.ts @@ -0,0 +1,3 @@ +export { QueryClientProvider } from './queryClient'; +export { usePoolPick } from './usePoolPick'; +export type { PoolV3Chain } from './types'; diff --git a/src/queries/queryClient.tsx b/src/queries/queryClient.tsx new file mode 100644 index 000000000..a054b70c1 --- /dev/null +++ b/src/queries/queryClient.tsx @@ -0,0 +1,25 @@ +import { + QueryClient, + QueryClientProvider as QueryClientP, + QueryClientConfig, +} from '@tanstack/react-query'; +import { ReactNode } from 'react'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { queryOptionsDefaults } from 'queries/queryOptions'; + +const config: QueryClientConfig = { + defaultOptions: { + queries: queryOptionsDefaults(), + }, +}; + +const queryClient = new QueryClient(config); + +export const QueryClientProvider = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + + ); +}; diff --git a/src/queries/queryKeyFactory.ts b/src/queries/queryKeyFactory.ts new file mode 100644 index 000000000..ce70def55 --- /dev/null +++ b/src/queries/queryKeyFactory.ts @@ -0,0 +1,64 @@ +export abstract class QueryKey { + private static _v2 = () => ['v2']; + private static _v3 = () => ['v3']; + private static _chain = () => [...this._v3(), 'chain']; + private static _chainPools = (key?: number | string) => [ + ...this._chain(), + 'pools', + key, + ]; + + static chainPoolIds = () => [...this._chainPools('poolIds')]; + static chainPoolTokenIds = (key?: number) => [ + ...this._chainPools(key), + 'poolTokenIds', + ]; + static chainSymbols = (key?: number) => [...this._chainPools(key), 'symbols']; + static chainDecimals = (key?: number) => [ + ...this._chainPools(key), + 'decimals', + ]; + static chainName = (key?: number) => [...this._chainPools(key), 'name']; + static chainTradingEnabled = (key?: number) => [ + ...this._chainPools(key), + 'tradingEnabled', + ]; + static chainTradingLiquidity = (key?: number) => [ + ...this._chainPools(key), + 'tradingLiquidity', + ]; + static chainDepositingEnabled = (key?: number) => [ + ...this._chainPools(key), + 'depositingEnabled', + ]; + static chainStakedBalance = (key?: number) => [ + ...this._chainPools(key), + 'stakedBalance', + ]; + static chainTradingFee = (key?: number) => [ + ...this._chainPools(key), + 'tradingFee', + ]; + static chainLatestProgram = (key?: number) => [ + ...this._chainPools(key), + 'latestProgramId', + ]; + static chainPrograms = (key?: number) => [ + ...this._chainPools(key), + 'programs', + ]; + static chainBalances = (user?: string | null) => [ + ...this._chain(), + 'balances', + user, + ]; + static chainVoteBalance = (user?: string) => [...this._chain(), 'vote', user]; + + static v2ApiWelcome = () => [...this._v2(), 'api', 'welcome']; + + private static _v3Api = () => [...this._v3(), 'api']; + static apiPools = () => [...this._v3Api(), 'pools']; + static apiTokens = () => [...this._v3Api(), 'tokens']; + static apiBnt = () => [...this._v3Api(), 'bnt']; + static apiStatistics = () => [...this._v3Api(), 'statistics']; +} diff --git a/src/queries/queryOptions.ts b/src/queries/queryOptions.ts new file mode 100644 index 000000000..9447a3c9f --- /dev/null +++ b/src/queries/queryOptions.ts @@ -0,0 +1,24 @@ +export const queryOptionsDefaults = (enabled?: boolean) => ({ + enabled, + refetchInterval: 30 * 1000, + staleTime: 15 * 1000, + useErrorBoundary: false, +}); + +export const queryOptionsNoInterval = (enabled?: boolean) => ({ + enabled, + refetchInterval: 0, + staleTime: 6 * 60 * 60 * 1000, +}); + +export const queryOptionsStaleTime2m = (enabled?: boolean) => ({ + enabled, + refetchInterval: 2 * 60 * 1000, + staleTime: 2 * 60 * 1000, +}); + +export const queryOptionsStaleTime15s = (enabled?: boolean) => ({ + enabled, + refetchInterval: 15 * 1000, + staleTime: 15 * 1000, +}); diff --git a/src/queries/types.ts b/src/queries/types.ts new file mode 100644 index 000000000..13fbd9b95 --- /dev/null +++ b/src/queries/types.ts @@ -0,0 +1,59 @@ +import { RewardsProgramRaw } from 'services/web3/v3/portfolio/standardStaking'; +import { PriceDictionary } from 'services/api/bancorApi/bancorApi.types'; + +export interface PriceDictionaryV3 { + bnt?: string; + usd?: string; + eur?: string; + eth?: string; + tkn: string; +} + +export interface PoolApr { + tradingFees: number; + standardRewards: number; + autoCompounding: number; + total: number; +} + +export interface PoolV3Chain { + poolDltId: string; + poolTokenDltId: string; + name: string; + symbol: string; + decimals: number; + tradingLiquidity?: { + BNT: PriceDictionary; + TKN: PriceDictionary; + }; + stakedBalance: PriceDictionaryV3; + tradingFeePPM: number; + tradingEnabled: boolean; + depositingEnabled: boolean; + programs: RewardsProgramRaw[]; + rate?: PriceDictionary; + rate24hAgo?: PriceDictionary; + latestProgram?: RewardsProgramRaw; + balance?: { + tkn: string; + bnTkn: string; + }; + volume?: { + volume7d: PriceDictionaryV3; + volume24h: PriceDictionaryV3; + }; + fees?: { + fees7d: PriceDictionaryV3; + fees24h: PriceDictionaryV3; + }; + apr?: { + apr24h: PoolApr; + apr7d: PoolApr; + }; + standardRewards?: { + claimed24h: PriceDictionaryV3; + providerJoined: PriceDictionaryV3; + providerLeft: PriceDictionaryV3; + staked: PriceDictionaryV3; + }; +} diff --git a/src/queries/usePoolPick.ts b/src/queries/usePoolPick.ts new file mode 100644 index 000000000..7b20bed23 --- /dev/null +++ b/src/queries/usePoolPick.ts @@ -0,0 +1,162 @@ +import { useChainSymbol } from 'queries/chain/useChainSymbol'; +import { useChainDecimals } from 'queries/chain/useChainDecimals'; +import { useChainPoolTokenIds } from 'queries/chain/useChainPoolTokenIds'; +import { useChainPrograms } from 'queries/chain/useChainPrograms'; +import { useChainTradingEnabled } from 'queries/chain/useChainTradingEnabled'; +import { useApiApr } from 'queries/api/useApiApr'; +import { useApiTradingLiquidity } from 'queries/api/useApiTradingLiquidity'; +import { useApiFees } from 'queries/api/useApiFees'; +import { useApiVolume } from 'queries/api/useApiVolume'; +import { useApiStakedBalance } from 'queries/api/useApiStakedBalance'; +import { useChainLatestProgram } from 'queries/chain/useChainLatestProgram'; +import { PoolV3Chain } from 'queries/types'; +import { useChainBalances } from 'queries/chain/useChainBalances'; +import { useChainPoolIds } from 'queries/chain/useChainPoolIds'; +import { useApiRate } from 'queries/api/useApiRate'; +import { useChainDepositingEnabled } from 'queries/chain/useChainDepositingEnabled'; + +type PoolNew = Omit< + PoolV3Chain, + 'name' | 'standardRewards' | 'tradingFeePPM' | 'rate24hAgo' +>; + +type PoolKey = keyof PoolNew; + +type Fetchers = { + [key in PoolKey]: { + getByID: (id: string) => PoolNew[key] | undefined; + isLoading: boolean; + isFetching: boolean; + isError: boolean; + }; +}; + +type PoolReturn = Pick extends infer R + ? { [key in keyof R]: R[key] } + : never; + +const useFetchers = (select: PoolKey[]) => { + const set = new Set(select.map((key) => key)); + + const symbol = useChainSymbol({ + enabled: set.has('symbol'), + }); + + const decimals = useChainDecimals({ + enabled: set.has('decimals'), + }); + + const poolTokenDltId = useChainPoolTokenIds({ + enabled: set.has('poolTokenDltId'), + }); + + const programs = useChainPrograms({ + enabled: set.has('programs'), + }); + + const tradingEnabled = useChainTradingEnabled({ + enabled: set.has('tradingEnabled'), + }); + + const depositingEnabled = useChainDepositingEnabled({ + enabled: set.has('depositingEnabled'), + }); + + const tradingLiquidity = useApiTradingLiquidity({ + enabled: set.has('tradingLiquidity'), + }); + + const latestProgram = useChainLatestProgram({ + enabled: set.has('latestProgram'), + }); + + const fees = useApiFees({ + enabled: set.has('fees'), + }); + + const apr = useApiApr({ + enabled: set.has('apr'), + }); + + const volume = useApiVolume({ + enabled: set.has('volume'), + }); + + const rate = useApiRate({ + enabled: set.has('rate'), + }); + + const stakedBalance = useApiStakedBalance({ + enabled: set.has('stakedBalance'), + }); + + const balance = useChainBalances({ + enabled: set.has('balance'), + }); + + const fetchers: Fetchers = { + poolDltId: { + getByID: (id: string) => id, + isError: false, + isFetching: false, + isLoading: false, + }, + symbol, + decimals, + poolTokenDltId, + programs, + tradingEnabled, + depositingEnabled, + apr, + tradingLiquidity, + fees, + volume, + stakedBalance, + latestProgram, + balance, + rate, + }; + + const isLoading = select.some((res) => fetchers[res].isLoading); + const isFetching = select.some((res) => fetchers[res].isFetching); + const isError = select.some((res) => fetchers[res].isError); + + return { fetchers, isLoading, isFetching, isError }; +}; + +const selectReduce = ( + id: string, + select: T, + fetchers: Fetchers +): PoolReturn => + select.reduce((res, key) => { + // @ts-ignore + res[key] = fetchers[key].getByID(id); + return res; + }, {} as PoolReturn); + +export const usePoolPick = (select: T) => { + const idsQuery = useChainPoolIds(); + const { fetchers, isLoading, isFetching, isError } = useFetchers(select); + + const isUndefined = isLoading || isError; + + const getOne = (id: string) => ({ + data: !isUndefined ? selectReduce(id, select, fetchers) : undefined, + isLoading, + isFetching, + isError, + }); + + const getMany = (ids = idsQuery.data) => ({ + data: + !isUndefined && ids + ? ids.map((id) => selectReduce(id, select, fetchers)) + : undefined, + isLoading, + isFetching, + isError, + }); + + return { getOne, getMany, isLoading, isFetching, isError }; +}; diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 2771604c9..36da1c035 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -9,5 +9,6 @@ declare namespace NodeJS { REACT_APP_PORTIS_DAPP_ID: string; REACT_APP_APY_VISION_TOKEN: string; REACT_APP_INTOTHEBLOCK_KEY: string; + REACT_APP_SENTRY_DSN: string; } } diff --git a/src/router/BancorRouter.tsx b/src/router/BancorRouter.tsx index 251205a19..3ceba7d67 100644 --- a/src/router/BancorRouter.tsx +++ b/src/router/BancorRouter.tsx @@ -1,9 +1,11 @@ -import { RouteObject, useLocation, useRoutes } from 'react-router-dom'; +import { RouteObject, useRoutes, useLocation } from 'react-router-dom'; import { useRoutesTrade } from 'router/useRoutesTrade'; import { useRoutesPortfolio } from 'router/useRoutesPortfolio'; import { useRoutesMain } from 'router/useRoutesMain'; import { useRoutesEarn } from 'router/useRoutesEarn'; import { useRoutesRedirect } from 'router/useRoutesRedirect'; +import { subscribeToObservables } from 'services/observables/triggers'; +import { useDispatch } from 'react-redux'; import { useEffect, useRef } from 'react'; import { useAppSelector } from 'store'; import { getDarkMode } from 'store/user/user'; @@ -13,12 +15,17 @@ export const BancorRouter = () => { const path = useLocation().pathname; const currentRoute = useRef(); const darkMode = useAppSelector(getDarkMode); + const dispatch = useDispatch(); useEffect(() => { if (currentRoute.current !== path) sendGTMPath(currentRoute.current, path, darkMode); currentRoute.current = path; - }, [path, darkMode]); + + if (path !== '/earn') { + subscribeToObservables(dispatch); + } + }, [path, darkMode, dispatch]); const routes: RouteObject[] = [ ...useRoutesTrade(), diff --git a/src/router/useRoutesMain.tsx b/src/router/useRoutesMain.tsx index 4bc1d7351..ddd70ce06 100644 --- a/src/router/useRoutesMain.tsx +++ b/src/router/useRoutesMain.tsx @@ -1,7 +1,7 @@ import { Navigate, RouteObject } from 'react-router-dom'; import { Tokens } from 'pages/Tokens'; import { Fiat } from 'pages/Fiat'; -import { Vote } from 'pages/Vote'; +import { Vote2 } from 'pages/Vote2'; import { TermsOfUse } from 'pages/TermsOfUse'; import { PrivacyPolicy } from 'pages/PrivacyPolicy'; import { NotFound } from 'pages/NotFound'; @@ -34,7 +34,7 @@ export const useRoutesMain = (): RouteObject[] => { }, { path: BancorURL.vote, - element: , + element: , }, { path: BancorURL.termsOfUse, diff --git a/src/sentry/ErrorBoundary.tsx b/src/sentry/ErrorBoundary.tsx new file mode 100644 index 000000000..bbdfe169e --- /dev/null +++ b/src/sentry/ErrorBoundary.tsx @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/react'; +import { BrowserTracing } from '@sentry/tracing'; +import { ReactNode } from 'react'; +import { ErrorBoundaryFallback } from 'sentry/ErrorBoundaryFallback'; + +Sentry.init({ + dsn: process.env.REACT_APP_SENTRY_DSN, + integrations: [new BrowserTracing()], + + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, +}); + +export const SentryErrorBoundary = ({ children }: { children: ReactNode }) => { + return ( + // @ts-ignore + + {children} + + ); +}; diff --git a/src/sentry/ErrorBoundaryFallback.tsx b/src/sentry/ErrorBoundaryFallback.tsx new file mode 100644 index 000000000..57be9ef98 --- /dev/null +++ b/src/sentry/ErrorBoundaryFallback.tsx @@ -0,0 +1,39 @@ +import { JSXElementConstructor, ReactElement } from 'react'; +import * as Sentry from '@sentry/react'; +import { Page } from 'components/Page'; + +type FallbackComponent = + | ReactElement> + | Sentry.FallbackRender + | undefined; + +export const ErrorBoundaryFallback: FallbackComponent = ({ + error, + componentStack, +}) => { + return ( + +

Description

+

+ {error.name && `${error.name} - `} {error.message && error.message} +

+ +
+ +

Component Stack

+
{componentStack}
+ +
+ +

Error JSON

+
+
{JSON.stringify(error, null, 2)}
+
+ + ); +}; diff --git a/src/services/api/bancorApi/bancorApi.types.ts b/src/services/api/bancorApi/bancorApi.types.ts index b4d3a9559..234833975 100644 --- a/src/services/api/bancorApi/bancorApi.types.ts +++ b/src/services/api/bancorApi/bancorApi.types.ts @@ -79,6 +79,14 @@ export interface PriceDictionary { tkn: string; } +export interface PriceDictionaryV3 { + bnt?: string; + usd?: string; + eur?: string; + eth?: string; + tkn: string; +} + export interface APIPoolV3 { poolDltId: string; poolTokenDltId: string; diff --git a/src/services/api/bancorApi/bancorApiV3.ts b/src/services/api/bancorApi/bancorApiV3.ts index 4d0612699..39ce7d90d 100644 --- a/src/services/api/bancorApi/bancorApiV3.ts +++ b/src/services/api/bancorApi/bancorApiV3.ts @@ -17,33 +17,19 @@ const axiosInstance = axios.create({ export abstract class BancorV3Api { static getPools = async (): Promise => { - try { - const { data } = await axiosInstance.get>( - '/pools' - ); - return data.data; - } catch (error) { - console.error(error); - } - - return []; + const { data } = await axiosInstance.get>('/pools'); + return data.data; }; static getTokens = async (): Promise => { - try { - const { data } = await axiosInstance.get>( - '/tokens' - ); - return data.data.map((token) => ({ - ...token, - // TODO remove after Bancor API v3 is updated - rateHistory7d: [], - })); - } catch (error) { - console.error(error); - } - - return []; + const { data } = await axiosInstance.get>( + '/tokens' + ); + return data.data.map((token) => ({ + ...token, + // TODO remove after Bancor API v3 is updated + rateHistory7d: [], + })); }; static getStatistics = async (): Promise => { @@ -55,4 +41,54 @@ export abstract class BancorV3Api { const { data } = await axiosInstance.get>('/bnt'); return data.data; }; + + static getPoolsWithBNT = async (): Promise => { + const [bnt, pools] = await Promise.all([this.getBnt(), this.getPools()]); + const bntPool: APIPoolV3 = { + poolDltId: bnt.poolDltId, + poolTokenDltId: bnt.poolTokenDltId, + name: bnt.name, + decimals: bnt.decimals, + tradingLiquidityTKN: { + ...bnt.tradingLiquidity, + tkn: bnt.tradingLiquidity.bnt, + }, + tradingLiquidityBNT: { + bnt: '0', + usd: '0', + eur: '0', + eth: '0', + tkn: '0', + }, + volume24h: { ...bnt.volume24h, tkn: bnt.volume24h.bnt }, + fees24h: { ...bnt.fees24h, tkn: bnt.fees24h.bnt }, + stakedBalance: { ...bnt.stakedBalance, tkn: bnt.stakedBalance.bnt }, + standardRewardsClaimed24h: { + ...bnt.standardRewardsClaimed24h, + tkn: bnt.standardRewardsClaimed24h.bnt, + }, + standardRewardsStaked: { + ...bnt.standardRewardsStaked, + tkn: bnt.standardRewardsStaked.bnt, + }, + volume7d: bnt.volume7d, + fees7d: bnt.fees7d, + standardRewardsProviderJoined: { + bnt: '0', + usd: '0', + eur: '0', + eth: '0', + tkn: '0', + }, + standardRewardsProviderLeft: { + bnt: '0', + usd: '0', + eur: '0', + eth: '0', + tkn: '0', + }, + tradingEnabled: true, + }; + return [bntPool, ...pools]; + }; } diff --git a/src/services/notifications/notifications.ts b/src/services/notifications/notifications.ts index 77cbe3c4c..121e87823 100644 --- a/src/services/notifications/notifications.ts +++ b/src/services/notifications/notifications.ts @@ -676,12 +676,13 @@ export const rewardsClaimedNotification = ( export const genericFailedNotification = ( dispatch: any, - msg = 'Unknown error occurred' + msg = 'Unknown error occurred', + title = 'Transaction Failed' ) => showNotification( { type: NotificationType.error, - title: 'Transaction Failed', + title, msg: `${msg} - Please try again or contact support`, }, dispatch diff --git a/src/services/observables/statistics.ts b/src/services/observables/statistics.ts deleted file mode 100644 index 21aa38948..000000000 --- a/src/services/observables/statistics.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { combineLatest } from 'rxjs'; -import { shareReplay } from 'rxjs/operators'; -import BigNumber from 'bignumber.js'; -import numbro from 'numbro'; -import { apiData$ } from 'services/observables/apiData'; -import { bntToken } from 'services/web3/config'; -import { oneMinute$ } from 'services/observables/timers'; -import { BancorApi } from 'services/api/bancorApi/bancorApi'; -import { switchMapIgnoreThrow } from 'services/observables/customOperators'; -import { toBigNumber } from 'utils/helperFunctions'; - -export interface Statistic { - label: string; - value: string; - change24h?: number; -} - -const averageFormat = { - average: true, - mantissa: 2, - optionalMantissa: true, - spaceSeparated: true, - lowPrecision: false, -}; - -export const statisticsV3$ = combineLatest([apiData$, oneMinute$]).pipe( - switchMapIgnoreThrow(async ([apiDataV2]) => { - const stats = await BancorApi.v3.getStatistics(); - - const bnt24hChange = new BigNumber(stats.bntRate) - .div(stats.bntRate24hAgo) - .times(100) - .minus(100) - .toNumber(); - - const bntSupply = apiDataV2.bnt_supply; - - const totalBntStakedV2: number = apiDataV2.pools.reduce((acc, item) => { - const bntReserve = item.reserves.find( - (reserve) => reserve.address === bntToken - ); - if (!bntReserve) return acc; - return Number(bntReserve.balance) + acc; - }, 0); - - const stakedBntPercentV2 = new BigNumber(totalBntStakedV2) - .div(toBigNumber(bntSupply).toExponential(18)) - .times(100); - - const stakedBntPercentV3 = new BigNumber(stats.stakedBalanceBNT.bnt) - .div(toBigNumber(bntSupply).toExponential(18)) - .times(100); - - const totalBNTStaked = new BigNumber(stakedBntPercentV2).plus( - stakedBntPercentV3 - ); - - const totalLiquidity = new BigNumber(stats.tradingLiquidityBNT.usd) - .plus(stats.tradingLiquidityTKN.usd) - .plus(apiDataV2.total_liquidity.usd); - - const totalVolume = new BigNumber(apiDataV2.total_volume_24h.usd).plus( - stats.totalVolume24h.usd - ); - const totalFees = new BigNumber(apiDataV2.total_fees_24h.usd).plus( - stats.totalFees24h.usd - ); - - const statistics: Statistic[] = [ - { - label: 'Total Liquidity', - value: '$' + numbro(totalLiquidity).format(averageFormat), - change24h: 0, - }, - { - label: 'Volume', - value: '$' + numbro(totalVolume).format(averageFormat), - change24h: 0, - }, - { - label: 'Fees (24h)', - value: '$' + numbro(totalFees).format(averageFormat), - change24h: 0, - }, - { - label: 'BNT Price', - value: '$' + numbro(stats.bntRate).format({ mantissa: 2 }), - change24h: bnt24hChange, - }, - { - label: 'BNT Staked', - value: numbro(totalBNTStaked).format({ mantissa: 2 }) + '%', - }, - ]; - return statistics; - }), - shareReplay(1) -); diff --git a/src/services/observables/triggers.ts b/src/services/observables/triggers.ts index 2d368521b..a9235886d 100644 --- a/src/services/observables/triggers.ts +++ b/src/services/observables/triggers.ts @@ -8,14 +8,12 @@ import { setAllTokenListTokens, setAllTokensV2, setKeeperDaoTokens, - setStatisticsV3, setTokenLists, setTokensV2, setTokensV3, } from 'store/bancor/bancor'; import { getTokenListLS, setTokenListLS } from 'utils/localStorage'; import { loadingLockedBnt$, loadingPositions$, loadingRewards$ } from './user'; -import { statisticsV3$ } from 'services/observables/statistics'; import { setv2Pools, setv3Pools } from 'store/bancor/pool'; import { setLoadingLockedBnt, @@ -99,10 +97,6 @@ export const subscribeToObservables = (dispatch: any) => { dispatch(setv2Pools(pools)); }); - statisticsV3$.subscribe((stats) => { - dispatch(setStatisticsV3(stats)); - }); - protectedPositions$.subscribe((protectedPositions) => { dispatch(setProtectedPositions(protectedPositions)); }); diff --git a/src/services/web3/approval/index.ts b/src/services/web3/approval/index.ts index 5a875b1b9..327416711 100644 --- a/src/services/web3/approval/index.ts +++ b/src/services/web3/approval/index.ts @@ -28,7 +28,7 @@ export enum ApprovalContract { Governance, } -const getApproval = async ( +export const getApproval = async ( token: string, user: string, spender: string, @@ -47,7 +47,7 @@ const getApproval = async ( return { allowanceWei, isApprovalRequired }; }; -const setApproval = async ( +export const setApproval = async ( token: string, user: string, spender: string, @@ -130,7 +130,7 @@ export const setNetworkContractApproval = async ( ); }; -const getApprovalAddress = async ( +export const getApprovalAddress = async ( contract: ApprovalContract | string ): Promise => { if (typeof contract === 'string') return contract; diff --git a/src/services/web3/config.ts b/src/services/web3/config.ts index 90c99ae20..35d789ae3 100644 --- a/src/services/web3/config.ts +++ b/src/services/web3/config.ts @@ -14,6 +14,7 @@ export interface EthNetworkVariables { } export const bntToken: string = '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C'; +export const vBntToken: string = '0x48Fb253446873234F2fEBbF9BdeAA72d9d387f94'; export const systemStore: string = '0xc4C5634De585d43DaEC8fA2a6Fb6286cd9B87131'; export const ethToken: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; export const zeroAddress: string = '0x0000000000000000000000000000000000000000'; diff --git a/src/services/web3/multicall/multicall.ts b/src/services/web3/multicall/multicall.ts index b9c3a62c9..c9f6559aa 100644 --- a/src/services/web3/multicall/multicall.ts +++ b/src/services/web3/multicall/multicall.ts @@ -1,7 +1,6 @@ -import { web3 } from 'services/web3'; -import { Multicall__factory } from 'services/web3/abis/types'; -import { multiCallContract } from 'services/web3/config'; import { Interface } from '@ethersproject/abi'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { utils } from 'ethers'; export interface MultiCall { contractAddress: string; @@ -11,11 +10,6 @@ export interface MultiCall { } export const multicall = async (calls: MultiCall[], blockHeight?: number) => { - const multicallContract = Multicall__factory.connect( - multiCallContract, - web3.provider - ); - try { const encoded = calls.map((call) => ({ target: call.contractAddress.toLocaleLowerCase(), @@ -24,9 +18,13 @@ export const multicall = async (calls: MultiCall[], blockHeight?: number) => { call.methodParameters ), })); - const encodedRes = await multicallContract.tryAggregate(false, encoded, { - blockTag: blockHeight, - }); + const encodedRes = await ContractsApi.Multicall.read.tryAggregate( + false, + encoded, + { + blockTag: blockHeight, + } + ); return encodedRes.map((call, i) => { if (!call.success) return []; @@ -40,3 +38,44 @@ export const multicall = async (calls: MultiCall[], blockHeight?: number) => { console.error(error); } }; + +export const fetchMulticall = async ( + calls: MultiCall[], + toUtf8String = false, + blockHeight?: number +): Promise => { + try { + const encoded = calls.map((call) => ({ + target: call.contractAddress, + callData: call.interface.encodeFunctionData( + call.methodName, + call.methodParameters + ), + })); + + const encodedRes = await ContractsApi.Multicall.read.tryAggregate( + false, + encoded, + { + blockTag: blockHeight, + } + ); + + return encodedRes.map((call, i) => { + if (!call.success) { + console.log(calls[i]); + throw new Error('multicall failed'); + } + if (toUtf8String) { + return utils.toUtf8String(call.returnData).replace(/[^a-zA-Z0-9]/g, ''); + } + const res = calls[i].interface.decodeFunctionResult( + calls[i].methodName, + call.returnData + ); + return res[0]; + }); + } catch (error) { + throw error; + } +}; diff --git a/src/services/web3/multicall/multicallFunctions.ts b/src/services/web3/multicall/multicallFunctions.ts new file mode 100644 index 000000000..4acf4597e --- /dev/null +++ b/src/services/web3/multicall/multicallFunctions.ts @@ -0,0 +1,84 @@ +import { fetchMulticall, MultiCall } from 'services/web3/multicall/multicall'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; + +export const fetchMulticallHelper = async ( + poolIds: string[], + buildMulticall: (id: string) => MultiCall, + toUtf8String?: boolean +): Promise> => { + const calls: MultiCall[] = poolIds.map((id) => buildMulticall(id)); + + const data = await fetchMulticall(calls, toUtf8String); + + return new Map(data.map((id, i) => [poolIds[i], id])); +}; + +export const buildMulticallSymbol = (id: string) => ({ + contractAddress: id, + interface: ContractsApi.Token(id).read.interface, + methodName: 'symbol', + methodParameters: [], +}); + +export const buildMulticallName = (id: string) => ({ + contractAddress: id, + interface: ContractsApi.Token(id).read.interface, + methodName: 'name', + methodParameters: [], +}); + +export const buildMulticallDecimal = (id: string) => ({ + contractAddress: id, + interface: ContractsApi.Token(id).read.interface, + methodName: 'decimals', + methodParameters: [], +}); + +export const buildMulticallPoolToken = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'poolToken', + methodParameters: [id], +}); + +export const buildMulticallTradingEnabled = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'tradingEnabled', + methodParameters: [id], +}); + +export const buildMulticallTradingLiquidity = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'tradingLiquidity', + methodParameters: [id], +}); + +export const buildMulticallDepositingEnabled = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'depositingEnabled', + methodParameters: [id], +}); + +export const buildMulticallStakedBalance = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'stakedBalance', + methodParameters: [id], +}); + +export const buildMulticallTradingFee = (id: string) => ({ + contractAddress: ContractsApi.BancorNetworkInfo.contractAddress, + interface: ContractsApi.BancorNetworkInfo.read.interface, + methodName: 'tradingFeePPM', + methodParameters: [id], +}); + +export const buildMulticallLatestProgramId = (id: string) => ({ + contractAddress: ContractsApi.StandardRewards.contractAddress, + interface: ContractsApi.StandardRewards.read.interface, + methodName: 'latestProgramId', + methodParameters: [id], +}); diff --git a/src/services/web3/token/token.ts b/src/services/web3/token/token.ts index c13e0361b..7556d6a77 100644 --- a/src/services/web3/token/token.ts +++ b/src/services/web3/token/token.ts @@ -14,6 +14,9 @@ export const fetchTokenBalanceMulticall = async ( tokenIds: string[], user: string ): Promise> => { + if (!user || tokenIds.length === 0) { + throw new Error('Multicall Error no user provided'); + } const calls = tokenIds.map((tokenId) => buildTokenBalanceCall(tokenId, user)); const res = await multicall(calls); if (!res || !res.length) { diff --git a/src/services/web3/v3/contractsApi.ts b/src/services/web3/v3/contractsApi.ts index be481a5b9..592ac30d2 100644 --- a/src/services/web3/v3/contractsApi.ts +++ b/src/services/web3/v3/contractsApi.ts @@ -17,6 +17,10 @@ import { PendingWithdrawals__factory, BancorPortal, BancorPortal__factory, + Multicall, + Multicall__factory, + Governance, + Governance__factory, } from 'services/web3/abis/types'; import { web3, writeWeb3 } from 'services/web3/index'; import { providers } from 'ethers'; @@ -28,6 +32,7 @@ import poolCollectionType1Address from 'services/web3/abis/v3/PoolCollectionType import stakingRewardsClaimAddress from 'services/web3/abis/StakingRewardsClaim.json'; import standardRewardsAddress from 'services/web3/abis/v3/StandardRewards_Proxy.json'; import bancorPortalAddress from 'services/web3/abis/v3/BancorPortal_Proxy.json'; +import { multiCallContract } from 'services/web3/config'; export class BancorContract { constructor(contractAddress: string, contractFactory: any) { @@ -109,6 +114,16 @@ export abstract class ContractsApi { BancorPortal__factory ); + static Multicall = new BancorContract( + multiCallContract, + Multicall__factory + ); + + static Governance = new BancorContract( + '0x892f481bd6e9d7d26ae365211d9b45175d5d00e4', + Governance__factory + ); + static Token = (tokenAddress: string) => { return new BancorContract(tokenAddress, Token__factory); }; diff --git a/src/store/bancor/bancor.ts b/src/store/bancor/bancor.ts index 828367deb..0bc9b3bde 100644 --- a/src/store/bancor/bancor.ts +++ b/src/store/bancor/bancor.ts @@ -1,4 +1,4 @@ -import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import { KeeprDaoToken } from 'services/api/keeperDao'; import { Token, TokenList, TokenMinimal } from 'services/observables/tokens'; import { RootState } from 'store'; @@ -6,7 +6,6 @@ import { orderBy, uniqBy } from 'lodash'; import { getAllTokensMap } from 'store/bancor/token'; import { utils } from 'ethers'; -import { Statistic } from 'services/observables/statistics'; import { NotificationType } from 'store/notification/notification'; interface BancorState { @@ -17,7 +16,6 @@ interface BancorState { allTokenListTokens: TokenMinimal[]; allTokens: Token[]; isLoadingTokens: boolean; - statistics: Statistic[]; } export const initialState: BancorState = { @@ -28,7 +26,6 @@ export const initialState: BancorState = { keeperDaoTokens: [], allTokenListTokens: [], isLoadingTokens: true, - statistics: [], }; const bancorSlice = createSlice({ @@ -54,9 +51,6 @@ const bancorSlice = createSlice({ setKeeperDaoTokens: (state, action) => { state.keeperDaoTokens = action.payload; }, - setStatisticsV3: (state, action: PayloadAction) => { - state.statistics = action.payload; - }, }, }); @@ -65,7 +59,6 @@ export const { setTokensV3, setTokenLists, setAllTokensV2, - setStatisticsV3, setAllTokenListTokens, setKeeperDaoTokens, } = bancorSlice.actions; diff --git a/src/store/bancor/pool.ts b/src/store/bancor/pool.ts index 2238fbdd6..4fa975445 100644 --- a/src/store/bancor/pool.ts +++ b/src/store/bancor/pool.ts @@ -1,24 +1,20 @@ import { createSelector, createSlice } from '@reduxjs/toolkit'; import { Token } from 'services/observables/tokens'; -import { Statistic } from 'services/observables/statistics'; import { RootState } from 'store'; import { isEqual, orderBy } from 'lodash'; import { createSelectorCreator, defaultMemoize } from 'reselect'; import { Pool, PoolV3 } from 'services/observables/pools'; -import { bntToken } from 'services/web3/config'; interface PoolState { v2Pools: Pool[]; v3Pools: PoolV3[]; isLoadingV3Pools: boolean; - statistics: Statistic[]; } const initialState: PoolState = { v2Pools: [], v3Pools: [], isLoadingV3Pools: true, - statistics: [], }; const poolSlice = createSlice({ @@ -32,19 +28,9 @@ const poolSlice = createSlice({ state.v3Pools = action.payload; state.isLoadingV3Pools = false; }, - setStats: (state, action) => { - state.statistics = action.payload; - }, }, }); -export interface TopPool { - tknSymbol: string; - tknLogoURI: string; - apr: number; - poolName: string; -} - export const getPools = createSelector( (state: RootState) => state.pool.v2Pools, (state: RootState) => state.bancor.tokensV2, @@ -84,51 +70,6 @@ export const getProtectedPools = createSelector(getPools, (pools: Pool[]) => pools.filter((p) => p.isProtected) ); -export const getTopPools = createSelector(getPools, (pools: Pool[]) => { - const filteredPools = pools - .filter((p) => p.isProtected && p.liquidity > 100000) - .map((p) => { - return { - tknSymbol: p.reserves[0].symbol, - tknLogoURI: p.reserves[0].logoURI, - tknApr: p.apr_24h + (p.reserves[0].rewardApr || 0), - bntSymbol: p.reserves[1].symbol, - bntLogoURI: p.reserves[1].logoURI, - bntApr: p.apr_24h + (p.reserves[1].rewardApr || 0), - poolName: p.name, - }; - }); - const winningBntPool = orderBy(filteredPools, 'bntApr', 'desc').slice(0, 1); - const topPools: TopPool[] = filteredPools.map((p) => { - return { - tknSymbol: p.tknSymbol, - tknLogoURI: p.tknLogoURI, - poolName: p.poolName, - apr: p.tknApr, - }; - }); - if (winningBntPool.length === 1) { - topPools.push({ - tknSymbol: winningBntPool[0].bntSymbol, - tknLogoURI: winningBntPool[0].bntLogoURI, - apr: winningBntPool[0].bntApr, - poolName: winningBntPool[0].poolName, - }); - } - return orderBy(topPools, 'apr', 'desc').slice(0, 20); -}); - -export const getTopPoolsV3 = createSelector( - (state: RootState) => state.pool.v3Pools, - (pools: PoolV3[]) => { - return orderBy( - pools.filter((p) => p.apr7d.total > 0), - 'apr7d.total', - 'desc' - ).slice(0, 20); - } -); - export const getIsV3Exist = createSelector( [(state: RootState) => getPoolsV3Map(state), (_: any, id: string) => id], (pools: Map, id): boolean => { @@ -136,11 +77,6 @@ export const getIsV3Exist = createSelector( } ); -export const getBNTPoolV3 = createSelector( - (state: RootState) => getPoolsV3Map(state), - (pools: Map): PoolV3 | undefined => pools.get(bntToken) -); - export interface SelectedPool { status: 'loading' | 'ready'; pool?: Pool; @@ -171,6 +107,6 @@ export const getPoolByIdWithoutV3 = (id: string) => return { status: 'ready', pool } as SelectedPool; }); -export const { setv2Pools, setv3Pools, setStats } = poolSlice.actions; +export const { setv2Pools, setv3Pools } = poolSlice.actions; export const pool = poolSlice.reducer; diff --git a/src/utils/pureFunctions.ts b/src/utils/pureFunctions.ts index 1a0bdb753..d72fffb0f 100644 --- a/src/utils/pureFunctions.ts +++ b/src/utils/pureFunctions.ts @@ -77,3 +77,6 @@ export const sorAlphaByKey = (a: T, b: T, key: string[]) => { export const openNewTab = (url: string) => window.open(url, '_blank', 'noopener'); + +export const getBancorLogoUrl = (id: string) => + `https://d1wmp5nysbq9xl.cloudfront.net/ethereum/tokens/${id.toLowerCase()}.svg`; diff --git a/yarn.lock b/yarn.lock index c0688f49c..f6e3d5579 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2349,6 +2349,69 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== +"@sentry/browser@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.7.0.tgz#7810ee98d4969bd0367e29ac0af6c5779db7e6c4" + integrity sha512-oyzpWcsjVZTaf14zAL89Ng1DUHlbjN+V8pl8dR9Y9anphbzL5BK9p0fSK4kPIrO4GukK+XrKnLJDPuE/o7WR3g== + dependencies: + "@sentry/core" "7.7.0" + "@sentry/types" "7.7.0" + "@sentry/utils" "7.7.0" + tslib "^1.9.3" + +"@sentry/core@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.7.0.tgz#1a2d477897552d179380f7c54c7d81a4e98ea29a" + integrity sha512-Z15ACiuiFINFcK4gbMrnejLn4AVjKBPJOWKrrmpIe8mh+Y9diOuswt5mMUABs+jhpZvqht3PBLLGBL0WMsYMYA== + dependencies: + "@sentry/hub" "7.7.0" + "@sentry/types" "7.7.0" + "@sentry/utils" "7.7.0" + tslib "^1.9.3" + +"@sentry/hub@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.7.0.tgz#9ad3471cf5ecaf1a9d3a3a04ca2515ffec9e2c09" + integrity sha512-6gydK234+a0nKhBRDdIJ7Dp42CaiW2juTiHegUVDq+482balVzbZyEAmESCmuzKJhx5BhlCElVxs/cci1NjMpg== + dependencies: + "@sentry/types" "7.7.0" + "@sentry/utils" "7.7.0" + tslib "^1.9.3" + +"@sentry/react@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.7.0.tgz#a519dd727f863c1abf1a77beac01a3336c856828" + integrity sha512-93Khad3YAln6mKU9E15QH09XC1ARIOpNTRpnBl6AGl3bVhSdLExsbKDLa7Rx0mW2j4z/prOC6VEHf5mBvg4mPg== + dependencies: + "@sentry/browser" "7.7.0" + "@sentry/types" "7.7.0" + "@sentry/utils" "7.7.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/tracing@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.7.0.tgz#67324b755a28e115289755e44a0b8b467a63d0b2" + integrity sha512-HNmvTwemuc21q/K6HXsSp9njkne6N1JQ71TB+QGqYU5VtxsVgYSUhhYqV6WcHz7LK4Hj6TvNFoeu69/rO0ysgw== + dependencies: + "@sentry/hub" "7.7.0" + "@sentry/types" "7.7.0" + "@sentry/utils" "7.7.0" + tslib "^1.9.3" + +"@sentry/types@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.7.0.tgz#dd6bd3d119d7efea0e85dbaa4b17de1c22b63c7a" + integrity sha512-4x8O7uerSGLnYC10krHl9t8h7xXHn5FextqKYbTCXCnx2hC8D+9lz8wcbQAFo0d97wiUYqI8opmEgFVGx7c5hQ== + +"@sentry/utils@7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.7.0.tgz#013e3097c4268a76de578494c7df999635fb0ad4" + integrity sha512-fD+ROSFpeJlK7bEvUT2LOW7QqgjBpXJwVISKZ0P2fuzclRC3KoB2pbZgBM4PXMMTiSzRGWhvfRRjBiBvQJBBJQ== + dependencies: + "@sentry/types" "7.7.0" + tslib "^1.9.3" + "@sinclair/typebox@^0.24.1": version "0.24.21" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.21.tgz#f2e435ac4c1919ae89c2b693a0d4213d09899290" @@ -3390,6 +3453,36 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tanstack/match-sorter-utils@^8.0.0-alpha.82": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834" + integrity sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A== + dependencies: + remove-accents "0.4.2" + +"@tanstack/query-core@^4.0.0-beta.1": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.0.5.tgz#70c7275a9f1fbc8e6520d895991f9bace0fd5bb8" + integrity sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA== + +"@tanstack/react-query-devtools@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.0.5.tgz#6ee95d7e4367fde0e3a7bd1b778e3b393ff1b757" + integrity sha512-OTRDYDBGFuW1S+oFDvPTso0mdo13/TWq79S42bsGbZ+leSHr9l3yB4UXzZsJvwLG8pEQpN/RigZOmXCOuMeSlw== + dependencies: + "@tanstack/match-sorter-utils" "^8.0.0-alpha.82" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + +"@tanstack/react-query@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.0.5.tgz#4597ac03394ddfa6ad8b5e1beb6282468623d398" + integrity sha512-tIggVlhoFevVpY/LkZroPmrERFHN8tw4aZLtgwSArzHmMJ03WQcaNvbbHy6GERidXtaMdUz+IeQryrE7cO7WPQ== + dependencies: + "@tanstack/query-core" "^4.0.0-beta.1" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -3956,6 +4049,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/webpack-env@^1.16.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" @@ -14503,6 +14601,11 @@ remark-squeeze-paragraphs@4.0.0: dependencies: mdast-squeeze-paragraphs "^4.0.0" +remove-accents@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" + integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -16259,7 +16362,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -16637,6 +16740,11 @@ use-async-effect@^2.2.3: resolved "https://registry.yarnpkg.com/use-async-effect/-/use-async-effect-2.2.6.tgz#00f2dee71e2f6188c5daf6a067e2bc151f7566b7" integrity sha512-wKUpaHkuF4rzBHawP87o1KzoK2IxJ6De8fUyQ3GN2114zb4zqT87+SEbCHS+F+0inZ2Y+k6Tm1LOCQgYTSD9ww== +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"