diff --git a/packages/web-lib/.editorconfig b/.editorconfig similarity index 100% rename from packages/web-lib/.editorconfig rename to .editorconfig diff --git a/packages/web-lib/.eslintignore b/.eslintignore similarity index 100% rename from packages/web-lib/.eslintignore rename to .eslintignore diff --git a/packages/web-lib/.eslintrc.cjs b/.eslintrc.cjs similarity index 94% rename from packages/web-lib/.eslintrc.cjs rename to .eslintrc.cjs index 86adc064..f5a2b3bb 100755 --- a/packages/web-lib/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,9 +6,6 @@ module.exports = { }, 'extends': [ 'eslint:recommended', - 'plugin:import/recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', 'plugin:tailwindcss/recommended', 'plugin:@typescript-eslint/recommended', 'next/core-web-vitals' @@ -35,7 +32,6 @@ module.exports = { }, 'rules': { 'import/default': 0, - 'no-mixed-spaces-and-tabs': 2, 'react/prop-types': 0, 'no-async-promise-executor': 0, 'import/no-unresolved': 0, //Issue with package exports @@ -50,7 +46,6 @@ module.exports = { 'array-bracket-newline': ['error', {'multiline': true}], 'react/jsx-curly-brace-presence': ['error', {'props': 'always', 'children': 'always'}], 'react/jsx-first-prop-new-line': ['error', 'multiline'], - 'react/jsx-max-props-per-line': ['error', {'maximum': {'single': 2, 'multi': 1}}], 'react/jsx-closing-tag-location': 2, 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ @@ -84,9 +79,9 @@ module.exports = { 'fixStyle': 'separate-type-imports' } ], - 'no-multi-spaces': ['error', {ignoreEOLComments: false}], '@typescript-eslint/no-var-requires': 0, '@typescript-eslint/no-unused-vars': 2, + '@typescript-eslint/no-explicit-any': [1], '@typescript-eslint/array-type': ['error', {'default': 'array'}], '@typescript-eslint/consistent-type-assertions': ['error', {'assertionStyle': 'as', 'objectLiteralTypeAssertions': 'never'}], '@typescript-eslint/consistent-type-definitions': ['error', 'type'], @@ -145,7 +140,6 @@ module.exports = { '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-includes': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/promise-function-async': 'error', '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/type-annotation-spacing': [ @@ -159,12 +153,12 @@ module.exports = { '@typescript-eslint/brace-style': ['error', '1tbs'], 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': ['error'], - 'comma-spacing': 'off', - '@typescript-eslint/comma-spacing': ['error'], - 'dot-notation': 'off', - '@typescript-eslint/dot-notation': ['error'], + '@typescript-eslint/prefer-optional-chain': 'error', 'indent': 'off', - '@typescript-eslint/indent': ['error', 'tab'] + '@typescript-eslint/indent': ['error', 'tab'], + 'no-multi-spaces': ['error', {ignoreEOLComments: false}], + 'no-mixed-spaces-and-tabs': 'error', + 'react/jsx-max-props-per-line': 'off' }, overrides: [ { diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d4e6644f..aa94cdc0 100755 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,14 +25,8 @@ jobs: with: node-version: 16 - - name: Move to dir - run: cd packages/web-lib - - name: Install project dependencies run: yarn --prefer-offline id: install - - name: Run linters - uses: wearerequired/lint-action@v2 - with: - eslint: true \ No newline at end of file + - name: yarn lint diff --git a/packages/web-lib/.npmignore b/.npmignore similarity index 100% rename from packages/web-lib/.npmignore rename to .npmignore diff --git a/LICENSE.md b/LICENSE.md index c347bd50..aab8bfe6 100755 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 yearn +Copyright (c) 2023 yearn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f02510d0..95136803 100755 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ![](../../.github/og.jpeg) -Yearn web Lib is a library of standard components used through Yearn's Projects. -This library is made for React projects with the idea to be light, efficient and easy to use. +Yearn web Lib is a library of standard components used through Yearn's Projects. +This library is made for React projects with the idea to be light, efficient and easy to use. We are using React + Tailwindcss + ethersjs for the web3 package, and some contexts are available to correctly wrap your app. Please check @yearn/web-template for documentation and usage. @@ -24,20 +24,8 @@ This repo is mirrored on [NPM](https://www.npmjs.com/package/@yearn-finance/web- yarn add @yearn-finance/web-lib ``` -### Useful Commands -- `yarn dev` - Run all packages locally - -### Apps & Packages -The following packages and applications are available - -- `package/docs`: Documentation site for the library -- `packages/web-lib`: Actual library for Yearn's projects - -Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). - - ### Releasing -When running `yarn build` in the `package/web-lib` folder, the library will be bumped to the next minor version, the code will be compiled and the various files will be copied in the `package/web-lib/dist` folder. +When running `yarn build` in the root folder, the library will be bumped to the next minor version, the code will be compiled and the various files will be copied in the `package/web-lib/dist` folder. From there the library can be published to NPM via the `yarn publish ./dist` command. ### How to setup @@ -95,12 +83,14 @@ WEB_SOCKET_URL: { 1: process.env.WS_URL_MAINNET, 10: process.env.WS_URL_OPTIMISM, 250: process.env.WS_URL_FANTOM, + 8453: process.env.WS_URL_BASE, 42161: process.env.WS_URL_ARBITRUM }, JSON_RPC_URL: { 1: process.env.RPC_URL_MAINNET, 10: process.env.RPC_URL_OPTIMISM, 250: process.env.RPC_URL_FANTOM, + 8453: process.env.RPC_URL_BASE, 42161: process.env.RPC_URL_ARBITRUM }, ALCHEMY_KEY: process.env.ALCHEMY_KEY, @@ -115,7 +105,7 @@ import {WithYearn} from '@yearn-finance/web-lib/contexts'; function MyApp(props: AppProps): ReactElement { const {Component, pageProps} = props; - + return ( + {shouldDisplayTooltip ? ( + copyToClipboard(address)} + className={'tooltipLight top-full cursor-copy pt-1'}> +
+ {address} +
+
+ ) : } + {shouldDisplayTooltip ? ensHandle : address} + + ); +} diff --git a/packages/web-lib/components/Banner.tsx b/components/Banner.tsx similarity index 100% rename from packages/web-lib/components/Banner.tsx rename to components/Banner.tsx diff --git a/packages/web-lib/components/Button.tsx b/components/Button.tsx similarity index 100% rename from packages/web-lib/components/Button.tsx rename to components/Button.tsx diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100755 index 00000000..62296544 --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,294 @@ +import React, {cloneElement, Fragment, useEffect, useMemo, useState} from 'react'; +import assert from 'assert'; +import {useConnect, usePublicClient} from 'wagmi'; +import {Listbox, Transition} from '@headlessui/react'; +import {useIsMounted} from '@react-hookz/web'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toSafeChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import IconChevronBottom from '@yearn-finance/web-lib/icons/IconChevronBottom'; +import IconWallet from '@yearn-finance/web-lib/icons/IconWallet'; +import {truncateHex} from '@yearn-finance/web-lib/utils/address'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; + +import type {AnchorHTMLAttributes, DetailedHTMLProps, ReactElement} from 'react'; +import type {Chain} from 'wagmi'; + +const Link = (props: (DetailedHTMLProps, HTMLAnchorElement>) & {tag: ReactElement}): ReactElement => { + const {tag, ...rest} = props; + const Element = cloneElement(tag, rest); + return Element; +}; + +export type TMenu = {path: string, label: string | ReactElement, target?: string}; +export type TNavbar = { + nav: TMenu[], + linkComponent?: ReactElement, + currentPathName: string +}; +function Navbar({nav, linkComponent = , currentPathName}: TNavbar): ReactElement { + return ( + + ); +} + +function NetworkButton({label, isDisabled, onClick}: { + label: string, + isDisabled?: boolean, + onClick?: () => void, +}): ReactElement { + return ( + + ); +} + +export type TNetwork = {value: number, label: string}; +function NetworkSelector({networks}: {networks: number[]}): ReactElement { + const {onSwitchChain} = useWeb3(); + const publicClient = usePublicClient(); + const {connectors} = useConnect(); + const safeChainID = toSafeChainID(publicClient?.chain.id, Number(process.env.BASE_CHAINID)); + + const supportedNetworks = useMemo((): TNetwork[] => { + const injectedConnector = connectors.find((e): boolean => (e.id).toLocaleLowerCase() === 'injected'); + assert(injectedConnector, 'No injected connector found'); + const chainsForInjected = injectedConnector.chains; + + return ( + chainsForInjected + .filter(({id}): boolean => id !== 1337 && ((networks.length > 0 && networks.includes(id)) || true)) + .map((network: Chain): TNetwork => ( + {value: network.id, label: network.name} + )) + ); + }, [connectors, networks]); + + const currentNetwork = useMemo((): TNetwork | undefined => ( + supportedNetworks.find((network): boolean => network.value === safeChainID) + ), [safeChainID, supportedNetworks]); + + if (supportedNetworks.length === 1) { + if (publicClient?.chain.id === 1337) { + return ; + } + if (currentNetwork?.value === supportedNetworks[0]?.value) { + return ; + } + return ( + onSwitchChain(supportedNetworks[0].value)} /> + ); + } + + return ( +
+ onSwitchChain((value as {value: number}).value)}> + {({open}): ReactElement => ( + <> + +
+ {currentNetwork?.label || 'Ethereum'} +
+
+ +
+
+ +
+ +
+ + + +
+ {supportedNetworks.map((network): ReactElement => ( + + {({active}): ReactElement => ( +
+ {network?.label || 'Ethereum'} +
+ )} +
+ ))} +
+
+
+
+ + + )} + +
+ ); +} + +function WalletSelector(): ReactElement { + const {options, isActive, address, ens, lensProtocolHandle, openLoginModal, onDesactivate, onSwitchChain} = useWeb3(); + const [walletIdentity, set_walletIdentity] = useState(undefined); + const isMounted = useIsMounted(); + + useEffect((): void => { + if (!isMounted()) { + return; + } + if (!isActive && address) { + set_walletIdentity('Invalid Network'); + } else if (ens) { + set_walletIdentity(ens); + } else if (lensProtocolHandle) { + set_walletIdentity(lensProtocolHandle); + } else if (address) { + set_walletIdentity(truncateHex(address, 4)); + } else { + set_walletIdentity(undefined); + } + }, [ens, lensProtocolHandle, address, isActive, isMounted]); + + return ( + <> +
{ + if (isActive) { + onDesactivate(); + } else if (!isActive && address) { + onSwitchChain(options?.defaultChainID || 1); + } else { + openLoginModal(); + } + }}> +

+ {walletIdentity ? walletIdentity : ( + + + + + )} +

+
+
{ + if (isActive) { + onDesactivate(); + } else if (!isActive && address) { + onSwitchChain(options?.defaultChainID || 1); + } else { + openLoginModal(); + } + }} + className={cl('fixed inset-x-0 bottom-0 z-[87] border-t border-neutral-900 bg-neutral-0 md:hidden', walletIdentity ? 'hidden pointer-events-none' : '')}> +
+ {'You are not connected. Please connect to a wallet to continue.'} + +
+ +
+ + ); +} + +export type THeader = { + logo: ReactElement, + extra?: ReactElement, + linkComponent?: ReactElement, + nav: TMenu[], + supportedNetworks?: number[], + currentPathName: string, + onOpenMenuMobile: () => void +} +function Header({ + logo, + extra, + linkComponent, + nav, + currentPathName, + supportedNetworks, + onOpenMenuMobile +}: THeader): ReactElement { + return ( +
+ +
+ +
+
+
+ {logo} +
+
+
+ + + {extra} +
+
+ ); +} + +export default Header; diff --git a/components/ImageWithFallback.tsx b/components/ImageWithFallback.tsx new file mode 100755 index 00000000..c0e322e7 --- /dev/null +++ b/components/ImageWithFallback.tsx @@ -0,0 +1,28 @@ +import React, {useState} from 'react'; +import Image from 'next/image'; +import performBatchedUpdates from '@yearn-finance/web-lib/utils/performBatchedUpdates'; + +import type {ImageProps} from 'next/image'; +import type {CSSProperties, ReactElement} from 'react'; + +function ImageWithFallback(props: ImageProps): ReactElement { + const {alt, src, ...rest} = props; + const [imageSrc, set_imageSrc] = useState(`${src}?fallback=true`); + const [imageStyle, set_imageStyle] = useState({}); + + return ( + {alt} { + performBatchedUpdates((): void => { + set_imageSrc('/placeholder.png'); + set_imageStyle({filter: 'opacity(0.2)'}); + }); + }} + {...rest} /> + ); +} + +export {ImageWithFallback}; diff --git a/packages/web-lib/components/Modal.tsx b/components/Modal.tsx similarity index 100% rename from packages/web-lib/components/Modal.tsx rename to components/Modal.tsx diff --git a/packages/web-lib/components/ModalLogin.tsx b/components/ModalLogin.tsx similarity index 83% rename from packages/web-lib/components/ModalLogin.tsx rename to components/ModalLogin.tsx index 27fad69d..a535247d 100755 --- a/packages/web-lib/components/ModalLogin.tsx +++ b/components/ModalLogin.tsx @@ -1,7 +1,7 @@ import React, {cloneElement, useCallback} from 'react'; -import {useWeb3} from '@yearn-finance/web-lib//contexts/useWeb3'; import {Modal} from '@yearn-finance/web-lib/components/Modal'; import {yToast} from '@yearn-finance/web-lib/components/yToast'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; import {useInjectedWallet} from '@yearn-finance/web-lib/hooks/useInjectedWallet'; import IconWalletWalletConnect from '@yearn-finance/web-lib/icons/IconWalletWalletConnect'; @@ -49,7 +49,12 @@ function ModalLogin(props: TModalLogin): ReactElement { onClick={(): void => { onConnect( 'WALLET_CONNECT', - (error): string => toast({content: error.message ?? 'Invalid chain', type: 'error'}), + (error: unknown): string => { + if (!error || typeof error !== 'object' || !('message' in error) || typeof error.message !== 'string') { + return toast({content: 'Impossible to connect wallet', type: 'error'}); + } + return toast({content: error.message, type: 'error'}); + }, (): void => onClose() ); }} diff --git a/packages/web-lib/components/ModalMobileMenu.tsx b/components/ModalMobileMenu.tsx similarity index 86% rename from packages/web-lib/components/ModalMobileMenu.tsx rename to components/ModalMobileMenu.tsx index 593af6e2..d78ce83b 100755 --- a/packages/web-lib/components/ModalMobileMenu.tsx +++ b/components/ModalMobileMenu.tsx @@ -1,16 +1,17 @@ -import React, {cloneElement, Fragment, useEffect, useRef, useState} from 'react'; +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 {yToast} from '@yearn-finance/web-lib/components/yToast'; import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; -import {useChain} from '@yearn-finance/web-lib/hooks/useChain'; import {useInjectedWallet} from '@yearn-finance/web-lib/hooks/useInjectedWallet'; import IconWalletWalletConnect from '@yearn-finance/web-lib/icons/IconWalletWalletConnect'; import {truncateHex} from '@yearn-finance/web-lib/utils/address'; -import {useSupportedChainsID} from '../hooks/useSupportedChainsID'; - import type {ReactElement, ReactNode} from 'react'; +import type {Chain} from 'wagmi'; import type {TModal} from '@yearn-finance/web-lib/components/Modal'; +import type {TNetwork} from './Header'; type TModalMobileMenu = { isOpen: boolean @@ -68,12 +69,21 @@ function Modal(props: TModal): ReactElement { function ModalMobileMenu(props: TModalMobileMenu): ReactElement { const {isOpen, onClose, shouldUseWallets = true, shouldUseNetworks = true, children} = props; const {onSwitchChain, isActive, address, ens, lensProtocolHandle, onDesactivate, onConnect} = useWeb3(); + const {chain} = useNetwork(); const [walletIdentity, set_walletIdentity] = useState('Connect a wallet'); - const [optionsForSelect, set_optionsForSelect] = useState([]); const detectedWalletProvider = useInjectedWallet(); - const supportedChainsID = useSupportedChainsID(); - const chains = useChain(); const {toast} = yToast(); + const {connectors} = useConnect(); + + const supportedNetworks = useMemo((): TNetwork[] => { + const injectedConnector = connectors.find((e): boolean => (e.id).toLocaleLowerCase() === 'injected'); + assert(injectedConnector, 'No injected connector found'); + const chainsForInjected = injectedConnector.chains; + const noTestnet = chainsForInjected.filter(({id}): boolean => id !== 1337); + return noTestnet.map((network: Chain): TNetwork => ( + {value: network.id, label: network.name} + )); + }, [connectors]); useEffect((): void => { if (!isActive && address) { @@ -129,10 +139,6 @@ function ModalMobileMenu(props: TModalMobileMenu): ReactElement { return null; } - useEffect((): void => { - set_optionsForSelect(supportedChainsID); - }, [supportedChainsID]); - return ( onSwitchChain(Number(e?.target?.value))} className={'yearn--select-no-arrow yearn--select-reset !pr-6 text-sm'}> - {optionsForSelect.map((id: number): ReactElement => { - const label = chains.get(id)?.displayName || `Unknown chain (${id})`; - - const {chainID} = chains.getCurrent() || {}; - const isSelected = Number(chainID) === id; - + {supportedNetworks.map((network): ReactElement => { + const label = network.label || `Unknown chain (${network.value})`; + const selectedID = chain?.id || 1; + const isSelected = selectedID === network.value; return ( ); diff --git a/packages/web-lib/components/Renderable.tsx b/components/Renderable.tsx similarity index 100% rename from packages/web-lib/components/Renderable.tsx rename to components/Renderable.tsx diff --git a/packages/web-lib/components/yToast.tsx b/components/yToast.tsx similarity index 100% rename from packages/web-lib/components/yToast.tsx rename to components/yToast.tsx diff --git a/packages/web-lib/contexts/WithYearn.tsx b/contexts/WithYearn.tsx similarity index 54% rename from packages/web-lib/contexts/WithYearn.tsx rename to contexts/WithYearn.tsx index f777c842..51f8dba1 100755 --- a/packages/web-lib/contexts/WithYearn.tsx +++ b/contexts/WithYearn.tsx @@ -4,27 +4,23 @@ import {UIContextApp} from '@yearn-finance/web-lib/contexts/useUI'; import {Web3ContextApp} from '@yearn-finance/web-lib/contexts/useWeb3'; import type {ReactElement} from 'react'; -import type {FallbackTransport} from 'viem'; -import type {Config, PublicClient, WebSocketPublicClient} from 'wagmi'; -import type {TSettingsBase, TSettingsOptions, TUIOptions, TWeb3Options} from '@yearn-finance/web-lib/types/contexts'; +import type {Chain} from 'viem'; +import type {TSettingsBase, TUIOptions, TWeb3Options} from '@yearn-finance/web-lib/types/contexts'; -function WithYearn({children, config, options}: { +function WithYearn({children, supportedChains, options}: { children: ReactElement - config: Config, WebSocketPublicClient> + supportedChains: Chain[], options?: { ui?: TUIOptions, web3?: TWeb3Options, - networks?: TSettingsOptions, baseSettings?: Partial, } }): ReactElement { return ( - + {children} diff --git a/contexts/useSettings.tsx b/contexts/useSettings.tsx new file mode 100755 index 00000000..61e0b209 --- /dev/null +++ b/contexts/useSettings.tsx @@ -0,0 +1,62 @@ +import React, {createContext, useCallback, useContext, useMemo} from 'react'; +import {deepMerge} from '@yearn-finance/web-lib/contexts/utils'; +import {useLocalStorage} from '@yearn-finance/web-lib/hooks/useLocalStorage'; +import performBatchedUpdates from '@yearn-finance/web-lib/utils/performBatchedUpdates'; + +import type {TSettingsBase, TSettingsContext, TSettingsContextApp} from '@yearn-finance/web-lib/types/contexts'; + +const defaultSettings = { + yDaemonBaseURI: 'https://ydaemon.yearn.finance', + metaBaseURI: 'https://meta.yearn.finance', + apiBaseURI: 'https://api.yearn.finance' +}; + +const SettingsContext = createContext({ + settings: defaultSettings, + onUpdateBaseSettings: (): null => null +}); + +/* 💙 - Yearn Finance ************************************************************************* +** Handle some global parameters for the app. This should be used to store specific elements +** we want dApps to be able to customize, without being specific to the dApp. +** One of theses parameters is the list of networks with the specific Yearn's endpoints. +**********************************************************************************************/ +export const SettingsContextApp = ({ + children, + baseOptions = defaultSettings +}: TSettingsContextApp): React.ReactElement => { + const [baseSettings, set_baseSettings] = useLocalStorage( + 'yearnSettingsBase_0.0.1', + deepMerge(defaultSettings, baseOptions) as TSettingsBase, + {currentVersion: 1, shouldMigratePreviousVersion: true} + ); + + /* 💙 - Yearn Finance ********************************************************************* + ** The app can provide a new list of base options. They will be deep merged with the + ** existing ones, aka the existing declarations will be overwritten but the new ones will + ** be added. Networks settings are updated accordingly. + ******************************************************************************************/ + const onUpdateBaseSettings = useCallback((newSettings: TSettingsBase): void => { + performBatchedUpdates((): void => { + set_baseSettings(newSettings); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + /* 💙 - Yearn Finance ********************************************************************* + ** Render the SettingContext with it's parameters. + ** The parameters will be accessible to the children via the useSettings hook. + ******************************************************************************************/ + const contextValue = useMemo((): TSettingsContext => ({ + settings: baseSettings as TSettingsBase, + onUpdateBaseSettings: onUpdateBaseSettings + }), [baseSettings, onUpdateBaseSettings]); + + return ( + + {children} + + ); +}; + +export const useSettings = (): TSettingsContext => useContext(SettingsContext); +export default useSettings; diff --git a/packages/web-lib/contexts/useUI.tsx b/contexts/useUI.tsx similarity index 100% rename from packages/web-lib/contexts/useUI.tsx rename to contexts/useUI.tsx diff --git a/packages/web-lib/contexts/useWeb3.tsx b/contexts/useWeb3.tsx similarity index 82% rename from packages/web-lib/contexts/useWeb3.tsx rename to contexts/useWeb3.tsx index 37160bfb..f7f20999 100755 --- a/packages/web-lib/contexts/useWeb3.tsx +++ b/contexts/useWeb3.tsx @@ -1,17 +1,22 @@ import React, {createContext, useCallback, useContext, useMemo, useState} from 'react'; +import assert from 'assert'; import {useAccount, useConnect, useDisconnect, useEnsName, useNetwork, usePublicClient, useSwitchNetwork, useWalletClient, WagmiConfig} from 'wagmi'; import {useIsMounted, useUpdateEffect} from '@react-hookz/web'; +import {EthereumClient} from '@web3modal/ethereum'; +import {Web3Modal} from '@web3modal/react'; import {ModalLogin} from '@yearn-finance/web-lib/components/ModalLogin'; import {deepMerge} from '@yearn-finance/web-lib/contexts/utils'; -import {useSupportedChainsID} from '@yearn-finance/web-lib/hooks/useSupportedChainsID'; import {toAddress} from '@yearn-finance/web-lib/utils/address'; import {isIframe} from '@yearn-finance/web-lib/utils/helpers'; +import {getConfig, getSupportedProviders} from '../utils/wagmi/config'; +import {configureChains} from '../utils/wagmi/configChain.tmp'; + import type {ReactElement} from 'react'; import type {BaseError, FallbackTransport} from 'viem'; import type {Config, PublicClient, WebSocketPublicClient} from 'wagmi'; import type {TWeb3Context, TWeb3Options} from '@yearn-finance/web-lib/types/contexts'; -import type {ConnectResult} from '@wagmi/core'; +import type {Chain, ConnectResult} from '@wagmi/core'; const defaultState = { address: undefined, @@ -48,9 +53,16 @@ export const Web3ContextAppWrapper = ({children, options}: {children: ReactEleme const publicClient = usePublicClient(); const isMounted = useIsMounted(); const web3Options = deepMerge(defaultOptions, options) as TWeb3Options; - const supportedChainsID = useSupportedChainsID(); const [isModalLoginOpen, set_isModalLoginOpen] = useState(false); + const supportedChainsID = useMemo((): number[] => { + const injectedConnector = connectors.find((e): boolean => (e.id).toLocaleLowerCase() === 'injected'); + assert(injectedConnector, 'No injected connector found'); + const chainsForInjected = injectedConnector.chains; + const noTestnet = chainsForInjected.filter(({id}): boolean => id !== 1337); + return noTestnet.map((network: Chain): number => network.id); + }, [connectors]); + useUpdateEffect((): void => { set_currentChainID(chain?.id); }, [chain]); @@ -83,6 +95,7 @@ export const Web3ContextAppWrapper = ({children, options}: {children: ReactEleme if (r?.account) { return onSuccess?.(); } + return; } if (providerType === 'INJECTED' && injectedConnector) { @@ -196,17 +209,33 @@ export const Web3ContextAppWrapper = ({children, options}: {children: ReactEleme ); }; -export const Web3ContextApp = ({children, config, options}: { +export const Web3ContextApp = ({children, supportedChains, options}: { children: ReactElement, - config: Config, WebSocketPublicClient>, + supportedChains: Chain[], options?: TWeb3Options }): ReactElement => { + + const config = useMemo((): Config, WebSocketPublicClient> => { + const {chains, publicClient, webSocketPublicClient} = configureChains( + supportedChains, + getSupportedProviders() + ); + return getConfig({chains, publicClient, webSocketPublicClient}); + }, [supportedChains]); + + const ethereumClient = useMemo((): EthereumClient => new EthereumClient(config, supportedChains), [config, supportedChains]); + return ( - - - {children} - - + <> + + + {children} + + + + ); }; diff --git a/packages/web-lib/contexts/utils.ts b/contexts/utils.ts similarity index 100% rename from packages/web-lib/contexts/utils.ts rename to contexts/utils.ts diff --git a/packages/web-lib/hooks/useAddToken.ts b/hooks/useAddToken.ts similarity index 73% rename from packages/web-lib/hooks/useAddToken.ts rename to hooks/useAddToken.ts index 236b07c4..2d8ca9da 100644 --- a/packages/web-lib/hooks/useAddToken.ts +++ b/hooks/useAddToken.ts @@ -1,7 +1,7 @@ import {yToast} from '@yearn-finance/web-lib/components/yToast'; import {useDismissToasts} from '@yearn-finance/web-lib/hooks/useDismissToasts'; -declare let window: any; +declare let window: unknown; type TWatchAssetOptions = { address: string; // The address of the token contract @@ -16,13 +16,24 @@ export function useAddToken(): (options: TWatchAssetOptions) => void { return (options: TWatchAssetOptions): void => { - window.ethereum.request({ - method: 'wallet_watchAsset', - params: { - type: 'ERC20', - options + (window as { + ethereum: { + request: (args: { + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options: TWatchAssetOptions + } + }) => Promise } - }) + }).ethereum + .request({ + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options + } + }) .then((success: boolean): void => { dismissAllToasts(); diff --git a/packages/web-lib/hooks/useAutoConnect.ts b/hooks/useAutoConnect.ts similarity index 100% rename from packages/web-lib/hooks/useAutoConnect.ts rename to hooks/useAutoConnect.ts diff --git a/hooks/useBalances.ts b/hooks/useBalances.ts new file mode 100644 index 00000000..82b63ca8 --- /dev/null +++ b/hooks/useBalances.ts @@ -0,0 +1,385 @@ +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {erc20ABI, useChainId} from 'wagmi'; +import {deserialize, multicall, serialize} from '@wagmi/core'; +import {useUI} from '@yearn-finance/web-lib/contexts/useUI'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import AGGREGATE3_ABI from '@yearn-finance/web-lib/utils/abi/aggregate.abi'; +import {isZeroAddress, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {MULTICALL3_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {decodeAsBigInt, decodeAsNumber, decodeAsString} from '@yearn-finance/web-lib/utils/decoder'; +import {toBigInt, toNormalizedValue} from '@yearn-finance/web-lib/utils/format'; +import {isEth} from '@yearn-finance/web-lib/utils/isEth'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import performBatchedUpdates from '@yearn-finance/web-lib/utils/performBatchedUpdates'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; + +import type {DependencyList} from 'react'; +import type {ContractFunctionConfig} from 'viem'; +import type {Connector} from 'wagmi'; +import type {TAddress, TDict, TNDict} from '@yearn-finance/web-lib/types'; +import type {TBalanceData, TDefaultStatus} from '@yearn-finance/web-lib/types/hooks'; +import type {TYDaemonPrices} from '@yearn-finance/web-lib/utils/schemas/yDaemonPricesSchema'; + +/* 🔵 - Yearn Finance ********************************************************** +** Request, Response and helpers for the useBalances hook. +******************************************************************************/ +export type TUseBalancesTokens = { + token: string, + decimals?: number, + name?: string, + symbol?: string, + for?: string, +} +export type TUseBalancesReq = { + key?: string | number, + tokens: TUseBalancesTokens[] + prices?: TYDaemonPrices, + effectDependencies?: DependencyList + provider?: Connector, +} + +export type TUseBalancesRes = { + data: TDict, + update: () => Promise>, + updateSome: (token: TUseBalancesTokens[]) => Promise>, + error?: Error, + status: 'error' | 'loading' | 'success' | 'unknown', + nonce: number +} & TDefaultStatus + +type TDataRef = { + nonce: number, + address: TAddress, + balances: TDict, +} + +/* 🔵 - Yearn Finance ********************************************************** +** Default status for the loading state. +******************************************************************************/ +const defaultStatus = { + isLoading: false, + isFetching: false, + isSuccess: false, + isError: false, + isFetched: false, + isRefetching: false +}; + +async function performCall( + chainID: number, + calls: ContractFunctionConfig[], + tokens: TUseBalancesTokens[], + prices?: TYDaemonPrices +): Promise<[TDict, Error | undefined]> { + const _data: TDict = {}; + const results = await multicall({contracts: calls as never[], chainId: chainID}); + + let rIndex = 0; + for (const element of tokens) { + const {token, decimals: injectedDecimals, name: injectedName, symbol: injectedSymbol} = element; + const balanceOf = decodeAsBigInt(results[rIndex++]); + const decimals = decodeAsNumber(results[rIndex++]) || injectedDecimals || 18; + const rawPrice = toBigInt(prices?.[toAddress(token)]); + let symbol = decodeAsString(results[rIndex++]) || injectedSymbol || ''; + let name = decodeAsString(results[rIndex++]) || injectedName || ''; + if (isEth(token)) { + const nativeTokenWrapper = getNetwork(chainID)?.contracts?.wrappedToken; + if (nativeTokenWrapper) { + symbol = nativeTokenWrapper.coinSymbol; + name = nativeTokenWrapper.coinName; + } + } + + _data[toAddress(token)] = { + decimals: decimals, + symbol: symbol, + name: name, + raw: balanceOf, + rawPrice, + normalized: toNormalizedValue(balanceOf, decimals), + normalizedPrice: toNormalizedValue(rawPrice, 6), + normalizedValue: (toNormalizedValue(balanceOf, decimals) * toNormalizedValue(rawPrice, 6)) + }; + } + return [_data, undefined]; +} + +async function getBalances( + chainID: number, + address: TAddress, + tokens: TUseBalancesTokens[], + prices?: TYDaemonPrices +): Promise<[TDict, Error | undefined]> { + let result: TDict = {}; + const calls: ContractFunctionConfig[] = []; + + for (const element of tokens) { + const {token} = element; + const ownerAddress = address; + if (isEth(token)) { + const nativeTokenWrapper = getNetwork(chainID)?.contracts?.wrappedToken; + if (!nativeTokenWrapper) { + console.error('No native token wrapper found for chainID', chainID); + continue; + } + const multicall3Contract = {address: MULTICALL3_ADDRESS, abi: AGGREGATE3_ABI}; + const baseContract = {address: nativeTokenWrapper.address, abi: erc20ABI}; + calls.push({...multicall3Contract, functionName: 'getEthBalance', args: [ownerAddress]}); + calls.push({...baseContract, functionName: 'decimals'}); + calls.push({...baseContract, functionName: 'symbol'}); + calls.push({...baseContract, functionName: 'name'}); + } else { + const baseContract = {address: toAddress(token), abi: erc20ABI}; + calls.push({...baseContract, functionName: 'balanceOf', args: [ownerAddress]}); + calls.push({...baseContract, functionName: 'decimals'}); + calls.push({...baseContract, functionName: 'symbol'}); + calls.push({...baseContract, functionName: 'name'}); + } + } + + try { + const [callResult] = await performCall(chainID, calls, tokens, prices); + result = {...result, ...callResult}; + return [result, undefined]; + } catch (_error) { + console.error(_error); + return [result, _error as Error]; + } +} + + +/* 🔵 - Yearn Finance ****************************************************** +** This hook can be used to fetch balance information for any ERC20 tokens. +**************************************************************************/ +export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { + const {address: web3Address, isActive, provider} = useWeb3(); + const chainID = useChainId(); + const {onLoadStart, onLoadDone} = useUI(); + const [nonce, set_nonce] = useState(0); + const [status, set_status] = useState(defaultStatus); + const [error, set_error] = useState(undefined); + const [balances, set_balances] = useState>>({}); + const data = useRef>({1: {nonce: 0, address: toAddress(), balances: {}}}); + const stringifiedTokens = useMemo((): string => serialize(props?.tokens || []), [props?.tokens]); + + const updateBalancesCall = useCallback((chainID: number, newRawData: TDict): TDict => { + if (toAddress(web3Address) !== data?.current?.[chainID]?.address) { + data.current[chainID] = { + address: toAddress(web3Address), + balances: {}, + nonce: 0 + }; + } + data.current[chainID].address = toAddress(web3Address); + + for (const [address, element] of Object.entries(newRawData)) { + element.raw = element.raw || 0n; + data.current[chainID].balances[address] = { + ...data.current[chainID].balances[address], + ...element + }; + } + data.current[chainID].nonce += 1; + + performBatchedUpdates((): void => { + set_balances((b): TNDict> => ({ + ...b, + [chainID]: { + ...(b[chainID] || {}), + ...data.current[chainID].balances + } + })); + set_nonce((n): number => n + 1); + }); + return data.current[chainID].balances; + }, [web3Address]); + + /* 🔵 - Yearn Finance ****************************************************** + ** onUpdate will take the stringified tokens and fetch the balances for each + ** token. It will then update the balances state with the new balances. + ** This takes the whole list and is not optimized for performance, aka not + ** send in a worker. + **************************************************************************/ + const onUpdate = useCallback(async (): Promise> => { + if (!web3Address || !provider) { + return {}; + } + const tokenList = deserialize(stringifiedTokens) || []; + const tokens = tokenList.filter(({token}: TUseBalancesTokens): boolean => !isZeroAddress(token)); + if (isZero(tokens.length)) { + return {}; + } + set_status({...defaultStatus, isLoading: true, isFetching: true, isRefetching: defaultStatus.isFetched}); + onLoadStart(); + + const chunks = []; + for (let i = 0; i < tokens.length; i += 5_000) { + chunks.push(tokens.slice(i, i + 5_000)); + } + + for (const chunkTokens of chunks) { + const [newRawData, err] = await getBalances((chainID || 1), web3Address, chunkTokens); + if (toAddress(web3Address as string) !== data?.current?.[chainID]?.address) { + data.current[chainID] = { + address: toAddress(web3Address as string), + balances: {}, + nonce: 0 + }; + } + data.current[chainID].address = toAddress(web3Address as string); + + for (const [address, element] of Object.entries(newRawData)) { + data.current[chainID].balances[address] = { + ...data.current[chainID].balances[address], + ...element + }; + } + data.current[chainID].nonce += 1; + + performBatchedUpdates((): void => { + set_balances((b): TNDict> => ({ + ...b, + [chainID]: { + ...(b[chainID] || {}), + ...data.current[chainID].balances + } + })); + set_nonce((n): number => n + 1); + set_error(err as Error); + set_status({...defaultStatus, isSuccess: true, isFetched: true}); + }); + } + onLoadDone(); + + return data.current[chainID].balances; + }, [onLoadDone, onLoadStart, provider, stringifiedTokens, web3Address, chainID]); + + /* 🔵 - Yearn Finance ****************************************************** + ** onUpdateSome takes a list of tokens and fetches the balances for each + ** token. Even if it's not optimized for performance, it should not be an + ** issue as it should only be used for a little list of tokens. + **************************************************************************/ + const onUpdateSome = useCallback(async (tokenList: TUseBalancesTokens[]): Promise> => { + set_status({...defaultStatus, isLoading: true, isFetching: true, isRefetching: defaultStatus.isFetched}); + onLoadStart(); + const tokens = tokenList.filter(({token}: TUseBalancesTokens): boolean => !isZeroAddress(token)); + const chunks = []; + for (let i = 0; i < tokens.length; i += 2_000) { + chunks.push(tokens.slice(i, i + 2_000)); + } + + const tokensAdded: TDict = {}; + for (const chunkTokens of chunks) { + const [newRawData, err] = await getBalances((chainID || 1), toAddress(web3Address as string), chunkTokens); + if (toAddress(web3Address as string) !== data?.current?.[chainID]?.address) { + data.current[chainID] = { + address: toAddress(web3Address as string), + balances: {}, + nonce: 0 + }; + } + data.current[chainID].address = toAddress(web3Address as string); + + for (const [address, element] of Object.entries(newRawData)) { + tokensAdded[address] = element; + data.current[chainID].balances[address] = { + ...data.current[chainID].balances[address], + ...element + }; + } + data.current[chainID].nonce += 1; + + performBatchedUpdates((): void => { + set_balances((b): TNDict> => ({ + ...b, + [chainID]: { + ...(b[chainID] || {}), + ...data.current[chainID].balances + } + })); + set_nonce((n): number => n + 1); + set_error(err as Error); + set_status({...defaultStatus, isSuccess: true, isFetched: true}); + }); + } + onLoadDone(); + return tokensAdded; + }, [onLoadDone, onLoadStart, web3Address, chainID]); + + const assignPrices = useCallback((_rawData: TNDict>): TNDict> => { + for (const chainIDStr of Object.keys(_rawData)) { + const chainID = Number(chainIDStr); + for (const address of Object.keys(_rawData[chainID])) { + const tokenAddress = toAddress(address); + const rawPrice = toBigInt(props?.prices?.[tokenAddress]); + if (!_rawData[chainID]) { + _rawData[chainID] = {}; + } + _rawData[chainID][tokenAddress] = { + ..._rawData[chainID][tokenAddress], + rawPrice, + normalizedPrice: toNormalizedValue(rawPrice, 6), + normalizedValue: ((_rawData[chainID]?.[tokenAddress] || 0).normalized * toNormalizedValue(rawPrice, 6)) + }; + } + } + return _rawData; + }, [props?.prices]); + + const asyncUseEffect = useCallback(async (): Promise => { + if (!isActive || !web3Address || !provider) { + return; + } + set_status({...defaultStatus, isLoading: true, isFetching: true, isRefetching: defaultStatus.isFetched}); + onLoadStart(); + + const tokens = JSON.parse(stringifiedTokens) || []; + const chunks = []; + for (let i = 0; i < tokens.length; i += 100) { + chunks.push(tokens.slice(i, i + 100)); + } + const allPromises = []; + for (const chunkTokens of chunks) { + allPromises.push( + getBalances((chainID || 1), web3Address, chunkTokens) + .then(async ([newRawData, err]): Promise => { + updateBalancesCall((chainID || 1), newRawData); + set_error(err); + }) + ); + } + await Promise.all(allPromises); + onLoadDone(); + set_status({...defaultStatus, isSuccess: true, isFetched: true}); + }, [stringifiedTokens, isActive, web3Address, provider, onLoadStart, chainID, updateBalancesCall, onLoadDone]); + + /* 🔵 - Yearn Finance ****************************************************** + ** Everytime the stringifiedTokens change, we need to update the balances. + ** This is the main hook and is optimized for performance, using a worker + ** to fetch the balances, preventing the UI to freeze. + **************************************************************************/ + useEffect((): void => { + asyncUseEffect(); + }, [asyncUseEffect]); + + const contextValue = useMemo((): TUseBalancesRes => ({ + data: assignPrices(balances || {})?.[chainID] || {}, + nonce, + update: onUpdate, + updateSome: onUpdateSome, + error, + isLoading: status.isLoading, + isFetching: status.isFetching, + isSuccess: status.isSuccess, + isError: status.isError, + isFetched: status.isFetched, + isRefetching: status.isRefetching, + status: ( + status.isError ? 'error' : + (status.isLoading || status.isFetching) ? 'loading' : + (status.isSuccess) ? 'success' : 'unknown' + ) + }), [assignPrices, balances, chainID, error, nonce, onUpdate, onUpdateSome, status.isError, status.isFetched, status.isFetching, status.isLoading, status.isRefetching, status.isSuccess]); + + return (contextValue); +} diff --git a/packages/web-lib/hooks/useChainID.tsx b/hooks/useChainID.tsx similarity index 100% rename from packages/web-lib/hooks/useChainID.tsx rename to hooks/useChainID.tsx diff --git a/packages/web-lib/hooks/useDismissToasts.ts b/hooks/useDismissToasts.ts similarity index 100% rename from packages/web-lib/hooks/useDismissToasts.ts rename to hooks/useDismissToasts.ts diff --git a/packages/web-lib/hooks/useInjectedWallet.tsx b/hooks/useInjectedWallet.tsx similarity index 100% rename from packages/web-lib/hooks/useInjectedWallet.tsx rename to hooks/useInjectedWallet.tsx diff --git a/packages/web-lib/hooks/useLocalStorage.tsx b/hooks/useLocalStorage.tsx similarity index 100% rename from packages/web-lib/hooks/useLocalStorage.tsx rename to hooks/useLocalStorage.tsx diff --git a/packages/web-lib/hooks/useSessionStorage.ts b/hooks/useSessionStorage.ts similarity index 95% rename from packages/web-lib/hooks/useSessionStorage.ts rename to hooks/useSessionStorage.ts index a77aebf0..527258fa 100644 --- a/packages/web-lib/hooks/useSessionStorage.ts +++ b/hooks/useSessionStorage.ts @@ -15,7 +15,7 @@ type TSetValue = Dispatch> const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; function useEventCallback(fn: (...args: TArgs) => TR): (...args: TArgs) => TR { - const ref = useRef((): any => { + const ref = useRef((): never => { throw new Error('Cannot call an event handler while rendering.'); }); @@ -23,7 +23,7 @@ function useEventCallback(fn: (...args: TArgs) => T ref.current = fn; }, [fn]); - return useCallback((...args: TArgs): any => ref.current(...args), [ref]); + return useCallback((...args: TArgs): TR => ref.current(...args), [ref]); } // Window Event based useEventListener interface @@ -80,7 +80,7 @@ function useEventListener< } // Create event listener that calls handler function stored in ref - const eventListener: typeof handler = (event): any => savedHandler.current(event); + const eventListener: typeof handler = (event): unknown => savedHandler.current(event); targetElement.addEventListener(eventName, eventListener, options); diff --git a/packages/web-lib/icons/IconAddToMetamask.tsx b/icons/IconAddToMetamask.tsx similarity index 100% rename from packages/web-lib/icons/IconAddToMetamask.tsx rename to icons/IconAddToMetamask.tsx diff --git a/packages/web-lib/icons/IconAlertCritical.tsx b/icons/IconAlertCritical.tsx similarity index 100% rename from packages/web-lib/icons/IconAlertCritical.tsx rename to icons/IconAlertCritical.tsx diff --git a/packages/web-lib/icons/IconAlertError.tsx b/icons/IconAlertError.tsx similarity index 100% rename from packages/web-lib/icons/IconAlertError.tsx rename to icons/IconAlertError.tsx diff --git a/packages/web-lib/icons/IconAlertWarning.tsx b/icons/IconAlertWarning.tsx similarity index 100% rename from packages/web-lib/icons/IconAlertWarning.tsx rename to icons/IconAlertWarning.tsx diff --git a/packages/web-lib/icons/IconArrowDown.tsx b/icons/IconArrowDown.tsx similarity index 100% rename from packages/web-lib/icons/IconArrowDown.tsx rename to icons/IconArrowDown.tsx diff --git a/packages/web-lib/icons/IconCheckmark.tsx b/icons/IconCheckmark.tsx similarity index 100% rename from packages/web-lib/icons/IconCheckmark.tsx rename to icons/IconCheckmark.tsx diff --git a/packages/web-lib/icons/IconChevron.tsx b/icons/IconChevron.tsx similarity index 100% rename from packages/web-lib/icons/IconChevron.tsx rename to icons/IconChevron.tsx diff --git a/packages/web-lib/icons/IconChevronBottom.tsx b/icons/IconChevronBottom.tsx similarity index 100% rename from packages/web-lib/icons/IconChevronBottom.tsx rename to icons/IconChevronBottom.tsx diff --git a/packages/web-lib/icons/IconCopy.tsx b/icons/IconCopy.tsx similarity index 100% rename from packages/web-lib/icons/IconCopy.tsx rename to icons/IconCopy.tsx diff --git a/packages/web-lib/icons/IconCross.tsx b/icons/IconCross.tsx similarity index 100% rename from packages/web-lib/icons/IconCross.tsx rename to icons/IconCross.tsx diff --git a/packages/web-lib/icons/IconDashboard.tsx b/icons/IconDashboard.tsx similarity index 100% rename from packages/web-lib/icons/IconDashboard.tsx rename to icons/IconDashboard.tsx diff --git a/packages/web-lib/icons/IconGrip.tsx b/icons/IconGrip.tsx similarity index 100% rename from packages/web-lib/icons/IconGrip.tsx rename to icons/IconGrip.tsx diff --git a/packages/web-lib/icons/IconHamburger.tsx b/icons/IconHamburger.tsx similarity index 100% rename from packages/web-lib/icons/IconHamburger.tsx rename to icons/IconHamburger.tsx diff --git a/packages/web-lib/icons/IconHome.tsx b/icons/IconHome.tsx similarity index 100% rename from packages/web-lib/icons/IconHome.tsx rename to icons/IconHome.tsx diff --git a/packages/web-lib/icons/IconLabs.tsx b/icons/IconLabs.tsx similarity index 100% rename from packages/web-lib/icons/IconLabs.tsx rename to icons/IconLabs.tsx diff --git a/packages/web-lib/icons/IconLinkOut.tsx b/icons/IconLinkOut.tsx similarity index 100% rename from packages/web-lib/icons/IconLinkOut.tsx rename to icons/IconLinkOut.tsx diff --git a/packages/web-lib/icons/IconLoader.tsx b/icons/IconLoader.tsx similarity index 100% rename from packages/web-lib/icons/IconLoader.tsx rename to icons/IconLoader.tsx diff --git a/packages/web-lib/icons/IconLogoYearn.tsx b/icons/IconLogoYearn.tsx similarity index 100% rename from packages/web-lib/icons/IconLogoYearn.tsx rename to icons/IconLogoYearn.tsx diff --git a/packages/web-lib/icons/IconNetworkArbitrum.tsx b/icons/IconNetworkArbitrum.tsx similarity index 100% rename from packages/web-lib/icons/IconNetworkArbitrum.tsx rename to icons/IconNetworkArbitrum.tsx diff --git a/packages/web-lib/icons/IconNetworkEthereum.tsx b/icons/IconNetworkEthereum.tsx similarity index 100% rename from packages/web-lib/icons/IconNetworkEthereum.tsx rename to icons/IconNetworkEthereum.tsx diff --git a/packages/web-lib/icons/IconNetworkFantom.tsx b/icons/IconNetworkFantom.tsx similarity index 100% rename from packages/web-lib/icons/IconNetworkFantom.tsx rename to icons/IconNetworkFantom.tsx diff --git a/packages/web-lib/icons/IconNetworkOptimism.tsx b/icons/IconNetworkOptimism.tsx similarity index 100% rename from packages/web-lib/icons/IconNetworkOptimism.tsx rename to icons/IconNetworkOptimism.tsx diff --git a/packages/web-lib/icons/IconSearch.tsx b/icons/IconSearch.tsx similarity index 100% rename from packages/web-lib/icons/IconSearch.tsx rename to icons/IconSearch.tsx diff --git a/packages/web-lib/icons/IconSettings.tsx b/icons/IconSettings.tsx similarity index 100% rename from packages/web-lib/icons/IconSettings.tsx rename to icons/IconSettings.tsx diff --git a/packages/web-lib/icons/IconSocialDiscord.tsx b/icons/IconSocialDiscord.tsx similarity index 100% rename from packages/web-lib/icons/IconSocialDiscord.tsx rename to icons/IconSocialDiscord.tsx diff --git a/packages/web-lib/icons/IconSocialGithub.tsx b/icons/IconSocialGithub.tsx similarity index 100% rename from packages/web-lib/icons/IconSocialGithub.tsx rename to icons/IconSocialGithub.tsx diff --git a/packages/web-lib/icons/IconSocialMedium.tsx b/icons/IconSocialMedium.tsx similarity index 100% rename from packages/web-lib/icons/IconSocialMedium.tsx rename to icons/IconSocialMedium.tsx diff --git a/packages/web-lib/icons/IconSocialTwitter.tsx b/icons/IconSocialTwitter.tsx similarity index 100% rename from packages/web-lib/icons/IconSocialTwitter.tsx rename to icons/IconSocialTwitter.tsx diff --git a/packages/web-lib/icons/IconThemeDark.tsx b/icons/IconThemeDark.tsx similarity index 100% rename from packages/web-lib/icons/IconThemeDark.tsx rename to icons/IconThemeDark.tsx diff --git a/packages/web-lib/icons/IconThemeLight.tsx b/icons/IconThemeLight.tsx similarity index 100% rename from packages/web-lib/icons/IconThemeLight.tsx rename to icons/IconThemeLight.tsx diff --git a/packages/web-lib/icons/IconVault.tsx b/icons/IconVault.tsx similarity index 100% rename from packages/web-lib/icons/IconVault.tsx rename to icons/IconVault.tsx diff --git a/packages/web-lib/icons/IconWallet.tsx b/icons/IconWallet.tsx similarity index 100% rename from packages/web-lib/icons/IconWallet.tsx rename to icons/IconWallet.tsx diff --git a/packages/web-lib/icons/IconWalletCoinbase.tsx b/icons/IconWalletCoinbase.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletCoinbase.tsx rename to icons/IconWalletCoinbase.tsx diff --git a/packages/web-lib/icons/IconWalletFrame.tsx b/icons/IconWalletFrame.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletFrame.tsx rename to icons/IconWalletFrame.tsx diff --git a/packages/web-lib/icons/IconWalletLedger.tsx b/icons/IconWalletLedger.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletLedger.tsx rename to icons/IconWalletLedger.tsx diff --git a/packages/web-lib/icons/IconWalletMetamask.tsx b/icons/IconWalletMetamask.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletMetamask.tsx rename to icons/IconWalletMetamask.tsx diff --git a/packages/web-lib/icons/IconWalletOKX.tsx b/icons/IconWalletOKX.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletOKX.tsx rename to icons/IconWalletOKX.tsx diff --git a/packages/web-lib/icons/IconWalletPhantom.tsx b/icons/IconWalletPhantom.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletPhantom.tsx rename to icons/IconWalletPhantom.tsx diff --git a/packages/web-lib/icons/IconWalletSafe.tsx b/icons/IconWalletSafe.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletSafe.tsx rename to icons/IconWalletSafe.tsx diff --git a/packages/web-lib/icons/IconWalletTrustWallet.tsx b/icons/IconWalletTrustWallet.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletTrustWallet.tsx rename to icons/IconWalletTrustWallet.tsx diff --git a/packages/web-lib/icons/IconWalletWalletConnect.tsx b/icons/IconWalletWalletConnect.tsx similarity index 100% rename from packages/web-lib/icons/IconWalletWalletConnect.tsx rename to icons/IconWalletWalletConnect.tsx diff --git a/packages/web-lib/jest.config.cjs b/jest.config.cjs similarity index 100% rename from packages/web-lib/jest.config.cjs rename to jest.config.cjs diff --git a/lerna.json b/lerna.json deleted file mode 100755 index 37e0ed86..00000000 --- a/lerna.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "useWorkspaces": true, - "version": "0.2.0" -} diff --git a/package.json b/package.json index bbc7771b..fb8ebe21 100755 --- a/package.json +++ b/package.json @@ -1,17 +1,118 @@ { - "name": "@yearn-finance/web-lib-root", - "version": "0.13.28", - "private": true, - "workspaces": [ - "packages/*" - ], - "devDependencies": { - "lerna": "5.1.6" - }, - "scripts": { - "bootstrap": "lerna bootstrap --hoist", - "start": "lerna run dev --stream", - "watch": "lerna run watch --stream", - "lint": "lerna run lint --stream" - } -} + "name": "@yearn-finance/web-lib", + "version": "0.20.149", + "files": [ + "." + ], + "type": "module", + "scripts": { + "dev": "tsc --watch", + "exportStyle": "npx tailwindcss --postcss -c ./tailwind.config.cjs -i style.css -o dist/build.css --minify && sed -i -e 's/rem/em/g' dist/build.css", + "exportNextStyle": "npx tailwindcss --postcss -c ./tailwind.config.cjs -i style.css -o dist/build.css --minify && sed -i -e 's/rem/em/g' dist/build.css", + "prebuild": "rm -rf dist", + "build": "bump && tsc --module es2022 --outDir dist --jsx react", + "buildNoBump": "tsc --module es2022 --outDir dist --jsx react", + "postbuild": "sh ./scripts/postbuild.sh && npm run exportStyle && npm run exportNextStyle", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + }, + "dependencies": { + "@babel/preset-typescript": "^7.22.11", + "@headlessui/react": "^1.7.17", + "@ledgerhq/iframe-provider": "^0.4.3", + "@react-hookz/deep-equal": "^1.0.4", + "@react-hookz/web": "^23.1.0", + "@safe-global/safe-apps-provider": "^0.17.1", + "@safe-global/safe-apps-sdk": "^8.0.0", + "@tailwindcss/forms": "^0.5.6", + "@tailwindcss/typography": "^0.5.9", + "@types/node": "^20.5.7", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.5.0", + "@typescript-eslint/parser": "^6.5.0", + "@wagmi/core": "^1.3.10", + "@web3modal/ethereum": "^2.7.1", + "@web3modal/react": "^2.7.1", + "axios": "^1.5.0", + "babel-loader": "^9.1.3", + "dayjs": "^1.11.9", + "eslint": "^8.48.0", + "eslint-config-next": "^13.4.19", + "eslint-plugin-tailwindcss": "^3.13.0", + "eslint-plugin-unused-imports": "^3.0.0", + "ethers": "5.7.2", + "graphql": "^16.8.0", + "graphql-request": "^6.1.0", + "next": "^13.4.19", + "nprogress": "^0.2.0", + "postcss": "^8.4.29", + "postcss-nesting": "^12.0.1", + "react": "18.2.0", + "react-dom": "^18.2.0", + "react-hot-toast": "2.4.1", + "sass": "^1.66.1", + "sharp": "^0.32.5", + "tailwindcss": "^3.3.3", + "ts-jest": "^29.1.1", + "ts-loader": "^9.4.4", + "tsup": "^7.2.0", + "typescript": "^5.2.2", + "viem": "^1.9.3", + "vitest": "^0.34.3", + "wagmi": "^1.3.11", + "zod": "^3.22.2" + }, + "devDependencies": { + "@babel/preset-typescript": "^7.22.5", + "@types/express": "^4.17.17", + "@types/ioredis": "^4.28.10", + "@types/jest": "^29.5.4", + "@types/node": "^20.4.5", + "@types/nprogress": "^0.2.0", + "@types/react": "^18.2.17", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.0", + "autoprefixer": "^10.4.15", + "babel-loader": "^9.1.3", + "bump": "^0.2.5", + "eslint": "^8.45.0", + "eslint-config-next": "^13.4.12", + "eslint-import-resolver-typescript": "^3.6.0", + "eslint-plugin-brackets": "^0.1.3", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-tailwindcss": "^3.13.0", + "eslint-plugin-unused-imports": "^3.0.0", + "jest": "^29.6.4", + "postcss": "^8.4.27", + "postcss-import": "^15.1.0", + "postcss-nesting": "^12.0.0", + "sass": "^1.64.1", + "sharp": "^0.32.4", + "tailwindcss": "^3.3.3", + "ts-jest": "^29.1.1", + "ts-loader": "^9.4.4", + "tsup": "^7.1.0", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "@headlessui/react": ">= 1.7.0", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", + "eslint": ">= 8.25.0", + "eslint-config-next": ">= 12.3.1", + "eslint-import-resolver-typescript": ">= 3.5.1", + "eslint-plugin-brackets": ">= 0.1.3", + "eslint-plugin-import": ">= 2.26.0", + "eslint-plugin-react": ">= 7.31.10", + "eslint-plugin-simple-import-sort": ">= 8.0.0", + "eslint-plugin-tailwindcss": ">= 3.6.2", + "eslint-plugin-unused-imports": ">= 2.0.0", + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" + } +} \ No newline at end of file diff --git a/packages/docs/.eslintrc.js b/packages/docs/.eslintrc.js deleted file mode 100755 index f25ac059..00000000 --- a/packages/docs/.eslintrc.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - 'extends': ['../../node_modules/@yearn-finance/web-lib/.eslintrc.cjs'], - 'parser': '@typescript-eslint/parser', - 'parserOptions': { - 'ecmaFeatures': { - 'jsx': true - }, - 'tsconfigRootDir': __dirname, - 'ecmaVersion': 2022, - 'sourceType': 'module', - 'project': ['./tsconfig.json'] - } -}; diff --git a/packages/docs/components/CodeExample.tsx b/packages/docs/components/CodeExample.tsx deleted file mode 100755 index 8a9de171..00000000 --- a/packages/docs/components/CodeExample.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import {RadialBackground} from 'components/RadialBackground'; - -import type {ReactElement, ReactNode} from 'react'; - -function CodeExample({children}: {children: ReactNode}): ReactElement { - return ( -
- {children} - -
- ); -} - -export {CodeExample}; -export default CodeExample; diff --git a/packages/docs/components/Header.tsx b/packages/docs/components/Header.tsx deleted file mode 100755 index c9cced93..00000000 --- a/packages/docs/components/Header.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import Image from 'next/image'; - -import type {ReactElement} from 'react'; - -export function Header(): ReactElement { - return ( -
-
-

{'Yearn Web Lib'}

- - - - - -
- -

- {'Yearn Web Lib'} -

- -
-
- ); -} diff --git a/packages/docs/components/RadialBackground.tsx b/packages/docs/components/RadialBackground.tsx deleted file mode 100755 index 62541f74..00000000 --- a/packages/docs/components/RadialBackground.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import type {ReactElement} from 'react'; - -function RadialBackground(): ReactElement { - return ( -
-
-
- ); -} -export {RadialBackground}; diff --git a/packages/docs/components/specific/DropdownExample.tsx b/packages/docs/components/specific/DropdownExample.tsx deleted file mode 100755 index bc356c7a..00000000 --- a/packages/docs/components/specific/DropdownExample.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, {useState} from 'react'; -import {CodeExample} from 'components/CodeExample'; -import {Dropdown} from '@yearn-finance/web-lib/components/Dropdown'; -import IconNetworkArbitrum from '@yearn-finance/web-lib/icons/IconNetworkArbitrum'; -import IconNetworkEthereum from '@yearn-finance/web-lib/icons/IconNetworkEthereum'; -import IconNetworkFantom from '@yearn-finance/web-lib/icons/IconNetworkFantom'; -import IconNetworkOptimism from '@yearn-finance/web-lib/icons/IconNetworkOptimism'; - -function DropdownExample(): React.ReactElement { - const options = [ - {icon: , label: 'Ethereum', value: 1}, - {icon: , label: 'Optimism', value: 10}, - {icon: , label: 'Fantom', value: 250}, - {icon: , label: 'Arbitrum', value: 42161}, - {label: 'No Icon', value: 123} - ]; - const [selectedOption, set_selectedOption] = useState(options[0]); - - return ( - - set_selectedOption(option)} /> - - ); -} - -export {DropdownExample}; -export default DropdownExample; diff --git a/packages/docs/components/specific/ModalExample.tsx b/packages/docs/components/specific/ModalExample.tsx deleted file mode 100755 index 7aaedae7..00000000 --- a/packages/docs/components/specific/ModalExample.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, {useState} from 'react'; -import {CodeExample} from 'components/CodeExample'; -import {Button} from '@yearn-finance/web-lib/components/Button'; -import {Card} from '@yearn-finance/web-lib/components/Card'; -import {Modal} from '@yearn-finance/web-lib/components/Modal'; - -function ModalExample(): React.ReactElement { - const [isOpen, set_isOpen] = useState(false); - - return ( - - - set_isOpen(false)}> - - - - - - ); -} - -export {ModalExample}; -export default ModalExample; diff --git a/packages/docs/middleware.ts b/packages/docs/middleware.ts deleted file mode 100755 index b91f7e53..00000000 --- a/packages/docs/middleware.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {NextResponse} from 'next/server'; - -import type {NextRequest} from 'next/server'; - -export const config = { - matcher: [ - '/docs', - '/docs/install/:path*', - '/docs/hooks/:path*' - ] -}; - -export async function middleware(request: NextRequest): Promise { - if (request.nextUrl.pathname === ('/docs')) { - const url = request.nextUrl.clone(); - url.pathname = url.pathname.replace('docs', ''); - return NextResponse.redirect(url); - } - if (request.nextUrl.pathname.startsWith('/docs/install')) { - const url = request.nextUrl.clone(); - url.pathname = url.pathname.replace('install', '1-install'); - return NextResponse.redirect(url); - } - if (request.nextUrl.pathname.startsWith('/docs/hooks')) { - const url = request.nextUrl.clone(); - url.pathname = url.pathname.replace('hooks', '3-web3Hooks'); - return NextResponse.redirect(url); - } - return NextResponse.next(); -} diff --git a/packages/docs/next-env.d.ts b/packages/docs/next-env.d.ts deleted file mode 100755 index 4f11a03d..00000000 --- a/packages/docs/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/docs/next.config.js b/packages/docs/next.config.js deleted file mode 100755 index 53d1ab98..00000000 --- a/packages/docs/next.config.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable no-undef */ -const withTM = require('next-transpile-modules')(['@yearn-finance/web-lib']); -const withNextra = require('nextra')({ - theme: 'nextra-theme-docs', - themeConfig: './theme.config.js', - unstable_contentDump: true, - unstable_flexsearch: true, - unstable_staticImage: true -}); - -/** @type {import('next').NextConfig} */ -const config = { - reactStrictMode: true, - typescript: { - // Disable type checking since eslint handles this - ignoreBuildErrors: true - }, - images: { - domains: ['img.shields.io'] - } -}; - -if (process.env.NODE_ENV === 'development') { - const withPreconstruct = require('@preconstruct/next'); - module.exports = withPreconstruct(withTM(withNextra(config))); -} else { - const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.ANALYZE === 'true' - }); - module.exports = withBundleAnalyzer(withTM(withNextra(config))); -} diff --git a/packages/docs/package.json b/packages/docs/package.json deleted file mode 100755 index bbdb0215..00000000 --- a/packages/docs/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "docs", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx" - }, - "dependencies": { - "@reach/skip-nav": "^0.17.0", - "@yearn-finance/web-lib": "*", - "ethers": "^5.6.9", - "next": "^12.2.3", - "next-themes": "^0.2.0", - "nextra": "2.0.0-alpha.56", - "nextra-theme-docs": "2.0.0-alpha.59", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwindcss": "^3.1.6" - }, - "devDependencies": { - "@next/bundle-analyzer": "^12.2.0", - "@next/eslint-plugin-next": "^12.2.0", - "@preconstruct/next": "^4.0.0", - "@types/node": "^17.0.38", - "@types/react": "18.0.10", - "autoprefixer": "^10.4.7", - "dotenv-webpack": "^7.1.0", - "eslint": "8.16.0", - "next-transpile-modules": "9.0.0", - "postcss": "^8.4.14", - "postcss-import": "^14.1.0", - "postcss-nesting": "^10.1.10", - "sass": "^1.53.0", - "ts-loader": "^9.3.1", - "typescript": "^4.7.4" - } -} diff --git a/packages/docs/pages/_app.tsx b/packages/docs/pages/_app.tsx deleted file mode 100755 index fd7112a1..00000000 --- a/packages/docs/pages/_app.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import {useTheme} from 'next-themes'; -import {useClientEffect} from '@yearn-finance/web-lib/hooks/useClientEffect'; - -import type {AppProps} from 'next/app'; - -import '../style.css'; - -function WrappedApp({Component, pageProps}: AppProps): React.ReactElement { - const {theme} = useTheme(); - - useClientEffect((): void => { - document.body.dataset.theme = theme; - }, [theme]); - - return ( - - ); -} - -function App(props: AppProps): React.ReactElement { - const getLayout = (props.Component as any).getLayout || ((page: React.ReactElement): React.ReactElement => page); // eslint-disable-line - - return (getLayout()); -} - -export default App; diff --git a/packages/docs/pages/_document.tsx b/packages/docs/pages/_document.tsx deleted file mode 100755 index d9d84923..00000000 --- a/packages/docs/pages/_document.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import Document, {Head, Html, Main, NextScript} from 'next/document'; -import {SkipNavLink} from '@reach/skip-nav'; - -import type {DocumentContext, DocumentInitialProps} from 'next/document'; -import type {ReactElement} from 'react'; - -const modeScript = ` - let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') - - updateMode() - darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions) - window.addEventListener('storage', updateModeWithoutTransitions) - - function updateMode() { - let isSystemDarkMode = darkModeMediaQuery.matches - let isDarkMode = true - - if (isDarkMode) { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - - if (isDarkMode === isSystemDarkMode) { - delete window.localStorage.isDarkMode - } - } - - function updateModeWithoutTransitions() { - updateMode() - } -`; - -class MyDocument extends Document { - static async getInitialProps(ctx: DocumentContext): Promise { - const initialProps = await Document.getInitialProps(ctx); - return {...initialProps}; - } - - render(): ReactElement { - return ( - - - - -