diff --git a/.env.sample b/.env.sample index d2595e90..c55c15f3 100644 --- a/.env.sample +++ b/.env.sample @@ -8,10 +8,10 @@ ALCHEMY_API_KEY= ETHERSCAN_API_KEY= # supported networks for connecting wallet -SUPPORTED_CHAINS=1,4,5 +SUPPORTED_CHAINS=1,5 # this chain uses when a wallet is not connected -DEFAULT_CHAIN=4 +DEFAULT_CHAIN=1 # comma-separated trusted hosts for Content Security Policy CSP_TRUSTED_HOSTS= diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index ee47cfa2..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,136 +0,0 @@ -const esRules = { - 'no-var': 'error', - 'no-undef': 'error', - 'no-unused-vars': 'error', - 'no-unused-expressions': 'error', - 'no-param-reassign': 'error', - 'no-shadow': 'error', - 'no-else-return': 'error', - 'no-useless-escape': 'error', - 'no-return-await': 'error', - 'default-case': 'error', - 'no-fallthrough': 'error', - 'prettier/prettier': ['error', { usePrettierrc: true }], -} - -// React -const reactRules = { - 'react/no-children-prop': 'off', - 'react/self-closing-comp': 'error', - 'react/no-unused-prop-types': 'error', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': [ - 'error', - // { additionalHooks: '(useLifecycledEffect|useMyOtherCustomHook)' }, - ], -} - -// Typescript -const tsRules = { - 'no-undef': 'off', - 'no-shadow': 'off', - 'no-unused-vars': 'off', - 'react/jsx-no-undef': 'off', - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-use-before-define': 'error', - '@typescript-eslint/no-redeclare': ['error'], - '@typescript-eslint/adjacent-overload-signatures': 'error', - '@typescript-eslint/array-type': 'error', - '@typescript-eslint/consistent-type-assertions': [ - 'error', - { assertionStyle: 'as' }, - ], - '@typescript-eslint/no-array-constructor': 'error', - '@typescript-eslint/member-ordering': 'error', - '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-misused-new': 'error', - '@typescript-eslint/no-unnecessary-condition': 'error', - '@typescript-eslint/no-unnecessary-qualifier': 'error', - '@typescript-eslint/no-unnecessary-type-arguments': 'error', - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-useless-constructor': 'error', - '@typescript-eslint/prefer-includes': 'error', - '@typescript-eslint/require-await': 'error', -} - -// Import -const importRules = { - 'import/no-unresolved': ['error', { ignore: ['.svg'] }], - 'import/no-dynamic-require': 'error', - 'import/no-self-import': 'error', - 'import/no-useless-path-segments': ['error', { noUselessIndex: true }], - 'import/no-duplicates': 'error', - 'import/extensions': [ - 'error', - 'never', - { - ignorePackages: true, - svg: 'always', - }, - ], - 'import/export': 'error', - 'import/newline-after-import': 'error', - 'import/first': 'error', - 'import/no-mutable-exports': 'error', - 'import/no-cycle': 'error', - 'import/order': 'off', -} - -const env = { browser: true, node: true } - -const settings = { - react: { version: 'detect' }, - 'import/resolver': { - node: { - paths: ['.'], - }, - }, -} - -module.exports = { - overrides: [ - { - env, - files: ['**/*.js', '**/*.jsx', '**/*.json'], - extends: ['next', 'prettier'], - plugins: [ - 'prettier', - 'react', - 'react-hooks', - // 'import' - ], - settings, - rules: { - ...esRules, - ...reactRules, - // ...importRules, - }, - }, - { - env, - files: ['**/*.d.ts', '**/*.ts', '**/*.tsx'], - extends: ['next', 'plugin:import/typescript', 'prettier'], - plugins: [ - 'prettier', - '@typescript-eslint', - 'react', - 'react-hooks', - // 'import', - ], - settings, - rules: { - ...esRules, - ...tsRules, - ...reactRules, - ...importRules, - }, - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { jsx: true }, - ecmaVersion: 2020, - sourceType: 'module', - project: './tsconfig.json', - }, - }, - ], -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..5d4697cf --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,83 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": ["next", "plugin:import/typescript", "prettier"], + "plugins": ["prettier", "@typescript-eslint", "react", "react-hooks"], + "settings": { + "react": { "version": "detect" }, + "import/resolver": { + "node": { + "paths": ["."] + } + } + }, + "rules": { + "no-var": "error", + "no-unused-expressions": "error", + "no-param-reassign": "error", + "no-else-return": "error", + "no-useless-escape": "error", + "no-return-await": "error", + "default-case": "error", + "no-fallthrough": "error", + "prettier/prettier": ["error", { "usePrettierrc": true }], + "no-undef": "off", + "no-shadow": "off", + "no-unused-vars": "off", + "react/jsx-no-undef": "off", + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-redeclare": ["error"], + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/consistent-type-assertions": [ + "error", + { "assertionStyle": "as" } + ], + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-empty-function": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/require-await": "error", + "react/no-children-prop": "off", + "react/self-closing-comp": "error", + "react/no-unused-prop-types": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": ["error"], + "import/no-unresolved": "off", + "import/no-dynamic-require": "error", + "import/no-self-import": "error", + "import/no-useless-path-segments": ["error", { "noUselessIndex": true }], + "import/no-duplicates": "error", + "import/extensions": [ + "error", + "never", + { + "ignorePackages": true, + "svg": "always" + } + ], + "import/export": "error", + "import/newline-after-import": "error", + "import/first": "error", + "import/no-mutable-exports": "error", + "import/no-cycle": "error", + "import/order": "off" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { "jsx": true }, + "ecmaVersion": 2021, + "sourceType": "module", + "project": "./tsconfig.json" + } +} diff --git a/.github/workflows/ci-prod.yml b/.github/workflows/ci-prod.yml index 8494c897..0a6f4c67 100644 --- a/.github/workflows/ci-prod.yml +++ b/.github/workflows/ci-prod.yml @@ -22,4 +22,4 @@ jobs: APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} TARGET_REPO: "lidofinance/infra-mainnet" TAG: "${{ github.event.release.tag_name }}" - TARGET_WORKFLOW: "build_mainnet_dao_voting_ui.yaml" + TARGET_WORKFLOW: "build_critical_dao_voting_ui.yaml" diff --git a/.github/workflows/ci-staging.yml b/.github/workflows/ci-staging.yml index 8f20444d..efd9681b 100644 --- a/.github/workflows/ci-staging.yml +++ b/.github/workflows/ci-staging.yml @@ -25,5 +25,5 @@ jobs: APP_ID: ${{ secrets.APP_ID }} APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} TARGET_REPO: "lidofinance/infra-mainnet" - TARGET_WORKFLOW: "deploy_staging_mainnet_dao_voting_ui.yaml" + TARGET_WORKFLOW: "deploy_staging_critical_dao_voting_ui.yaml" TARGET: "main" diff --git a/.gitignore b/.gitignore index 39b5d6d0..99865417 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # vercel .vercel + +#ide +.vscode +.idea diff --git a/commitlint.config.js b/commitlint.config.cjs similarity index 100% rename from commitlint.config.js rename to commitlint.config.cjs diff --git a/modules/blockChain/hooks/useErrorMessage.ts b/modules/blockChain/hooks/useErrorMessage.ts index bc7bfd36..e9ff4582 100644 --- a/modules/blockChain/hooks/useErrorMessage.ts +++ b/modules/blockChain/hooks/useErrorMessage.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { useConfig } from 'modules/config/hooks/useConfig' -import { useSupportedChains, useConnectorError } from '@reef-knot/web3-react' +import { useSupportedChains, useConnectorError } from 'reef-knot/web3-react' import { getChainName } from 'modules/blockChain/chains' diff --git a/modules/blockChain/hooks/useIsContract.ts b/modules/blockChain/hooks/useIsContract.ts new file mode 100644 index 00000000..bcc2a880 --- /dev/null +++ b/modules/blockChain/hooks/useIsContract.ts @@ -0,0 +1,28 @@ +import { useEthereumSWR } from '@lido-sdk/react' + +export const useIsContract = ( + account?: string | null, +): { loading: boolean; isContract: boolean } => { + // eth_getCode returns hex string of bytecode at address + // for accounts it's "0x" + // for contract it's potentially very long hex (can't be safely&quickly parsed) + const result = useEthereumSWR({ + shouldFetch: !!account, + method: 'getCode', + params: [account, 'latest'], + config: { + // this is very stable request + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnMount: true, + revalidateOnReconnect: false, + refreshInterval: 0, + }, + }) + + return { + loading: result.loading, + isContract: result.data ? result.data !== '0x' : false, + } +} diff --git a/modules/blockChain/hooks/useIsMultisig.ts b/modules/blockChain/hooks/useIsMultisig.ts new file mode 100644 index 00000000..457a6421 --- /dev/null +++ b/modules/blockChain/hooks/useIsMultisig.ts @@ -0,0 +1,8 @@ +import { useWeb3 } from 'reef-knot/web3-react' +import { useIsContract } from './useIsContract' + +export const useIsMultisig = () => { + const { account } = useWeb3() + const { isContract, loading } = useIsContract(account ?? undefined) + return [isContract, loading] +} diff --git a/modules/blockChain/hooks/useSendTransactionGnosisWorkaround.ts b/modules/blockChain/hooks/useSendTransactionGnosisWorkaround.ts index 1ded1bc1..b502d289 100644 --- a/modules/blockChain/hooks/useSendTransactionGnosisWorkaround.ts +++ b/modules/blockChain/hooks/useSendTransactionGnosisWorkaround.ts @@ -2,12 +2,16 @@ import { useCallback } from 'react' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' import { PopulatedTransaction } from '@ethersproject/contracts' import { sendTransactionGnosisWorkaround } from '../utils/sendTransactionGnosisWorkaround' +import { useIsMultisig } from './useIsMultisig' export function useSendTransactionGnosisWorkaround() { const { library } = useWeb3() + // TODO: track loading state of this swr in the ui on yes/no/enact button + const [isMultisig] = useIsMultisig() + return useCallback( (tx: PopulatedTransaction) => - sendTransactionGnosisWorkaround(library.getSigner(), tx), - [library], + sendTransactionGnosisWorkaround(library?.getSigner(), tx, isMultisig), + [library, isMultisig], ) } diff --git a/modules/blockChain/hooks/useTransactionSender.ts b/modules/blockChain/hooks/useTransactionSender.ts index 9465ec8a..176c97e2 100644 --- a/modules/blockChain/hooks/useTransactionSender.ts +++ b/modules/blockChain/hooks/useTransactionSender.ts @@ -88,7 +88,7 @@ export function useTransactionSender( if (!resultTx) return const link = resultTx.type === 'safe' - ? getGnosisSafeLink(chainId, `${walletAddress}/transaction`) + ? getGnosisSafeLink(chainId, `${walletAddress}`) : getEtherscanLink(chainId, resultTx.tx.hash, 'tx') openWindow(link) }, [chainId, resultTx, walletAddress]) diff --git a/modules/blockChain/hooks/useWeb3.ts b/modules/blockChain/hooks/useWeb3.ts index 0be1f170..76425529 100644 --- a/modules/blockChain/hooks/useWeb3.ts +++ b/modules/blockChain/hooks/useWeb3.ts @@ -1,10 +1,12 @@ import { useMemo } from 'react' -import { useWeb3React } from '@web3-react/core' +import { useWeb3 as useWeb3ReefKnot } from 'reef-knot/web3-react' import { useConfig } from 'modules/config/hooks/useConfig' import { parseChainId } from '../chains' +import { useSDK } from '@lido-sdk/react' export function useWeb3() { - const web3 = useWeb3React() + const web3 = useWeb3ReefKnot() + const { providerWeb3 } = useSDK() const { defaultChain } = useConfig() const { chainId } = web3 @@ -18,5 +20,6 @@ export function useWeb3() { isWalletConnected: web3.active, walletAddress: web3.account, chainId: currentChain, + library: providerWeb3, } } diff --git a/modules/blockChain/utils/checkConnectedToSafe.ts b/modules/blockChain/utils/checkConnectedToSafe.ts deleted file mode 100644 index 2998975b..00000000 --- a/modules/blockChain/utils/checkConnectedToSafe.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getWalletNameFromProvider } from './getWalletNameFromProvider' - -export function checkConnectedToSafe(provider: any) { - const walletName = getWalletNameFromProvider(provider) - return Boolean(walletName?.startsWith('Gnosis Safe')) -} diff --git a/modules/blockChain/utils/createContractHelpers.ts b/modules/blockChain/utils/createContractHelpers.ts index 859c499e..1b9114d9 100644 --- a/modules/blockChain/utils/createContractHelpers.ts +++ b/modules/blockChain/utils/createContractHelpers.ts @@ -7,6 +7,7 @@ import { useContractSwr } from '../hooks/useContractSwr' import { useConfig } from 'modules/config/hooks/useConfig' import type { Signer, providers } from 'ethers' +import type { JsonRpcSigner } from '@ethersproject/providers' import { getStaticRpcBatchProvider } from '@lido-sdk/providers' import { getChainName } from 'modules/blockChain/chains' import type { FilterAsyncMethods } from '@lido-sdk/react/dist/esm/hooks/types' @@ -15,7 +16,7 @@ import { AsyncMethodParameters, } from 'modules/types/filter-async-methods' -type Library = Signer | providers.Provider +type Library = JsonRpcSigner | Signer | providers.Provider interface Factory { name: string @@ -81,11 +82,13 @@ export function createContractHelpers({ () => connect({ chainId, - library: library?.getSigner(), + // TODO: find a way to remove ! here + library: library?.getSigner()!, }), [ 'contract-web3-', active ? 'active' : 'inactive', + library ? 'with-signer' : 'no-signer', chainId, address[chainId], account, diff --git a/modules/blockChain/utils/getGnosisSafeLink.ts b/modules/blockChain/utils/getGnosisSafeLink.ts index 94a277fd..986671d7 100644 --- a/modules/blockChain/utils/getGnosisSafeLink.ts +++ b/modules/blockChain/utils/getGnosisSafeLink.ts @@ -3,8 +3,12 @@ import { CHAINS } from '@lido-sdk/constants' const PREFIXES = { [CHAINS.Mainnet]: 'eth', - [CHAINS.Rinkeby]: 'rin', + [CHAINS.Goerli]: 'gor', } as const export const getGnosisSafeLink = (chainId: CHAINS, address: string) => - `https://gnosis-safe.io/app/${get(PREFIXES, chainId, '?')}:${address}` + `https://app.safe.global/transactions/history?safe=${get( + PREFIXES, + chainId, + '?', + )}:${address}` diff --git a/modules/blockChain/utils/getWalletNameFromProvider.ts b/modules/blockChain/utils/getWalletNameFromProvider.ts deleted file mode 100644 index 7014f358..00000000 --- a/modules/blockChain/utils/getWalletNameFromProvider.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getWalletNameFromProvider(provider: any): string | undefined { - if (provider.isMetaMask) { - return 'MetaMask' - } - - return provider.walletMeta?.name -} diff --git a/modules/blockChain/utils/isContract.ts b/modules/blockChain/utils/isContract.ts new file mode 100644 index 00000000..e191a3ce --- /dev/null +++ b/modules/blockChain/utils/isContract.ts @@ -0,0 +1,10 @@ +import { type Provider } from '@ethersproject/abstract-provider' + +export const isContract = async ( + address: string, + provider: Provider, +): Promise => { + const code = await provider.getCode(address) + if (code != '0x') return true + return false +} diff --git a/modules/blockChain/utils/sendTransactionGnosisWorkaround.ts b/modules/blockChain/utils/sendTransactionGnosisWorkaround.ts index 802fc692..3da00cd9 100644 --- a/modules/blockChain/utils/sendTransactionGnosisWorkaround.ts +++ b/modules/blockChain/utils/sendTransactionGnosisWorkaround.ts @@ -1,22 +1,22 @@ -import { Signer } from '@ethersproject/abstract-signer' import { PopulatedTransaction } from '@ethersproject/contracts' import { ToastInfo, toast } from '@lidofinance/lido-ui' import { ResultTx } from '../types' -import { checkConnectedToSafe } from './checkConnectedToSafe' +import type { Signer } from '@ethersproject/abstract-signer' +import type { JsonRpcSigner } from '@ethersproject/providers' // This workaround exists because gnosis safe return making regular `sendTransaction` endlessly waiting // https://github.com/ethers-io/ethers.js/blob/7274cd06cf3f6f31c6df3fd6636706d8536b7ee2/packages/providers/src.ts/json-rpc-provider.ts#L226-L246 export async function sendTransactionGnosisWorkaround( - signer: Signer, + signer: Signer | JsonRpcSigner | undefined, transaction: PopulatedTransaction, + isMultisig: boolean, ): Promise { - const provider = (signer.provider as any)?.provider - const isGnosisSafe = checkConnectedToSafe(provider) + if (!signer) throw Error('signer is required') const pendingToastId = ToastInfo(`Confirm transaction in your wallet`, {}) - if (isGnosisSafe) { + if (isMultisig) { const hash: string = await (signer as any).sendUncheckedTransaction( transaction, ) diff --git a/modules/config/network.ts b/modules/config/network.ts index f735d576..2a5f4af6 100644 --- a/modules/config/network.ts +++ b/modules/config/network.ts @@ -5,8 +5,10 @@ import { parseChainId, getChainName } from 'modules/blockChain/chains' export const ETHERSCAN_API_URL = '/api/etherscan' export const ETHERSCAN_CACHE_TTL = ms('1h') -export const getRpcUrlDefault = (chainId: CHAINS) => - `/api/rpc?chainId=${parseChainId(chainId)}` +export const getRpcUrlDefault = (chainId: CHAINS) => { + const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin + return `${BASE_URL}/api/rpc?chainId=${parseChainId(chainId)}` +} export function getEtherscanUrl(chainId: CHAINS) { const chainName = getChainName(chainId!).toLowerCase() diff --git a/modules/config/ui/SettingsForm/SettingsForm.tsx b/modules/config/ui/SettingsForm/SettingsForm.tsx index a4abb46d..cdb31963 100644 --- a/modules/config/ui/SettingsForm/SettingsForm.tsx +++ b/modules/config/ui/SettingsForm/SettingsForm.tsx @@ -2,7 +2,6 @@ import { useCallback } from 'react' import { useForm } from 'react-hook-form' import { useConfig } from 'modules/config/hooks/useConfig' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' - import { Card } from 'modules/shared/ui/Common/Card' import { Fieldset } from 'modules/shared/ui/Common/Fieldset' import { Form } from 'modules/shared/ui/Controls/Form' @@ -11,9 +10,8 @@ import { CheckboxControl, CheckboxLabelWrap, } from 'modules/shared/ui/Controls/Checkbox' -import { Container, Button, ToastSuccess } from '@lidofinance/lido-ui' +import { Button, Container, ToastSuccess } from '@lidofinance/lido-ui' import { Actions, DescriptionText, DescriptionTitle } from './StyledFormStyle' - import { ethers } from 'ethers' import { getChainName } from 'modules/blockChain/chains' import { ContractVoting } from 'modules/blockChain/contracts' diff --git a/modules/dashboard/ui/DashboardGrid/DashboardGrid.tsx b/modules/dashboard/ui/DashboardGrid/DashboardGrid.tsx index 1199b24b..55889c43 100644 --- a/modules/dashboard/ui/DashboardGrid/DashboardGrid.tsx +++ b/modules/dashboard/ui/DashboardGrid/DashboardGrid.tsx @@ -1,16 +1,13 @@ import range from 'lodash/range' import Router from 'next/router' - import { useEffect } from 'react' import { useSWR } from 'modules/network/hooks/useSwr' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' - import { Container, Pagination } from '@lidofinance/lido-ui' import { DashboardVote } from '../DashboardVote' import { DashboardVoteSkeleton } from '../DashboardVoteSkeleton' import { SkeletonBar } from 'modules/shared/ui/Skeletons/SkeletonBar' import { GridWrap, PaginationWrap } from './DashboardGridStyle' - import { ContractVoting } from 'modules/blockChain/contracts' import { getVoteStatus } from 'modules/votes/utils/getVoteStatus' import { getEventStartVote } from 'modules/votes/utils/getEventVoteStart' diff --git a/modules/modal/ModalProvider.tsx b/modules/modal/ModalProvider.tsx index c98fccf0..4c2c1da0 100644 --- a/modules/modal/ModalProvider.tsx +++ b/modules/modal/ModalProvider.tsx @@ -27,8 +27,13 @@ function ModalProviderRaw({ children }: Props) { ) const closeModal = useCallback(() => { - stateRef.current = null - update() + // setTimeout helps to get rid of this error: + // "Can't perform a react state update on an unmounted component" + // after WalletConnect connection + setTimeout(() => { + stateRef.current = null + update() + }, 0) }, [update]) const context = useMemo( diff --git a/modules/shared/ui/Layout/Header/Header.tsx b/modules/shared/ui/Layout/Header/Header.tsx index 40093d70..20da6d48 100644 --- a/modules/shared/ui/Layout/Header/Header.tsx +++ b/modules/shared/ui/Layout/Header/Header.tsx @@ -2,9 +2,9 @@ import { useState, useCallback } from 'react' import { useRouter } from 'next/dist/client/router' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' import { useScrollLock } from 'modules/shared/hooks/useScrollLock' - import Link from 'next/link' import { Text } from 'modules/shared/ui/Common/Text' +import { NoSSRWrapper } from 'modules/shared/ui/Utils/NoSSRWrapper' import { HeaderWallet } from '../HeaderWallet' import { ThemeToggler } from '@lidofinance/lido-ui' import { HeaderVoteInput } from 'modules/votes/ui/HeaderVoteInput' @@ -25,7 +25,6 @@ import { MobileNavItems, MobileNetworkWrap, MobileSpacer, - HeaderSpacer, NavBurger, ThemeTogglerWrap, } from './HeaderStyle' @@ -75,7 +74,6 @@ export function Header() { return ( <> -