diff --git a/package.json b/package.json index 9d5b82ef..7e5876a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yearn-finance/web-lib", - "version": "3.0.180", + "version": "3.0.181", "main": "./dist/index.js", "types": "./dist/index.d.js", "files": [ diff --git a/src/contexts/useYearn.helper.tsx b/src/contexts/useYearn.helper.tsx new file mode 100644 index 00000000..882d91fc --- /dev/null +++ b/src/contexts/useYearn.helper.tsx @@ -0,0 +1,156 @@ +import {useCallback, useMemo} from 'react'; +import {useBalances} from '@builtbymom/web3/hooks/useBalances.multichains'; +import {toAddress} from '@builtbymom/web3/utils'; +import {useDeepCompareMemo} from '@react-hookz/web'; + +import { + CRV_TOKEN_ADDRESS, + CVXCRV_TOKEN_ADDRESS, + ETH_TOKEN_ADDRESS, + LPYCRV_TOKEN_ADDRESS, + LPYCRV_V2_TOKEN_ADDRESS, + YCRV_CURVE_POOL_V2_ADDRESS, + YCRV_TOKEN_ADDRESS, + YVBOOST_TOKEN_ADDRESS, + YVECRV_TOKEN_ADDRESS +} from '../utils/constants'; +import {useYearn} from './useYearn'; + +import type {TUseBalancesTokens} from '@builtbymom/web3/hooks/useBalances.multichains'; +import type {TDict} from '@builtbymom/web3/types'; +import type {TYChainTokens} from '../types'; +import type {TYDaemonVault} from '../utils/schemas/yDaemonVaultsSchemas'; + +export function useYearnTokens({shouldUseForknetBalances}: {shouldUseForknetBalances: boolean}): TUseBalancesTokens[] { + const {vaults, vaultsMigrations, vaultsRetired, isLoadingVaultList} = useYearn(); + + const availableTokens = useMemo((): TUseBalancesTokens[] => { + if (isLoadingVaultList) { + return []; + } + const tokens: TUseBalancesTokens[] = []; + const tokensExists: TDict = {}; + const extraTokens: TUseBalancesTokens[] = []; + extraTokens.push( + ...[ + {chainID: 1, address: ETH_TOKEN_ADDRESS}, + {chainID: 10, address: ETH_TOKEN_ADDRESS}, + {chainID: 137, address: ETH_TOKEN_ADDRESS}, + {chainID: 250, address: ETH_TOKEN_ADDRESS}, + {chainID: 8453, address: ETH_TOKEN_ADDRESS}, + {chainID: 42161, address: ETH_TOKEN_ADDRESS}, + {chainID: 1, address: YCRV_TOKEN_ADDRESS}, + {chainID: 1, address: LPYCRV_TOKEN_ADDRESS}, + {chainID: 1, address: CRV_TOKEN_ADDRESS}, + {chainID: 1, address: YVBOOST_TOKEN_ADDRESS}, + {chainID: 1, address: YVECRV_TOKEN_ADDRESS}, + {chainID: 1, address: CVXCRV_TOKEN_ADDRESS}, + {chainID: 1, address: YCRV_CURVE_POOL_V2_ADDRESS}, + {chainID: 1, address: LPYCRV_V2_TOKEN_ADDRESS} + ] + ); + + for (const token of extraTokens) { + tokensExists[token.address] = true; + tokens.push(token); + } + + Object.values(vaults || {}).forEach((vault?: TYDaemonVault): void => { + if (!vault) { + return; + } + if (vault?.address && !tokensExists[toAddress(vault?.address)]) { + tokens.push({address: vault.address, chainID: vault.chainID}); + tokensExists[vault.address] = true; + } + if (vault?.token?.address && !tokensExists[toAddress(vault?.token?.address)]) { + tokens.push({address: vault.token.address, chainID: vault.chainID}); + tokensExists[vault.token.address] = true; + } + if (vault?.staking?.available && !tokensExists[toAddress(vault?.staking?.address)]) { + tokens.push({ + address: vault?.staking?.address, + chainID: vault.chainID, + symbol: vault.symbol, + decimals: vault.decimals, + name: vault.name + }); + tokensExists[vault?.staking?.address] = true; + } + }); + + return tokens; + }, [isLoadingVaultList, vaults]); + + //List all vaults with a possible migration + const migratableTokens = useMemo((): TUseBalancesTokens[] => { + const tokens: TUseBalancesTokens[] = []; + Object.values(vaultsMigrations || {}).forEach((vault?: TYDaemonVault): void => { + if (!vault) { + return; + } + tokens.push({address: vault.address, chainID: vault.chainID}); + }); + return tokens; + }, [vaultsMigrations]); + + const retiredTokens = useMemo((): TUseBalancesTokens[] => { + const tokens: TUseBalancesTokens[] = []; + Object.values(vaultsRetired || {}).forEach((vault?: TYDaemonVault): void => { + if (!vault) { + return; + } + tokens.push({address: vault.address, chainID: vault.chainID}); + }); + return tokens; + }, [vaultsRetired]); + + const allTokens = useMemo((): TUseBalancesTokens[] => { + const tokens = [...availableTokens, ...migratableTokens, ...retiredTokens]; + if (!shouldUseForknetBalances) { + return tokens; + } + for (const token of tokens) { + if (token.chainID === 1) { + //remove it + tokens.push({...token, chainID: 1337}); + } + } + return tokens; + }, [availableTokens, migratableTokens, retiredTokens, shouldUseForknetBalances]); + + return allTokens; +} + +export function useYearnBalances({shouldUseForknetBalances}: {shouldUseForknetBalances: boolean}): { + tokens: TYChainTokens; + isLoading: boolean; + onRefresh: (tokenToUpdate?: TUseBalancesTokens[]) => Promise; +} { + const {prices} = useYearn(); + const allTokens = useYearnTokens({shouldUseForknetBalances}); + const {data: tokensRaw, onUpdate, onUpdateSome, isLoading} = useBalances({tokens: allTokens, prices}); + + const tokens = useDeepCompareMemo((): TYChainTokens => { + const _tokens = {...tokensRaw}; + if (shouldUseForknetBalances) { + _tokens[1] = _tokens[1337]; // eslint-disable-line prefer-destructuring + } + + return _tokens as TYChainTokens; + }, [tokensRaw, shouldUseForknetBalances]); + + const onRefresh = useCallback( + async (tokenToUpdate?: TUseBalancesTokens[]): Promise => { + if (tokenToUpdate) { + const updatedBalances = await onUpdateSome(tokenToUpdate); + return updatedBalances as TYChainTokens; + } + const updatedBalances = await onUpdate(); + return updatedBalances as TYChainTokens; + }, + [onUpdate, onUpdateSome] + ); + + return {tokens, isLoading, onRefresh}; +} diff --git a/src/contexts/useYearn.tsx b/src/contexts/useYearn.tsx index d9bf0432..ddcd5bda 100755 --- a/src/contexts/useYearn.tsx +++ b/src/contexts/useYearn.tsx @@ -1,6 +1,7 @@ -import {createContext, memo, useContext, useEffect} from 'react'; +import {createContext, memo, useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {deserialize, serialize} from 'wagmi'; -import {toAddress} from '@builtbymom/web3/utils'; +import {useWeb3} from '@builtbymom/web3/contexts/useWeb3'; +import {isZeroAddress, toAddress, toNormalizedBN, zeroNormalizedBN} from '@builtbymom/web3/utils'; import {useLocalStorageValue} from '@react-hookz/web'; import {useFetchYearnEarnedForUser} from '../hooks/useFetchYearnEarnedForUser'; @@ -8,12 +9,13 @@ import {useFetchYearnPrices} from '../hooks/useFetchYearnPrices'; import {useFetchYearnTokens} from '../hooks/useFetchYearnTokens'; import {useFetchYearnVaults} from '../hooks/useFetchYearnVaults'; import {Solver} from '../utils/schemas/yDaemonTokenListBalances'; -import {useYearnWallet} from './useYearnWallet'; +import {useYearnBalances} from './useYearn.helper'; import type {ReactElement} from 'react'; import type {KeyedMutator} from 'swr'; import type {TUseBalancesTokens} from '@builtbymom/web3/hooks/useBalances.multichains'; -import type {TAddress, TDict} from '@builtbymom/web3/types'; +import type {TAddress, TDict, TNormalizedBN} from '@builtbymom/web3/types'; +import type {TYChainTokens, TYToken} from '../types'; import type {TYDaemonEarned} from '../utils/schemas/yDaemonEarnedSchema'; import type {TYDaemonPricesChain} from '../utils/schemas/yDaemonPricesSchema'; import type {TSolver} from '../utils/schemas/yDaemonTokenListBalances'; @@ -23,6 +25,7 @@ import type {TYDaemonVault, TYDaemonVaults} from '../utils/schemas/yDaemonVaults export const DEFAULT_SLIPPAGE = 0.5; export const DEFAULT_MAX_LOSS = 1n; +type TTokenAndChain = {address: TAddress; chainID: number}; export type TYearnContext = { currentPartner: TAddress; earned?: TYDaemonEarned; @@ -41,6 +44,31 @@ export type TYearnContext = { set_zapSlippage: (value: number) => void; set_zapProvider: (value: TSolver) => void; set_isStakingOpBoostedVaults: (value: boolean) => void; + // + //Yearn wallet context + getToken: ({address, chainID}: TTokenAndChain) => TYToken; + getBalance: ({address, chainID}: TTokenAndChain) => TNormalizedBN; + getPrice: ({address, chainID}: TTokenAndChain) => TNormalizedBN; + balances: TYChainTokens; + cumulatedValueInV2Vaults: number; + cumulatedValueInV3Vaults: number; + isLoading: boolean; + shouldUseForknetBalances: boolean; + onRefresh: (tokenList?: TUseBalancesTokens[]) => Promise; + triggerForknetBalances: () => void; +}; + +const defaultToken: TYToken = { + address: toAddress(''), + name: '', + symbol: '', + decimals: 18, + chainID: 1, + value: 0, + stakingValue: 0, + price: zeroNormalizedBN, + balance: zeroNormalizedBN, + supportedZaps: [] }; const YearnContext = createContext({ @@ -65,11 +93,24 @@ const YearnContext = createContext({ set_maxLoss: (): void => undefined, set_zapSlippage: (): void => undefined, set_zapProvider: (): void => undefined, - set_isStakingOpBoostedVaults: (): void => undefined + set_isStakingOpBoostedVaults: (): void => undefined, + // + //Yearn wallet context + getToken: (): TYToken => defaultToken, + getBalance: (): TNormalizedBN => zeroNormalizedBN, + getPrice: (): TNormalizedBN => zeroNormalizedBN, + balances: {}, + cumulatedValueInV2Vaults: 0, + cumulatedValueInV3Vaults: 0, + isLoading: true, + shouldUseForknetBalances: false, + onRefresh: async (): Promise => ({}), + triggerForknetBalances: (): void => {} }); export const YearnContextApp = memo(function YearnContextApp({children}: {children: ReactElement}): ReactElement { - const {onRefresh} = useYearnWallet(); + const {address: userAddress} = useWeb3(); + const [shouldUseForknetBalances, set_shouldUseForknetBalances] = useState(false); const {value: maxLoss, set: set_maxLoss} = useLocalStorageValue('yearn.fi/max-loss', { defaultValue: DEFAULT_MAX_LOSS, parse: (str, fallback): bigint => (str ? deserialize(str) : fallback ?? DEFAULT_MAX_LOSS), @@ -92,6 +133,7 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr const tokens = useFetchYearnTokens(); const earned = useFetchYearnEarnedForUser(); const {vaults, vaultsMigrations, vaultsRetired, isLoading, mutate} = useFetchYearnVaults(); + const {tokens: balances, isLoading: isLoadingBalances, onRefresh} = useYearnBalances({shouldUseForknetBalances}); useEffect(() => { const tokensToRefresh: TUseBalancesTokens[] = []; @@ -108,6 +150,57 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr onRefresh(tokensToRefresh); }, [tokens, onRefresh]); + const [cumulatedValueInV2Vaults, cumulatedValueInV3Vaults] = useMemo((): [number, number] => { + let cumulatedValueInV2Vaults = 0; + let cumulatedValueInV3Vaults = 0; + for (const [, perChain] of Object.entries(balances)) { + for (const [tokenAddress, tokenData] of Object.entries(perChain)) { + if (tokenData.value + tokenData.stakingValue === 0) { + continue; + } + if (vaults?.[toAddress(tokenAddress)]) { + if (vaults[toAddress(tokenAddress)].version.split('.')?.[0] === '3') { + cumulatedValueInV3Vaults += tokenData.value + tokenData.stakingValue; + } else { + cumulatedValueInV2Vaults += tokenData.value + tokenData.stakingValue; + } + } else if (vaultsMigrations?.[toAddress(tokenAddress)]) { + if (vaultsMigrations[toAddress(tokenAddress)].version.split('.')?.[0] === '3') { + cumulatedValueInV3Vaults += tokenData.value + tokenData.stakingValue; + } else { + cumulatedValueInV2Vaults += tokenData.value + tokenData.stakingValue; + } + } + } + } + return [cumulatedValueInV2Vaults, cumulatedValueInV3Vaults]; + }, [vaults, vaultsMigrations, balances]); + + const getToken = useCallback( + ({address, chainID}: TTokenAndChain): TYToken => balances?.[chainID || 1]?.[address] || defaultToken, + [balances] + ); + const getBalance = useCallback( + ({address, chainID}: TTokenAndChain): TNormalizedBN => { + if (isZeroAddress(userAddress)) { + return zeroNormalizedBN; + } + return balances?.[chainID || 1]?.[address]?.balance || zeroNormalizedBN; + }, + [balances, userAddress] + ); + + const getPrice = useCallback( + ({address, chainID}: TTokenAndChain): TNormalizedBN => { + const price = balances?.[chainID || 1]?.[address]?.price; + if (!price) { + return toNormalizedBN(prices?.[chainID]?.[address] || 0, 6) || zeroNormalizedBN; + } + return price; + }, + [prices, balances] + ); + return ( + set_shouldUseForknetBalances((s): boolean => { + const isEnabled = !s; + if (!(window as any).ethereum) { + (window as any).ethereum = {}; + } + (window as any).ethereum.useForknetForMainnet = isEnabled; + return isEnabled; + }) }}> {children} diff --git a/src/contexts/useYearnWallet.tsx b/src/contexts/useYearnWallet.tsx index 83936349..8d5335c4 100755 --- a/src/contexts/useYearnWallet.tsx +++ b/src/contexts/useYearnWallet.tsx @@ -202,20 +202,18 @@ function useYearnBalances({shouldUseForknetBalances}: {shouldUseForknetBalances: ** interact with our app, aka mostly the balances and the token prices. ******************************************************************************/ const YearnWalletContext = createContext(defaultProps); -export const YearnWalletContextApp = memo(function YearnWalletContextApp({ - children -}: { +export const YearnWalletContextApp = memo(function YearnWalletContextApp(props: { children: ReactElement; }): ReactElement { const {address: userAddress} = useWeb3(); const {vaults, prices, vaultsMigrations} = useYearn(); const [shouldUseForknetBalances, set_shouldUseForknetBalances] = useState(false); - const {tokens, isLoading, onRefresh} = useYearnBalances({shouldUseForknetBalances}); + const {tokens: balances, isLoading: isLoadingBalances, onRefresh} = useYearnBalances({shouldUseForknetBalances}); const [cumulatedValueInV2Vaults, cumulatedValueInV3Vaults] = useMemo((): [number, number] => { let cumulatedValueInV2Vaults = 0; let cumulatedValueInV3Vaults = 0; - for (const [, perChain] of Object.entries(tokens)) { + for (const [, perChain] of Object.entries(balances)) { for (const [tokenAddress, tokenData] of Object.entries(perChain)) { if (tokenData.value + tokenData.stakingValue === 0) { continue; @@ -236,31 +234,31 @@ export const YearnWalletContextApp = memo(function YearnWalletContextApp({ } } return [cumulatedValueInV2Vaults, cumulatedValueInV3Vaults]; - }, [vaults, vaultsMigrations, tokens]); + }, [vaults, vaultsMigrations, balances]); const getToken = useCallback( - ({address, chainID}: TTokenAndChain): TYToken => tokens?.[chainID || 1]?.[address] || defaultToken, - [tokens] + ({address, chainID}: TTokenAndChain): TYToken => balances?.[chainID || 1]?.[address] || defaultToken, + [balances] ); const getBalance = useCallback( ({address, chainID}: TTokenAndChain): TNormalizedBN => { if (isZeroAddress(userAddress)) { return zeroNormalizedBN; } - return tokens?.[chainID || 1]?.[address]?.balance || zeroNormalizedBN; + return balances?.[chainID || 1]?.[address]?.balance || zeroNormalizedBN; }, - [tokens, userAddress] + [balances, userAddress] ); const getPrice = useCallback( ({address, chainID}: TTokenAndChain): TNormalizedBN => { - const price = tokens?.[chainID || 1]?.[address]?.price; + const price = balances?.[chainID || 1]?.[address]?.price; if (!price) { return toNormalizedBN(prices?.[chainID]?.[address] || 0, 6) || zeroNormalizedBN; } return price; }, - [prices, tokens] + [prices, balances] ); /* 🔵 - Yearn Finance ****************************************************** @@ -271,10 +269,10 @@ export const YearnWalletContextApp = memo(function YearnWalletContextApp({ getToken, getBalance, getPrice, - balances: tokens, + balances: balances, cumulatedValueInV2Vaults, cumulatedValueInV3Vaults, - isLoading: isLoading || false, + isLoading: isLoadingBalances || false, shouldUseForknetBalances, onRefresh, triggerForknetBalances: (): void => @@ -291,16 +289,16 @@ export const YearnWalletContextApp = memo(function YearnWalletContextApp({ getToken, getBalance, getPrice, - tokens, + balances, cumulatedValueInV2Vaults, cumulatedValueInV3Vaults, - isLoading, + isLoadingBalances, shouldUseForknetBalances, onRefresh ] ); - return {children}; + return {props.children}; }); export const useYearnWallet = (): TWalletContext => useContext(YearnWalletContext); diff --git a/src/hooks/useYearnBalance.ts b/src/hooks/useYearnBalance.ts index fde30410..d036bc01 100644 --- a/src/hooks/useYearnBalance.ts +++ b/src/hooks/useYearnBalance.ts @@ -1,12 +1,12 @@ import {toAddress} from '@builtbymom/web3/utils'; -import {useYearnWallet} from '../contexts/useYearnWallet'; +import {useYearn} from '../contexts/useYearn'; import type {TAddress, TDict, TNormalizedBN} from '@builtbymom/web3/types'; /****************************************************************************** ** The useYearnBalance hook is used to retrieve the balance of a token from - ** the useYearnWallet context. + ** the useYearn context. *****************************************************************************/ export function useYearnBalance({ address, @@ -16,7 +16,7 @@ export function useYearnBalance({ chainID: number; source?: TDict; }): TNormalizedBN { - const {getBalance} = useYearnWallet(); + const {getBalance} = useYearn(); return getBalance({address: toAddress(address), chainID: chainID}); } diff --git a/src/hooks/useYearnToken.ts b/src/hooks/useYearnToken.ts index bdcdb969..18c7affe 100644 --- a/src/hooks/useYearnToken.ts +++ b/src/hooks/useYearnToken.ts @@ -1,7 +1,7 @@ import {useMemo} from 'react'; import {toAddress} from '@builtbymom/web3/utils'; -import {useYearnWallet} from '../contexts/useYearnWallet'; +import {useYearn} from '../contexts/useYearn'; import type {TAddress} from '@builtbymom/web3/types'; import type {TYToken} from '../types'; @@ -11,7 +11,7 @@ import type {TYToken} from '../types'; ** context. The token is returned as a TYToken. *****************************************************************************/ export function useYearnToken({address, chainID}: {address: string | TAddress; chainID: number}): TYToken { - const {getToken} = useYearnWallet(); + const {getToken} = useYearn(); const balance = useMemo((): TYToken => { return getToken({address: toAddress(address), chainID: chainID});