From bb414603c0fe98ec12ede59d3528a24b1fd98d81 Mon Sep 17 00:00:00 2001 From: Majorfi Date: Fri, 12 Jan 2024 18:39:51 +0100 Subject: [PATCH] build: bump --- package.json | 4 +- src/components/Header.tsx | 2 +- src/components/ModalMobileMenu.tsx | 2 +- src/contexts/useWallet.tsx | 311 +++++++++++++++++++++++++++++ src/contexts/useWeb3.tsx | 2 +- src/contexts/useYearn.tsx | 275 +++++++++++++++++++++++++ src/hooks/useBalances.ts | 8 +- src/hooks/useYDaemonBaseURI.ts | 22 ++ src/types/curves.ts | 3 + src/utils/assert.ts | 14 ++ src/utils/fetch.ts | 42 ++++ src/utils/helpers.ts | 25 +++ src/utils/wagmi/actions.ts | 112 +++++++++++ src/utils/wagmi/provider.ts | 2 +- src/utils/wagmi/utils.ts | 14 +- yarn.lock | 10 +- 16 files changed, 826 insertions(+), 22 deletions(-) create mode 100755 src/contexts/useWallet.tsx create mode 100755 src/contexts/useYearn.tsx create mode 100644 src/hooks/useYDaemonBaseURI.ts create mode 100755 src/types/curves.ts create mode 100644 src/utils/assert.ts create mode 100644 src/utils/fetch.ts create mode 100644 src/utils/wagmi/actions.ts diff --git a/package.json b/package.json index 860742c6..906c0600 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yearn-finance/web-lib", - "version": "3.0.100", + "version": "3.0.102", "main": "./dist/index.js", "types": "./dist/index.d.js", "files": [ @@ -41,7 +41,7 @@ "lint-staged": "^15.2.0", "next": "^14.0.4", "next-seo": "^6.4.0", - "prettier": "^3.2.0", + "prettier": "2.8.8", "react-hot-toast": "2.4.1", "react-paginate": "^8.2.0", "sass": "^1.69.7", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index be52ad8e..6bdea25e 100755 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,4 @@ import {cloneElement, Fragment, useEffect, useMemo, useState} from 'react'; -import assert from 'assert'; import {useConnect, usePublicClient} from 'wagmi'; import {Listbox, Transition} from '@headlessui/react'; import {useAccountModal, useChainModal, useConnectModal} from '@rainbow-me/rainbowkit'; @@ -10,6 +9,7 @@ import {toSafeChainID} from '../hooks/useChainID.js'; import {IconChevronBottom} from '../icons/IconChevronBottom.js'; import {IconWallet} from '../icons/IconWallet.js'; import {truncateHex} from '../utils/address.js'; +import {assert} from '../utils/assert.js'; import {cl} from '../utils/cl.js'; import {Button} from './Button.js'; diff --git a/src/components/ModalMobileMenu.tsx b/src/components/ModalMobileMenu.tsx index 0bb20620..f238f589 100755 --- a/src/components/ModalMobileMenu.tsx +++ b/src/components/ModalMobileMenu.tsx @@ -1,11 +1,11 @@ import React, {cloneElement, Fragment, useEffect, useMemo, useRef, useState} from 'react'; -import assert from 'assert'; import {useConnect, useNetwork} from 'wagmi'; import {Dialog, Transition} from '@headlessui/react'; import {useWeb3} from '../contexts/useWeb3.js'; import {useInjectedWallet} from '../hooks/useInjectedWallet.js'; import {truncateHex} from '../utils/address.js'; +import {assert} from '../utils/assert.js'; import type {ReactElement, ReactNode} from 'react'; import type {Chain} from 'wagmi'; diff --git a/src/contexts/useWallet.tsx b/src/contexts/useWallet.tsx new file mode 100755 index 00000000..9e06bca8 --- /dev/null +++ b/src/contexts/useWallet.tsx @@ -0,0 +1,311 @@ +import {createContext, memo, useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import {useDeepCompareMemo} from '@react-hookz/web'; +import {onLoadDone, onLoadStart} from '@yearn-finance/web-lib/contexts/useUI'; +import {toAddress, zeroAddress} from '@yearn-finance/web-lib/utils/address'; +import { + BAL_TOKEN_ADDRESS, + BALWETH_TOKEN_ADDRESS, + CRV_TOKEN_ADDRESS, + CVXCRV_TOKEN_ADDRESS, + ETH_TOKEN_ADDRESS, + LPYBAL_TOKEN_ADDRESS, + LPYCRV_TOKEN_ADDRESS, + LPYCRV_V2_TOKEN_ADDRESS, + STYBAL_TOKEN_ADDRESS, + YBAL_TOKEN_ADDRESS, + YCRV_CURVE_POOL_V2_ADDRESS, + YCRV_TOKEN_ADDRESS, + YVBOOST_TOKEN_ADDRESS, + YVECRV_TOKEN_ADDRESS +} from '@yearn-finance/web-lib/utils/constants'; +import {toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; + +import {type TUseBalancesTokens, useBalances} from '../hooks/useMultichainBalances'; +import {useYearn} from './useYearn'; + +import type {ReactElement} from 'react'; +import type {TAddress, TChainTokens, TDict, TNormalizedBN, TToken} from '../types'; +import type {TYDaemonVault} from '../utils/schemas/yDaemonVaultsSchemas'; + +export type TWalletContext = { + getToken: ({address, chainID}: TTokenAndChain) => TToken; + getBalance: ({address, chainID}: TTokenAndChain) => TNormalizedBN; + getPrice: ({address, chainID}: TTokenAndChain) => TNormalizedBN; + balances: TChainTokens; + cumulatedValueInV2Vaults: number; + cumulatedValueInV3Vaults: number; + isLoading: boolean; + shouldUseForknetBalances: boolean; + refresh: (tokenList?: TUseBalancesTokens[]) => Promise; + triggerForknetBalances: () => void; +}; +type TTokenAndChain = {address: TAddress; chainID: number}; + +const defaultToken: TToken = { + address: zeroAddress, + name: '', + symbol: '', + decimals: 18, + chainID: 1, + value: 0, + stakingValue: 0, + price: toNormalizedBN(0), + balance: toNormalizedBN(0) +}; + +const defaultProps = { + getToken: (): TToken => defaultToken, + getBalance: (): TNormalizedBN => toNormalizedBN(0), + getPrice: (): TNormalizedBN => toNormalizedBN(0), + balances: {}, + cumulatedValueInV2Vaults: 0, + cumulatedValueInV3Vaults: 0, + isLoading: true, + shouldUseForknetBalances: false, + refresh: async (): Promise => ({}), + triggerForknetBalances: (): void => {} +}; + +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: BAL_TOKEN_ADDRESS}, + {chainID: 1, address: YBAL_TOKEN_ADDRESS}, + {chainID: 1, address: BALWETH_TOKEN_ADDRESS}, + {chainID: 1, address: STYBAL_TOKEN_ADDRESS}, + {chainID: 1, address: LPYBAL_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; +} + +function useYearnBalances({shouldUseForknetBalances}: {shouldUseForknetBalances: boolean}): { + tokens: TChainTokens; + 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((): TChainTokens => { + const _tokens = {...tokensRaw}; + if (shouldUseForknetBalances) { + _tokens[1] = _tokens[1337]; // eslint-disable-line prefer-destructuring + } + return _tokens; + }, [tokensRaw, shouldUseForknetBalances]); + + const onRefresh = useCallback( + async (tokenToUpdate?: TUseBalancesTokens[]): Promise => { + if (tokenToUpdate) { + const updatedBalances = await onUpdateSome(tokenToUpdate); + return updatedBalances; + } + const updatedBalances = await onUpdate(); + return updatedBalances; + }, + [onUpdate, onUpdateSome] + ); + + useEffect((): void => { + if (isLoading) { + onLoadStart(); + } else { + onLoadDone(); + } + }, [isLoading]); + + return {tokens, isLoading, onRefresh}; +} + +/* 🔵 - Yearn Finance ********************************************************** + ** This context controls most of the user's wallet data we may need to + ** interact with our app, aka mostly the balances and the token prices. + ******************************************************************************/ +const WalletContext = createContext(defaultProps); +export const WalletContextApp = memo(function WalletContextApp({children}: {children: ReactElement}): ReactElement { + const {vaults, prices, vaultsMigrations} = useYearn(); + const [shouldUseForknetBalances, set_shouldUseForknetBalances] = useState(false); + const {tokens, isLoading, onRefresh} = useYearnBalances({shouldUseForknetBalances}); + + const [cumulatedValueInV2Vaults, cumulatedValueInV3Vaults] = useMemo((): [number, number] => { + let cumulatedValueInV2Vaults = 0; + let cumulatedValueInV3Vaults = 0; + for (const [, perChain] of Object.entries(tokens)) { + 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, tokens]); + + const getToken = useCallback( + ({address, chainID}: TTokenAndChain): TToken => tokens?.[chainID || 1]?.[address] || defaultToken, + [tokens] + ); + const getBalance = useCallback( + ({address, chainID}: TTokenAndChain): TNormalizedBN => + tokens?.[chainID || 1]?.[address]?.balance || toNormalizedBN(0), + [tokens] + ); + const getPrice = useCallback( + ({address, chainID}: TTokenAndChain): TNormalizedBN => { + const price = tokens?.[chainID || 1]?.[address]?.price; + if (!price) { + return toNormalizedBN(prices?.[chainID]?.[address] || 0, 6) || toNormalizedBN(0); + } + return price; + }, + [prices, tokens] + ); + + /* 🔵 - Yearn Finance ****************************************************** + ** Setup and render the Context provider to use in the app. + ***************************************************************************/ + const contextValue = useMemo( + (): TWalletContext => ({ + getToken, + getBalance, + getPrice, + balances: tokens, + cumulatedValueInV2Vaults, + cumulatedValueInV3Vaults, + isLoading: isLoading || false, + shouldUseForknetBalances, + refresh: onRefresh, + triggerForknetBalances: (): void => + set_shouldUseForknetBalances((s): boolean => { + const isEnabled = !s; + if (!(window as any).ethereum) { + (window as any).ethereum = {}; + } + (window as any).ethereum.useForknetForMainnet = isEnabled; + return isEnabled; + }) + }), + [ + getToken, + getBalance, + getPrice, + tokens, + cumulatedValueInV2Vaults, + cumulatedValueInV3Vaults, + isLoading, + shouldUseForknetBalances, + onRefresh + ] + ); + + return {children}; +}); + +export const useWallet = (): TWalletContext => useContext(WalletContext); diff --git a/src/contexts/useWeb3.tsx b/src/contexts/useWeb3.tsx index 1612926a..2db6f351 100755 --- a/src/contexts/useWeb3.tsx +++ b/src/contexts/useWeb3.tsx @@ -1,5 +1,4 @@ import {createContext, useCallback, useContext, useMemo, useState} from 'react'; -import assert from 'assert'; import { configureChains, useAccount, @@ -17,6 +16,7 @@ import {useIsMounted, useMountEffect, useUpdateEffect} from '@react-hookz/web'; import {toast} from '../components/yToast.js'; import {toAddress} from '../utils/address.js'; +import {assert} from '../utils/assert.js'; import {isIframe} from '../utils/helpers.js'; import {getConfig, getSupportedProviders} from '../utils/wagmi/config.js'; import {deepMerge} from './utils.js'; diff --git a/src/contexts/useYearn.tsx b/src/contexts/useYearn.tsx new file mode 100755 index 00000000..c82a4ba5 --- /dev/null +++ b/src/contexts/useYearn.tsx @@ -0,0 +1,275 @@ +import {createContext, memo, useContext} from 'react'; +import {deserialize, serialize} from 'wagmi'; +import {useDeepCompareMemo, useLocalStorageValue} from '@react-hookz/web'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; + +import {useFetch} from '../hooks/useFetch'; +import {useYDaemonBaseURI} from '../hooks/useYDaemonBaseURI'; +import {yDaemonEarnedSchema} from '../utils/schemas/yDaemonEarnedSchema'; +import {yDaemonPricesChainSchema} from '../utils/schemas/yDaemonPricesSchema'; +import {Solver} from '../utils/schemas/yDaemonTokenListBalances'; +import {yDaemonTokensChainSchema} from '../utils/schemas/yDaemonTokensSchema'; +import {yDaemonVaultsSchema} from '../utils/schemas/yDaemonVaultsSchemas'; + +import type {ReactElement} from 'react'; +import type {KeyedMutator} from 'swr'; +import type {TAddress, TDict} from '@yearn-finance/web-lib/types'; +import type {TYDaemonEarned} from '../utils/schemas/yDaemonEarnedSchema'; +import type {TYDaemonPricesChain} from '../utils/schemas/yDaemonPricesSchema'; +import type {TSolver} from '../utils/schemas/yDaemonTokenListBalances'; +import type {TYDaemonTokens, TYDaemonTokensChain} from '../utils/schemas/yDaemonTokensSchema'; +import type {TYDaemonVault, TYDaemonVaults} from '../utils/schemas/yDaemonVaultsSchemas'; + +export const DEFAULT_SLIPPAGE = 0.5; +export const DEFAULT_MAX_LOSS = 1n; + +export type TYearnContext = { + currentPartner: TAddress; + earned?: TYDaemonEarned; + prices?: TYDaemonPricesChain; + tokens?: TYDaemonTokens; + vaults: TDict; + vaultsMigrations: TDict; + vaultsRetired: TDict; + isLoadingVaultList: boolean; + zapSlippage: number; + maxLoss: bigint; + zapProvider: TSolver; + isStakingOpBoostedVaults: boolean; + mutateVaultList: KeyedMutator; + set_maxLoss: (value: bigint) => void; + set_zapSlippage: (value: number) => void; + set_zapProvider: (value: TSolver) => void; + set_isStakingOpBoostedVaults: (value: boolean) => void; +}; + +const YearnContext = createContext({ + currentPartner: toAddress(process.env.PARTNER_ID_ADDRESS), + earned: { + earned: {}, + totalRealizedGainsUSD: 0, + totalUnrealizedGainsUSD: 0 + }, + prices: {}, + tokens: {}, + vaults: {}, + vaultsMigrations: {}, + vaultsRetired: {}, + isLoadingVaultList: false, + maxLoss: DEFAULT_MAX_LOSS, + zapSlippage: 0.1, + zapProvider: Solver.enum.Cowswap, + isStakingOpBoostedVaults: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mutateVaultList: (): any => undefined, + set_maxLoss: (): void => undefined, + set_zapSlippage: (): void => undefined, + set_zapProvider: (): void => undefined, + set_isStakingOpBoostedVaults: (): void => undefined +}); + +function useYearnPrices(): TYDaemonPricesChain { + const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI(); + const {data: prices} = useFetch({ + endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`, + schema: yDaemonPricesChainSchema + }); + + const pricesUpdated = useDeepCompareMemo((): TYDaemonPricesChain => { + if (!prices) { + return {}; + } + return prices; + }, [prices]); + + return pricesUpdated; +} + +function useYearnTokens(): TYDaemonTokens { + const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI(); + const {data: tokens} = useFetch({ + endpoint: `${yDaemonBaseUriWithoutChain}/tokens/all`, + schema: yDaemonTokensChainSchema + }); + + const tokensUpdated = useDeepCompareMemo((): TYDaemonTokens => { + if (!tokens) { + return {}; + } + const _tokens: TYDaemonTokens = {}; + for (const [, tokensData] of Object.entries(tokens)) { + for (const [tokenAddress, token] of Object.entries(tokensData)) { + if (token) { + _tokens[toAddress(tokenAddress)] = token; + } + } + } + return _tokens; + }, [tokens]); + + return tokensUpdated; +} + +function useYearnEarned(): TYDaemonEarned { + const {address} = useWeb3(); + const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI(); + + const {data: earned} = useFetch({ + endpoint: address + ? `${yDaemonBaseUriWithoutChain}/earned/${address}?${new URLSearchParams({ + chainIDs: [1, 10].join(',') + })}` + : null, + schema: yDaemonEarnedSchema + }); + + const memorizedEarned = useDeepCompareMemo((): TYDaemonEarned => { + if (!earned) { + return { + earned: {}, + totalRealizedGainsUSD: 0, + totalUnrealizedGainsUSD: 0 + }; + } + return earned; + }, [earned]); + + return memorizedEarned; +} + +export const YearnContextApp = memo(function YearnContextApp({children}: {children: ReactElement}): ReactElement { + const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI(); + const {value: maxLoss, set: set_maxLoss} = useLocalStorageValue('yearn.fi/max-loss', { + defaultValue: DEFAULT_MAX_LOSS, + parse: (str: string, fallback: bigint): bigint => (str ? deserialize(str) : fallback), + stringify: (data: bigint): string => serialize(data) + }); + const {value: zapSlippage, set: set_zapSlippage} = useLocalStorageValue('yearn.fi/zap-slippage', { + defaultValue: DEFAULT_SLIPPAGE + }); + const {value: zapProvider, set: set_zapProvider} = useLocalStorageValue('yearn.fi/zap-provider', { + defaultValue: Solver.enum.Cowswap + }); + const {value: isStakingOpBoostedVaults, set: set_isStakingOpBoostedVaults} = useLocalStorageValue( + 'yearn.fi/staking-op-boosted-vaults', + { + defaultValue: true + } + ); + + const prices = useYearnPrices(); + const tokens = useYearnTokens(); + const earned = useYearnEarned(); + + const { + data: vaults, + isLoading: isLoadingVaultList, + mutate: mutateVaultList + } = useFetch({ + endpoint: `${yDaemonBaseUriWithoutChain}/vaults?${new URLSearchParams({ + hideAlways: 'true', + orderBy: 'featuringScore', + orderDirection: 'desc', + strategiesDetails: 'withDetails', + strategiesRisk: 'withRisk', + strategiesCondition: 'inQueue', + chainIDs: [1, 10, 137, 250, 8453, 42161].join(','), + limit: '2500' + })}`, + schema: yDaemonVaultsSchema + }); + + const {data: vaultsMigrations} = useFetch({ + endpoint: `${yDaemonBaseUriWithoutChain}/vaults?${new URLSearchParams({ + migratable: 'nodust' + })}`, + schema: yDaemonVaultsSchema + }); + + const {data: vaultsRetired} = useFetch({ + endpoint: `${yDaemonBaseUriWithoutChain}/vaults/retired`, + schema: yDaemonVaultsSchema + }); + + const vaultsObject = useDeepCompareMemo((): TDict => { + const _vaultsObject = (vaults ?? []).reduce((acc: TDict, vault): TDict => { + if (!vault.migration.available) { + acc[toAddress(vault.address)] = vault; + } + return acc; + }, {}); + return _vaultsObject; + }, [vaults]); + + const vaultsMigrationsObject = useDeepCompareMemo((): TDict => { + const _migratableVaultsObject = (vaultsMigrations ?? []).reduce( + (acc: TDict, vault): TDict => { + if (toAddress(vault.address) !== toAddress(vault.migration.address)) { + acc[toAddress(vault.address)] = vault; + } + return acc; + }, + {} + ); + return _migratableVaultsObject; + }, [vaultsMigrations]); + + const vaultsRetiredObject = useDeepCompareMemo((): TDict => { + const _retiredVaultsObject = (vaultsRetired ?? []).reduce( + (acc: TDict, vault): TDict => { + acc[toAddress(vault.address)] = vault; + return acc; + }, + {} + ); + return _retiredVaultsObject; + }, [vaultsRetired]); + + /* 🔵 - Yearn Finance ****************************************************** + ** Setup and render the Context provider to use in the app. + ***************************************************************************/ + const contextValue = useDeepCompareMemo( + (): TYearnContext => ({ + currentPartner: toAddress(process.env.PARTNER_ID_ADDRESS), + prices, + tokens, + earned, + zapSlippage: zapSlippage ?? DEFAULT_SLIPPAGE, + maxLoss: maxLoss ?? DEFAULT_MAX_LOSS, + zapProvider: zapProvider ?? Solver.enum.Cowswap, + isStakingOpBoostedVaults: isStakingOpBoostedVaults ?? true, + set_zapSlippage, + set_maxLoss, + set_zapProvider, + set_isStakingOpBoostedVaults, + vaults: vaultsObject, + vaultsMigrations: vaultsMigrationsObject, + vaultsRetired: vaultsRetiredObject, + isLoadingVaultList, + mutateVaultList + }), + [ + prices, + tokens, + earned, + zapSlippage, + maxLoss, + zapProvider, + isStakingOpBoostedVaults, + set_zapSlippage, + set_maxLoss, + set_zapProvider, + set_isStakingOpBoostedVaults, + vaultsObject, + vaultsMigrationsObject, + vaultsRetiredObject, + isLoadingVaultList, + mutateVaultList + ] + ); + + return {children}; +}); + +export const useYearn = (): TYearnContext => useContext(YearnContext); diff --git a/src/hooks/useBalances.ts b/src/hooks/useBalances.ts index 1d5ecc7b..8fb96325 100644 --- a/src/hooks/useBalances.ts +++ b/src/hooks/useBalances.ts @@ -437,10 +437,10 @@ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { status: status.isError ? 'error' : status.isLoading || status.isFetching - ? 'loading' - : status.isSuccess - ? 'success' - : 'unknown' + ? 'loading' + : status.isSuccess + ? 'success' + : 'unknown' }), [ error, diff --git a/src/hooks/useYDaemonBaseURI.ts b/src/hooks/useYDaemonBaseURI.ts new file mode 100644 index 00000000..2d4c9367 --- /dev/null +++ b/src/hooks/useYDaemonBaseURI.ts @@ -0,0 +1,22 @@ +import {useSettings} from '@yearn-finance/web-lib/contexts/useSettings'; + +type TProps = { + chainID: number | string; +}; + +export function useYDaemonBaseURI(props?: TProps): {yDaemonBaseUri: string} { + // eslint-disable-next-line @typescript-eslint/naming-convention + const {settings} = useSettings(); + + const yDaemonBaseUri = settings.yDaemonBaseURI || process.env.YDAEMON_BASE_URI; + + if (!yDaemonBaseUri) { + throw new Error('YDAEMON_BASE_URI is not defined'); + } + + if (!props?.chainID) { + return {yDaemonBaseUri}; + } + + return {yDaemonBaseUri: `${yDaemonBaseUri}/${props.chainID}`}; +} diff --git a/src/types/curves.ts b/src/types/curves.ts new file mode 100755 index 00000000..c9e5a90c --- /dev/null +++ b/src/types/curves.ts @@ -0,0 +1,3 @@ +import type {TDict} from '@yearn-finance/web-lib/types'; + +export type TCurveGaugeVersionRewards = TDict>; diff --git a/src/utils/assert.ts b/src/utils/assert.ts new file mode 100644 index 00000000..959eee0b --- /dev/null +++ b/src/utils/assert.ts @@ -0,0 +1,14 @@ +import actualAssert from 'assert'; + +export function assert( + expression: unknown, + message?: string | Error, + doSomething?: (error: unknown) => void +): asserts expression { + try { + actualAssert(expression, message); + } catch (error) { + doSomething?.(error); + throw error; + } +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 00000000..44f25974 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import {serialize} from '@wagmi/core'; + +import type {AxiosRequestConfig} from 'axios'; +import type {z} from 'zod'; + +type TFetchProps = { + endpoint: string | null; + schema: z.ZodSchema; + config?: AxiosRequestConfig; +}; + +export type TFetchReturn = Promise<{data: T | null; error?: Error}>; + +export async function fetch({endpoint, schema, config}: TFetchProps): TFetchReturn { + if (!endpoint) { + return {data: null, error: new Error('No endpoint provided')}; + } + + try { + const {data} = await axios.get(endpoint, config); + + if (!data) { + return {data: null, error: new Error('No data')}; + } + + const parsedData = schema.safeParse(data); + + if (!parsedData.success) { + console.error(endpoint, parsedData.error); + return {data, error: parsedData.error}; + } + + return {...data, data: parsedData.data}; + } catch (error) { + console.error(endpoint, error); + if (error instanceof Error) { + return {data: null, error}; + } + return {data: null, error: new Error(serialize(error))}; + } +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 5c05c01c..f1cf2ebb 100755 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,4 +1,7 @@ import {yToast} from '../components/yToast.js'; +import {toNormalizedBN} from './format.bigNumber.js'; + +import type {TSortDirection} from '../types/index.js'; /* 🔵 - Yearn Finance ****************************************************** ** Yearn Meta uses some markdown for some rich content. Instead of using @@ -56,3 +59,25 @@ export function isIframe(): boolean { } return false; } + +/* 🔵 - Yearn Finance ****************************************************** + ** Framer Motion animation constants + **************************************************************************/ +export const motionTransition = {duration: 0.4, ease: 'easeInOut'}; +export const motionVariants = { + initial: {y: -80, opacity: 0, motionTransition}, + enter: {y: 0, opacity: 1, motionTransition}, + exit: {y: -80, opacity: 0, motionTransition} +}; + +/* 🔵 - Yearn Finance ****************************************************** + ** Helper function to sort elements based on the type of the element. + **************************************************************************/ +export const stringSort = ({a, b, sortDirection}: {a: string; b: string; sortDirection: TSortDirection}): number => + sortDirection === 'desc' ? a.localeCompare(b) : b.localeCompare(a); + +export const numberSort = ({a, b, sortDirection}: {a?: number; b?: number; sortDirection: TSortDirection}): number => + sortDirection === 'desc' ? (b ?? 0) - (a ?? 0) : (a ?? 0) - (b ?? 0); + +export const bigNumberSort = ({a, b, sortDirection}: {a: bigint; b: bigint; sortDirection: TSortDirection}): number => + Number(toNormalizedBN(sortDirection === 'desc' ? b - a : a - b).normalized); diff --git a/src/utils/wagmi/actions.ts b/src/utils/wagmi/actions.ts new file mode 100644 index 00000000..6767157c --- /dev/null +++ b/src/utils/wagmi/actions.ts @@ -0,0 +1,112 @@ +import {erc20ABI, readContract} from '@wagmi/core'; +import {MAX_UINT_256} from '@yearn-finance/web-lib/utils/constants'; +import {handleTx, toWagmiProvider} from '@yearn-finance/web-lib/utils/wagmi/provider'; +import {assertAddress} from '@yearn-finance/web-lib/utils/wagmi/utils'; + +import type {Connector} from 'wagmi'; +import type {TAddress} from '@yearn-finance/web-lib/types'; +import type {TWriteTransaction} from '@yearn-finance/web-lib/utils/wagmi/provider'; +import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction'; + +function getChainID(chainID: number): number { + if (typeof window !== 'undefined' && (window as any)?.ethereum?.useForknetForMainnet) { + if (chainID === 1) { + return 1337; + } + } + return chainID; +} + +//Because USDT do not return a boolean on approve, we need to use this ABI +const ALTERNATE_ERC20_APPROVE_ABI = [ + { + constant: false, + inputs: [ + {name: '_spender', type: 'address'}, + {name: '_value', type: 'uint256'} + ], + name: 'approve', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } +] as const; + +/* 🔵 - Yearn Finance ********************************************************** + ** isApprovedERC20 is a _VIEW_ function that checks if a token is approved for + ** a spender. + ******************************************************************************/ +export async function isApprovedERC20( + connector: Connector | undefined, + chainID: number, + tokenAddress: TAddress, + spender: TAddress, + amount = MAX_UINT_256 +): Promise { + const wagmiProvider = await toWagmiProvider(connector as Connector); + const result = await readContract({ + ...wagmiProvider, + abi: erc20ABI, + chainId: getChainID(chainID), + address: tokenAddress, + functionName: 'allowance', + args: [wagmiProvider.address, spender] + }); + return (result || 0n) >= amount; +} + +/* 🔵 - Yearn Finance ********************************************************** + ** allowanceOf is a _VIEW_ function that returns the amount of a token that is + ** approved for a spender. + ******************************************************************************/ +type TAllowanceOf = { + connector: Connector | undefined; + chainID: number; + tokenAddress: TAddress; + spenderAddress: TAddress; +}; +export async function allowanceOf(props: TAllowanceOf): Promise { + const wagmiProvider = await toWagmiProvider(props.connector); + const result = await readContract({ + ...wagmiProvider, + chainId: getChainID(props.chainID), + abi: erc20ABI, + address: props.tokenAddress, + functionName: 'allowance', + args: [wagmiProvider.address, props.spenderAddress] + }); + return result || 0n; +} + +/* 🔵 - Yearn Finance ********************************************************** + ** approveERC20 is a _WRITE_ function that approves a token for a spender. + ** + ** @param spenderAddress - The address of the spender. + ** @param amount - The amount of collateral to deposit. + ******************************************************************************/ +type TApproveERC20 = TWriteTransaction & { + spenderAddress: TAddress | undefined; + amount: bigint; +}; +export async function approveERC20(props: TApproveERC20): Promise { + assertAddress(props.spenderAddress, 'spenderAddress'); + assertAddress(props.contractAddress); + + props.onTrySomethingElse = async (): Promise => { + assertAddress(props.spenderAddress, 'spenderAddress'); + return await handleTx(props, { + address: props.contractAddress, + abi: ALTERNATE_ERC20_APPROVE_ABI, + functionName: 'approve', + args: [props.spenderAddress, props.amount] + }); + }; + + return await handleTx(props, { + address: props.contractAddress, + abi: erc20ABI, + functionName: 'approve', + args: [props.spenderAddress, props.amount] + }); +} diff --git a/src/utils/wagmi/provider.ts b/src/utils/wagmi/provider.ts index 73d4b722..f74df65a 100644 --- a/src/utils/wagmi/provider.ts +++ b/src/utils/wagmi/provider.ts @@ -1,8 +1,8 @@ import {toast} from 'react-hot-toast'; -import assert from 'assert'; import {BaseError} from 'viem'; import {prepareWriteContract, switchNetwork, waitForTransaction, writeContract} from '@wagmi/core'; +import {assert} from '../assert.js'; import {toBigInt} from '../format.bigNumber.js'; import {defaultTxStatus} from '../web3/transaction.js'; import {assertAddress} from './utils.js'; diff --git a/src/utils/wagmi/utils.ts b/src/utils/wagmi/utils.ts index c76d3ce7..bfe4a4ca 100644 --- a/src/utils/wagmi/utils.ts +++ b/src/utils/wagmi/utils.ts @@ -1,8 +1,8 @@ -import assert from 'assert'; import {createPublicClient, http} from 'viem'; import * as wagmiChains from 'viem/chains'; -import {toAddress} from '../address.js'; +import {toAddress} from '../address'; +import {assert} from '../assert'; import { ARB_WETH_TOKEN_ADDRESS, BASE_WETH_TOKEN_ADDRESS, @@ -13,13 +13,13 @@ import { ZAP_ETH_WETH_OPT_CONTRACT, ZAP_FTM_WFTM_CONTRACT, ZERO_ADDRESS -} from '../constants.js'; -import {isEth} from '../isEth.js'; -import {isTAddress} from '../isTAddress.js'; -import {localhost} from './networks.js'; +} from '../constants'; +import {isEth} from '../isEth'; +import {isTAddress} from '../isTAddress'; +import {localhost} from './networks'; import type {Chain, PublicClient} from 'viem'; -import type {TAddress, TNDict} from '../../types/index.js'; +import type {TAddress, TNDict} from '../../types/index'; export type TChainContract = { address: TAddress; diff --git a/yarn.lock b/yarn.lock index 6bf68b4a..44c5a28f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7288,16 +7288,16 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + prettier@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw== -prettier@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.0.tgz#aedf3dfb42c00ee4eda533c1380f7d9624f768b8" - integrity sha512-/vBUecTGaPlRVwyZVROVC58bYIScqaoEJzZmzQXXrZOzqn0TwWz0EnOozOlFO/YAImRnb7XsKpTCd3m1SjS2Ww== - pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"