diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 95e86e78db..e5140df252 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -21,6 +21,6 @@ jobs: path-to-signatures: 'signatures/version1/cla.json' path-to-cla-document: 'https://github.com/gnosis/safe-react/blob/main/GNOSISCLA.md' branch: 'cla-signatures' - allowlist: lukasschor,mikheevm,rmeissner,germartinez,davidalbela,Uxio0,dasanra,francovenica,tschubotz,luarx,giacomolicari,gnosis-info,bot*,katspaugh,DaniSomoza,iamacook,yagopv,usame-algan,InoMurko + allowlist: lukasschor,mikheevm,rmeissner,germartinez,davidalbela,Uxio0,dasanra,francovenica,tschubotz,luarx,giacomolicari,gnosis-info,bot*,katspaugh,DaniSomoza,iamacook,yagopv,usame-algan empty-commit-flag: false blockchain-storage-flag: false diff --git a/.github/workflows/mint.yml b/.github/workflows/mint.yml new file mode 100644 index 0000000000..f7405dceda --- /dev/null +++ b/.github/workflows/mint.yml @@ -0,0 +1,46 @@ +name: Mint PR NFT + +on: + pull_request: + branches: + - dev + +jobs: + mint: + environment: Manual + name: Mint + runs-on: ubuntu-latest + steps: + - name: Check if already minted + uses: web3actions/tx@d3833db41e58cb4e7f329027ad30211a22e1c5e5 + with: + rpc-node: ${{ secrets.RPC_NODE}} + wallet-key: ${{ secrets.WALLET_KEY }} + contract: ${{ secrets.CONTRACT_ADDRESS }} + function: "tokenURI(uint256 _tokenId)" + inputs: '[ ${{ github.event.number }} ]' + value: "0" + + - name: Mint + id: mint + if: ${{ failure() }} + uses: web3actions/tx@d3833db41e58cb4e7f329027ad30211a22e1c5e5 + with: + rpc-node: ${{ secrets.RPC_NODE}} + wallet-key: ${{ secrets.WALLET_KEY }} + contract: ${{ secrets.CONTRACT_ADDRESS }} + function: "mint(address _to, uint256 _tokenId, string _uri)" + inputs: '[ "${{ secrets.WALLET_ADDRESS }}", ${{ github.event.number }}, "https://github.com/gnosis/safe-react/pull/${{ github.event.number }}" ]' + value: "0" + + - name: Set success comment + if: steps.mint.outcome == 'success' + uses: peter-evans/create-or-update-comment@v1 + with: + issue-number: ${{ github.event.number }} + body: | + [GitMint NFT preview](https://blockscout.com/xdai/mainnet/token/${{ secrets.CONTRACT_ADDRESS }}/instance/${{ github.event.number }}) + + Dear @${{ github.event.pull_request.user.login }}, + Thank you for your contribution! Please, let us know your Ethereum address to receive [this NFT on Gnosis Chain](https://epor.io/tokens/${{ secrets.CONTRACT_ADDRESS }}/${{ github.event.number }}?network=xDai). + Cheers! 🏆 diff --git a/README.md b/README.md index 3eb15ff763..41e192baa4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Please see the [transaction](docs/transactions.md) notes for more information ab ## Getting Started -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#deployment) for notes on how to deploy the project on a live system. +These instructions will help you get a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#deployment) for notes on how to deploy the project on a live system. ### Prerequisites @@ -88,7 +88,7 @@ docker-compose build && docker-compose up ### Building -Te get a complete bundle using the current configuration use: +To get a complete bundle using the current configuration use: ``` yarn build diff --git a/docs/release-procedure.md b/docs/release-procedure.md index ffe0b8e8d4..e4df6144f8 100644 --- a/docs/release-procedure.md +++ b/docs/release-procedure.md @@ -15,10 +15,24 @@ git log origin/main..origin/dev --pretty=format:'* %s' ### QA * The QA team do regression testing on this branch * If issues are found, bugfixes are merged into this branch -* Once the QA is done, we push the branch to `main` -* `main` is automatically deployed to staging – some extra QA can be done there if needed +* Once the QA is done, proceed to the next step ### Tag & release +Wait for all the checks on GitHub to pass. +* Switch to the main branch and make sure it's up to date: +``` +git checkout main +git fetch --all +git reset --hard origin/main +``` +* Pull from the release branch: +``` +git pull origin release/3.15.0 +``` +* Push to main: +``` +git push origin main +``` * Create and push a new version tag : ``` git tag v3.15.0 diff --git a/package.json b/package.json index 7c752024f9..c1ca403211 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "3.18.0", + "version": "3.20.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { @@ -86,10 +86,11 @@ "@ethersproject/hash": "^5.5.0", "@gnosis.pm/safe-apps-sdk": "6.2.0", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", - "@gnosis.pm/safe-core-sdk": "^1.3.0", + "@gnosis.pm/safe-core-sdk": "^2.0.0", "@gnosis.pm/safe-deployments": "^1.8.0", "@gnosis.pm/safe-react-components": "^0.9.8", "@gnosis.pm/safe-react-gateway-sdk": "2.8.3", + "@gnosis.pm/safe-web3-lib": "^1.0.0", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.0", "@material-ui/lab": "4.0.0-alpha.60", @@ -100,7 +101,7 @@ "abi-decoder": "^2.4.0", "axios": "0.21.4", "bignumber.js": "9.0.1", - "bnc-onboard": "~1.35.3", + "bnc-onboard": "^1.37.3", "classnames": "^2.2.6", "currency-flags": "3.2.1", "date-fns": "^2.20.2", @@ -139,13 +140,14 @@ "reselect": "^4.0.0", "semver": "^7.3.2", "styled-components": "^5.3.0", - "web3": "1.6.0", - "web3-core": "^1.6.0", - "web3-eth-contract": "^1.6.0", - "web3-utils": "^1.6.0" + "ua-parser-js": "^1.0.2", + "web3": "1.7.0", + "web3-core": "^1.7.0", + "web3-eth-contract": "^1.7.0", + "web3-utils": "^1.7.0" }, "devDependencies": { - "@gnosis.pm/safe-core-sdk-types": "^0.1.1", + "@gnosis.pm/safe-core-sdk-types": "1.0.0", "@rescripts/cli": "^0.0.16", "@sentry/cli": "^1.67.2", "@storybook/addon-actions": "^6.3.8", @@ -168,6 +170,7 @@ "@types/react-router-dom": "^5.1.9", "@types/redux-actions": "^2.6.2", "@types/styled-components": "^5.1.11", + "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "concurrently": "^6.0.0", diff --git a/public/index.html b/public/index.html index a6ac19c7ab..fd0cfd2979 100644 --- a/public/index.html +++ b/public/index.html @@ -39,7 +39,7 @@ } - +
diff --git a/public/resources/logo.svg b/public/resources/logo.svg new file mode 100644 index 0000000000..a420e8b8a3 --- /dev/null +++ b/public/resources/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/resources/logo_120x120.png b/public/resources/logo_120x120.png new file mode 100644 index 0000000000..0d9fcc4ae8 Binary files /dev/null and b/public/resources/logo_120x120.png differ diff --git a/public/resources/safe.png b/public/resources/safe.png deleted file mode 100644 index fec10acfd3..0000000000 Binary files a/public/resources/safe.png and /dev/null differ diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 5af95122d1..581459779f 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -1,5 +1,4 @@ import ClickAwayListener from '@material-ui/core/ClickAwayListener' -import Grow from '@material-ui/core/Grow' import List from '@material-ui/core/List' import Popper from '@material-ui/core/Popper' import { withStyles } from '@material-ui/core/styles' @@ -68,25 +67,22 @@ const styles = () => ({ }) const WalletPopup = ({ anchorEl, providerDetails, classes, open, onClose }) => { + if (!open) { + return null + } return ( - {({ TransitionProps }) => ( - - <> - - - {providerDetails} - - - - - )} + + + {providerDetails} + + ) } diff --git a/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx index 07fc1a2e4a..732ff46815 100644 --- a/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx +++ b/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx @@ -1,53 +1,58 @@ +import { lazy, ReactElement } from 'react' import { withStyles } from '@material-ui/core/styles' -import { ReactElement } from 'react' +import { Card } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' import ConnectButton from 'src/components/ConnectButton' - import Block from 'src/components/layout/Block' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing' -import { Card } from '@gnosis.pm/safe-react-components' -import styled from 'styled-components' +import { isPairingSupported } from 'src/logic/wallets/pairing/utils' +// We need lazy import because the component imports static css that should only be applied if the component is rendered +const PairingDetails = lazy(() => import('src/components/AppLayout/Header/components/ProviderDetails/PairingDetails')) const styles = () => ({ - logo: { - justifyContent: 'center', - }, - text: { - letterSpacing: '-0.6px', + header: { + letterSpacing: '0.4px', flexGrow: 1, textAlign: 'center', + fontWeight: 600, + fontSize: '18px', }, - connect: { + centerText: { textAlign: 'center', - marginTop: '60px', }, - connectText: { - letterSpacing: '1px', + justifyCenter: { + justifyContent: 'center', }, - img: { - margin: '0px 2px', + appStore: { + height: '35px', }, }) const StyledCard = styled(Card)` padding: 20px; + max-width: 240px; ` + const ConnectDetails = ({ classes }): ReactElement => ( - + Connect a Wallet - + - + + + + {isPairingSupported() && } ) diff --git a/src/components/AppLayout/Header/components/ProviderDetails/PairingDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/PairingDetails.tsx new file mode 100644 index 0000000000..2964c4e172 --- /dev/null +++ b/src/components/AppLayout/Header/components/ProviderDetails/PairingDetails.tsx @@ -0,0 +1,81 @@ +import { CSSProperties, ReactElement } from 'react' +import Skeleton from '@material-ui/lab/Skeleton' +import RefreshIcon from '@material-ui/icons/Refresh' +import IconButton from '@material-ui/core/IconButton' +import { Divider, Link } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import QRCode from 'qrcode.react' + +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' +import usePairing from 'src/logic/wallets/pairing/hooks/usePairing' +import { initPairing, isPairingModule } from 'src/logic/wallets/pairing/utils' + +// Hides first wallet in Onboard modal (pairing module) +import 'src/components/AppLayout/Header/components/ProviderDetails/hidePairingModule.css' +import { useGetPairingUri } from 'src/logic/wallets/pairing/hooks/useGetPairingUri' + +const StyledDivider = styled(Divider)` + width: calc(100% + 40px); + margin-left: -20px; +` + +const QR_DIMENSION = 120 + +const qrRefresh: CSSProperties = { + width: QR_DIMENSION, + height: QR_DIMENSION, +} + +const PairingDetails = ({ classes }: { classes: Record }): ReactElement => { + const uri = useGetPairingUri() + const isPairingLoaded = isPairingModule() + usePairing() + + return ( + <> + + + + + Connect to Mobile + + + + + {uri ? ( + + ) : isPairingLoaded ? ( + + ) : ( + + + + )} + + + + + Scan this code in the{' '} + Gnosis Safe app to sign + transactions with your mobile device. +
+ Learn more about this + feature. +
+
+ + + + Download on the App Store + + + + ) +} + +export default PairingDetails diff --git a/src/components/AppLayout/Header/components/ProviderDetails/hidePairingModule.css b/src/components/AppLayout/Header/components/ProviderDetails/hidePairingModule.css new file mode 100644 index 0000000000..4db81d387b --- /dev/null +++ b/src/components/AppLayout/Header/components/ProviderDetails/hidePairingModule.css @@ -0,0 +1,4 @@ +/* Hides pairing module from Onboard wallet selection modal */ +.bn-onboard-modal-select-wallets li:first-of-type { + display: none; +} diff --git a/src/components/AppLayout/Header/components/WalletIcon/icons/icon-safe-mobile.svg b/src/components/AppLayout/Header/components/WalletIcon/icons/icon-safe-mobile.svg new file mode 100644 index 0000000000..ae7929c147 --- /dev/null +++ b/src/components/AppLayout/Header/components/WalletIcon/icons/icon-safe-mobile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/AppLayout/Header/components/WalletIcon/icons/index.ts b/src/components/AppLayout/Header/components/WalletIcon/icons/index.ts index 069b98ca8f..8d6efbf512 100644 --- a/src/components/AppLayout/Header/components/WalletIcon/icons/index.ts +++ b/src/components/AppLayout/Header/components/WalletIcon/icons/index.ts @@ -13,6 +13,7 @@ import coinbaseIcon from './icon-coinbase.svg' import operaIcon from './icon-opera.png' import squarelinkIcon from './icon-squarelink.png' import keystoneIcon from './icon-keystone.png' +import safeMobileIcon from './icon-safe-mobile.svg' import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3' @@ -73,6 +74,10 @@ const WALLET_ICONS: { [key in WALLET_PROVIDER]: { src: string; height: number } src: squarelinkIcon, height: 25, }, + [WALLET_PROVIDER.SAFE_MOBILE]: { + src: safeMobileIcon, + height: 25, + }, } export default WALLET_ICONS diff --git a/src/components/AppLayout/Header/index.tsx b/src/components/AppLayout/Header/index.tsx index 75aad66807..6dde386248 100644 --- a/src/components/AppLayout/Header/index.tsx +++ b/src/components/AppLayout/Header/index.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useSelector, useDispatch } from 'react-redux' +import { useSelector } from 'react-redux' import Layout from './components/Layout' import ConnectDetails from './components/ProviderDetails/ConnectDetails' @@ -14,9 +14,9 @@ import { userAccountSelector, userEnsSelector, } from 'src/logic/wallets/store/selectors' -import { removeProvider } from 'src/logic/wallets/store/actions' -import onboard from 'src/logic/wallets/onboard' -import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher' +import onboard, { loadLastUsedProvider } from 'src/logic/wallets/onboard' +import { isSupportedWallet } from 'src/logic/wallets/utils/walletList' +import { wrapInSuspense } from 'src/utils/wrapInSuspense' const HeaderComponent = (): React.ReactElement => { const provider = useSelector(providerNameSelector) @@ -25,12 +25,12 @@ const HeaderComponent = (): React.ReactElement => { const ensName = useSelector(userEnsSelector) const loaded = useSelector(loadedSelector) const available = useSelector(availableSelector) - const dispatch = useDispatch() useEffect(() => { const tryToConnectToLastUsedProvider = async () => { const lastUsedProvider = loadLastUsedProvider() - if (lastUsedProvider) { + const isProviderEnabled = lastUsedProvider && isSupportedWallet(lastUsedProvider) + if (isProviderEnabled) { await onboard().walletSelect(lastUsedProvider) } } @@ -44,7 +44,7 @@ const HeaderComponent = (): React.ReactElement => { } const onDisconnect = () => { - dispatch(removeProvider()) + onboard().walletReset() } const getProviderInfoBased = () => { @@ -57,7 +57,7 @@ const HeaderComponent = (): React.ReactElement => { const getProviderDetailsBased = () => { if (!loaded) { - return + return wrapInSuspense() } return ( diff --git a/src/components/AppLayout/Sidebar/DevTools/index.tsx b/src/components/AppLayout/Sidebar/DevTools/index.tsx deleted file mode 100644 index fb146bfc7b..0000000000 --- a/src/components/AppLayout/Sidebar/DevTools/index.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { ReactElement, useMemo } from 'react' -import { useSelector } from 'react-redux' -import styled from 'styled-components' -import { fireEvent, screen, waitForElementToBeRemoved } from '@testing-library/react' -import { Button } from '@gnosis.pm/safe-react-components' -import List from '@material-ui/core/List' -import ListItem from '@material-ui/core/ListItem' -import ListItemText from '@material-ui/core/ListItemText' -import throttle from 'lodash/throttle' - -import { currentSafe, currentSafeEthBalance } from 'src/logic/safe/store/selectors' -import { extractSafeAddress } from 'src/routes/routes' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { nextTransaction } from 'src/logic/safe/store/selectors/gatewayTransactions' -import { grantedSelector } from 'src/routes/safe/container/selector' - -const TX_AMOUNT = '0.0001' - -const prepareTx = async (address: string): Promise => { - const newTxBtn = screen.getByText('New transaction') - fireEvent.click(newTxBtn) - - const sendBtn = await screen.findByText('Send funds') - fireEvent.click(sendBtn) - - const recipientInput = await screen.findByTestId('address-book-input') - fireEvent.change(recipientInput, { target: { value: address } }) - - const tokenInput = await screen.findByTestId('token-input') - fireEvent.change(tokenInput, { target: { value: ZERO_ADDRESS } }) - - const amountInput = await screen.findByPlaceholderText('Amount*') - fireEvent.change(amountInput, { target: { value: TX_AMOUNT } }) - - const reviewBtn = await screen.findByText('Review') - fireEvent.click(reviewBtn) - - const estimatingBtn = await screen.findByText('Estimating') - await waitForElementToBeRemoved(estimatingBtn, { timeout: 10000 }) -} - -const submitTx = async (): Promise => { - const submitBtn = await screen.findByText('Submit') - fireEvent.click(submitBtn) -} - -const stopExecution = async (): Promise => { - const executionCheckbox = await screen.findByTestId('execute-checkbox') - fireEvent.click(executionCheckbox) -} - -const createQueuedTx = async (address: string, threshold = 1): Promise => { - await prepareTx(address) - if (threshold === 1) { - await stopExecution() - } - await submitTx() -} - -const createExecutedTx = async (address: string): Promise => { - await prepareTx(address) - await submitTx() -} - -// const getStatusUrl = (address: string): string => { -// return `https://rimeissner.dev/safe-status-check/#/${getShortName()}:${address}` -// } - -const DevTools = (): ReactElement => { - const { owners, threshold = 1 } = useSelector(currentSafe) ?? {} - const safeAddress = extractSafeAddress() - const nextTx = useSelector(nextTransaction) - const isGranted = useSelector(grantedSelector) - const ethBalance = useSelector(currentSafeEthBalance) - - const hasSufficientFunds = (): boolean => { - let hasFunds = false - try { - hasFunds = parseFloat(ethBalance) > parseFloat(TX_AMOUNT) - } catch (err) {} - return hasFunds - } - - const throttledCreatedQueuedTx = useMemo(() => throttle(createQueuedTx, 1000), []) - - return ( - <> - - - - - {/* - history.push(TRANSACTIONS_QUEUE)}>Queue - - - history.push(TRANSACTIONS_HISTORY)}>History - - - window.open(getStatusUrl(safeAddress), '_blank')}>Safe Status - */} - - - throttledCreatedQueuedTx(safeAddress, threshold)} - size="md" - variant="bordered" - disabled={!isGranted || !hasSufficientFunds()} - > - Queue - - createExecutedTx(safeAddress)} - size="md" - variant="bordered" - disabled={!isGranted || !hasSufficientFunds() || !nextTx || threshold > 1} - > - Execute - - - - ) -} - -export default DevTools - -const StyledButton = styled(Button)` - &.MuiButton-root { - padding: 0 12px !important; - min-width: 45% !important; - } - - & .MuiButton-label { - font-size: 14px !important; - } -` - -const ButtonWrapper = styled.div` - display: flex; - justify-content: space-between; - margin: 0 12px; -` diff --git a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx index a24bb19396..f250e0f681 100644 --- a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx +++ b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx @@ -19,6 +19,7 @@ import { ChainInfo } from '@gnosis.pm/safe-react-gateway-sdk' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' import { copyShortNameSelector } from 'src/logic/appearance/selectors' import { ADDRESSED_ROUTE, extractShortChainName } from 'src/routes/routes' +import Threshold from 'src/components/AppLayout/Sidebar/Threshold' export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' @@ -36,6 +37,7 @@ const IdenticonContainer = styled.div` display: flex; justify-content: space-between; align-items: center; + position: relative; div:first-of-type { width: 32px; @@ -158,6 +160,7 @@ const SafeHeader = ({ {/* Identicon */} + diff --git a/src/components/AppLayout/Sidebar/Threshold/index.tsx b/src/components/AppLayout/Sidebar/Threshold/index.tsx new file mode 100644 index 0000000000..9a047380cf --- /dev/null +++ b/src/components/AppLayout/Sidebar/Threshold/index.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components' +import { useSelector } from 'react-redux' +import { primaryLite, primaryActive, smallFontSize } from 'src/theme/variables' +import { currentSafe } from 'src/logic/safe/store/selectors' + +const Container = styled.div` + background: ${primaryLite}; + color: ${primaryActive}; + font-size: ${smallFontSize}; + font-weight: bold; + border-radius: 100%; + padding: 4px; + position: absolute; + z-index: 2; + top: -6px; + left: 50%; + transform: translateX(-110%); +` + +const Threshold = (): React.ReactElement | null => { + const { owners, threshold } = useSelector(currentSafe) + if (!threshold) return null + + return ( + + {threshold}/{owners.length} + + ) +} + +export default Threshold diff --git a/src/components/AppLayout/Sidebar/index.tsx b/src/components/AppLayout/Sidebar/index.tsx index 75ad08b7f1..fcd3039d02 100644 --- a/src/components/AppLayout/Sidebar/index.tsx +++ b/src/components/AppLayout/Sidebar/index.tsx @@ -78,9 +78,8 @@ const Sidebar = ({ onReceiveClick, onNewTransactionClick, }: Props): React.ReactElement => { + const debugToggle = useMemo(() => (IS_PRODUCTION ? null : lazyLoad('./DebugToggle')), []) const dispatch = useDispatch() - const devTools = useMemo(() => lazyLoad('./DevTools'), []) - const debugToggle = useMemo(() => lazyLoad('./DebugToggle'), []) const handleClick = async () => { const cookiesState = await loadFromCookie(COOKIES_KEY) @@ -116,14 +115,10 @@ const Sidebar = ({ ) : null} + - {!IS_PRODUCTION && safeAddress && ( - <> - - {devTools} - - )} - {!IS_PRODUCTION && debugToggle} + {debugToggle} + diff --git a/src/components/ConnectButton/index.tsx b/src/components/ConnectButton/index.tsx index e8b28942b4..6d35842342 100644 --- a/src/components/ConnectButton/index.tsx +++ b/src/components/ConnectButton/index.tsx @@ -1,26 +1,7 @@ import { ReactElement } from 'react' -import Button from 'src/components/layout/Button' -import { _getChainId } from 'src/config' -import { getWeb3 } from 'src/logic/wallets/getWeb3' -import onboard from 'src/logic/wallets/onboard' -import { shouldSwitchNetwork, switchNetwork } from 'src/logic/wallets/utils/network' - -const checkWallet = async (): Promise => { - if (shouldSwitchNetwork()) { - switchNetwork(onboard().getState().wallet, _getChainId()).catch((e) => e.log()) - } - return await onboard().walletCheck() -} - -export const onboardUser = async (): Promise => { - // before calling walletSelect you want to check if web3 has been instantiated - // which indicates that a wallet has already been selected - // and web3 has been instantiated with that provider - const web3 = getWeb3() - const walletSelected = web3 ? true : await onboard().walletSelect() - return walletSelected && checkWallet() -} +import Button from 'src/components/layout/Button' +import onboard, { checkWallet } from 'src/logic/wallets/onboard' export const onConnectButtonClick = async (): Promise => { const walletSelected = await onboard().walletSelect() diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx index 5b3100d994..b97c167d87 100644 --- a/src/components/DecodeTxs/index.tsx +++ b/src/components/DecodeTxs/index.tsx @@ -113,7 +113,7 @@ export const getParameterElement = (parameter: DecodedDataBasicParameter, index: ) } - if (parameter.type.startsWith('bytes')) { + if (parameter.type === 'bytes') { valueElement = ( {getByteLength(parameter.value)} bytes diff --git a/src/components/ExecuteCheckbox/index.test.tsx b/src/components/ExecuteCheckbox/index.test.tsx index 8d5dcebc7c..059ca760c8 100644 --- a/src/components/ExecuteCheckbox/index.test.tsx +++ b/src/components/ExecuteCheckbox/index.test.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { fireEvent, render, screen, waitFor, act } from 'src/utils/test-utils' import { history } from 'src/routes/routes' import ExecuteCheckbox from '.' @@ -7,8 +8,20 @@ describe('ExecuteCheckbox', () => { const onChange = jest.fn() history.push('/rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A/balances') + const ControlledInputWrapper = () => { + const [checked, setChecked] = useState(true) + return ( + { + setChecked(value) + onChange(value) + }} + /> + ) + } await act(async () => { - render() + render() }) await waitFor(() => { diff --git a/src/components/ExecuteCheckbox/index.tsx b/src/components/ExecuteCheckbox/index.tsx index cb120e219d..5e538cef11 100644 --- a/src/components/ExecuteCheckbox/index.tsx +++ b/src/components/ExecuteCheckbox/index.tsx @@ -23,17 +23,18 @@ const StyledFormControlLabel = styled(FormControlLabel)` ` interface ExecuteCheckboxProps { + checked: boolean onChange: (val: boolean) => unknown } -const ExecuteCheckbox = ({ onChange }: ExecuteCheckboxProps): ReactElement => { +const ExecuteCheckbox = ({ checked, onChange }: ExecuteCheckboxProps): ReactElement => { const handleChange = (e: React.ChangeEvent): void => { onChange(e.target.checked) } return ( } + control={} label="Execute transaction" data-testid="execute-checkbox" /> diff --git a/src/components/ReviewInfoText/ReviewInfoText.test.tsx b/src/components/ReviewInfoText/ReviewInfoText.test.tsx index a97373aa73..a66255493e 100644 --- a/src/components/ReviewInfoText/ReviewInfoText.test.tsx +++ b/src/components/ReviewInfoText/ReviewInfoText.test.tsx @@ -1,57 +1,72 @@ -import { useSelector } from 'react-redux' import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' import { render, screen } from 'src/utils/test-utils' import { ReviewInfoText } from './index' -import { history } from 'src/routes/routes' -const safeAddress = '0xC245cb45B044d66fbE8Fb33C26c0b28B4fc367B2' -const url = `/rin:${safeAddress}/settings/advanced` -history.location.pathname = url +jest.mock('src/logic/hooks/useRecommendedNonce', () => ({ + __esModule: true, + default: () => 9, +})) describe('', () => { const initialData = { gasCostFormatted: '0', isExecution: true, - isCreation: false, + isCreation: true, + isRejection: false, isOffChainSignature: false, txEstimationExecutionStatus: EstimationStatus.SUCCESS, } - const customState = { - safes: { - safes: { - [safeAddress]: { - address: safeAddress, - nonce: 8, - modules: null, - guard: '', - currentVersion: '1.3.0', - }, - }, - }, - } - const testId = 'reviewInfoText-component' - const warningCommonCopy = - 'will need to be created and executed before this transaction, are you sure you want to do this?' - it('Renders ReviewInfoText with safeNonce being one lastTxNonce + 1', () => { - const lastTxNonce = 10 - const safeNonce = `${lastTxNonce + 1}` + it('renders only base text with safeNonce in order', () => { + render() + + expect(screen.getByText(/You're about to create a transaction/)).toBeInTheDocument() + }) + + it('renders only base text with safeNonce in order and not a creation', () => { + render() + + expect(screen.getByText(/You're about to execute a transaction/)).toBeInTheDocument() + }) + + it('renders only base text that is not a creation and nonce in future', () => { + render() - render(, customState) + expect(screen.getByText(/You're about to execute a transaction/)).toBeInTheDocument() + expect(screen.queryByText(/will need to be created and executed before this transaction/)).not.toBeInTheDocument() + }) + + it('renders only base text for a rejection tx with a nonce in the past', () => { + render() - expect(screen.getByTestId(testId)).toBeInTheDocument() - expect(screen.queryByText(warningCommonCopy)).not.toBeInTheDocument() + expect(screen.getByText(/You're about to create a rejection transaction/)).toBeInTheDocument() + expect(screen.queryByText(/will need to be created and executed before this transaction/)).not.toBeInTheDocument() }) - it('Renders ReviewInfoText with safeNonce more than one transaction ahead of lastTxNonce', () => { - const lastTxNonce = 10 - const safeNonce = `${lastTxNonce + 4}` - const expectedCopy = 'transactions ' + warningCommonCopy + it('renders a warning with a safeNonce +1 tx in the future', () => { + render() + + expect(screen.getByText(/1/)).toBeInTheDocument() + expect( + screen.getByText(/transaction will need to be created and executed before this transaction/), + ).toBeInTheDocument() + }) + + it('renders a warning with a safeNonce +2 txs the future', () => { + render() + + expect(screen.getByText(/2/)).toBeInTheDocument() + expect( + screen.getByText(/transactions will need to be created and executed before this transaction/), + ).toBeInTheDocument() + }) - render(, customState) + it('renders a warning with an already used safeNonce', () => { + render() - expect(screen.getByTestId(testId)).toBeInTheDocument() - expect(screen.getByText('6')).toBeInTheDocument() - expect(screen.queryByText(expectedCopy)).toBeInTheDocument() + expect(screen.getByText(/6/)).toBeInTheDocument() + expect(screen.getByText(/9/)).toBeInTheDocument() + expect(screen.getByText(/is below the latest transaction's nonce./)).toBeInTheDocument() + expect(screen.getByText(/Your transaction might fail./)).toBeInTheDocument() }) }) diff --git a/src/components/ReviewInfoText/index.tsx b/src/components/ReviewInfoText/index.tsx index e7a4f63891..20adc61bb0 100644 --- a/src/components/ReviewInfoText/index.tsx +++ b/src/components/ReviewInfoText/index.tsx @@ -1,16 +1,13 @@ -import styled from 'styled-components' -import { Text } from '@gnosis.pm/safe-react-components' +import { ReactElement } from 'react' import { useSelector } from 'react-redux' +import styled from 'styled-components' import Paragraph from 'src/components/layout/Paragraph' -import { currentSafe } from 'src/logic/safe/store/selectors' -import { getLastTxNonce } from 'src/logic/safe/store/selectors/gatewayTransactions' import { lg } from 'src/theme/variables' -import { getRecommendedNonce } from 'src/logic/safe/api/fetchSafeTxGasEstimation' -import { extractSafeAddress } from 'src/routes/routes' -import { useEffect, useState } from 'react' import { TransactionFailText } from '../TransactionFailText' import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import useRecommendedNonce from 'src/logic/hooks/useRecommendedNonce' +import { currentSafeWithNames } from 'src/logic/safe/store/selectors' const ReviewInfoTextWrapper = styled.div` padding: 0 ${lg}; @@ -20,6 +17,7 @@ type ReviewInfoTextProps = { txEstimationExecutionStatus: EstimationStatus isExecution: boolean isCreation: boolean + isRejection: boolean safeNonce?: string testId?: string } @@ -27,54 +25,42 @@ type ReviewInfoTextProps = { export const ReviewInfoText = ({ isCreation, isExecution, - safeNonce: txParamsSafeNonce = '', + isRejection, + safeNonce = '', testId, txEstimationExecutionStatus, -}: ReviewInfoTextProps): React.ReactElement => { - const { nonce } = useSelector(currentSafe) - const safeNonceNumber = parseInt(txParamsSafeNonce, 10) - const lastTxNonce = useSelector(getLastTxNonce) - const storeNextNonce = `${lastTxNonce && lastTxNonce + 1}` - const safeAddress = extractSafeAddress() - const [recommendedNonce, setRecommendedNonce] = useState(storeNextNonce) - const transactionAction = isCreation ? 'create' : isExecution ? 'execute' : 'approve' +}: ReviewInfoTextProps): ReactElement => { + const safeTxNonce = parseInt(safeNonce, 10) + const { address: safeAddress } = useSelector(currentSafeWithNames) + const recommendedNonce = useRecommendedNonce(safeAddress) - useEffect(() => { - const fetchRecommendedNonce = async () => { - try { - const recommendedNonce = (await getRecommendedNonce(safeAddress)).toString() - setRecommendedNonce(recommendedNonce) - } catch (e) { - return - } - } - fetchRecommendedNonce() - }, [safeAddress]) + const isTxNonceOutOfOrder = () => { + // safeNonce can be undefined while waiting for the request. + if (isNaN(safeTxNonce)) return false + if (safeTxNonce === recommendedNonce) return false + return true + } - const warningMessage = () => { - const isTxNonceOutOfOrder = () => { - // safeNonce can be undefined while waiting for the request. - if (isNaN(safeNonceNumber) || safeNonceNumber === nonce) return false - if (lastTxNonce !== undefined && safeNonceNumber === lastTxNonce + 1) return false - return true - } - const shouldShowWarning = isTxNonceOutOfOrder() - if (!shouldShowWarning) return null + const getWarning = (): ReactElement | null => { + if (!isCreation || isRejection) return null + if (!isTxNonceOutOfOrder()) return null + + const transactionsToGo = safeTxNonce - recommendedNonce - const transactionsToGo = safeNonceNumber - nonce return ( - {transactionsToGo < 0 ? ( - `Nonce ${txParamsSafeNonce} has already been used. Your transaction will fail. Please use nonce ${recommendedNonce}.` + {transactionsToGo > 0 ? ( + /* tx in the future */ <> + {transactionsToGo} +  {`transaction${transactionsToGo > 1 ? 's' : ''}`} will need to be created and executed before + this transaction, are you sure you want to do this? + ) : ( - <> - - {transactionsToGo} - - {` transaction${ - transactionsToGo > 1 ? 's' : '' - } will need to be created and executed before this transaction, - are you sure you want to do this?`} + /* tx in the past */ <> + Nonce  + {safeTxNonce} +  is below the latest transaction's nonce. Your transaction might fail. Please use nonce  + {recommendedNonce}. )} @@ -83,11 +69,12 @@ export const ReviewInfoText = ({ return ( - {warningMessage() || ( + {getWarning() || ( <> - You're about to {transactionAction} a transaction and will have to confirm it with your currently - connected wallet. + You're about to {isCreation ? 'create' : isExecution ? 'execute' : 'approve'} a{' '} + {isRejection ? 'rejection ' : ''}transaction and will have to confirm it with your currently connected + wallet. )} diff --git a/src/components/TransactionFailText/index.test.tsx b/src/components/TransactionFailText/index.test.tsx new file mode 100644 index 0000000000..102b84f288 --- /dev/null +++ b/src/components/TransactionFailText/index.test.tsx @@ -0,0 +1,83 @@ +import { render, screen, act } from 'src/utils/test-utils' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFailText, _ErrorMessage } from '.' + +jest.mock('src/logic/wallets/store/selectors', () => { + const original = jest.requireActual('src/logic/wallets/store/selectors') + return { + ...original, + shouldSwitchWalletChain: () => false, + } +}) + +jest.mock('src/routes/safe/container/selector', () => { + const original = jest.requireActual('src/routes/safe/container/selector') + return { + ...original, + grantedSelector: () => true, + } +}) + +describe('TransactionFailText', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('shows the create & execute error', async () => { + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(`${_ErrorMessage.general} ${_ErrorMessage.creation}`)).toBeDefined() + }) + + it('shows the execution error when execution an existing tx', async () => { + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(`${_ErrorMessage.general} ${_ErrorMessage.execution}`)).toBeDefined() + }) + + it('shows a wrong chain error', async () => { + const sel = require('src/logic/wallets/store/selectors') + ;(sel.shouldSwitchWalletChain as jest.Mocked) = jest.fn(() => true) + + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(_ErrorMessage.wrongChain)).toBeDefined() + }) + + it('shows an owner error', async () => { + const sel = require('src/routes/safe/container/selector') + ;(sel.grantedSelector as jest.Mocked) = jest.fn(() => false) + + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(_ErrorMessage.notOwner)).toBeDefined() + }) + + it('renders null if neither execution nor creation error', async () => { + await act(async () => { + render() + }) + + expect(() => screen.getByAltText('Info Tooltip')).toThrow() + }) + + it('renders null if estimation status is not failure', async () => { + await act(async () => { + render() + }) + + expect(() => screen.getByAltText('Info Tooltip')).toThrow() + }) +}) diff --git a/src/components/TransactionFailText/index.tsx b/src/components/TransactionFailText/index.tsx index ac4c01cf7f..770bfdc0fb 100644 --- a/src/components/TransactionFailText/index.tsx +++ b/src/components/TransactionFailText/index.tsx @@ -6,11 +6,18 @@ import Img from 'src/components/layout/Img' import InfoIcon from 'src/assets/icons/info_red.svg' import { useSelector } from 'react-redux' -import { currentSafeThreshold } from 'src/logic/safe/store/selectors' import { shouldSwitchWalletChain } from 'src/logic/wallets/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +enum ErrorMessage { + general = 'This transaction will most likely fail.', + creation = 'To save gas costs, avoid creating the transaction.', + execution = 'To save gas costs, reject this transaction.', + notOwner = `You are currently not an owner of this Safe and won't be able to submit this transaction.`, + wrongChain = 'Your wallet is connected to the wrong chain.', +} + const styles = createStyles({ executionWarningRow: { display: 'flex', @@ -35,27 +42,17 @@ export const TransactionFailText = ({ estimationStatus, }: TransactionFailTextProps): React.ReactElement | null => { const classes = useStyles() - const threshold = useSelector(currentSafeThreshold) const isWrongChain = useSelector(shouldSwitchWalletChain) - const isGranted = useSelector(grantedSelector) + const isOwner = useSelector(grantedSelector) - if (estimationStatus !== EstimationStatus.FAILURE && !(isCreation && !isGranted)) { - return null - } + const showError = + isWrongChain || (isExecution && estimationStatus === EstimationStatus.FAILURE) || (isCreation && !isOwner) + if (!showError) return null - let errorDesc = 'To save gas costs, avoid creating the transaction.' - if (isExecution) { - errorDesc = - threshold && threshold > 1 - ? `To save gas costs, reject this transaction` - : `To save gas costs, avoid executing the transaction.` - } + const errorDesc = isCreation ? ErrorMessage.creation : ErrorMessage.execution + const defaultMsg = `${ErrorMessage.general} ${errorDesc}` - const defaultMsg = `This transaction will most likely fail. ${errorDesc}` - const notOwnerMsg = `You are currently not an owner of this Safe and won't be able to submit this transaction.` - const wrongChainMsg = 'Your wallet is connected to the wrong chain.' - - const error = isGranted ? defaultMsg : isWrongChain ? wrongChainMsg : isCreation ? notOwnerMsg : defaultMsg + const error = isWrongChain ? ErrorMessage.wrongChain : isCreation && !isOwner ? ErrorMessage.notOwner : defaultMsg return ( @@ -66,3 +63,6 @@ export const TransactionFailText = ({ ) } + +// For tests +export const _ErrorMessage = ErrorMessage diff --git a/src/components/forms/AddressInput/index.tsx b/src/components/forms/AddressInput/index.tsx index 7fb02aeccb..a29d810c16 100644 --- a/src/components/forms/AddressInput/index.tsx +++ b/src/components/forms/AddressInput/index.tsx @@ -68,7 +68,6 @@ const AddressInput = ({ // A crypto domain name if (isValidEnsName(address) || isValidCryptoDomainName(address)) { setResolutions((prev) => ({ ...prev, [rawVal]: '' })) - getAddressFromDomain(address) .then((resolverAddr) => { const formattedAddress = checksumAddress(resolverAddr) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index a18ac04818..96fe18cc44 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -124,7 +124,7 @@ export const minMaxDecimalsLength = return minMaxLengthErrMsg ? `Should be ${minLen} to ${maxLen} decimals` : undefined } -export const ADDRESS_REPEATED_ERROR = 'Address already introduced' +export const ADDRESS_REPEATED_ERROR = 'Address already added' export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.' export const THRESHOLD_ERROR = 'You cannot set more confirmations than owners' diff --git a/src/config/chain.d.ts b/src/config/chain.d.ts index 1092b4f793..ad4be8f6fe 100644 --- a/src/config/chain.d.ts +++ b/src/config/chain.d.ts @@ -17,6 +17,7 @@ export const CHAIN_ID: Record = { // Values match that required of onboard and returned by CGW export enum WALLETS { + SAFE_MOBILE = 'safeMobile', METAMASK = 'metamask', WALLET_CONNECT = 'walletConnect', TREZOR = 'trezor', diff --git a/src/config/index.ts b/src/config/index.ts index b32de1d124..f6baef91ed 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -69,8 +69,7 @@ const formatRpcServiceUrl = ({ authentication, value }: RpcUri, TOKEN: string): return needsToken ? `${value}${TOKEN}` : value } -export const getRpcServiceUrl = (): string => { - const { rpcUri } = getChainInfo() +export const getRpcServiceUrl = (rpcUri = getChainInfo().rpcUri): string => { return formatRpcServiceUrl(rpcUri, INFURA_TOKEN) } diff --git a/src/logic/config/store/middleware/index.ts b/src/logic/config/store/middleware/index.ts index 209e6f7223..2d60b81b4c 100644 --- a/src/logic/config/store/middleware/index.ts +++ b/src/logic/config/store/middleware/index.ts @@ -1,4 +1,4 @@ -import { Action } from 'redux' +import { Action } from 'redux-actions' import { clearCurrentSession } from 'src/logic/currentSession/store/actions/clearCurrentSession' import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' @@ -7,11 +7,13 @@ import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStor import { Dispatch } from 'src/logic/safe/store/actions/types' import { CONFIG_ACTIONS } from '../actions' import { store as reduxStore } from 'src/store' +import { ChainId } from 'src/config/chain' +import onboard from 'src/logic/wallets/onboard' export const configMiddleware = ({ dispatch }: typeof reduxStore) => (next: Dispatch) => - async (action: Action) => { + async (action: Action) => { const handledAction = next(action) switch (action.type) { diff --git a/src/logic/contracts/safeContractErrors.ts b/src/logic/contracts/safeContractErrors.ts index fa5970606c..8ef0bb1471 100644 --- a/src/logic/contracts/safeContractErrors.ts +++ b/src/logic/contracts/safeContractErrors.ts @@ -13,7 +13,7 @@ export const decodeMessage = (message: string): string => { return code ? `${code}: ${CONTRACT_ERRORS[code]}` : message } -const getContractErrorMessage = async ({ +export const getContractErrorMessage = async ({ safeInstance, from, data, @@ -21,7 +21,7 @@ const getContractErrorMessage = async ({ safeInstance: GnosisSafe from: string data: string -}): Promise => { +}): Promise => { const web3 = getWeb3() try { @@ -38,24 +38,5 @@ const getContractErrorMessage = async ({ return decodeMessage(contractOutput) } catch (err) { logError(Errors._817, err.message) - return null } } - -export const fetchOnchainError = async ( - data: string, - safeInstance: GnosisSafe, - from: string, -): Promise => { - const contractErrorMessage = await getContractErrorMessage({ - safeInstance, - from, - data, - }) - - if (contractErrorMessage) { - logError(Errors._803, contractErrorMessage) - } - - return contractErrorMessage -} diff --git a/src/logic/hooks/__tests__/useEstimateSafeTxGas.test.ts b/src/logic/hooks/__tests__/useEstimateSafeTxGas.test.ts new file mode 100644 index 0000000000..b1055bf06d --- /dev/null +++ b/src/logic/hooks/__tests__/useEstimateSafeTxGas.test.ts @@ -0,0 +1,78 @@ +import { useEstimateSafeTxGas } from 'src/logic/hooks/useEstimateSafeTxGas' +import { renderHook } from '@testing-library/react-hooks' +import * as gas from 'src/logic/safe/transactions/gas' + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux') + return { + ...original, + useSelector: jest.fn, + } +}) + +describe('useEstimateSafeTxGas', () => { + it(`should return 0 if it is not a tx creation`, () => { + const spy = jest.spyOn(gas, 'estimateSafeTxGas') + + const { result } = renderHook(() => + useEstimateSafeTxGas({ + txAmount: '', + txData: '', + txRecipient: '', + isCreation: false, + isRejectTx: false, + }), + ) + expect(result.current).toBe('0') + expect(spy).toHaveBeenCalledTimes(0) + }) + + it(`should return 0 if it is a reject tx`, () => { + const spy = jest.spyOn(gas, 'estimateSafeTxGas') + + const { result } = renderHook(() => + useEstimateSafeTxGas({ + txAmount: '', + txData: '', + txRecipient: '', + isCreation: false, + isRejectTx: true, + }), + ) + expect(result.current).toBe('0') + expect(spy).toHaveBeenCalledTimes(0) + }) + + it(`calls estimateSafeTxGas if it is a tx creation`, () => { + const spy = jest.spyOn(gas, 'estimateSafeTxGas') + + renderHook(() => + useEstimateSafeTxGas({ + txAmount: '', + txData: '', + txRecipient: '', + isCreation: true, + isRejectTx: false, + }), + ) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it(`returns 0 if estimateSafeTxGas throws`, () => { + const spy = jest.spyOn(gas, 'estimateSafeTxGas').mockImplementation(() => { + throw new Error() + }) + + const { result } = renderHook(() => + useEstimateSafeTxGas({ + txAmount: '', + txData: '', + txRecipient: '', + isCreation: true, + isRejectTx: false, + }), + ) + expect(spy).toHaveBeenCalledTimes(1) + expect(result.current).toBe('0') + }) +}) diff --git a/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts index a7979eaaf0..f5f9d57b3f 100644 --- a/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts +++ b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts @@ -1,144 +1,242 @@ import { - checkIfTxIsApproveAndExecution, - checkIfTxIsCreation, calculateTotalGasCost, + EstimationStatus, + getDefaultGasEstimation, + useEstimateTransactionGas, } from 'src/logic/hooks/useEstimateTransactionGas' +import { renderHook } from '@testing-library/react-hooks' +import { DEFAULT_MAX_GAS_FEE, DEFAULT_MAX_PRIO_FEE } from 'src/logic/wallets/ethTransactions' +import { fromWei, toWei } from 'web3-utils' +import * as ethTransactions from 'src/logic/wallets/ethTransactions' +import * as gas from 'src/logic/safe/transactions/gas' +import { waitFor } from 'src/utils/test-utils' + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux') + return { + ...original, + useSelector: jest.fn, + } +}) -describe('checkIfTxIsCreation', () => { - it(`should return true if there are no confirmations for the transaction and the transaction is not spendingLimit`, () => { - // given - const transactionConfirmations = 0 - const transactionType = '' +describe('useEstimateTransactionGas', () => { + let mockParams + let initialState + let failureState + + beforeAll(() => { + mockParams = { + txData: 'mocktxdata', + txRecipient: '', + txAmount: '', + safeTxGas: '', + operation: 1, + isExecution: true, + approvalAndExecution: false, + } + initialState = getDefaultGasEstimation({ + txEstimationExecutionStatus: EstimationStatus.LOADING, + gasPrice: '0', + gasPriceFormatted: '0', + gasMaxPrioFee: '0', + gasMaxPrioFeeFormatted: '0', + }) + failureState = getDefaultGasEstimation({ + txEstimationExecutionStatus: EstimationStatus.FAILURE, + gasPrice: DEFAULT_MAX_GAS_FEE.toString(), + gasPriceFormatted: fromWei(DEFAULT_MAX_GAS_FEE.toString(), 'gwei'), + gasMaxPrioFee: DEFAULT_MAX_PRIO_FEE.toString(), + gasMaxPrioFeeFormatted: fromWei(DEFAULT_MAX_PRIO_FEE.toString(), 'gwei'), + }) + }) - // when - const result = checkIfTxIsCreation(transactionConfirmations, transactionType) + let gasPriceEstimationSpy, prioFeeEstimationSpy, gasLimitEstimationSpy + beforeEach(() => { + gasPriceEstimationSpy = jest.spyOn(ethTransactions, 'calculateGasPrice').mockImplementation(() => { + return Promise.resolve('0') + }) + prioFeeEstimationSpy = jest.spyOn(ethTransactions, 'getFeesPerGas').mockImplementation(() => { + return Promise.resolve({ + maxPriorityFeePerGas: 0, + maxFeePerGas: 0, + }) + }) + gasLimitEstimationSpy = jest.spyOn(gas, 'estimateGasForTransactionExecution').mockImplementation(() => { + return Promise.resolve(0) + }) + jest.spyOn(gas, 'checkTransactionExecution').mockImplementation(jest.fn()) + }) - // then - expect(result).toBe(true) + afterEach(() => { + jest.restoreAllMocks() }) - it(`should return false if there are no confirmations for the transaction and the transaction is spendingLimit`, () => { - // given - const transactionConfirmations = 0 - const transactionType = 'spendingLimit' - // when - const result = checkIfTxIsCreation(transactionConfirmations, transactionType) + it('returns initial estimation and successful loading state if tx is not execution', () => { + const { result } = renderHook(() => useEstimateTransactionGas({ ...mockParams, isExecution: false })) - // then - expect(result).toBe(false) + expect(result.current).toStrictEqual({ ...initialState, txEstimationExecutionStatus: EstimationStatus.SUCCESS }) }) - it(`should return false if there are confirmations for the transaction`, () => { - // given - const transactionConfirmations = 2 - const transactionType = '' - // when - const result = checkIfTxIsCreation(transactionConfirmations, transactionType) + it('returns initial estimation and successful loading state if there is no txData', () => { + const { result } = renderHook(() => useEstimateTransactionGas({ ...mockParams, txData: '' })) - // then - expect(result).toBe(false) + expect(result.current).toStrictEqual({ ...initialState, txEstimationExecutionStatus: EstimationStatus.SUCCESS }) }) -}) -describe('checkIfTxIsApproveAndExecution', () => { - const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B' - it(`should return true if there is only one confirmation left to reach the safe threshold and there is a preApproving account`, () => { - // given - const transactionConfirmations = 2 - const safeThreshold = 3 - const transactionType = '' - const preApprovingOwner = mockedEthAccount - - // when - const result = checkIfTxIsApproveAndExecution( - safeThreshold, - transactionConfirmations, - transactionType, - preApprovingOwner, + it('estimates gas price, max priority fee and gas limit', async () => { + renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(gasPriceEstimationSpy).toHaveBeenCalledTimes(1) + expect(prioFeeEstimationSpy).toHaveBeenCalledTimes(1) + expect(gasLimitEstimationSpy).toHaveBeenCalledTimes(1) + }) + }) + + it('returns manualGasPrice in Wei if it exists instead of estimation', async () => { + const mockManualGasPrice = '1' + const mockGasPrice = toWei(mockManualGasPrice, 'gwei') + + const { result } = renderHook(() => + useEstimateTransactionGas({ ...mockParams, manualGasPrice: mockManualGasPrice }), ) - // then - expect(result).toBe(true) - }) - it(`should return false if there is only one confirmation left to reach the safe threshold and but there is no preApproving account`, () => { - // given - const transactionConfirmations = 2 - const safeThreshold = 3 - const transactionType = '' - - // when - const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) - - // then - expect(result).toBe(false) - }) - it(`should return true if the transaction is spendingLimit and there is a preApproving account`, () => { - // given - const transactionConfirmations = 0 - const transactionType = 'spendingLimit' - const safeThreshold = 3 - const preApprovingOwner = mockedEthAccount - - // when - const result = checkIfTxIsApproveAndExecution( - safeThreshold, - transactionConfirmations, - transactionType, - preApprovingOwner, + await waitFor(() => { + expect(result.current.gasPrice).toBe(mockGasPrice) + expect(gasPriceEstimationSpy).toHaveBeenCalledTimes(0) + }) + }) + + it('returns manualGasLimit if it exists instead of estimation', async () => { + const mockManualGasLimit = '30000' + + const { result } = renderHook(() => + useEstimateTransactionGas({ ...mockParams, manualGasLimit: mockManualGasLimit }), ) - // then - expect(result).toBe(true) - }) - it(`should return false if the transaction is spendingLimit and there is no preApproving account`, () => { - // given - const transactionConfirmations = 0 - const transactionType = 'spendingLimit' - const safeThreshold = 3 - const preApprovingOwner = mockedEthAccount - - // when - const result = checkIfTxIsApproveAndExecution( - safeThreshold, - transactionConfirmations, - transactionType, - preApprovingOwner, + await waitFor(() => { + expect(result.current.gasLimit).toBe(mockManualGasLimit) + expect(gasLimitEstimationSpy).toHaveBeenCalledTimes(0) + }) + }) + + it('returns manualMaxPrioFee post EIP-1559 if it exists instead of estimation', async () => { + jest.spyOn(gas, 'isMaxFeeParam').mockImplementation(() => true) + const mockManualMaxPrioFee = '1' + const mockMaxPrioFee = toWei(mockManualMaxPrioFee, 'gwei') + + const { result } = renderHook(() => + useEstimateTransactionGas({ ...mockParams, manualMaxPrioFee: mockManualMaxPrioFee }), ) - // then - expect(result).toBe(true) + await waitFor(() => { + expect(result.current.gasMaxPrioFee).toBe(mockMaxPrioFee) + expect(prioFeeEstimationSpy).toHaveBeenCalledTimes(0) + }) + }) + + it('returns 0 for maxPrioFee pre EIP-1559', async () => { + jest.spyOn(gas, 'isMaxFeeParam').mockImplementation(() => false) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current.gasMaxPrioFee).toBe('0') + expect(prioFeeEstimationSpy).toHaveBeenCalledTimes(0) + }) }) - it(`should return false if the are missing more than one confirmations to reach the safe threshold and the transaction is not spendingLimit`, () => { - // given - const transactionConfirmations = 0 - const transactionType = '' - const safeThreshold = 3 - // when - const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) + it('returns a failure state if checkTransactionExecution is false', async () => { + jest.spyOn(gas, 'checkTransactionExecution').mockImplementation(() => { + return Promise.resolve(false) + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current.txEstimationExecutionStatus).toBe(EstimationStatus.FAILURE) + }) + }) + + it('returns a success state if checkTransactionExecution is true', async () => { + jest.spyOn(gas, 'checkTransactionExecution').mockImplementation(() => { + return Promise.resolve(true) + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current.txEstimationExecutionStatus).toBe(EstimationStatus.SUCCESS) + }) + }) + + it('returns failure state if getFeesPerGas throws', async () => { + jest.spyOn(gas, 'isMaxFeeParam').mockImplementation(() => true) + jest.spyOn(ethTransactions, 'getFeesPerGas').mockImplementation(() => { + throw new Error() + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current).toStrictEqual(failureState) + }) + }) + + it('returns failure state if estimateGasForTransactionExecution throws', async () => { + jest.spyOn(gas, 'estimateGasForTransactionExecution').mockImplementation(() => { + throw new Error() + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current).toStrictEqual(failureState) + }) + }) + + it('returns failure state if estimateGasForTransactionExecution throws', async () => { + jest.spyOn(gas, 'checkTransactionExecution').mockImplementation(() => { + throw new Error() + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) + + await waitFor(() => { + expect(result.current).toStrictEqual(failureState) + }) + }) + + it('returns failure state if estimateGasForTransactionExecution throws', async () => { + jest.spyOn(ethTransactions, 'calculateGasPrice').mockImplementation(() => { + throw new Error() + }) + + const { result } = renderHook(() => useEstimateTransactionGas(mockParams)) - // then - expect(result).toBe(false) + await waitFor(() => { + expect(result.current).toStrictEqual(failureState) + }) }) }) describe('calculateTotalGasCost', () => { it('calculates total gas cost for pre-EIP-1559 txns', () => { - const [gasCost, gasCostFormatted] = calculateTotalGasCost('53160', '264000000000', '0', 18) + const { gasCost, gasCostFormatted } = calculateTotalGasCost('53160', '264000000000', '0', 18) expect(gasCost).toBe('0.01403424') expect(gasCostFormatted).toBe('0.01403') }) it('calculates total gas cost for EIP-1559 txns', () => { - const [gasCost, gasCostFormatted] = calculateTotalGasCost('53160', '264000000000', '2500000000', 18) + const { gasCost, gasCostFormatted } = calculateTotalGasCost('53160', '264000000000', '2500000000', 18) expect(gasCost).toBe('0.01416714') expect(gasCostFormatted).toBe('0.01417') }) it('calculates total gas cost with a non-default max prio fee', () => { - const [gasCost, gasCostFormatted] = calculateTotalGasCost('53160', '264000000000', '1000000000000', 18) + const { gasCost, gasCostFormatted } = calculateTotalGasCost('53160', '264000000000', '1000000000000', 18) expect(gasCost).toBe('0.06719424') expect(gasCostFormatted).toBe('0.06719') diff --git a/src/logic/hooks/useEstimateSafeCreationGas.tsx b/src/logic/hooks/useEstimateSafeCreationGas.tsx index 5088c774bd..4456154b19 100644 --- a/src/logic/hooks/useEstimateSafeCreationGas.tsx +++ b/src/logic/hooks/useEstimateSafeCreationGas.tsx @@ -7,6 +7,7 @@ import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { calculateGasPrice, getFeesPerGas, setMaxPrioFeePerGas } from 'src/logic/wallets/ethTransactions' import { userAccountSelector } from '../wallets/store/selectors' import { getNativeCurrency } from 'src/config' +import { isMaxFeeParam } from 'src/logic/safe/transactions/gas' type EstimateSafeCreationGasProps = { addresses: string[] @@ -32,7 +33,7 @@ const estimateGas = async ( const [gasEstimation, gasPrice, feesPerGas] = await Promise.all([ estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt), calculateGasPrice(), - getFeesPerGas(), + isMaxFeeParam() ? getFeesPerGas() : { maxPriorityFeePerGas: 0, maxFeePerGas: 0 }, ]) const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) diff --git a/src/logic/hooks/useEstimateSafeTxGas.tsx b/src/logic/hooks/useEstimateSafeTxGas.tsx new file mode 100644 index 0000000000..636f227e00 --- /dev/null +++ b/src/logic/hooks/useEstimateSafeTxGas.tsx @@ -0,0 +1,51 @@ +import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas' +import { currentSafe } from 'src/logic/safe/store/selectors' + +type UseEstimateSafeTxGasProps = { + isCreation: boolean + isRejectTx: boolean + txData: string + txRecipient: string + txAmount: string + operation?: Operation +} + +export const useEstimateSafeTxGas = ({ + isCreation, + isRejectTx, + txData, + txRecipient, + txAmount, + operation, +}: UseEstimateSafeTxGasProps): string => { + const [safeTxGasEstimation, setSafeTxGasEstimation] = useState('0') + const { address: safeAddress, currentVersion: safeVersion } = useSelector(currentSafe) ?? {} + + useEffect(() => { + if (!isCreation || isRejectTx) return + const estimateSafeTxGasCall = async () => { + try { + const safeTxGasEstimation = await estimateSafeTxGas( + { + safeAddress, + txData, + txRecipient, + txAmount: txAmount || '0', + operation: operation || Operation.CALL, + }, + safeVersion, + ) + setSafeTxGasEstimation(safeTxGasEstimation) + } catch (error) { + console.warn(error.message) + } + } + estimateSafeTxGasCall() + }, [isCreation, isRejectTx, operation, safeAddress, safeVersion, txAmount, txData, txRecipient]) + + return safeTxGasEstimation +} diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index e4777883c3..4ae64facd1 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -7,20 +7,22 @@ import { fromWei, toWei } from 'web3-utils' import { getNativeCurrency } from 'src/config' import { checkTransactionExecution, - estimateSafeTxGas, - estimateTransactionGasLimit, + estimateGasForTransactionExecution, isMaxFeeParam, } from 'src/logic/safe/transactions/gas' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { calculateGasPrice, setMaxPrioFeePerGas, getFeesPerGas } from 'src/logic/wallets/ethTransactions' +import { + calculateGasPrice, + setMaxPrioFeePerGas, + getFeesPerGas, + DEFAULT_MAX_GAS_FEE, + DEFAULT_MAX_PRIO_FEE, +} from 'src/logic/wallets/ethTransactions' import { currentSafe } from 'src/logic/safe/store/selectors' import { providerSelector } from 'src/logic/wallets/store/selectors' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' -import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { isSpendingLimit } from 'src/routes/safe/components/Transactions/helpers/utils' -import useCanTxExecute from './useCanTxExecute' export enum EstimationStatus { LOADING = 'LOADING', @@ -28,38 +30,22 @@ export enum EstimationStatus { SUCCESS = 'SUCCESS', } -export const checkIfTxIsApproveAndExecution = ( - threshold: number, - txConfirmations: number, - txType?: string, - preApprovingOwner?: string, -): boolean => { - if (txConfirmations === threshold) return false - if (!preApprovingOwner) return false - return txConfirmations + 1 === threshold || isSpendingLimit(txType) -} - -export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean => - txConfirmations === 0 && !isSpendingLimit(txType) - type UseEstimateTransactionGasProps = { txData: string txRecipient: string txConfirmations?: List txAmount?: string - preApprovingOwner?: string operation?: number safeTxGas?: string - txType?: string manualGasPrice?: string manualMaxPrioFee?: string manualGasLimit?: string - manualSafeNonce?: number // Edited nonce + isExecution: boolean + approvalAndExecution: boolean } -export type TransactionGasEstimationResult = { +type TransactionGasEstimationResult = { txEstimationExecutionStatus: EstimationStatus - gasEstimation: string // Amount of gas needed for execute or approve the transaction gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice) gasCostFormatted: string // Cost of gas in format '< | > 100' gasPrice: string // Current price of gas unit @@ -67,8 +53,6 @@ export type TransactionGasEstimationResult = { gasMaxPrioFee: string // Current max prio gas price gasMaxPrioFeeFormatted: string // Current max prio gas formatted gasLimit: string // Minimum gas requited to execute the Tx - isCreation: boolean // Returns true if the transaction is a creation transaction - isOffChainSignature: boolean // Returns true if offChainSignature is available } type DefaultGasEstimationParams = { @@ -77,21 +61,16 @@ type DefaultGasEstimationParams = { gasPriceFormatted: string gasMaxPrioFee: string gasMaxPrioFeeFormatted: string - isCreation?: boolean - isOffChainSignature?: boolean } -const getDefaultGasEstimation = ({ +export const getDefaultGasEstimation = ({ txEstimationExecutionStatus, gasPrice, gasPriceFormatted, gasMaxPrioFee, gasMaxPrioFeeFormatted, - isCreation = false, - isOffChainSignature = false, }: DefaultGasEstimationParams): TransactionGasEstimationResult => { return { txEstimationExecutionStatus, - gasEstimation: '0', gasCost: '0', gasCostFormatted: '< 0.001', gasPrice, @@ -99,8 +78,6 @@ const getDefaultGasEstimation = ({ gasMaxPrioFee, gasMaxPrioFeeFormatted, gasLimit: '0', - isCreation, - isOffChainSignature, } } @@ -109,12 +86,15 @@ export const calculateTotalGasCost = ( gasPrice: string, gasMaxPrioFee: string, decimals: number, -): [string, string] => { +): { gasCost: string; gasCostFormatted: string } => { const totalPricePerGas = parseInt(gasPrice, 10) + parseInt(gasMaxPrioFee || '0', 10) const estimatedGasCosts = parseInt(gasLimit, 10) * totalPricePerGas const gasCost = fromTokenUnit(estimatedGasCosts, decimals) - const formattedGasCost = formatAmount(gasCost) - return [gasCost, formattedGasCost] + const gasCostFormatted = formatAmount(gasCost) + return { + gasCost, + gasCostFormatted, + } } export const useEstimateTransactionGas = ({ @@ -122,14 +102,13 @@ export const useEstimateTransactionGas = ({ txData, txConfirmations, txAmount, - preApprovingOwner, operation, safeTxGas, - txType, manualGasPrice, manualMaxPrioFee, manualGasLimit, - manualSafeNonce, + isExecution, + approvalAndExecution, }: UseEstimateTransactionGasProps): TransactionGasEstimationResult => { const [gasEstimation, setGasEstimation] = useState( getDefaultGasEstimation({ @@ -141,118 +120,57 @@ export const useEstimateTransactionGas = ({ }), ) const nativeCurrency = getNativeCurrency() - const { address: safeAddress = '', threshold = 1, currentVersion: safeVersion = '' } = useSelector(currentSafe) ?? {} - const { account: from, smartContractWallet, name: providerName } = useSelector(providerSelector) - - const canTxExecute = useCanTxExecute(preApprovingOwner, txConfirmations?.size) + const { address: safeAddress, currentVersion: safeVersion } = useSelector(currentSafe) ?? {} + const { account: from } = useSelector(providerSelector) useEffect(() => { - const estimateGas = async () => { - if (!txData.length) { - return - } - const isOffChainSignature = checkIfOffChainSignatureIsPossible(canTxExecute, smartContractWallet, safeVersion) - const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType) - - const { maxPriorityFeePerGas, maxFeePerGas } = await getFeesPerGas() - const maxPrioFeePerGas = setMaxPrioFeePerGas(maxPriorityFeePerGas, maxFeePerGas) + if (!isExecution || !txData) { + setGasEstimation((prev) => ({ ...prev, txEstimationExecutionStatus: EstimationStatus.SUCCESS })) + return + } - if (isOffChainSignature && !isCreation) { - setGasEstimation( - getDefaultGasEstimation({ - txEstimationExecutionStatus: EstimationStatus.SUCCESS, - gasPrice: fromWei(maxFeePerGas.toString(), 'gwei'), - gasPriceFormatted: maxFeePerGas.toString(), - gasMaxPrioFee: fromWei(maxPrioFeePerGas.toString(), 'gwei'), - gasMaxPrioFeeFormatted: maxPrioFeePerGas.toString(), - isCreation, - isOffChainSignature, - }), - ) - return + const estimateGas = async () => { + const txParameters = { + safeAddress, + safeVersion, + txRecipient, + txConfirmations, + txAmount: txAmount || '0', + txData, + operation: operation || Operation.CALL, + from, + gasPrice: '0', + gasToken: ZERO_ADDRESS, + refundReceiver: ZERO_ADDRESS, + safeTxGas: safeTxGas || '0', + approvalAndExecution, } - const approvalAndExecution = checkIfTxIsApproveAndExecution( - Number(threshold), - txConfirmations?.size || 0, - txType, - preApprovingOwner, - ) try { - let safeTxGasEstimation = safeTxGas || '0' - let ethGasLimitEstimation = 0 - let transactionCallSuccess = true - let txEstimationExecutionStatus = EstimationStatus.LOADING - - if (isCreation) { - safeTxGasEstimation = await estimateSafeTxGas( - { - safeAddress, - txData, - txRecipient, - txAmount: txAmount || '0', - operation: operation || Operation.CALL, - }, - safeVersion, - ) - } - - if (canTxExecute || approvalAndExecution) { - ethGasLimitEstimation = await estimateTransactionGasLimit({ - safeAddress, - safeVersion, - txRecipient, - txData, - txAmount: txAmount || '0', - txConfirmations, - isExecution: canTxExecute, - operation: operation || Operation.CALL, - from, - safeTxGas: safeTxGasEstimation, - approvalAndExecution, - }) - } + const gasLimit = manualGasLimit ?? (await estimateGasForTransactionExecution(txParameters)).toString() + const didTxCallSucceed = await checkTransactionExecution({ + ...txParameters, + gasLimit, + }) + const txEstimationExecutionStatus = didTxCallSucceed ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE const gasPrice = manualGasPrice ? toWei(manualGasPrice, 'gwei') : await calculateGasPrice() const gasPriceFormatted = fromWei(gasPrice, 'gwei') const gasMaxPrioFee = isMaxFeeParam() ? manualMaxPrioFee ? toWei(manualMaxPrioFee, 'gwei') - : setMaxPrioFeePerGas(maxPriorityFeePerGas, parseInt(gasPrice)).toString() + : setMaxPrioFeePerGas((await getFeesPerGas()).maxPriorityFeePerGas, parseInt(gasPrice)).toString() : '0' const gasMaxPrioFeeFormatted = fromWei(gasMaxPrioFee.toString(), 'gwei') - const gasLimit = manualGasLimit || ethGasLimitEstimation.toString() - const [gasCost, gasCostFormatted] = calculateTotalGasCost( + const { gasCost, gasCostFormatted } = calculateTotalGasCost( gasLimit, gasPrice, gasMaxPrioFee, nativeCurrency.decimals, ) - if (canTxExecute) { - transactionCallSuccess = await checkTransactionExecution({ - safeAddress, - safeVersion, - txRecipient, - txData, - txAmount: txAmount || '0', - txConfirmations, - operation: operation || Operation.CALL, - from, - gasPrice: '0', - gasToken: ZERO_ADDRESS, - gasLimit, - refundReceiver: ZERO_ADDRESS, - safeTxGas: safeTxGasEstimation, - approvalAndExecution, - }) - } - - txEstimationExecutionStatus = transactionCallSuccess ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE - setGasEstimation({ txEstimationExecutionStatus, - gasEstimation: safeTxGasEstimation, gasCost, gasCostFormatted, gasPrice, @@ -260,19 +178,16 @@ export const useEstimateTransactionGas = ({ gasMaxPrioFee, gasMaxPrioFeeFormatted, gasLimit, - isCreation, - isOffChainSignature, }) } catch (error) { console.warn(error.message) - // If safeTxGas estimation fail we set this value to 0 (so up to all gasLimit can be used) setGasEstimation( getDefaultGasEstimation({ txEstimationExecutionStatus: EstimationStatus.FAILURE, - gasPrice: maxFeePerGas.toString(), - gasPriceFormatted: fromWei(maxFeePerGas.toString(), 'gwei'), - gasMaxPrioFee: maxPrioFeePerGas.toString(), - gasMaxPrioFeeFormatted: fromWei(maxPrioFeePerGas.toString(), 'gwei'), + gasPrice: DEFAULT_MAX_GAS_FEE.toString(), + gasPriceFormatted: fromWei(DEFAULT_MAX_GAS_FEE.toString(), 'gwei'), + gasMaxPrioFee: DEFAULT_MAX_PRIO_FEE.toString(), + gasMaxPrioFeeFormatted: fromWei(DEFAULT_MAX_PRIO_FEE.toString(), 'gwei'), }), ) } @@ -280,26 +195,21 @@ export const useEstimateTransactionGas = ({ estimateGas() }, [ - txData, - safeAddress, - txRecipient, - txConfirmations, - txAmount, - preApprovingOwner, - nativeCurrency.decimals, - threshold, + approvalAndExecution, + isExecution, from, - operation, - safeVersion, - smartContractWallet, safeTxGas, - txType, - providerName, + manualGasLimit, manualGasPrice, manualMaxPrioFee, - manualGasLimit, - manualSafeNonce, - canTxExecute, + nativeCurrency.decimals, + operation, + safeAddress, + safeVersion, + txAmount, + txConfirmations, + txData, + txRecipient, ]) return gasEstimation diff --git a/src/logic/hooks/useGetRecommendedNonce.tsx b/src/logic/hooks/useRecommendedNonce.tsx similarity index 64% rename from src/logic/hooks/useGetRecommendedNonce.tsx rename to src/logic/hooks/useRecommendedNonce.tsx index af08fd512b..a820c60a15 100644 --- a/src/logic/hooks/useGetRecommendedNonce.tsx +++ b/src/logic/hooks/useRecommendedNonce.tsx @@ -1,16 +1,12 @@ import { SafeTransactionEstimation } from '@gnosis.pm/safe-react-gateway-sdk' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { getRecommendedNonce } from '../safe/api/fetchSafeTxGasEstimation' -import { getLastTxNonce } from '../safe/store/selectors/gatewayTransactions' +import { getRecommendedNonce } from 'src/logic/safe/api/fetchSafeTxGasEstimation' +import { getLastTxNonce } from 'src/logic/safe/store/selectors/gatewayTransactions' -type UseGetRecommendedNonce = (safeAddress: string) => number | undefined - -const useGetRecommendedNonce: UseGetRecommendedNonce = (safeAddress) => { +const useRecommendedNonce = (safeAddress: string): number => { const lastTxNonce = useSelector(getLastTxNonce) - const storeNextNonce = lastTxNonce ? lastTxNonce + 1 : undefined - - const [recommendedNonce, setRecommendedNonce] = useState(storeNextNonce) + const [recommendedNonce, setRecommendedNonce] = useState(lastTxNonce ? lastTxNonce + 1 : 0) useEffect(() => { let isCurrent = true @@ -37,4 +33,4 @@ const useGetRecommendedNonce: UseGetRecommendedNonce = (safeAddress) => { return recommendedNonce } -export default useGetRecommendedNonce +export default useRecommendedNonce diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index fb2c98e5dd..3aea87b083 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -270,7 +270,8 @@ export const createTxNotifications = ( dispatch: Dispatch, ): { closePending: () => void - showOnError: (err: Error & { code: number }, contractErrorMessage: string | null) => void + showOnError: (err: Error & { code: number }, contractErrorMessage?: string) => void + showOnRejection: (err: Error & { code: number }, contractErrorMessage?: string) => void } => { // Notifications // Each tx gets a slot in the global snackbar queue @@ -282,7 +283,11 @@ export const createTxNotifications = ( return { closePending: () => dispatch(closeSnackbarAction({ key: beforeExecutionKey })), - showOnError: (err: Error & { code: number }, customErrorMessage: string | null) => { + showOnRejection: (err: Error & { code?: number }) => { + dispatch(enqueueSnackbar({ key: err.code, ...notificationSlot.afterRejection })) + }, + + showOnError: (err: Error & { code: number }, customErrorMessage?: string) => { const msg = isTxPendingError(err) ? NOTIFICATIONS.TX_PENDING_MSG : { diff --git a/src/logic/safe/hooks/useLocalSafes.tsx b/src/logic/safe/hooks/useLocalSafes.tsx index 8a5d9e3012..d41a231ee8 100644 --- a/src/logic/safe/hooks/useLocalSafes.tsx +++ b/src/logic/safe/hooks/useLocalSafes.tsx @@ -7,7 +7,7 @@ import { ChainId } from 'src/config/chain.d' import { SafeRecordProps } from '../store/models/safe' import { getLocalNetworkSafesById } from '../utils' -type LocalSafes = Record +export type LocalSafes = Record const getEmptyLocalSafes = (): LocalSafes => { return getChains().reduce((safes, { chainId }) => ({ ...safes, [chainId]: [] }), {} as LocalSafes) diff --git a/src/logic/safe/store/actions/__tests__/TxSender.test.ts b/src/logic/safe/store/actions/__tests__/TxSender.test.ts new file mode 100644 index 0000000000..0922f826be --- /dev/null +++ b/src/logic/safe/store/actions/__tests__/TxSender.test.ts @@ -0,0 +1,330 @@ +import { TxSender } from 'src/logic/safe/store/actions/createTransaction' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { store } from 'src/store' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import * as utils from 'src/logic/safe/store/actions/utils' +import * as walletSelectors from 'src/logic/wallets/store/selectors' +import * as safeSelectors from 'src/logic/safe/store/selectors' +import * as safeContracts from 'src/logic/contracts/safeContracts' +import * as notificationBuilder from 'src/logic/notifications/notificationBuilder' +import * as safeTxSigner from 'src/logic/safe/safeTxSigner' +import * as offChainSigner from 'src/logic/safe/transactions/offchainSigner' +import * as txHistory from 'src/logic/safe/transactions/txHistory' +import * as pendingTransactions from 'src/logic/safe/store/actions/pendingTransactions' +import * as fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' +import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' +import * as send from 'src/logic/safe/transactions/send' +import * as getWeb3 from 'src/logic/wallets/getWeb3' +import { waitFor } from '@testing-library/react' +import { LocalTransactionStatus } from 'src/logic/safe/store/models/types/gateway.d' + +jest.mock('bnc-onboard', () => () => ({ + config: jest.fn(), + getState: jest.fn(() => ({ + appNetworkId: 4, + wallet: { + provider: { + name: 'MetaMask', + account: '0x123', + hardwareWallet: false, + smartContractWallet: false, + network: '4', + available: true, + loaded: true, + ensDomain: '', + }, + }, + })), + walletCheck: jest.fn(), + walletReset: jest.fn(), + walletSelect: jest.fn(), // returns true or false +})) + +jest.mock('src/logic/safe/store/actions/transactions/fetchTransactions', () => { + const original = jest.requireActual('src/logic/safe/store/actions/transactions/fetchTransactions') + return { + __esModule: true, + ...original, + default: jest.fn(), + } +}) + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux') + return { + ...original, + useSelector: jest.fn, + } +}) + +const mockTransactionDetails = { + txId: '', + executedAt: null, + txStatus: LocalTransactionStatus['SUCCESS'], + txInfo: { + type: 'Transfer', + sender: { + value: '', + name: null, + logoUri: null, + }, + recipient: { + value: '', + name: null, + logoUri: null, + }, + direction: 'OUTGOING', + transferInfo: { + type: 'ERC20', + tokenAddress: '', + tokenName: null, + tokenSymbol: null, + logoUri: null, + decimals: null, + value: '', + }, + }, + txData: null, + detailedExecutionInfo: null, + txHash: null, + safeAppInfo: null, +} + +const mockTxProps = { + from: '', + to: '', + valueInWei: '', + notifiedTransaction: '', + safeAddress: '', + txData: EMPTY_DATA, + operation: 0, + navigateToTransactionsTab: false, + origin: null, + safeTxGas: '', + txNonce: '0', +} + +const mockPromiEvent = { + once: jest.fn((_, handler) => handler('mocktxhash')) as any, + on: jest.fn(), + then: jest.fn(), + catch: jest.fn(), + finally: jest.fn(), + [Symbol.toStringTag]: '', +} + +describe('TxSender', () => { + let tryOffChainSigningSpy, saveTxToHistorySpy, addPendingTransactionSpy, navigateToTxSpy, fetchTransactionsSpy + + beforeEach(() => { + jest.restoreAllMocks() + jest.spyOn(utils, 'getNonce') + jest.spyOn(safeSelectors, 'currentSafeCurrentVersion') + jest.spyOn(walletSelectors, 'providerSelector').mockImplementation(() => ({ + name: 'MetaMask', + account: '0x123', + hardwareWallet: false, + smartContractWallet: false, + network: '4', + available: true, + loaded: true, + ensDomain: '', + })) + jest.spyOn(safeContracts, 'getGnosisSafeInstanceAt') + jest.spyOn(notificationBuilder, 'createTxNotifications') + jest.spyOn(getWeb3, 'isSmartContractWallet') + tryOffChainSigningSpy = jest + .spyOn(offChainSigner, 'tryOffChainSigning') + .mockImplementation(() => Promise.resolve('mocksignature')) + saveTxToHistorySpy = jest + .spyOn(txHistory, 'saveTxToHistory') + .mockImplementation(() => Promise.resolve(mockTransactionDetails as any)) + addPendingTransactionSpy = jest.spyOn(pendingTransactions, 'addPendingTransaction') + navigateToTxSpy = jest.spyOn(utils, 'navigateToTx') + fetchTransactionsSpy = jest.spyOn(fetchTransactions, 'default') + + // Mock the onboard check + TxSender._isOnboardReady = jest.fn(() => Promise.resolve(true)) + }) + + it('handles approving a transaction', async () => { + jest.spyOn(safeTxSigner, 'checkIfOffChainSignatureIsPossible').mockImplementation(() => true) + const sender = new TxSender() + + await sender.prepare(jest.fn(), store.getState(), mockTxProps) + + sender.isFinalization = false + sender.txId = '1' + sender.safeTxHash = '' + sender.txArgs = { + safeInstance: sender.safeInstance, + to: mockTxProps.to, + valueInWei: mockTxProps.valueInWei, + data: mockTxProps.txData, + operation: mockTxProps.operation, + nonce: Number.parseInt(sender.nonce), + safeTxGas: mockTxProps.safeTxGas, + baseGas: '0', + gasPrice: '0', + gasToken: ZERO_ADDRESS, + refundReceiver: ZERO_ADDRESS, + sender: mockTxProps.from, + sigs: '', + } + + sender.submitTx(store.getState()) + + await waitFor(() => { + expect(tryOffChainSigningSpy).toHaveBeenCalledTimes(1) + expect(saveTxToHistorySpy).toHaveBeenCalledTimes(1) + expect(addPendingTransactionSpy).toHaveBeenCalledTimes(0) + expect(navigateToTxSpy).toHaveBeenCalledTimes(0) + expect(fetchTransactionsSpy).toHaveBeenCalledTimes(1) + }) + }) + + xit('handles creating a transaction', async () => { + jest.spyOn(safeTxSigner, 'checkIfOffChainSignatureIsPossible').mockImplementation(() => true) + const sender = new TxSender() + + mockTxProps.navigateToTransactionsTab = true + + await sender.prepare(jest.fn(), store.getState(), mockTxProps) + + sender.isFinalization = false + sender.safeTxHash = '' + sender.txArgs = { + safeInstance: sender.safeInstance, + to: mockTxProps.to, + valueInWei: mockTxProps.valueInWei, + data: mockTxProps.txData, + operation: mockTxProps.operation, + nonce: Number.parseInt(sender.nonce), + safeTxGas: mockTxProps.safeTxGas, + baseGas: '0', + gasPrice: '0', + gasToken: ZERO_ADDRESS, + refundReceiver: ZERO_ADDRESS, + sender: mockTxProps.from, + sigs: '', + } + + sender.submitTx(store.getState()) + + await waitFor(() => { + expect(tryOffChainSigningSpy).toHaveBeenCalledTimes(1) + expect(saveTxToHistorySpy).toHaveBeenCalledTimes(1) + expect(addPendingTransactionSpy).toHaveBeenCalledTimes(0) + expect(navigateToTxSpy).toHaveBeenCalledTimes(1) + expect(fetchTransactionsSpy).toHaveBeenCalledTimes(1) + }) + }) + + it('handles immediately executing a transaction', async () => { + jest.spyOn(safeTxSigner, 'checkIfOffChainSignatureIsPossible').mockImplementation(() => false) + const setNonceSpy = jest.spyOn(aboutToExecuteTx, 'setNonce') + const getExecutionTransactionSpy = jest.spyOn(send, 'getExecutionTransaction').mockImplementation(() => ({ + arguments: [], + call: jest.fn(), + send: jest.fn(() => ({ + ...mockPromiEvent, + once: jest.fn((type, handler) => { + handler('mocktxhash') + return mockPromiEvent + }), + })) as any, + estimateGas: jest.fn(), + encodeABI: jest.fn(), + })) + saveTxToHistorySpy = jest + .spyOn(txHistory, 'saveTxToHistory') + .mockImplementation(() => Promise.resolve({ ...mockTransactionDetails, txId: 'mockId' } as any)) + + const sender = new TxSender() + + mockTxProps.navigateToTransactionsTab = true + + await sender.prepare(jest.fn(), store.getState(), mockTxProps) + + sender.isFinalization = true + sender.safeTxHash = '' + sender.txArgs = { + safeInstance: sender.safeInstance, + to: mockTxProps.to, + valueInWei: mockTxProps.valueInWei, + data: mockTxProps.txData, + operation: mockTxProps.operation, + nonce: Number.parseInt(sender.nonce), + safeTxGas: mockTxProps.safeTxGas, + baseGas: '0', + gasPrice: '0', + gasToken: ZERO_ADDRESS, + refundReceiver: ZERO_ADDRESS, + sender: mockTxProps.from, + sigs: '', + } + + sender.submitTx(store.getState()) + + await waitFor(() => { + expect(getExecutionTransactionSpy).toHaveBeenCalledTimes(1) + expect(setNonceSpy).toHaveBeenCalledTimes(1) + expect(saveTxToHistorySpy).toHaveBeenCalledTimes(1) + expect(addPendingTransactionSpy).toHaveBeenCalledTimes(1) + expect(navigateToTxSpy).toHaveBeenCalledTimes(1) + expect(fetchTransactionsSpy).toHaveBeenCalledTimes(1) + }) + }) + + it('handles executing a transaction', async () => { + jest.spyOn(safeTxSigner, 'checkIfOffChainSignatureIsPossible').mockImplementation(() => false) + const setNonceSpy = jest.spyOn(aboutToExecuteTx, 'setNonce') + const getExecutionTransactionSpy = jest.spyOn(send, 'getExecutionTransaction').mockImplementation(() => ({ + arguments: [], + call: jest.fn(), + send: jest.fn(() => ({ + ...mockPromiEvent, + once: jest.fn((type, handler) => { + handler('mocktxhash') + return mockPromiEvent + }), + })) as any, + estimateGas: jest.fn(), + encodeABI: jest.fn(), + })) + + const sender = new TxSender() + + await sender.prepare(jest.fn(), store.getState(), mockTxProps) + + sender.isFinalization = true + sender.txId = 'mockId' + sender.safeTxHash = '' + sender.txArgs = { + safeInstance: sender.safeInstance, + to: mockTxProps.to, + valueInWei: mockTxProps.valueInWei, + data: mockTxProps.txData, + operation: mockTxProps.operation, + nonce: Number.parseInt(sender.nonce), + safeTxGas: mockTxProps.safeTxGas, + baseGas: '0', + gasPrice: '0', + gasToken: ZERO_ADDRESS, + refundReceiver: ZERO_ADDRESS, + sender: mockTxProps.from, + sigs: '', + } + + sender.submitTx(store.getState()) + + await waitFor(() => { + expect(getExecutionTransactionSpy).toHaveBeenCalledTimes(1) + expect(setNonceSpy).toHaveBeenCalledTimes(1) + expect(addPendingTransactionSpy).toHaveBeenCalledTimes(1) + expect(saveTxToHistorySpy).toHaveBeenCalledTimes(0) + expect(navigateToTxSpy).toHaveBeenCalledTimes(0) + expect(fetchTransactionsSpy).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 448b209be6..271c843e3e 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -2,7 +2,8 @@ import { Operation, TransactionDetails } from '@gnosis.pm/safe-react-gateway-sdk import { AnyAction } from 'redux' import { ThunkAction } from 'redux-thunk' -import { onboardUser } from 'src/components/ConnectButton' +import onboard, { checkWallet } from 'src/logic/wallets/onboard' +import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { createTxNotifications } from 'src/logic/notifications' import { @@ -17,24 +18,21 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { providerSelector } from 'src/logic/wallets/store/selectors' import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' -import { getNonce, canExecuteCreatedTx } from 'src/logic/safe/store/actions/utils' +import { getNonce, canExecuteCreatedTx, navigateToTx } from 'src/logic/safe/store/actions/utils' import fetchTransactions from './transactions/fetchTransactions' import { AppReduxState } from 'src/store' import { Dispatch, DispatchReturn } from './types' import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { Errors, logError } from 'src/logic/exceptions/CodedException' -import { extractShortChainName, history, SAFE_ROUTES } from 'src/routes/routes' -import { getPrefixedSafeAddressSlug, SAFE_ADDRESS_SLUG, TRANSACTION_ID_SLUG } from 'src/routes/routes' -import { generatePath } from 'react-router-dom' -import { fetchOnchainError } from 'src/logic/contracts/safeContractErrors' -import { isMultiSigExecutionDetails } from '../models/types/gateway.d' import { removePendingTransaction, addPendingTransaction } from 'src/logic/safe/store/actions/pendingTransactions' import { _getChainId } from 'src/config' import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' -import { getLastTransaction } from '../selectors/gatewayTransactions' -import { TxArgs } from '../models/types/transaction' +import { getLastTransaction } from 'src/logic/safe/store/selectors/gatewayTransactions' +import { TxArgs } from 'src/logic/safe/store/models/types/transaction' +import { getContractErrorMessage } from 'src/logic/contracts/safeContractErrors' +import { isWalletRejection } from 'src/logic/wallets/errors' export interface CreateTransactionArgs { navigateToTransactionsTab?: boolean @@ -58,21 +56,6 @@ type CreateTransactionAction = ThunkAction, AppReduxState type ConfirmEventHandler = (safeTxHash: string) => void type ErrorEventHandler = () => void -export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001 - -const navigateToTx = (safeAddress: string, txDetails: TransactionDetails) => { - if (!isMultiSigExecutionDetails(txDetails.detailedExecutionInfo)) { - return - } - const prefixedSafeAddress = getPrefixedSafeAddressSlug({ shortName: extractShortChainName(), safeAddress }) - const txRoute = generatePath(SAFE_ROUTES.TRANSACTIONS_SINGULAR, { - [SAFE_ADDRESS_SLUG]: prefixedSafeAddress, - [TRANSACTION_ID_SLUG]: txDetails.detailedExecutionInfo.safeTxHash, - }) - - history.push(txRoute) -} - const getSafeTxGas = async (txProps: RequiredTxProps, safeVersion: string): Promise => { const estimationProps: SafeTxGasEstimationProps = { safeAddress: txProps.safeAddress, @@ -104,6 +87,9 @@ export class TxSender { safeVersion: string txId: string + // Assigned upon `transactionHash` promiEvent + txHash: undefined | string + // On transaction completion (either confirming or executing) async onComplete(signature?: string, confirmCallback?: ConfirmEventHandler): Promise { const { txArgs, safeTxHash, txProps, dispatch, notifications, isFinalization } = this @@ -114,17 +100,16 @@ export class TxSender { let txDetails: TransactionDetails | null = null if (!isFinalization || !this.txId) { try { - txDetails = await saveTxToHistory({ ...txArgs, signature, origin }) + txDetails = await saveTxToHistory({ ...txArgs, signature, origin: txProps.origin }) } catch (err) { logError(Errors._816, err.message) return } } - // If threshold reached except for last sig, and owner chooses to execute the created tx immediately - // we retrieve txId of newly created tx from the proposal response - if (isFinalization && txDetails) { - dispatch(addPendingTransaction({ id: txDetails.txId })) + const id = txDetails?.txId || this.txId + if (isFinalization && id && this.txHash) { + dispatch(addPendingTransaction({ id, txHash: this.txHash })) } notifications.closePending() @@ -141,9 +126,7 @@ export class TxSender { } async onError(err: Error & { code: number }, errorCallback?: ErrorEventHandler): Promise { - const { txArgs, isFinalization, from, txProps, dispatch, notifications, safeInstance, txId } = this - - logError(Errors._803, err.message) + const { txArgs, isFinalization, from, txProps, dispatch, notifications, safeInstance, txId, txHash } = this errorCallback?.() @@ -154,23 +137,44 @@ export class TxSender { dispatch(removePendingTransaction({ id: txId })) } - const executeDataUsedSignatures = safeInstance.methods - .execTransaction( - txProps.to, - txProps.valueInWei, - txProps.txData, - txProps.operation, - 0, - 0, - 0, - ZERO_ADDRESS, - ZERO_ADDRESS, - txArgs.sigs, - ) - .encodeABI() - const contractErrorMessage = await fetchOnchainError(executeDataUsedSignatures, safeInstance, from) - - notifications.showOnError(err, contractErrorMessage) + // Display a notification when user rejects the tx + if (isWalletRejection(err)) { + // show snackbar + notifications.showOnRejection(err) + return + } + + const executeData = isFinalization + ? safeInstance.methods + .execTransaction( + txProps.to, + txProps.valueInWei, + txProps.txData, + txProps.operation, + 0, + 0, + 0, + ZERO_ADDRESS, + ZERO_ADDRESS, + txArgs.sigs, + ) + .encodeABI() + : txHash && safeInstance.methods.approveHash(txHash).encodeABI() + + if (!executeData) { + return + } + + const contractErrorMessage = await getContractErrorMessage({ + safeInstance, + from, + data: executeData, + }) + + if (contractErrorMessage) { + logError(Errors._803, contractErrorMessage) + notifications.showOnError(err, contractErrorMessage) + } } async onlyConfirm(hardwareWallet: boolean): Promise { @@ -184,61 +188,78 @@ export class TxSender { ) } - async sendTx(): Promise { - const { txArgs, isFinalization, from, safeTxHash, txProps, dispatch, txId } = this + async sendTx(confirmCallback?: ConfirmEventHandler): Promise { + const { txArgs, isFinalization, from, safeTxHash, txProps } = this const tx = isFinalization ? getExecutionTransaction(txArgs) : getApprovalTransaction(this.safeInstance, safeTxHash) const sendParams = createSendParams(from, txProps.ethParameters || {}) - const promiEvent = tx.send(sendParams) - // When signing on-chain don't mark as pending as it is never removed - if (isFinalization) { - // Finalising existing transaction (txId exists) - if (txId) { - dispatch(addPendingTransaction({ id: txId })) - } - aboutToExecuteTx.setNonce(txArgs.nonce) - } + return await tx + .send(sendParams) + .once('transactionHash', (hash) => { + this.txHash = hash + + if (isFinalization) { + aboutToExecuteTx.setNonce(txArgs.nonce) + } + this.onComplete(undefined, confirmCallback) + }) + .then(({ transactionHash }) => transactionHash) + } - return new Promise((resolve, reject) => { - promiEvent.once('transactionHash', resolve) // this happens much faster than receipt - promiEvent.once('error', reject) - }) + async canSignOffchain(state: AppReduxState): Promise { + const { isFinalization, safeVersion } = this + const { smartContractWallet } = providerSelector(state) + + return checkIfOffChainSignatureIsPossible(isFinalization, smartContractWallet, safeVersion) } async submitTx( state: AppReduxState, confirmCallback?: ConfirmEventHandler, errorCallback?: ErrorEventHandler, - ): Promise { - const { isFinalization, safeVersion } = this - const { hardwareWallet, smartContractWallet } = providerSelector(state) - const canSignOffChain = checkIfOffChainSignatureIsPossible(isFinalization, smartContractWallet, safeVersion) + ): Promise { + const isOffchain = await this.canSignOffchain(state) + // Off-chain signature - if (!isFinalization && canSignOffChain) { + if (!this.isFinalization && isOffchain) { try { + const { hardwareWallet } = providerSelector(state) const signature = await this.onlyConfirm(hardwareWallet) - this.onComplete(signature, confirmCallback) + + // WC + Safe receives "NaN" as a string instead of a sig + if (signature && signature !== 'NaN') { + this.onComplete(signature, confirmCallback) + } else { + throw Error('No signature received') + } } catch (err) { - // User likely rejected transaction logError(Errors._814, err.message) + this.onError(err, errorCallback) } return } // On-chain signature or execution try { - await this.sendTx() - this.onComplete(undefined, confirmCallback) + await this.sendTx(confirmCallback) } catch (err) { + logError(Errors._803, err.message) this.onError(err, errorCallback) } + + // Return txHash to check if transaction was successful + return this.txHash + } + + static async _isOnboardReady(): Promise { + // web3 is set on wallet connection + const walletSelected = getWeb3() ? true : await onboard().walletSelect() + return walletSelected && checkWallet() } async prepare(dispatch: Dispatch, state: AppReduxState, txProps: RequiredTxProps): Promise { - // Wallet connection - const ready = await onboardUser() - if (!ready) { + if (!(await TxSender._isOnboardReady())) { throw Error('No wallet connection') } diff --git a/src/logic/safe/store/actions/pendingTransactions.ts b/src/logic/safe/store/actions/pendingTransactions.ts index 3a759b1815..39a2d0afc0 100644 --- a/src/logic/safe/store/actions/pendingTransactions.ts +++ b/src/logic/safe/store/actions/pendingTransactions.ts @@ -1,10 +1,15 @@ import { createAction } from 'redux-actions' -import { PendingTransactionPayload } from 'src/logic/safe/store/reducer/pendingTransactions' +import { + AddPendingTransactionPayload, + RemovePendingTransactionPayload, +} from 'src/logic/safe/store/reducer/pendingTransactions' export enum PENDING_TRANSACTIONS_ACTIONS { ADD = 'pendingTransactions/add', REMOVE = 'pendingTransactions/remove', } -export const addPendingTransaction = createAction(PENDING_TRANSACTIONS_ACTIONS.ADD) -export const removePendingTransaction = createAction(PENDING_TRANSACTIONS_ACTIONS.REMOVE) +export const addPendingTransaction = createAction(PENDING_TRANSACTIONS_ACTIONS.ADD) +export const removePendingTransaction = createAction( + PENDING_TRANSACTIONS_ACTIONS.REMOVE, +) diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index e17be2a92a..f2983d4d36 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -1,5 +1,7 @@ -import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' +import { generatePath } from 'react-router-dom' +import { SafeInfo, TransactionDetails } from '@gnosis.pm/safe-react-gateway-sdk' +import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' import { LATEST_SAFE_VERSION } from 'src/utils/constants' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits' @@ -7,15 +9,24 @@ import { buildModulesLinkedList } from 'src/logic/safe/utils/modules' import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion' import { checksumAddress } from 'src/utils/checksumAddress' import { ChainId } from 'src/config/chain.d' -import { SafeInfo } from '@gnosis.pm/safe-react-gateway-sdk' + import { Transaction, isMultisigExecutionInfo, LocalTransactionStatus, + isMultiSigExecutionDetails, } from 'src/logic/safe/store/models/types/gateway.d' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { logError, Errors } from 'src/logic/exceptions/CodedException' import { getRecommendedNonce } from '../../api/fetchSafeTxGasEstimation' +import { + extractShortChainName, + getPrefixedSafeAddressSlug, + history, + SAFE_ADDRESS_SLUG, + SAFE_ROUTES, + TRANSACTION_ID_SLUG, +} from 'src/routes/routes' export const canExecuteCreatedTx = async ( safeInstance: GnosisSafe, @@ -120,3 +131,16 @@ export const getNonce = async (safeAddress: string, safeVersion: string): Promis } return nextNonce } + +export const navigateToTx = (safeAddress: string, txDetails: TransactionDetails): void => { + if (!isMultiSigExecutionDetails(txDetails.detailedExecutionInfo)) { + return + } + const prefixedSafeAddress = getPrefixedSafeAddressSlug({ shortName: extractShortChainName(), safeAddress }) + const txRoute = generatePath(SAFE_ROUTES.TRANSACTIONS_SINGULAR, { + [SAFE_ADDRESS_SLUG]: prefixedSafeAddress, + [TRANSACTION_ID_SLUG]: txDetails.detailedExecutionInfo.safeTxHash, + }) + + history.push(txRoute) +} diff --git a/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts b/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts index f070d1456b..f64cb912b5 100644 --- a/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts +++ b/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts @@ -7,7 +7,7 @@ import { addPendingTransaction, removePendingTransaction, } from 'src/logic/safe/store/actions/pendingTransactions' -import { PENDING_TRANSACTIONS_ID, PendingTransactionPayload } from 'src/logic/safe/store/reducer/pendingTransactions' +import { PENDING_TRANSACTIONS_ID, PendingTransactionPayloads } from 'src/logic/safe/store/reducer/pendingTransactions' import { Dispatch } from 'src/logic/safe/store/actions/types' import { allPendingTxIds } from 'src/logic/safe/store/selectors/pendingTransactions' @@ -46,7 +46,7 @@ if (channel) { export const pendingTransactionsMiddleware = ({ getState }: typeof reduxStore) => (next: Dispatch) => - async (action: Action): Promise> => { + async (action: Action): Promise> => { const handledAction = next(action) switch (action.type) { diff --git a/src/logic/safe/store/reducer/pendingTransactions.ts b/src/logic/safe/store/reducer/pendingTransactions.ts index eaaa804e7d..72a4de2044 100644 --- a/src/logic/safe/store/reducer/pendingTransactions.ts +++ b/src/logic/safe/store/reducer/pendingTransactions.ts @@ -7,32 +7,38 @@ import { _getChainId } from 'src/config' export const PENDING_TRANSACTIONS_ID = 'pendingTransactions' -export type PendingTransactionsState = Record> +export type PendingTransactionsState = Record> const initialPendingTxsState = session.getItem(PENDING_TRANSACTIONS_ID) || {} -export type PendingTransactionPayload = { +export type RemovePendingTransactionPayload = { id: string isBroadcast?: boolean } -export const pendingTransactionsReducer = handleActions( +export type AddPendingTransactionPayload = RemovePendingTransactionPayload & { + txHash: string | boolean +} + +export type PendingTransactionPayloads = AddPendingTransactionPayload | RemovePendingTransactionPayload + +export const pendingTransactionsReducer = handleActions( { [PENDING_TRANSACTIONS_ACTIONS.ADD]: ( state: PendingTransactionsState, - action: Action, + action: Action, ) => { const chainId = _getChainId() - const { id } = action.payload + const { id, txHash } = action.payload return { ...state, - [chainId]: { ...state[chainId], [id]: true }, + [chainId]: { ...state[chainId], [id]: txHash }, } }, [PENDING_TRANSACTIONS_ACTIONS.REMOVE]: ( state: PendingTransactionsState, - action: Action, + action: Action, ) => { const chainId = _getChainId() const { id } = action.payload diff --git a/src/logic/safe/transactions/__tests__/gas.test.ts b/src/logic/safe/transactions/__tests__/gas.test.ts index 480027eec7..8f19f390c2 100644 --- a/src/logic/safe/transactions/__tests__/gas.test.ts +++ b/src/logic/safe/transactions/__tests__/gas.test.ts @@ -2,7 +2,52 @@ import { _getChainId } from 'src/config' import { CHAIN_ID } from 'src/config/chain.d' import { setChainId } from 'src/logic/config/utils' -import { createSendParams, isMaxFeeParam } from '../gas' +import { createSendParams, estimateSafeTxGas, isMaxFeeParam } from '../gas' +import * as safeVersion from 'src/logic/safe/utils/safeVersion' +import * as safeTxGasEstimation from 'src/logic/safe/api/fetchSafeTxGasEstimation' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' + +describe('estimateSafeTxGas', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + it('returns 0 if safeTxGas is optional for current save version', async () => { + jest.spyOn(safeVersion, 'hasFeature').mockImplementation(() => true) + const mockParams = { + safeAddress: ZERO_ADDRESS, + txData: '', + txRecipient: '', + txAmount: '0', + operation: 0, + } + const result = await estimateSafeTxGas(mockParams, 'mockSaveVersion') + + expect(result).toBe('0') + }) + + it('fetches safeTxGas estimation if safeTxGas is required for current save version', async () => { + jest.spyOn(safeVersion, 'hasFeature').mockImplementation(() => false) + const spy = jest.spyOn(safeTxGasEstimation, 'fetchSafeTxGasEstimation').mockImplementation(() => { + return Promise.resolve({ + currentNonce: 1, + recommendedNonce: 2, + safeTxGas: '1', + }) + }) + + const mockParams = { + safeAddress: ZERO_ADDRESS, + txData: '', + txRecipient: '', + txAmount: '0', + operation: 0, + } + const result = await estimateSafeTxGas(mockParams, 'mockSaveVersion') + + expect(result).toBe('1') + expect(spy).toHaveBeenCalledTimes(1) + }) +}) describe('Get gas param', () => { let initialNetworkId = CHAIN_ID.RINKEBY diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index b5b1453a75..d24456f24a 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -3,7 +3,6 @@ import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { calculateGasOf } from 'src/logic/wallets/ethTransactions' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' import { fetchSafeTxGasEstimation } from 'src/logic/safe/api/fetchSafeTxGasEstimation' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' @@ -44,73 +43,6 @@ export const estimateSafeTxGas = async ( } } -type TransactionEstimationProps = { - txData: string - safeAddress: string - safeVersion: string - txRecipient: string - txConfirmations?: List - txAmount: string - operation: number - gasPrice?: string - gasToken?: string - refundReceiver?: string // Address of receiver of gas payment (or 0 if tx.origin). - safeTxGas?: string - from?: string - isExecution: boolean - isOffChainSignature?: boolean - approvalAndExecution?: boolean -} - -export const estimateTransactionGasLimit = async ({ - txData, - safeAddress, - safeVersion, - txRecipient, - txConfirmations, - txAmount, - operation, - gasPrice, - gasToken, - refundReceiver, - safeTxGas, - from, - isExecution, - approvalAndExecution, -}: TransactionEstimationProps): Promise => { - if (!from) { - throw new Error('No from provided for approving or execute transaction') - } - - if (isExecution) { - return estimateGasForTransactionExecution({ - safeAddress, - safeVersion, - txRecipient, - txConfirmations, - txAmount, - txData, - operation, - from, - gasPrice: gasPrice || '0', - gasToken: gasToken || ZERO_ADDRESS, - refundReceiver: refundReceiver || ZERO_ADDRESS, - safeTxGas: safeTxGas || '0', - approvalAndExecution, - }) - } - - return estimateGasForTransactionApproval({ - safeAddress, - safeVersion, - operation, - txData, - txAmount, - txRecipient, - from, - }) -} - type TransactionExecutionEstimationProps = { txData: string safeAddress: string @@ -128,7 +60,7 @@ type TransactionExecutionEstimationProps = { approvalAndExecution?: boolean } -const estimateGasForTransactionExecution = async ({ +export const estimateGasForTransactionExecution = async ({ safeAddress, safeVersion, txRecipient, @@ -187,41 +119,6 @@ export const checkTransactionExecution = async ({ .catch(() => false) } -type TransactionApprovalEstimationProps = { - safeAddress: string - safeVersion: string - txRecipient: string - txAmount: string - txData: string - operation: number - from: string -} - -export const estimateGasForTransactionApproval = async ({ - safeAddress, - safeVersion, - txRecipient, - txAmount, - txData, - operation, - from, -}: TransactionApprovalEstimationProps): Promise => { - const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion) - - const nonce = await safeInstance.methods.nonce().call() - const txHash = await safeInstance.methods - .getTransactionHash(txRecipient, txAmount, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce) - .call({ - from, - }) - const approveTransactionTxData = safeInstance.methods.approveHash(txHash).encodeABI() - return calculateGasOf({ - data: approveTransactionTxData, - from, - to: safeAddress, - }) -} - export const isMaxFeeParam = (): boolean => { return hasFeature(FEATURES.EIP1559) } diff --git a/src/logic/safe/transactions/offchainSigner/index.ts b/src/logic/safe/transactions/offchainSigner/index.ts index b7cbcbde57..dea7e4981f 100644 --- a/src/logic/safe/transactions/offchainSigner/index.ts +++ b/src/logic/safe/transactions/offchainSigner/index.ts @@ -1,6 +1,7 @@ import semverSatisfies from 'semver/functions/satisfies' -import { METAMASK_REJECT_CONFIRM_TX_ERROR_CODE } from 'src/logic/safe/store/actions/createTransaction' +import { isPairingModule } from 'src/logic/wallets/pairing/utils' +import { isWalletRejection } from 'src/logic/wallets/errors' import { getEIP712Signer, SigningTxArgs } from './EIP712Signer' import { ethSigner, EthSignerArgs } from './ethSigner' @@ -19,12 +20,16 @@ export const SAFE_VERSION_FOR_OFF_CHAIN_SIGNATURES = '>=1.0.0' // hardware wallets support eth_sign only // eth_sign is only supported by safes >= 1.1.0 -const getSupportedSigners = (isHW: boolean, safeVersion: string) => { +type SupportedSigners = typeof SIGNERS[keyof typeof SIGNERS][] +const getSupportedSigners = (isHW: boolean, safeVersion: string): SupportedSigners => { + // v1 of desktop pairing only supports eth_sign + if (isPairingModule()) { + return [SIGNERS.ETH_SIGN] + } + const safeSupportsEthSigner = semverSatisfies(safeVersion, '>=1.1.0') - const signers: typeof SIGNERS[keyof typeof SIGNERS][] = isHW - ? [] - : [SIGNERS.EIP712_V3, SIGNERS.EIP712_V4, SIGNERS.EIP712] + const signers: SupportedSigners = isHW ? [] : [SIGNERS.EIP712_V3, SIGNERS.EIP712_V4, SIGNERS.EIP712] if (safeSupportsEthSigner) { signers.push(SIGNERS.ETH_SIGN) @@ -33,14 +38,6 @@ const getSupportedSigners = (isHW: boolean, safeVersion: string) => { return signers } -const isKeystoneError = (err: unknown): boolean => { - if (err instanceof Error) { - return err.message?.startsWith('#ktek_error') - } - - return false -} - export const tryOffChainSigning = async ( safeTxHash: string, txArgs: Omit, @@ -56,12 +53,11 @@ export const tryOffChainSigning = async ( break } catch (err) { - if (err.code === METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) { - throw err - } - if (isKeystoneError(err)) { + if (isWalletRejection(err)) { + // user rejected, exit throw err } + // continue to the next signing method } } diff --git a/src/logic/wallets/__tests__/network.test.ts b/src/logic/wallets/__tests__/network.test.ts index 6df0c1d288..c1aef86d6e 100644 --- a/src/logic/wallets/__tests__/network.test.ts +++ b/src/logic/wallets/__tests__/network.test.ts @@ -26,6 +26,7 @@ describe('src/logic/wallets/utils/network', () => { return Promise.reject(err) }), }, + name: 'Test', } expect(switchNetwork(wallet as Wallet, '1438' as unknown as ChainId)).rejects.toThrow( @@ -42,6 +43,7 @@ describe('src/logic/wallets/utils/network', () => { return Promise.reject(err) }), }, + name: 'Test', } expect(switchNetwork(wallet as Wallet, '1438' as unknown as ChainId)).rejects.toThrow( @@ -58,6 +60,7 @@ describe('src/logic/wallets/utils/network', () => { return Promise.reject(err) }), }, + name: 'Test', } expect(switchNetwork(wallet as Wallet, '1438' as unknown as ChainId)).resolves.toEqual(undefined) diff --git a/src/logic/wallets/errors.ts b/src/logic/wallets/errors.ts new file mode 100644 index 0000000000..ea33124ca9 --- /dev/null +++ b/src/logic/wallets/errors.ts @@ -0,0 +1,20 @@ +const isKeystoneError = (err: unknown): boolean => { + if (err instanceof Error) { + return err.message?.startsWith('#ktek_error') + } + return false +} + +const isWCRejection = (err: Error): boolean => { + return /User rejected/.test(err?.message) +} + +const isMMRejection = (err: Error & { code?: number }): boolean => { + const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001 + + return err.code === METAMASK_REJECT_CONFIRM_TX_ERROR_CODE +} + +export const isWalletRejection = (err: Error & { code?: number }): boolean => { + return isMMRejection(err) || isWCRejection(err) || isKeystoneError(err) +} diff --git a/src/logic/wallets/ethTransactions.ts b/src/logic/wallets/ethTransactions.ts index ddd5aed00e..977e507d6f 100644 --- a/src/logic/wallets/ethTransactions.ts +++ b/src/logic/wallets/ethTransactions.ts @@ -1,18 +1,18 @@ -import { EthAdapterTransaction } from '@gnosis.pm/safe-core-sdk' +import { EthAdapterTransaction } from '@gnosis.pm/safe-core-sdk-types' import { GasPriceOracle } from '@gnosis.pm/safe-react-gateway-sdk' import axios from 'axios' import { BigNumber } from 'bignumber.js' import { FeeHistoryResult } from 'web3-eth' import { hexToNumber } from 'web3-utils' -import { getSDKWeb3ReadOnly, getWeb3, getWeb3ReadOnly } from 'src/logic/wallets/getWeb3' +import { getSDKWeb3ReadOnly, getWeb3ReadOnly } from 'src/logic/wallets/getWeb3' import { getFixedGasPrice, getGasPriceOracles } from 'src/config' import { CodedException, Errors, logError } from 'src/logic/exceptions/CodedException' export const EMPTY_DATA = '0x' -const DEFAULT_MAX_GAS_FEE = 3.5e9 // 3.5 GWEI -const DEFAULT_MAX_PRIO_FEE = 2.5e9 // 2.5 GWEI +export const DEFAULT_MAX_GAS_FEE = 3.5e9 // 3.5 GWEI +export const DEFAULT_MAX_PRIO_FEE = 2.5e9 // 2.5 GWEI const fetchGasPrice = async (gasPriceOracle: GasPriceOracle): Promise => { const { uri, gasParameter, gweiFactor } = gasPriceOracle @@ -98,7 +98,7 @@ export const calculateGasOf = async (txConfig: EthAdapterTransaction): Promise => { - const web3 = getWeb3() + const web3 = getWeb3ReadOnly() try { return await web3.eth.getTransactionCount(userAddress, 'pending') } catch (error) { diff --git a/src/logic/wallets/getWeb3.ts b/src/logic/wallets/getWeb3.ts index 0f6d2f4fa3..1173e0fbf9 100644 --- a/src/logic/wallets/getWeb3.ts +++ b/src/logic/wallets/getWeb3.ts @@ -4,17 +4,19 @@ import { Contract } from 'web3-eth-contract' import { provider as Provider } from 'web3-core' import { ContentHash } from 'web3-eth-ens' import { namehash } from '@ethersproject/hash' -import Safe, { Web3Adapter } from '@gnosis.pm/safe-core-sdk' +import Safe from '@gnosis.pm/safe-core-sdk' +import Web3Adapter from '@gnosis.pm/safe-web3-lib' import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' -import { sameAddress, ZERO_ADDRESS } from './ethAddresses' +import { ZERO_ADDRESS } from './ethAddresses' import { EMPTY_DATA } from './ethTransactions' -import { ProviderProps } from './store/model/provider' import { getRpcServiceUrl, _getChainId } from 'src/config' import { CHAIN_ID, ChainId } from 'src/config/chain.d' import { isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses' import { getAddressFromUnstoppableDomain } from './utils/unstoppableDomains' import { hasFeature } from 'src/logic/safe/utils/safeVersion' +import { checksumAddress } from 'src/utils/checksumAddress' +import { isValidAddress } from 'src/utils/isValidAddress' // This providers have direct relation with name assigned in bnc-onboard configuration export enum WALLET_PROVIDER { @@ -33,11 +35,13 @@ export enum WALLET_PROVIDER { TREZOR = 'TREZOR', LATTICE = 'LATTICE', KEYSTONE = 'KEYSTONE', + // Safe name as PAIRING_MODULE_NAME + SAFE_MOBILE = 'SAFE MOBILE', } // With some wallets from web3connect you have to use their provider instance only for signing // And our own one to fetch data -const httpProviderOptions = { +export const web3HttpProviderOptions = { timeout: 10_000, } @@ -47,7 +51,7 @@ export const getWeb3ReadOnly = (): Web3 => { if (!web3ReadOnly[chainId]) { web3ReadOnly[chainId] = new Web3( process.env.NODE_ENV !== 'test' - ? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions) + ? new Web3.providers.HttpProvider(getRpcServiceUrl(), web3HttpProviderOptions) : 'ws://localhost:8545', ) } @@ -67,18 +71,10 @@ export const resetWeb3 = (): void => { web3 = web3ReadOnly[_getChainId()] } -export const getAccountFrom = async (web3Provider: Web3): Promise => { - const accounts = await web3Provider.eth.getAccounts() - return accounts && accounts.length > 0 ? accounts[0] : null -} - export const getChainIdFrom = (web3Provider: Web3): Promise => { return web3Provider.eth.getChainId() } -const isHardwareWallet = (walletName: string) => - sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName) - export const isSmartContractWallet = async (account: string): Promise => { if (!account) { return false @@ -91,27 +87,6 @@ export const isSmartContractWallet = async (account: string): Promise = } return !!contractCode && contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== '' } - -export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet'): Promise => { - const account = (await getAccountFrom(web3Instance)) || '' - const ensDomain = account ? await reverseENSLookup(account) : '' - const network = await getChainIdFrom(web3Instance) - const smartContractWallet = await isSmartContractWallet(account) - const hardwareWallet = isHardwareWallet(providerName) - const available = Boolean(account) - - return { - name: providerName, - available, - loaded: true, - account, - ensDomain, - network: network.toString() as ChainId, - smartContractWallet, - hardwareWallet, - } -} - export const getAddressFromDomain = (name: string): Promise => { if (isValidCryptoDomainName(name)) { return getAddressFromUnstoppableDomain(name) @@ -120,7 +95,7 @@ export const getAddressFromDomain = (name: string): Promise => { } export const reverseENSLookup = async (address: string): Promise => { - if (!hasFeature(FEATURES.DOMAIN_LOOKUP)) { + if (!address || !hasFeature(FEATURES.DOMAIN_LOOKUP) || !isValidAddress(address)) { return '' } @@ -144,7 +119,7 @@ export const reverseENSLookup = async (address: string): Promise => { return '' } - return verifiedAddress === address ? name : '' + return checksumAddress(verifiedAddress) === checksumAddress(address) ? name : '' } export const getContentFromENS = (name: string): Promise => web3.eth.ens.getContenthash(name) diff --git a/src/logic/wallets/onboard.ts b/src/logic/wallets/onboard.ts index 78c7e5920d..4233b2d070 100644 --- a/src/logic/wallets/onboard.ts +++ b/src/logic/wallets/onboard.ts @@ -1,56 +1,103 @@ import Onboard from 'bnc-onboard' -import { API, Wallet } from 'bnc-onboard/dist/src/interfaces' -import { store } from 'src/store' +import { API, Initialization } from 'bnc-onboard/dist/src/interfaces' +import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' + import { _getChainId, getChainName } from 'src/config' -import { setWeb3 } from './getWeb3' -import { fetchProvider, removeProvider } from './store/actions' -import transactionDataCheck from './transactionDataCheck' -import { getSupportedWallets } from './utils/walletList' +import { setWeb3, resetWeb3 } from 'src/logic/wallets/getWeb3' +import transactionDataCheck from 'src/logic/wallets/transactionDataCheck' +import { getSupportedWallets } from 'src/logic/wallets/utils/walletList' import { ChainId, CHAIN_ID } from 'src/config/chain.d' +import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts' +import { loadFromStorageWithExpiry, removeFromStorage, saveToStorageWithExpiry } from 'src/utils/storage' +import { store } from 'src/store' +import updateProviderWallet from 'src/logic/wallets/store/actions/updateProviderWallet' +import updateProviderAccount from 'src/logic/wallets/store/actions/updateProviderAccount' +import updateProviderNetwork from 'src/logic/wallets/store/actions/updateProviderNetwork' +import updateProviderEns from 'src/logic/wallets/store/actions/updateProviderEns' +import closeSnackbar from 'src/logic/notifications/store/actions/closeSnackbar' +import { getChains } from 'src/config/cache/chains' +import { shouldSwitchNetwork, switchNetwork } from 'src/logic/wallets/utils/network' +import { isPairingModule } from 'src/logic/wallets/pairing/utils' + +const LAST_USED_PROVIDER_KEY = 'SAFE__lastUsedProvider' + +const saveLastUsedProvider = (name: string) => { + const expireInDays = (days: number) => 60 * 60 * 24 * 1000 * days + const expiry = isPairingModule(name) ? expireInDays(1) : expireInDays(365) + saveToStorageWithExpiry(LAST_USED_PROVIDER_KEY, name, expiry) +} + +export const loadLastUsedProvider = (): string | undefined => { + return loadFromStorageWithExpiry(LAST_USED_PROVIDER_KEY) +} + +const getNetworkName = (chainId: ChainId) => { + // 'mainnet' is hardcoded in onboard v1 + const NETWORK_NAMES: Record = { + [CHAIN_ID.ETHEREUM]: 'mainnet', + } + + // Ledger requires lowercase names + return NETWORK_NAMES[chainId] || getChainName().toLowerCase() +} -const NETWORK_NAMES: Record = { - [CHAIN_ID.ETHEREUM]: 'mainnet', +const hasENSSupport = (chainId: ChainId): boolean => { + return getChains().some((chain) => chain.chainId === chainId && chain.features.includes(FEATURES.DOMAIN_LOOKUP)) } -const getOnboardConfiguration = () => { - let lastUsedAddress = '' - let providerName: string | null = null - let lastNetworkId = '' +let prevAddress = '' - return { - networkId: parseInt(_getChainId(), 10), - // Is it mandatory for Ledger to work to send network name in lowercase - // @FIXME: Move to CGW - networkName: NETWORK_NAMES[_getChainId()] || getChainName().toLowerCase(), +const getOnboard = (chainId: ChainId): API => { + const config: Initialization = { + networkId: parseInt(chainId, 10), + networkName: getNetworkName(chainId), subscriptions: { - wallet: (wallet: Wallet) => { + wallet: async (wallet) => { if (wallet.provider) { - // this function will intialize web3 and store it somewhere available throughout the dapp and - // can also instantiate your contracts with the web3 instance setWeb3(wallet.provider) - providerName = wallet.name + instantiateSafeContracts() } + + // Cache wallet for reconnection + if (wallet.name) { + saveLastUsedProvider(wallet.name) + } + + store.dispatch( + updateProviderWallet({ + name: wallet.name || '', + hardwareWallet: wallet.type === 'hardware', + }), + ) }, - address: (address: string) => { - const networkId = _getChainId() + // Non-checksummed address + address: (address) => { + // isSmartContract is checked when address changes (in middleware) + store.dispatch(updateProviderAccount(address || '')) - if (!lastUsedAddress && address && providerName) { - lastUsedAddress = address - lastNetworkId = networkId - store.dispatch(fetchProvider(providerName)) + if (address) { + prevAddress = address } - // we don't have an unsubscribe event so we rely on this - if (!address && lastUsedAddress) { - lastUsedAddress = '' - providerName = null - store.dispatch(removeProvider({ keepStorageKey: lastNetworkId !== networkId })) + // Wallet disconnected + if (!address && prevAddress) { + resetWeb3() + removeFromStorage(LAST_USED_PROVIDER_KEY) } }, + network: (networkId) => { + store.dispatch(updateProviderNetwork(networkId?.toString() || '')) + store.dispatch(closeSnackbar({ dismissAll: true })) + }, + ens: hasENSSupport(chainId) + ? (ens) => { + store.dispatch(updateProviderEns(ens?.name || '')) + } + : undefined, }, walletSelect: { description: 'Please select a wallet to connect to Gnosis Safe', - wallets: getSupportedWallets(), + wallets: getSupportedWallets(chainId), }, walletCheck: [ { checkName: 'derivationPath' }, @@ -60,16 +107,34 @@ const getOnboardConfiguration = () => { transactionDataCheck(), ], } + + return Onboard(config) } let currentOnboardInstance: API -export const onboard = (): API => { +const onboard = (): API => { const chainId = _getChainId() if (!currentOnboardInstance || currentOnboardInstance.getState().appNetworkId.toString() !== chainId) { - currentOnboardInstance = Onboard(getOnboardConfiguration()) + currentOnboardInstance = getOnboard(chainId) } return currentOnboardInstance } - export default onboard + +export const checkWallet = async (): Promise => { + const wallet = onboard().getState().wallet + + if (shouldSwitchNetwork(wallet)) { + switchNetwork(wallet, _getChainId()).catch((e) => e.log()) + } + + let isWalletConnected = false + try { + // Onboard requests `walletSelect()` be called first but we don't + // want to open the modal + isWalletConnected = await onboard().walletCheck() + } catch {} + + return isWalletConnected +} diff --git a/src/logic/wallets/pairing/hooks/useGetPairingUri.ts b/src/logic/wallets/pairing/hooks/useGetPairingUri.ts new file mode 100644 index 0000000000..2831b4e0ca --- /dev/null +++ b/src/logic/wallets/pairing/hooks/useGetPairingUri.ts @@ -0,0 +1,13 @@ +import { getPairingUri } from 'src/logic/wallets/pairing/utils' +import { useEffect, useState } from 'react' + +export const useGetPairingUri = (): string | undefined => { + const onboardUri = getPairingUri() + const [uri, setUri] = useState() + + useEffect(() => { + setUri(onboardUri) + }, [onboardUri]) + + return uri +} diff --git a/src/logic/wallets/pairing/hooks/usePairing.ts b/src/logic/wallets/pairing/hooks/usePairing.ts new file mode 100644 index 0000000000..d18b95a2da --- /dev/null +++ b/src/logic/wallets/pairing/hooks/usePairing.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react' + +import { initPairing, isPairingConnected } from 'src/logic/wallets/pairing/utils' + +const usePairing = (): void => { + useEffect(() => { + if (!isPairingConnected()) { + initPairing() + } + }, []) +} + +export default usePairing diff --git a/src/logic/wallets/pairing/module.ts b/src/logic/wallets/pairing/module.ts new file mode 100644 index 0000000000..16d1020145 --- /dev/null +++ b/src/logic/wallets/pairing/module.ts @@ -0,0 +1,81 @@ +import { IClientMeta } from '@walletconnect/types' +import { WalletModule } from 'bnc-onboard/dist/src/interfaces' +import UAParser from 'ua-parser-js' + +import { APP_VERSION, PUBLIC_URL } from 'src/utils/constants' +import { ChainId } from 'src/config/chain' +import { getWCWalletInterface, getWalletConnectProvider } from 'src/logic/wallets/walletConnect/utils' + +// Modified version of the built in WC module in Onboard v1.35.5 +// https://github.com/blocknative/onboard/blob/release/1.35.5/src/modules/select/wallets/wallet-connect.ts + +export const PAIRING_MODULE_NAME = 'Safe Mobile' + +let client = '' +const getClientMeta = (): IClientMeta => { + // Only instantiate parser if no app or client is set + if (!client) { + const parser = new UAParser() + const browser = parser.getBrowser() + const os = parser.getOS() + + client = `${browser.name} ${browser.major} (${os.name})` + } + + const app = `Safe Web v${APP_VERSION}` + const logo = `${location.origin}${PUBLIC_URL}/resources/logo_120x120.png` + + return { + name: app, + description: `${client};${app}`, + url: 'https://gnosis-safe.io/app', + icons: [logo], + } +} + +// Note: this shares a lot of similarities with the patchedWalletConnect module +const getPairingModule = (chainId: ChainId): WalletModule => { + const STORAGE_ID = 'SAFE__pairingProvider' + const clientMeta = getClientMeta() + + return { + name: PAIRING_MODULE_NAME, + wallet: async ({ resetWalletState }) => { + const provider = getWalletConnectProvider(chainId, { + storageId: STORAGE_ID, + qrcode: false, // Don't show QR modal + clientMeta, + }) + + // WalletConnect overrides the clientMeta, so we need to set it back + ;(provider.wc as any).clientMeta = clientMeta + ;(provider.wc as any)._clientMeta = clientMeta + + const onDisconnect = () => { + resetWalletState({ disconnected: true, walletName: PAIRING_MODULE_NAME }) + } + + provider.wc.on('disconnect', onDisconnect) + + window.addEventListener('unload', onDisconnect, { once: true }) + + // Establish WC connection + provider.enable() + + return { + provider, + interface: { + ...getWCWalletInterface(provider), + name: PAIRING_MODULE_NAME, + }, + } + }, + type: 'sdk', + desktop: true, + mobile: false, + // Must be preferred to position 1st in list (to hide via CSS) + preferred: true, + } +} + +export default getPairingModule diff --git a/src/logic/wallets/pairing/utils.ts b/src/logic/wallets/pairing/utils.ts new file mode 100644 index 0000000000..7b1d03a8b4 --- /dev/null +++ b/src/logic/wallets/pairing/utils.ts @@ -0,0 +1,30 @@ +import { Wallet } from 'bnc-onboard/dist/src/interfaces' + +import { getDisabledWallets } from 'src/config' +import { PAIRING_MODULE_NAME } from 'src/logic/wallets/pairing/module' +import { WALLETS } from 'src/config/chain.d' +import onboard from 'src/logic/wallets/onboard' + +export const initPairing = async (): Promise => { + await onboard().walletSelect(PAIRING_MODULE_NAME) +} + +// Is WC connected (may work for other providers) +export const isPairingConnected = (): boolean => { + return onboard().getState().wallet.provider?.connected +} + +export const isPairingSupported = (): boolean => { + return !getDisabledWallets().includes(WALLETS.SAFE_MOBILE) +} + +// Is pairing module initialised +export const isPairingModule = (name: Wallet['name'] = onboard().getState().wallet?.name): boolean => { + return name === PAIRING_MODULE_NAME +} + +export const getPairingUri = (): string | undefined => { + const wcUri = onboard().getState().wallet.provider?.wc?.uri + const PAIRING_MODULE_URI_PREFIX = 'safe-' + return wcUri ? `${PAIRING_MODULE_URI_PREFIX}${wcUri}` : undefined +} diff --git a/src/logic/wallets/patchedWalletConnect.ts b/src/logic/wallets/patchedWalletConnect.ts new file mode 100644 index 0000000000..552d1da826 --- /dev/null +++ b/src/logic/wallets/patchedWalletConnect.ts @@ -0,0 +1,105 @@ +import WalletConnectProvider from '@walletconnect/web3-provider' +import { IRPCMap } from '@walletconnect/types' +import { WalletModule, Helpers } from 'bnc-onboard/dist/src/interfaces' + +import { getRpcServiceUrl } from 'src/config' +import { getChains } from 'src/config/cache/chains' +import { INFURA_TOKEN } from 'src/utils/constants' +import { ChainId } from 'src/config/chain' + +// TODO: When desktop pairing is merged, import these into there +export const WC_BRIDGE = 'https://safe-walletconnect.gnosis.io/' + +// Modified version of the built in WC module in Onboard v1.35.5, including: +// https://github.com/blocknative/onboard/blob/release/1.35.5/src/modules/select/wallets/wallet-connect.ts + +// - No `balance` subscription as `eth_getBalance` is otherwise constantly requested +// but we do not request the balance from anywhere and is therefore not needed +// - A high polling interval to prevent unnecessary `eth_getBlockByNumber` polling +// https://github.com/WalletConnect/walletconnect-monorepo/issues/357#issuecomment-789663540 + +const walletConnectIcon = ` + + + +` + +export const getRpcMap = (): IRPCMap => { + return getChains().reduce((map, { chainId, rpcUri }) => { + return { + ...map, + [parseInt(chainId, 10)]: getRpcServiceUrl(rpcUri), + } + }, {}) +} + +const patchedWalletConnect = (chainId: ChainId): WalletModule => { + return { + name: 'WalletConnect', + svg: walletConnectIcon, + wallet: async ({ resetWalletState }: Helpers) => { + const provider = new WalletConnectProvider({ + infuraId: INFURA_TOKEN, + rpc: getRpcMap(), + chainId: parseInt(chainId, 10), + bridge: WC_BRIDGE, + // Prevent `eth_getBlockByNumber` polling every 4 seconds + pollingInterval: 60_000 * 60, // 1 hour + }) + + provider.autoRefreshOnNetworkChange = false + + provider.wc.on('disconnect', () => { + resetWalletState({ disconnected: true, walletName: 'WalletConnect' }) + }) + + return { + provider, + interface: { + name: 'WalletConnect', + connect: () => + new Promise((resolve, reject) => { + provider + .enable() + .then(resolve) + .catch(() => + reject({ + message: 'This dapp needs access to your account information.', + }), + ) + }), + address: { + onChange: (func) => { + provider.send('eth_accounts').then((accounts: string[]) => accounts[0] && func(accounts[0])) + provider.on('accountsChanged', (accounts: string[]) => func(accounts[0])) + }, + }, + network: { + onChange: (func) => { + provider.send('eth_chainId').then(func) + provider.on('chainChanged', func) + }, + }, + // Prevent continuous `eth_getBalance` requests + balance: {}, + disconnect: () => { + provider.wc.killSession() + provider.stop() + }, + }, + } + }, + type: 'sdk', + desktop: true, + mobile: true, + preferred: true, + } +} + +export default patchedWalletConnect diff --git a/src/logic/wallets/store/actions/addProvider.ts b/src/logic/wallets/store/actions/addProvider.ts deleted file mode 100644 index 4e9ebafa8b..0000000000 --- a/src/logic/wallets/store/actions/addProvider.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_PROVIDER = 'ADD_PROVIDER' - -const addProvider = createAction(ADD_PROVIDER, (provider) => provider) - -export default addProvider diff --git a/src/logic/wallets/store/actions/fetchProvider.ts b/src/logic/wallets/store/actions/fetchProvider.ts deleted file mode 100644 index 174e495ef0..0000000000 --- a/src/logic/wallets/store/actions/fetchProvider.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Dispatch } from 'redux' - -import addProvider from './addProvider' - -import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' -import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3' -import { makeProvider, ProviderProps } from 'src/logic/wallets/store/model/provider' -import { trackAnalyticsEvent, WALLET_EVENTS } from 'src/utils/googleAnalytics' - -export const processProviderResponse = (dispatch: Dispatch, provider: ProviderProps): void => { - const walletRecord = makeProvider(provider) - dispatch(addProvider(walletRecord)) -} - -const handleProviderNotification = (provider: ProviderProps, dispatch: Dispatch): void => { - const { available, loaded } = provider - - if (!loaded) { - dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG))) - return - } - - if (available) { - // NOTE: - // if you want to be able to dispatch a `closeSnackbar` action later on, - // you SHOULD pass your own `key` in the options. `key` can be any sequence - // of number or characters, but it has to be unique to a given snackbar. - - // Cannot import from useAnalytics here, so using fn directly - trackAnalyticsEvent({ - ...WALLET_EVENTS.CONNECT_WALLET, - label: provider.name, - }) - } else { - dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG))) - } -} - -export default (providerName: string): ((dispatch: Dispatch) => Promise) => - async (dispatch: Dispatch) => { - const web3 = getWeb3() - const providerInfo = await getProviderInfo(web3, providerName) - handleProviderNotification(providerInfo, dispatch) - processProviderResponse(dispatch, providerInfo) - } diff --git a/src/logic/wallets/store/actions/index.ts b/src/logic/wallets/store/actions/index.ts index 9e4dc96e7f..f60429534f 100644 --- a/src/logic/wallets/store/actions/index.ts +++ b/src/logic/wallets/store/actions/index.ts @@ -1,6 +1,7 @@ -export * from './addProvider' -export * from './fetchProvider' -export * from './removeProvider' -export { default as addProvider } from './addProvider' -export { default as fetchProvider } from './fetchProvider' -export { default as removeProvider } from './removeProvider' +export enum PROVIDER_ACTIONS { + WALLET = 'provider/walletUpdated', + ACCOUNT = 'provider/accountUpdated', + SMART_CONTRACT = 'provider/smartContract', + NETWORK = 'provider/networkUpdated', + ENS = 'provider/ensUpdated', +} diff --git a/src/logic/wallets/store/actions/removeProvider.ts b/src/logic/wallets/store/actions/removeProvider.ts deleted file mode 100644 index 0172b399d6..0000000000 --- a/src/logic/wallets/store/actions/removeProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Dispatch } from 'src/logic/safe/store/actions/types.d' -import { createAction } from 'redux-actions' - -import onboard from 'src/logic/wallets/onboard' -import { resetWeb3 } from 'src/logic/wallets/getWeb3' -import { ProviderState } from '../reducer/provider' - -export const REMOVE_PROVIDER = 'REMOVE_PROVIDER' - -const removeProvider = createAction(REMOVE_PROVIDER) - -export default (payload?: Pick) => - (dispatch: Dispatch): void => { - onboard().walletReset() - resetWeb3() - - dispatch(removeProvider(payload)) - } diff --git a/src/logic/wallets/store/actions/updateProviderAccount.ts b/src/logic/wallets/store/actions/updateProviderAccount.ts new file mode 100644 index 0000000000..e130e2e1c3 --- /dev/null +++ b/src/logic/wallets/store/actions/updateProviderAccount.ts @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions' + +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderAccountPayload } from 'src/logic/wallets/store/reducer' + +const updateProviderAccount = createAction(PROVIDER_ACTIONS.ACCOUNT) + +export default updateProviderAccount diff --git a/src/logic/wallets/store/actions/updateProviderEns.ts b/src/logic/wallets/store/actions/updateProviderEns.ts new file mode 100644 index 0000000000..b2c2cc0de6 --- /dev/null +++ b/src/logic/wallets/store/actions/updateProviderEns.ts @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions' + +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderEnsPayload } from 'src/logic/wallets/store/reducer' + +const updateProviderEns = createAction(PROVIDER_ACTIONS.ENS) + +export default updateProviderEns diff --git a/src/logic/wallets/store/actions/updateProviderNetwork.ts b/src/logic/wallets/store/actions/updateProviderNetwork.ts new file mode 100644 index 0000000000..4d6e26e3ab --- /dev/null +++ b/src/logic/wallets/store/actions/updateProviderNetwork.ts @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions' + +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderNetworkPayload } from 'src/logic/wallets/store/reducer' + +const updateProviderNetwork = createAction(PROVIDER_ACTIONS.NETWORK) + +export default updateProviderNetwork diff --git a/src/logic/wallets/store/actions/updateProviderSmartContract.ts b/src/logic/wallets/store/actions/updateProviderSmartContract.ts new file mode 100644 index 0000000000..0357b52f4e --- /dev/null +++ b/src/logic/wallets/store/actions/updateProviderSmartContract.ts @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions' + +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderSmartContractPayload } from 'src/logic/wallets/store/reducer' + +export const updateProviderSmartContract = createAction(PROVIDER_ACTIONS.SMART_CONTRACT) diff --git a/src/logic/wallets/store/actions/updateProviderWallet.ts b/src/logic/wallets/store/actions/updateProviderWallet.ts new file mode 100644 index 0000000000..9419e61e9f --- /dev/null +++ b/src/logic/wallets/store/actions/updateProviderWallet.ts @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions' + +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderWalletPayload } from 'src/logic/wallets/store/reducer' + +const updateProviderWallet = createAction(PROVIDER_ACTIONS.WALLET) + +export default updateProviderWallet diff --git a/src/logic/wallets/store/middleware/index.ts b/src/logic/wallets/store/middleware/index.ts new file mode 100644 index 0000000000..e230a8be9d --- /dev/null +++ b/src/logic/wallets/store/middleware/index.ts @@ -0,0 +1,76 @@ +import { Dispatch } from 'redux' +import { Action } from 'redux-actions' + +import { store as reduxStore } from 'src/store' +import { enhanceSnackbarForAction, NOTIFICATIONS } from 'src/logic/notifications' +import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { trackAnalyticsEvent, WALLET_EVENTS } from 'src/utils/googleAnalytics' +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { ProviderPayloads } from 'src/logic/wallets/store/reducer' +import { providerSelector } from '../selectors' +import { currentChainId } from 'src/logic/config/store/selectors' +import { isSmartContractWallet } from 'src/logic/wallets/getWeb3' +import { updateProviderSmartContract } from 'src/logic/wallets/store/actions/updateProviderSmartContract' + +let hasWallet = false +let hasAccount = false +let hasNetwork = false + +const providerMiddleware = + (store: ReturnType) => + (next: Dispatch) => + async (action: Action): Promise> => { + const handledAction = next(action) + + const { type, payload } = action + + // Onboard sends provider details via separate subscriptions: wallet, account, network + // Payloads from all three need to be combined to be `loaded` and `available` + if (type === PROVIDER_ACTIONS.WALLET) { + // Wallet has name, hardware/smart contract wallet flag set + hasWallet = Object.values(payload).some(Boolean) + } else if (type === PROVIDER_ACTIONS.ACCOUNT) { + hasAccount = !!payload + + // Check if wallet is smart contract + const smartContractWallet = typeof payload === 'string' ? await isSmartContractWallet(payload) : false + store.dispatch(updateProviderSmartContract(smartContractWallet)) + } else if (type === PROVIDER_ACTIONS.NETWORK) { + hasNetwork = !!payload + } else { + return handledAction + } + + if (!hasWallet || !hasAccount || !hasNetwork) { + return handledAction + } + + const state = store.getState() + const { available, loaded, name, network } = providerSelector(state) + + // @TODO: `loaded` flag that is/was always set to true - should be moved to wallet connection catch + // Wallet, account and network did not successfully load + if (!loaded) { + store.dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG))) + + return handledAction + } + + if (available) { + // Only track when wallet connects to same chain as chain displayed in UI + if (currentChainId(state) === network) { + const event = { + ...WALLET_EVENTS.CONNECT_WALLET, + label: name, + } + + trackAnalyticsEvent(event) + } + } else { + store.dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG))) + } + + return handledAction + } + +export default providerMiddleware diff --git a/src/logic/wallets/store/middlewares/providerWatcher.ts b/src/logic/wallets/store/middlewares/providerWatcher.ts deleted file mode 100644 index 556cb932db..0000000000 --- a/src/logic/wallets/store/middlewares/providerWatcher.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Dispatch } from 'redux' - -import { ChainId } from 'src/config/chain' -import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts' -import closeSnackbar from 'src/logic/notifications/store/actions/closeSnackbar' -import { getAccountFrom, getChainIdFrom, getWeb3 } from 'src/logic/wallets/getWeb3' -import { fetchProvider } from 'src/logic/wallets/store/actions' -import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider' -import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider' -import { store as reduxStore } from 'src/store/index' -import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/storage' -import { ProviderState } from '../reducer/provider' - -const watchedActions = [ADD_PROVIDER, REMOVE_PROVIDER] - -const LAST_USED_PROVIDER_KEY = 'LAST_USED_PROVIDER' - -export const loadLastUsedProvider = (): string | undefined => { - return loadFromStorage(LAST_USED_PROVIDER_KEY) -} - -type ProviderWatcherAction = { - type: string - payload: ProviderState -} - -let watcherInterval: NodeJS.Timer -const providerWatcherMware = - (store: ReturnType) => - (next: Dispatch) => - async (action: ProviderWatcherAction): Promise => { - const handledAction = next(action) - - if (watchedActions.includes(action.type)) { - switch (action.type) { - case ADD_PROVIDER: { - const currentProviderProps = action.payload.toJS() - - if (watcherInterval) { - clearInterval(watcherInterval) - } - - instantiateSafeContracts() - - saveToStorage(LAST_USED_PROVIDER_KEY, currentProviderProps.name) - - watcherInterval = setInterval(async () => { - const web3 = getWeb3() - - const network = (await getChainIdFrom(web3)).toString() as ChainId - const account = await getAccountFrom(web3) - - const hasChangedNetwork = currentProviderProps.network !== network - const hasChangedAccount = currentProviderProps.account !== account - - if (hasChangedNetwork) { - store.dispatch(closeSnackbar({ dismissAll: true })) - } - - if (hasChangedNetwork || hasChangedAccount) { - store.dispatch(fetchProvider(currentProviderProps.name)) - } - }, 2000) - - break - } - case REMOVE_PROVIDER: - clearInterval(watcherInterval) - if (!action.payload?.keepStorageKey) { - removeFromStorage(LAST_USED_PROVIDER_KEY) - } - break - default: - break - } - } - - return handledAction - } - -export default providerWatcherMware diff --git a/src/logic/wallets/store/model/provider.ts b/src/logic/wallets/store/model/provider.ts deleted file mode 100644 index 6bfb1d3e31..0000000000 --- a/src/logic/wallets/store/model/provider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Record, RecordOf } from 'immutable' - -import { ChainId, CHAIN_ID } from 'src/config/chain.d' - -export type ProviderProps = { - name: string - loaded: boolean - available: boolean - account: string - ensDomain: string - network: ChainId - smartContractWallet: boolean - hardwareWallet: boolean -} - -export const makeProvider = Record({ - name: '', - loaded: false, - available: false, - account: '', - ensDomain: '', - network: CHAIN_ID.UNKNOWN, - smartContractWallet: false, - hardwareWallet: false, -}) - -// Usage const someProvider: Provider = makeProvider({ name: 'METAMASK', loaded: false, available: false }) - -export type ProviderRecord = RecordOf diff --git a/src/logic/wallets/store/reducer/index.ts b/src/logic/wallets/store/reducer/index.ts new file mode 100644 index 0000000000..36569497e1 --- /dev/null +++ b/src/logic/wallets/store/reducer/index.ts @@ -0,0 +1,70 @@ +import { Action, handleActions } from 'redux-actions' + +import { ChainId } from 'src/config/chain.d' +import { PROVIDER_ACTIONS } from 'src/logic/wallets/store/actions' +import { checksumAddress } from 'src/utils/checksumAddress' + +export type ProvidersState = { + name: string + hardwareWallet: boolean + smartContractWallet: boolean + network: ChainId + account: string + available: boolean + ensDomain: string + loaded: boolean +} + +export type ProviderWalletPayload = Pick +export type ProviderNetworkPayload = ProvidersState['network'] +export type ProviderAccountPayload = ProvidersState['account'] +export type ProviderEnsPayload = ProvidersState['ensDomain'] +export type ProviderSmartContractPayload = ProvidersState['smartContractWallet'] + +export type ProviderPayloads = + | ProviderWalletPayload + | ProviderAccountPayload + | ProviderNetworkPayload + | ProviderEnsPayload + | ProviderSmartContractPayload + +const initialProviderState: ProvidersState = { + name: '', + hardwareWallet: false, + smartContractWallet: false, + account: '', + network: '', + ensDomain: '', + available: false, + loaded: false, +} + +const providerFactory = (provider: ProvidersState) => { + const { name, hardwareWallet, smartContractWallet, account, network } = provider + const hasWallet = !!name || hardwareWallet || smartContractWallet + return { ...provider, loaded: hasWallet && !!account && !!network } +} + +export const PROVIDER_REDUCER_ID = 'providers' + +const providerReducer = handleActions( + { + [PROVIDER_ACTIONS.WALLET]: (state: ProvidersState, { payload }: Action) => + providerFactory({ ...state, ...payload }), + [PROVIDER_ACTIONS.NETWORK]: (state: ProvidersState, { payload }: Action) => + providerFactory({ ...state, network: payload }), + [PROVIDER_ACTIONS.ACCOUNT]: (state: ProvidersState, { payload }: Action) => + providerFactory({ + ...state, + account: payload ? checksumAddress(payload) : '', + available: !!payload, + }), + [PROVIDER_ACTIONS.SMART_CONTRACT]: (state: ProvidersState, { payload }: Action) => + providerFactory({ ...state, smartContractWallet: payload }), + [PROVIDER_ACTIONS.ENS]: (state: ProvidersState, { payload }: Action) => + providerFactory({ ...state, ensDomain: payload }), + }, + initialProviderState, +) + +export default providerReducer diff --git a/src/logic/wallets/store/reducer/provider.ts b/src/logic/wallets/store/reducer/provider.ts deleted file mode 100644 index fa65c4a179..0000000000 --- a/src/logic/wallets/store/reducer/provider.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { handleActions } from 'redux-actions' - -import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider' -import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider' -import { makeProvider, ProviderRecord, ProviderProps } from 'src/logic/wallets/store/model/provider' - -export const PROVIDER_REDUCER_ID = 'providers' - -export type ProviderState = ProviderRecord & { keepStorageKey?: boolean } - -const providerReducer = handleActions( - { - [ADD_PROVIDER]: (_state: ProviderState, { payload }: { payload: ProviderProps }): ProviderState => - makeProvider(payload), - [REMOVE_PROVIDER]: (): ProviderState => makeProvider(), - }, - makeProvider(), -) - -export default providerReducer diff --git a/src/logic/wallets/store/selectors/index.ts b/src/logic/wallets/store/selectors/index.ts index 80946bc79e..3c7e0d8b20 100644 --- a/src/logic/wallets/store/selectors/index.ts +++ b/src/logic/wallets/store/selectors/index.ts @@ -2,46 +2,39 @@ import { createSelector } from 'reselect' import { ChainId, CHAIN_ID } from 'src/config/chain.d' import { currentChainId } from 'src/logic/config/store/selectors' -import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' +import { PROVIDER_REDUCER_ID, ProvidersState } from 'src/logic/wallets/store/reducer' import { AppReduxState } from 'src/store' -export const providerSelector = (state: AppReduxState): ProviderState => state[PROVIDER_REDUCER_ID] +export const providerSelector = (state: AppReduxState): ProvidersState => state[PROVIDER_REDUCER_ID] -export const userAccountSelector = createSelector(providerSelector, (provider: ProviderState): string => { - const account = provider.get('account') - return account || '' +export const userAccountSelector = createSelector(providerSelector, ({ account }: ProvidersState): string => { + return account }) -export const userEnsSelector = createSelector(providerSelector, (provider: ProviderState): string => { - const ensName = provider.get('ensDomain') - return ensName || '' +export const userEnsSelector = createSelector(providerSelector, ({ ensDomain }: ProvidersState): string => { + return ensDomain }) -export const providerNameSelector = createSelector(providerSelector, (provider: ProviderState): string | undefined => { - const name = provider.get('name') - return name ? name.toLowerCase() : undefined +export const providerNameSelector = createSelector(providerSelector, ({ name }: ProvidersState): string | undefined => { + return name }) -export const networkSelector = createSelector(providerSelector, (provider: ProviderState): ChainId => { - const networkId = provider.get('network') - - return networkId ?? CHAIN_ID.UNKNOWN +export const networkSelector = createSelector(providerSelector, ({ network }: ProvidersState): ChainId => { + return network ?? CHAIN_ID.UNKNOWN }) export const shouldSwitchWalletChain = createSelector( providerSelector, currentChainId, - (provider: ProviderState, currentChainId: ChainId): boolean => { - const account = provider.get('account') - const networkId = provider.get('network').toString() - return !!account && networkId !== currentChainId + ({ account, network }: ProvidersState, currentChainId: ChainId): boolean => { + return !!account && network !== currentChainId }, ) -export const loadedSelector = createSelector(providerSelector, (provider: ProviderState): boolean => - provider.get('loaded'), -) +export const loadedSelector = createSelector(providerSelector, ({ loaded }: ProvidersState): boolean => { + return loaded +}) -export const availableSelector = createSelector(providerSelector, (provider: ProviderState): boolean => - provider.get('available'), -) +export const availableSelector = createSelector(providerSelector, ({ available }: ProvidersState): boolean => { + return available +}) diff --git a/src/logic/wallets/utils/network.ts b/src/logic/wallets/utils/network.ts index 98a602f269..813b7c9821 100644 --- a/src/logic/wallets/utils/network.ts +++ b/src/logic/wallets/utils/network.ts @@ -5,6 +5,7 @@ import { numberToHex } from 'web3-utils' import { getChainInfo, getExplorerUrl, getPublicRpcUrl, _getChainId } from 'src/config' import { ChainId } from 'src/config/chain.d' import { Errors, CodedException } from 'src/logic/exceptions/CodedException' +import { isPairingModule } from 'src/logic/wallets/pairing/utils' const WALLET_ERRORS = { UNRECOGNIZED_CHAIN: 4902, @@ -17,14 +18,21 @@ const WALLET_ERRORS = { * @see https://github.com/MetaMask/metamask-extension/pull/10905 */ const requestSwitch = async (wallet: Wallet, chainId: ChainId): Promise => { - await wallet.provider.request({ - method: 'wallet_switchEthereumChain', - params: [ - { - chainId: numberToHex(chainId), - }, - ], - }) + // Note: This could support WC too + if (isPairingModule(wallet.name)) { + if (wallet.provider) { + wallet.provider.wc.updateSession({ chainId: parseInt(chainId, 10), accounts: wallet.provider.wc.accounts }) + } + } else { + await wallet.provider.request({ + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: numberToHex(chainId), + }, + ], + }) + } } /** @@ -35,7 +43,7 @@ const requestSwitch = async (wallet: Wallet, chainId: ChainId): Promise => const requestAdd = async (wallet: Wallet, chainId: ChainId): Promise => { const { chainName, nativeCurrency } = getChainInfo() - await wallet.provider.request({ + await wallet.provider?.request({ method: 'wallet_addEthereumChain', params: [ { @@ -76,10 +84,13 @@ export const switchNetwork = async (wallet: Wallet, chainId: ChainId): Promise { - const desiredNetwork = _getChainId() - const currentNetwork = wallet?.provider?.networkVersion - return currentNetwork ? desiredNetwork !== currentNetwork.toString() : false +export const shouldSwitchNetwork = (wallet: Wallet): boolean => { + // The current network can be stored under one of two keys + const isCurrentNetwork = [wallet?.provider?.networkVersion, wallet?.provider?.chainId].some( + (chainId) => chainId && chainId.toString() !== _getChainId(), + ) + + return isCurrentNetwork } export const switchWalletChain = async (): Promise => { @@ -89,6 +100,8 @@ export const switchWalletChain = async (): Promise => { } catch (e) { e.log() // Fallback to the onboard popup if switching isn't supported + // walletSelect must be called first: https://docs.blocknative.com/onboard#onboard-user + await onboard().walletSelect() await onboard().walletCheck() } } diff --git a/src/logic/wallets/utils/walletList.ts b/src/logic/wallets/utils/walletList.ts index fb25fe67b0..76d8005e9b 100644 --- a/src/logic/wallets/utils/walletList.ts +++ b/src/logic/wallets/utils/walletList.ts @@ -1,29 +1,26 @@ -import { WalletInitOptions } from 'bnc-onboard/dist/src/interfaces' +import { WalletInitOptions, WalletModule, WalletSelectModuleOptions } from 'bnc-onboard/dist/src/interfaces' -import { getRpcServiceUrl, getDisabledWallets, _getChainId } from 'src/config' -import { WALLETS } from 'src/config/chain.d' +import { getRpcServiceUrl, getDisabledWallets, getChainById } from 'src/config' +import { ChainId, WALLETS } from 'src/config/chain.d' import { FORTMATIC_KEY, PORTIS_ID } from 'src/utils/constants' +import getPairingModule from 'src/logic/wallets/pairing/module' +import { isPairingSupported } from 'src/logic/wallets/pairing/utils' +import getPatchedWCModule from 'src/logic/wallets/walletConnect/module' -type Wallet = WalletInitOptions & { - desktop: boolean +type Wallet = (WalletInitOptions | WalletModule) & { + desktop: boolean // Whether wallet supports desktop app walletName: WALLETS } -const wallets = (): Wallet[] => { - const rpcUrl = getRpcServiceUrl() - const chainId = _getChainId() +const wallets = (chainId: ChainId): Wallet[] => { + // Ensure RPC matches chainId drilled from Onboard init + const { rpcUri } = getChainById(chainId) + const rpcUrl = getRpcServiceUrl(rpcUri) return [ { walletName: WALLETS.METAMASK, preferred: true, desktop: false }, - { - walletName: WALLETS.WALLET_CONNECT, - preferred: true, - // as stated in the documentation, `infuraKey` is not mandatory if rpc is provided - rpc: { [chainId]: rpcUrl }, - networkId: parseInt(chainId, 10), - desktop: true, - bridge: 'https://safe-walletconnect.gnosis.io/', - }, + // A patched version of WalletConnect is spliced in at this index + // { preferred: true, desktop: true } { walletName: WALLETS.TREZOR, appUrl: 'gnosis-safe.io', @@ -71,14 +68,30 @@ const wallets = (): Wallet[] => { ] } -export const getSupportedWallets = (): WalletInitOptions[] => { - if (window.isDesktop) { - return wallets() - .filter((wallet) => wallet.desktop) - .map(({ desktop, ...rest }) => rest) +export const isSupportedWallet = (name: WALLETS | string): boolean => { + return !getDisabledWallets().some((walletName) => { + // walletName is config wallet name, name is the wallet module name and differ + return walletName.replace(/\s/g, '').toLowerCase() === name.replace(/\s/g, '').toLowerCase() + }) +} + +export const getSupportedWallets = (chainId: ChainId): WalletSelectModuleOptions['wallets'] => { + const supportedWallets: WalletSelectModuleOptions['wallets'] = wallets(chainId) + .filter(({ walletName, desktop }) => { + if (!isSupportedWallet(walletName)) { + return false + } + // Desktop vs. Web app wallet support + return window.isDesktop ? desktop : true + }) + .map(({ desktop: _, ...rest }) => rest) + + if (isSupportedWallet(WALLETS.WALLET_CONNECT)) { + const wc = getPatchedWCModule(chainId) + // Inset patched WC module at index 1 + supportedWallets?.splice(1, 0, wc) } - return wallets() - .map(({ desktop, ...rest }) => rest) - .filter((w) => !getDisabledWallets().includes(w.walletName)) + // Pairing must be 1st in list (to hide via CSS) + return isPairingSupported() ? [getPairingModule(chainId), ...supportedWallets] : supportedWallets } diff --git a/src/logic/wallets/walletConnect/module.ts b/src/logic/wallets/walletConnect/module.ts new file mode 100644 index 0000000000..ce5cf490f3 --- /dev/null +++ b/src/logic/wallets/walletConnect/module.ts @@ -0,0 +1,60 @@ +import { WalletModule, Helpers } from 'bnc-onboard/dist/src/interfaces' + +import { ChainId } from 'src/config/chain' +import { getWCWalletInterface, getWalletConnectProvider } from 'src/logic/wallets/walletConnect/utils' + +const walletConnectIcon = ` + + + +` + +// Modified version of the built in WC module in Onboard v1.35.5, including: +// https://github.com/blocknative/onboard/blob/release/1.35.5/src/modules/select/wallets/wallet-connect.ts + +const getPatchedWCModule = (chainId: ChainId): WalletModule => { + const MODULE_NAME = 'WalletConnect' + + return { + name: MODULE_NAME, + svg: walletConnectIcon, + wallet: async ({ resetWalletState }: Helpers) => { + const provider = getWalletConnectProvider(chainId) + + provider.wc.on('disconnect', () => { + resetWalletState({ disconnected: true, walletName: MODULE_NAME }) + }) + + return { + provider, + interface: { + ...getWCWalletInterface(provider), + name: MODULE_NAME, + connect: () => + new Promise((resolve, reject) => { + provider + .enable() + .then(resolve) + .catch(() => + reject({ + message: 'This dapp needs access to your account information.', + }), + ) + }), + }, + } + }, + type: 'sdk', + desktop: true, + mobile: true, + preferred: true, + } +} + +export default getPatchedWCModule diff --git a/src/logic/wallets/walletConnect/utils.ts b/src/logic/wallets/walletConnect/utils.ts new file mode 100644 index 0000000000..86aec9fc91 --- /dev/null +++ b/src/logic/wallets/walletConnect/utils.ts @@ -0,0 +1,62 @@ +import WalletConnectProvider from '@walletconnect/web3-provider' +import { IRPCMap, IWalletConnectProviderOptions } from '@walletconnect/types' +import { WalletInterface } from 'bnc-onboard/dist/src/interfaces' + +import { getRpcServiceUrl } from 'src/config' +import { getChains } from 'src/config/cache/chains' +import { ChainId } from 'src/config/chain' +import { INFURA_TOKEN } from 'src/utils/constants' + +type Options = Omit + +export const getWalletConnectProvider = (chainId: ChainId, options: Options = {}): WalletConnectProvider => { + const WC_BRIDGE = 'https://safe-walletconnect.gnosis.io/' + // Prevent `eth_getBlockByNumber` polling every 4 seconds + // https://github.com/WalletConnect/walletconnect-monorepo/issues/357#issuecomment-789663540 + const POLLING_INTERVAL = 60_000 * 60 // 1 hour + const RPC_MAP: IRPCMap = getChains().reduce((map, { chainId, rpcUri }) => { + return { + ...map, + [parseInt(chainId, 10)]: getRpcServiceUrl(rpcUri), + } + }, {}) + + const provider = new WalletConnectProvider({ + bridge: WC_BRIDGE, + pollingInterval: POLLING_INTERVAL, + infuraId: INFURA_TOKEN, + rpc: RPC_MAP, + chainId: parseInt(chainId, 10), + // Prevent `eth_getBlockByNumber` polling every 4 seconds + ...options, + }) + + provider.autoRefreshOnNetworkChange = false + + return provider +} + +export const getWCWalletInterface = ( + provider: WalletConnectProvider, +): Pick => { + return { + address: { + onChange: (func) => { + provider.send('eth_accounts').then((accounts: string[]) => accounts[0] && func(accounts[0])) + provider.on('accountsChanged', (accounts: string[]) => func(accounts[0])) + }, + }, + network: { + onChange: (func) => { + provider.send('eth_chainId').then(func) + provider.on('chainChanged', func) + }, + }, + // balance stateSynce prevents continuous `eth_getBalance` requests + // (prevents us from accessing balance via Onboard, but via web3 works) + balance: {}, + disconnect: () => { + provider.disconnect() + }, + } +} diff --git a/src/routes/CreateSafePage/CreateSafePage.test.tsx b/src/routes/CreateSafePage/CreateSafePage.test.tsx index 6331ff0b43..403b9f0e94 100644 --- a/src/routes/CreateSafePage/CreateSafePage.test.tsx +++ b/src/routes/CreateSafePage/CreateSafePage.test.tsx @@ -519,7 +519,7 @@ describe('', () => { const defaultOwnerInput = screen.getByTestId('owner-address-1') fireEvent.change(defaultOwnerInput, { target: { value: '0x680cde08860141F9D223cE4E620B10Cd6741037E' } }) - const errorText = 'Address already introduced' + const errorText = 'Address already added' expect(screen.getByText(errorText)).toBeInTheDocument() }) diff --git a/src/routes/CreateSafePage/CreateSafePage.tsx b/src/routes/CreateSafePage/CreateSafePage.tsx index 2c822a8220..3414506d56 100644 --- a/src/routes/CreateSafePage/CreateSafePage.tsx +++ b/src/routes/CreateSafePage/CreateSafePage.tsx @@ -4,7 +4,7 @@ import ChevronLeft from '@material-ui/icons/ChevronLeft' import styled from 'styled-components' import { useSelector } from 'react-redux' import queryString from 'query-string' -import { useLocation } from 'react-router' +import { useLocation } from 'react-router-dom' import { Loader } from '@gnosis.pm/safe-react-components' import Page from 'src/components/layout/Page' @@ -30,13 +30,13 @@ import { useMnemonicSafeName } from 'src/logic/hooks/useMnemonicName' import { providerNameSelector, shouldSwitchWalletChain, userAccountSelector } from 'src/logic/wallets/store/selectors' import OwnersAndConfirmationsNewSafeStep, { ownersAndConfirmationsNewSafeStepLabel, - ownersAndConfirmationsNewSafeStepValidations, } from './steps/OwnersAndConfirmationsNewSafeStep' import { currentNetworkAddressBookAsMap } from 'src/logic/addressBook/store/selectors' import ReviewNewSafeStep, { reviewNewSafeStepLabel } from './steps/ReviewNewSafeStep' import { loadFromStorage, saveToStorage } from 'src/utils/storage' import SafeCreationProcess from './components/SafeCreationProcess' import SelectWalletAndNetworkStep, { selectWalletAndNetworkStepLabel } from './steps/SelectWalletAndNetworkStep' +import { reverseENSLookup } from 'src/logic/wallets/getWeb3' function CreateSafePage(): ReactElement { const [safePendingToBeCreated, setSafePendingToBeCreated] = useState() @@ -76,9 +76,18 @@ function CreateSafePage(): ReactElement { const [initialFormValues, setInitialFormValues] = useState() useEffect(() => { + let isCurrent = true if (provider && userWalletAddress) { - const initialValuesFromUrl = getInitialValues(userWalletAddress, addressBook, location, safeRandomName) - setInitialFormValues(initialValuesFromUrl) + const getInitValues = async () => { + const initialValuesFromUrl = await getInitialValues(userWalletAddress, addressBook, location, safeRandomName) + if (isCurrent) { + setInitialFormValues(initialValuesFromUrl) + } + } + getInitValues() + } + return () => { + isCurrent = false } }, [provider, userWalletAddress, addressBook, location, safeRandomName]) @@ -90,6 +99,8 @@ function CreateSafePage(): ReactElement { ) } + const isInitializing = !provider || !initialFormValues + return !!safePendingToBeCreated ? ( ) : ( @@ -105,18 +116,14 @@ function CreateSafePage(): ReactElement { - + @@ -133,7 +140,7 @@ export default CreateSafePage const DEFAULT_THRESHOLD_VALUE = 1 // initial values can be present in the URL because the Old MultiSig migration -function getInitialValues(userAddress, addressBook, location, suggestedSafeName): CreateSafeFormValues { +async function getInitialValues(userAddress, addressBook, location, suggestedSafeName): Promise { const query = queryString.parse(location.search, { arrayFormat: 'comma' }) const { name, owneraddresses, ownernames, threshold } = query @@ -159,7 +166,15 @@ function getInitialValues(userAddress, addressBook, location, suggestedSafeName) nameFieldName: `owner-name-${index}`, addressFieldName: `owner-address-${index}`, })), - [FIELD_SAFE_OWNER_ENS_LIST]: {}, + [FIELD_SAFE_OWNER_ENS_LIST]: ( + await Promise.all( + owners.map(async (address) => { + return { [address]: await reverseENSLookup(address) } + }), + ) + ).reduce((acc, owner) => { + return { ...acc, ...owner } + }, {}), // we set owners address values as owner-address-${index} format in the form state ...owners.reduce( (ownerAddressFields, ownerAddress, index) => ({ diff --git a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx b/src/routes/CreateSafePage/components/SafeCreationProcess.tsx index d2b51a74d2..c2cdd11bf9 100644 --- a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx +++ b/src/routes/CreateSafePage/components/SafeCreationProcess.tsx @@ -100,6 +100,8 @@ function SafeCreationProcess(): ReactElement { return } + if (!userAddressAccount) return + setSafeCreationTxHash(safeCreationFormValues[FIELD_NEW_SAFE_CREATION_TX_HASH]) setCreationTxPromise( diff --git a/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx b/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx index f2b15efd72..6d54b60455 100644 --- a/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx +++ b/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx @@ -13,8 +13,8 @@ import { providerNameSelector } from 'src/logic/wallets/store/selectors' import { FIELD_CREATE_CUSTOM_SAFE_NAME, FIELD_CREATE_SUGGESTED_SAFE_NAME, - FIELD_SAFE_OWNER_ENS_LIST, FIELD_SAFE_OWNERS_LIST, + FIELD_SAFE_OWNER_ENS_LIST, } from '../fields/createSafeFields' import { useStepper } from 'src/components/Stepper/stepperContext' import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' @@ -42,14 +42,16 @@ function NameNewSafeStep(): ReactElement { const formValues = createNewSafeForm.getState().values const owners = formValues[FIELD_SAFE_OWNERS_LIST] const ownersWithENSName = await Promise.all( - owners.map(async ({ addressFieldName }) => { - const address = formValues[addressFieldName] - const ensName = await reverseENSLookup(address) - return { - address, - name: ensName, - } - }), + owners + .filter(({ addressFieldName }) => !!formValues[addressFieldName]) + .map(async ({ addressFieldName }) => { + const address = formValues[addressFieldName] + const ensName = await reverseENSLookup(address) + return { + address, + name: ensName, + } + }), ) const ownersWithENSNameRecord = ownersWithENSName.reduce>((acc, { address, name }) => { diff --git a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx index e29d0e2c0a..472b5eca80 100644 --- a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx +++ b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx @@ -39,6 +39,7 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { currentNetworkAddressBookAsMap } from 'src/logic/addressBook/store/selectors' import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' import { reverseENSLookup } from 'src/logic/wallets/getWeb3' +import { sameString } from 'src/utils/strings' export const ownersAndConfirmationsNewSafeStepLabel = 'Owners and Confirmations' @@ -89,10 +90,14 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { const getENSName = async (address: string): Promise => { const ensName = await reverseENSLookup(address) - const newOwnersWithENSName: Record = Object.assign(ownersWithENSName, { - [address]: ensName, - }) - createSafeForm.change(FIELD_SAFE_OWNER_ENS_LIST, newOwnersWithENSName) + createSafeForm.change(FIELD_SAFE_OWNER_ENS_LIST, { ...ownersWithENSName, [address]: ensName }) + } + + const handleScan = async (address: string, closeQrModal: () => void, addressFieldName: string): Promise => { + const scannedAddress = address.startsWith('ethereum:') ? address.replace('ethereum:', '') : address + await getENSName(scannedAddress) + createSafeForm.change(addressFieldName, scannedAddress) + closeQrModal() } return ( @@ -128,16 +133,18 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { - {owners.map(({ nameFieldName, addressFieldName }) => { + {owners.map(({ nameFieldName, addressFieldName }, i: number) => { const hasOwnerAddressError = formErrors[addressFieldName] const ownerAddress = createSafeFormValues[addressFieldName] const showDeleteIcon = addressFieldName !== 'owner-address-0' // we hide de delete icon for the first owner const ownerName = ownersWithENSName[ownerAddress] || 'Owner Name' - const handleScan = async (address: string, closeQrModal: () => void): Promise => { - await getENSName(address) - createSafeForm.change(addressFieldName, address) - closeQrModal() + const isRepeated = (value: string) => { + const prevOwners = owners.filter((_: typeof owners[number], index: number) => index !== i) + const repeated = prevOwners.some((owner: typeof owners[number]) => { + return sameString(createSafeFormValues[owner.addressFieldName], value) + }) + return repeated ? ADDRESS_REPEATED_ERROR : undefined } return ( @@ -156,11 +163,12 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { { - await getENSName(address) createSafeForm.change(addressFieldName, address) const addressName = addressBook[address]?.name if (addressName) { createSafeForm.change(nameFieldName, addressName) + } else { + await getENSName(address) } }} inputAdornment={ @@ -176,10 +184,14 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { placeholder="Owner Address*" text="Owner Address" testId={addressFieldName} + validators={[isRepeated]} /> - + handleScan(...args, addressFieldName)} + testId={`${addressFieldName}-scan-QR`} + /> {showDeleteIcon && ( @@ -212,7 +224,12 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { component={SelectField} data-testid="threshold-selector-input" name={FIELD_NEW_SAFE_THRESHOLD} - validate={composeValidators(required, minValue(1))} + validate={(val) => { + const isValidThreshold = () => { + return !!threshold && threshold <= owners.length ? undefined : THRESHOLD_ERROR + } + return composeValidators(required, minValue(1), isValidThreshold)(val) + }} > {owners.map((_, option) => ( > - [FIELD_NEW_SAFE_THRESHOLD]: number -}): Record => { - const errors = {} - - const owners = values[FIELD_SAFE_OWNERS_LIST] - const threshold = values[FIELD_NEW_SAFE_THRESHOLD] - const addresses = owners.map(({ addressFieldName }) => values[addressFieldName]) - - // we check repeated addresses - owners.forEach(({ addressFieldName }, index) => { - const address = values[addressFieldName] - const previousOwners = addresses.slice(0, index) - const isRepeated = previousOwners.includes(address) - if (isRepeated) { - errors[addressFieldName] = ADDRESS_REPEATED_ERROR - } - }) - - const isValidThreshold = !!threshold && threshold <= owners.length - if (!isValidThreshold) { - errors[FIELD_NEW_SAFE_THRESHOLD] = THRESHOLD_ERROR - } - - return errors -} - const BlockWithPadding = styled(Block)` padding: ${lg}; ` diff --git a/src/routes/CreateSafePage/steps/SelectWalletAndNetworkStep.tsx b/src/routes/CreateSafePage/steps/SelectWalletAndNetworkStep.tsx index d72e88d723..fd7257e1c1 100644 --- a/src/routes/CreateSafePage/steps/SelectWalletAndNetworkStep.tsx +++ b/src/routes/CreateSafePage/steps/SelectWalletAndNetworkStep.tsx @@ -16,7 +16,7 @@ import { setChainId } from 'src/logic/config/utils' import { lg } from 'src/theme/variables' import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' import Paragraph from 'src/components/layout/Paragraph' -import { providerNameSelector, shouldSwitchWalletChain } from 'src/logic/wallets/store/selectors' +import { availableSelector, shouldSwitchWalletChain } from 'src/logic/wallets/store/selectors' import ConnectButton from 'src/components/ConnectButton' import WalletSwitch from 'src/components/WalletSwitch' import { getChains } from 'src/config/cache/chains' @@ -25,7 +25,7 @@ export const selectWalletAndNetworkStepLabel = 'Connect wallet & select network' function SelectWalletAndNetworkStep(): ReactElement { const [isNetworkSelectorPopupOpen, setIsNetworkSelectorPopupOpen] = useState(false) - const isWalletConnected = !!useSelector(providerNameSelector) + const isWalletConnected = !!useSelector(availableSelector) const isWrongNetwork = useSelector(shouldSwitchWalletChain) function openNetworkSelectorPopup() { diff --git a/src/routes/opening/assets/safe-created.svg b/src/routes/opening/assets/safe-created.svg index 2cfaaaa745..eee8962a1f 100644 --- a/src/routes/opening/assets/safe-created.svg +++ b/src/routes/opening/assets/safe-created.svg @@ -3,7 +3,7 @@ - + diff --git a/src/routes/opening/index.tsx b/src/routes/opening/index.tsx index 468c247877..3c0328268a 100644 --- a/src/routes/opening/index.tsx +++ b/src/routes/opening/index.tsx @@ -103,7 +103,7 @@ export const SafeDeployment = ({ } }, [provider]) - // creating safe from from submission + // creating safe from form submission useEffect(() => { if (submittedPromise === undefined) { return @@ -139,16 +139,16 @@ export const SafeDeployment = ({ return } - const isTxMined = async (txHash) => { + const isTxMined = async (txHash: string) => { const web3 = getWeb3() const txResult = await web3.eth.getTransaction(txHash) - if (txResult.blockNumber === null) { + if (txResult?.blockNumber == null) { return false } const receipt = await web3.eth.getTransactionReceipt(txHash) - if (!receipt.status) { + if (!receipt?.status) { throw Error('TX status reverted') } @@ -197,19 +197,24 @@ export const SafeDeployment = ({ const web3 = getWeb3() const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash) - let safeAddress + let safeAddress = '' - if (receipt.events) { + if (receipt?.events) { safeAddress = receipt.events.ProxyCreation.returnValues.proxy } else { // If the node doesn't return the events we try to fetch it from logs - safeAddress = getNewSafeAddressFromLogs(receipt.logs) + safeAddress = getNewSafeAddressFromLogs(receipt?.logs || []) } setCreatedSafeAddress(safeAddress) interval = setInterval(async () => { - const code = await web3.eth.getCode(safeAddress) + let code = EMPTY_DATA + try { + code = await web3.eth.getCode(safeAddress) + } catch (err) { + console.log(err) + } if (code !== EMPTY_DATA) { setStepIndex(5) } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 90bfce7e9b..b21c5353a6 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -67,7 +67,7 @@ export const SAFE_ROUTES = { export const getNetworkRootRoutes = (): Array<{ chainId: ChainId; route: string; shortName: string }> => getChains().map(({ chainId, chainName, shortName }) => ({ chainId, - route: `/${chainName.replaceAll(' ', '-').toLowerCase()}`, + route: `/${chainName.replace(/\s+/g, '-').toLowerCase()}`, shortName, })) diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg index 0b2b8dd461..c45f3e0069 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg @@ -1,8 +1,8 @@ - + - + diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg index 09f27d517f..ae1cd66ce8 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg @@ -1,8 +1,8 @@ - + - + diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg index 9b8677ded0..99d724d15e 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg @@ -1,7 +1,7 @@ - + - + diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index 1aeab3a61a..bad8f9c03c 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -1,4 +1,4 @@ -import { lg, md, sm } from 'src/theme/variables' +import { background, lg, md, sm } from 'src/theme/variables' import { createStyles } from '@material-ui/core' export const styles = createStyles({ @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Apps/assets/addApp.svg b/src/routes/safe/components/Apps/assets/addApp.svg index 76500778d3..4bf6e3e5c1 100644 --- a/src/routes/safe/components/Apps/assets/addApp.svg +++ b/src/routes/safe/components/Apps/assets/addApp.svg @@ -10,7 +10,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 7128c773fd..1c9d0b9db8 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useState, useRef, useCallback, useEffect } from 'react' +import { ReactElement, useState, useRef, useCallback, useEffect, useMemo } from 'react' import styled from 'styled-components' import { Loader, Card, Title } from '@gnosis.pm/safe-react-components' import { @@ -10,7 +10,6 @@ import { SignMessageParams, RequestId, } from '@gnosis.pm/safe-apps-sdk' - import { useSelector } from 'react-redux' import { INTERFACE_MESSAGES, Transaction, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1' import Web3 from 'web3' @@ -32,6 +31,7 @@ import { logError, Errors } from 'src/logic/exceptions/CodedException' import { addressBookEntryName } from 'src/logic/addressBook/store/selectors' import { useSignMessageModal } from '../hooks/useSignMessageModal' import { SignMessageModal } from './SignMessageModal' +import { web3HttpProviderOptions } from 'src/logic/wallets/getWeb3' import { useThirdPartyCookies } from '../hooks/useThirdPartyCookies' import { ThirdPartyCookiesWarning } from './ThirdPartyCookiesWarning' @@ -80,10 +80,6 @@ const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { params: undefined, } -const safeAppWeb3Provider = new Web3.providers.HttpProvider(getSafeAppsRpcServiceUrl(), { - timeout: 10_000, -}) - const URL_NOT_PROVIDED_ERROR = 'App url No provided or it is invalid.' const APP_LOAD_ERROR = 'There was an error loading the Safe App. There might be a problem with the App provider.' @@ -104,6 +100,12 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { const [, setAppLoadError] = useState(false) const { thirdPartyCookiesDisabled, setThirdPartyCookiesDisabled } = useThirdPartyCookies() + const safeAppsRpc = getSafeAppsRpcServiceUrl() + const safeAppWeb3Provider = useMemo( + () => new Web3.providers.HttpProvider(safeAppsRpc, web3HttpProviderOptions), + [safeAppsRpc], + ) + useEffect(() => { const clearTimeouts = () => { clearTimeout(timer.current) @@ -260,6 +262,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { chainId, chainName, shortName, + safeAppWeb3Provider, ]) const onUserTxConfirm = (safeTxHash: string, requestId: RequestId) => { diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index bd2215142d..daa0a50755 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -1,4 +1,4 @@ -import { sm } from 'src/theme/variables' +import { background, sm } from 'src/theme/variables' import { createStyles } from '@material-ui/core' export const styles = createStyles({ @@ -12,7 +12,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index bc27da9dc3..7210bdb252 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -1,6 +1,6 @@ import { createStyles, makeStyles } from '@material-ui/core' -import { lg, md } from 'src/theme/variables' +import { background, lg, md } from 'src/theme/variables' export const useStyles = makeStyles( createStyles({ @@ -9,7 +9,7 @@ export const useStyles = makeStyles( }, hide: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 267e3911aa..28b5138887 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -19,7 +19,6 @@ import { getSafeSDK } from 'src/logic/wallets/getWeb3' import { Errors, logError } from 'src/logic/exceptions/CodedException' import { currentSafeCurrentVersion } from 'src/logic/safe/store/selectors' import { currentChainId } from 'src/logic/config/store/selectors' -import { _getChainId } from 'src/config' export type OwnerValues = { ownerAddress: string @@ -43,7 +42,7 @@ export const sendAddOwner = async ( ) const txData = safeTx.data.data - const txHash = await dispatch( + await dispatch( createTransaction({ safeAddress, to: safeAddress, @@ -56,14 +55,6 @@ export const sendAddOwner = async ( delayExecution, }), ) - - if (txHash) { - dispatch( - addressBookAddOrUpdate( - makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName, chainId: _getChainId() }), - ), - ) - } } type Props = { diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index f3dedb7533..2bc187281a 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -43,7 +43,7 @@ export const sendReplaceOwner = async ( ) const txData = safeTx.data.data - const txHash = await dispatch( + await dispatch( createTransaction({ safeAddress, to: safeAddress, @@ -56,11 +56,6 @@ export const sendReplaceOwner = async ( delayExecution, }), ) - - if (txHash) { - // update the AB - dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...newOwner, chainId: _getChainId() }))) - } } type ReplaceOwnerProps = { diff --git a/src/routes/safe/components/Settings/ManageOwners/style.ts b/src/routes/safe/components/Settings/ManageOwners/style.ts index 1184330b26..4f113ec434 100644 --- a/src/routes/safe/components/Settings/ManageOwners/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/style.ts @@ -1,6 +1,6 @@ import { createStyles, makeStyles } from '@material-ui/core' -import { lg, sm } from 'src/theme/variables' +import { background, lg, sm } from 'src/theme/variables' export const useStyles = makeStyles( createStyles({ @@ -16,7 +16,7 @@ export const useStyles = makeStyles( }, hide: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx index c2a323c855..dc4cf56090 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx @@ -11,24 +11,57 @@ import Row from 'src/components/layout/Row' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import removeSafe from 'src/logic/safe/store/actions/removeSafe' -import { getExplorerInfo } from 'src/config' +import { getChainById, getExplorerInfo } from 'src/config' import Col from 'src/components/layout/Col' -import { WELCOME_ROUTE, history } from 'src/routes/routes' +import { WELCOME_ROUTE, history, SafeRouteParams, generateSafeRoute, SAFE_ROUTES } from 'src/routes/routes' +import useLocalSafes, { LocalSafes } from 'src/logic/safe/hooks/useLocalSafes' +import { currentChainId } from 'src/logic/config/store/selectors' +import { useMemo } from 'react' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' type RemoveSafeModalProps = { isOpen: boolean onClose: () => void } +function getNextAvailableSafe(currentChainId: string, currentSafeAddress: string, localSafes: LocalSafes) { + const availableSafes = Object.values(localSafes) + .flat() + .filter((safe) => safe.address !== currentSafeAddress) + const sameNetworkSafes = availableSafes.filter((safe) => safe.chainId === currentChainId) + + if (sameNetworkSafes.length > 0) { + return sameNetworkSafes[0] + } +} + +function getDestinationRoute(nextAvailableSafe: SafeRecordProps | undefined) { + if (!nextAvailableSafe || !nextAvailableSafe.chainId) return WELCOME_ROUTE + + const { shortName } = getChainById(nextAvailableSafe.chainId) + const routesSlug: SafeRouteParams = { + shortName, + safeAddress: nextAvailableSafe.address, + } + return generateSafeRoute(SAFE_ROUTES.ASSETS_BALANCES, routesSlug) +} + const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => { const classes = useStyles() const { address: safeAddress, name: safeName } = useSelector(currentSafeWithNames) + const curChainId = useSelector(currentChainId) + const localSafes = useLocalSafes() + const nextAvailableSafe = useMemo( + () => getNextAvailableSafe(curChainId, safeAddress, localSafes), + [curChainId, safeAddress, localSafes], + ) const dispatch = useDispatch() const onRemoveSafeHandler = async () => { + const destination = getDestinationRoute(nextAvailableSafe) dispatch(removeSafe(safeAddress)) onClose() - history.push(WELCOME_ROUTE) + history.push(destination) } return ( diff --git a/src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg b/src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg index afe91aa360..d4411010c0 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg +++ b/src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg @@ -1,7 +1,7 @@ - + diff --git a/src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg b/src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg index f76cf0507d..951629ab84 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg +++ b/src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg @@ -1,7 +1,7 @@ - + diff --git a/src/routes/safe/components/Settings/SpendingLimit/assets/time.svg b/src/routes/safe/components/Settings/SpendingLimit/assets/time.svg index 29a49f6fc7..b9a82a9387 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/assets/time.svg +++ b/src/routes/safe/components/Settings/SpendingLimit/assets/time.svg @@ -1,7 +1,7 @@ - + diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 2a866b9ca2..c92d353cf0 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -21,7 +21,7 @@ export const useStyles = makeStyles( }, hide: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 08ab393154..bef60cb3b5 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -2,7 +2,7 @@ import { Breadcrumb, BreadcrumbElement, Loader, Icon, Menu } from '@gnosis.pm/sa import { makeStyles } from '@material-ui/core/styles' import { useState, lazy } from 'react' import { useSelector } from 'react-redux' -import { Route, Switch, useRouteMatch } from 'react-router-dom' +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' import { LoadingContainer } from 'src/components/LoaderContainer' import { styles } from './style' @@ -118,6 +118,7 @@ const Settings = (): React.ReactElement => { } /> } /> } /> + diff --git a/src/routes/safe/components/Transactions/TxList/QueueTxList.tsx b/src/routes/safe/components/Transactions/TxList/QueueTxList.tsx index fa90308ad0..97842fc433 100644 --- a/src/routes/safe/components/Transactions/TxList/QueueTxList.tsx +++ b/src/routes/safe/components/Transactions/TxList/QueueTxList.tsx @@ -1,5 +1,5 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' -import { Fragment, ReactElement, useContext } from 'react' +import { Fragment, ReactElement, useContext, useState } from 'react' import { useSelector } from 'react-redux' import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' @@ -59,24 +59,33 @@ type QueueTransactionProps = { transactions: Transaction[] } -const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => - transactions.length > 1 ? ( - +const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => { + const [nrChildrenExpanded, setNrChildrenExpanded] = useState(0) + + const handleChildExpand = (expand: number) => { + setNrChildrenExpanded((val) => val + expand) + } + + if (transactions.length === 1) { + return + } + + return ( + {transactions.map((transaction, index) => ( - + ))} - ) : ( - ) +} type QueueTxListProps = { transactions: TransactionDetails['transactions'] diff --git a/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx b/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx index 16a6a21d30..739f7d4865 100644 --- a/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx @@ -17,9 +17,10 @@ import { isTxPending, pendingTxByChain } from 'src/logic/safe/store/selectors/pe type TxQueueRowProps = { isGrouped?: boolean transaction: Transaction + onChildExpand?: (isExpanded: number) => void } -export const TxQueueRow = ({ isGrouped = false, transaction }: TxQueueRowProps): ReactElement => { +export const TxQueueRow = ({ isGrouped = false, transaction, onChildExpand }: TxQueueRowProps): ReactElement => { const { activeHover } = useContext(TxHoverContext) const [tx, setTx] = useState(transaction) const willBeReplaced = tx.txStatus === LocalTransactionStatus.WILL_BE_REPLACED ? ' will-be-replaced' : '' @@ -46,6 +47,7 @@ export const TxQueueRow = ({ isGrouped = false, transaction }: TxQueueRowProps): appear: true, }} className={willBeReplaced} + onChange={(_, expanded) => onChildExpand?.(expanded ? 1 : -1)} > diff --git a/src/routes/safe/components/Transactions/TxList/styled.tsx b/src/routes/safe/components/Transactions/TxList/styled.tsx index 65c3ec6519..1d62d91f6f 100644 --- a/src/routes/safe/components/Transactions/TxList/styled.tsx +++ b/src/routes/safe/components/Transactions/TxList/styled.tsx @@ -1,7 +1,7 @@ import { Text, Accordion, AccordionDetails, AccordionSummary, EthHashInfo } from '@gnosis.pm/safe-react-components' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' -import { lg, md, sm } from 'src/theme/variables' +import { grey400, lg, md, primary200, primary300, sm } from 'src/theme/variables' import styled, { css } from 'styled-components' import { isDeeplinkedTx } from './utils' @@ -25,12 +25,8 @@ export const ColumnDisplayAccordionDetails = styled(AccordionDetails)` export const NoPaddingAccordion = styled(Accordion).attrs((props) => isDeeplinkedTx() ? { expanded: true, ...props } : props, )` - &.MuiAccordion-root { - background-color: transparent; - - .MuiAccordionDetails-root { - padding: 0; - } + &.MuiAccordion-root .MuiAccordionDetails-root { + padding: 0; } ` @@ -82,38 +78,39 @@ export const SubTitle = styled(Text)` ` export const StyledTransactions = styled.div` - background-color: ${({ theme }) => theme.colors.white}; - border-radius: 8px; - box-shadow: #00000026 0 4px 12px 0; overflow: hidden; width: 100%; + display: flex; + flex-direction: column; + row-gap: 6px; + & > .MuiAccordion-root { + border: 2px solid ${grey400}; + border-radius: 8px; + &:first-child { - border-top: none; + border: 2px solid ${grey400}; } - &:last-child { - border-bottom: none; + & .MuiAccordionSummary-root.Mui-expanded, + & .MuiAccordionSummary-root:hover { + background-color: ${primary200}; } - &:last-of-type { - div { - row-gap: 0; - } + &.Mui-expanded { + border: 2px solid ${primary300}; } } ` -export const GroupedTransactionsCard = styled(StyledTransactions)` +export const GroupedTransactionsCard = styled(StyledTransactions)<{ expanded?: boolean }>` transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - background-color: transparent; - border-radius: 0; - box-shadow: none; + background-color: ${({ theme }) => theme.colors.white}}; - &:not(:last-child) { - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; - } + border: 2px solid ${({ expanded }) => (expanded ? `${primary300}` : `${grey400}`)}; + box-sizing: border-box; + border-radius: 8px; .MuiAccordion-root, .MuiAccordionSummary-root, @@ -126,17 +123,20 @@ export const GroupedTransactionsCard = styled(StyledTransactions)` } } + .disclaimer-container { + background-color: ${({ theme, expanded }) => (expanded ? `${primary200}` : theme.colors.inputField)}; + } + &:hover { - background-color: ${({ theme }) => theme.colors.background}; + background-color: ${primary200}; - .MuiAccordionDetails-root { - div[class^='tx-'] { - background-color: ${({ theme }) => theme.colors.background}; - } + .tx-data > div, + .tx-data ~ div > div { + background-color: ${primary200}; } .disclaimer-container { - background-color: ${({ theme }) => theme.colors.inputField}; + background-color: transparent; } } ` @@ -153,8 +153,13 @@ const gridColumns = { const willBeReplaced = css` .will-be-replaced { pointer-events: none; + } + + .will-be-replaced.tx-details-actions button, + .will-be-replaced img { filter: grayscale(1) opacity(0.8) !important; } + .will-be-replaced * { pointer-events: none; color: gray !important; @@ -276,10 +281,6 @@ export const GroupedTransactions = styled(StyledTransaction)` grid-column-end: span 6; grid-column-start: 2; - &:first-child { - border: 0; - } - &.Mui-expanded { justify-self: center; width: calc(100% - 32px); @@ -304,7 +305,7 @@ export const GroupedTransactions = styled(StyledTransaction)` ` export const DisclaimerContainer = styled(StyledTransaction)` - background-color: ${({ theme }) => theme.colors.inputField} !important; + background-color: ${({ theme }) => theme.colors.inputField}; border-radius: 4px; margin: 12px 8px 0 12px; padding: 8px 12px; @@ -320,7 +321,7 @@ export const DisclaimerContainer = styled(StyledTransaction)` } ` -export const TxDetailsContainer = styled.div<{ ownerRows?: number }>` +export const TxDetailsContainer = styled.div` ${willBeReplaced}; background-color: ${({ theme }) => theme.colors.separator} !important; diff --git a/src/routes/safe/components/Transactions/__tests__/TxModalWrapper.test.tsx b/src/routes/safe/components/Transactions/__tests__/TxModalWrapper.test.tsx new file mode 100644 index 0000000000..2db3346a77 --- /dev/null +++ b/src/routes/safe/components/Transactions/__tests__/TxModalWrapper.test.tsx @@ -0,0 +1,104 @@ +import { isApproveAndExecute, isMultisigCreation } from 'src/routes/safe/components/Transactions/helpers/TxModalWrapper' + +describe('isMultisigCreation', () => { + it(`should return true if there are no confirmations for the transaction and the transaction is not spendingLimit`, () => { + // given + const transactionConfirmations = 0 + const transactionType = '' + + // when + const result = isMultisigCreation(transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return false if there are no confirmations for the transaction and the transaction is spendingLimit`, () => { + // given + const transactionConfirmations = 0 + const transactionType = 'spendingLimit' + + // when + const result = isMultisigCreation(transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) + it(`should return false if there are confirmations for the transaction`, () => { + // given + const transactionConfirmations = 2 + const transactionType = '' + + // when + const result = isMultisigCreation(transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) +}) + +describe('isApproveAndExecute', () => { + const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B' + it(`should return true if there is only one confirmation left to reach the safe threshold and there is a preApproving account`, () => { + // given + const transactionConfirmations = 2 + const safeThreshold = 3 + const transactionType = '' + const preApprovingOwner = mockedEthAccount + + // when + const result = isApproveAndExecute(safeThreshold, transactionConfirmations, transactionType, preApprovingOwner) + + // then + expect(result).toBe(true) + }) + it(`should return false if there is only one confirmation left to reach the safe threshold and but there is no preApproving account`, () => { + // given + const transactionConfirmations = 2 + const safeThreshold = 3 + const transactionType = '' + + // when + const result = isApproveAndExecute(safeThreshold, transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) + it(`should return true if the transaction is spendingLimit and there is a preApproving account`, () => { + // given + const transactionConfirmations = 0 + const transactionType = 'spendingLimit' + const safeThreshold = 3 + const preApprovingOwner = mockedEthAccount + + // when + const result = isApproveAndExecute(safeThreshold, transactionConfirmations, transactionType, preApprovingOwner) + + // then + expect(result).toBe(true) + }) + it(`should return false if the transaction is spendingLimit and there is no preApproving account`, () => { + // given + const transactionConfirmations = 0 + const transactionType = 'spendingLimit' + const safeThreshold = 3 + const preApprovingOwner = mockedEthAccount + + // when + const result = isApproveAndExecute(safeThreshold, transactionConfirmations, transactionType, preApprovingOwner) + + // then + expect(result).toBe(true) + }) + it(`should return false if the are missing more than one confirmations to reach the safe threshold and the transaction is not spendingLimit`, () => { + // given + const transactionConfirmations = 0 + const transactionType = '' + const safeThreshold = 3 + + // when + const result = isApproveAndExecute(safeThreshold, transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) +}) diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx index 6d2f4c67db..2a615ac83f 100644 --- a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx @@ -23,7 +23,7 @@ import { import useSafeTxGas from 'src/routes/safe/components/Transactions/helpers/useSafeTxGas' import { isMaxFeeParam } from 'src/logic/safe/transactions/gas' import { extractSafeAddress } from 'src/routes/routes' -import useGetRecommendedNonce from 'src/logic/hooks/useGetRecommendedNonce' +import useRecommendedNonce from 'src/logic/hooks/useRecommendedNonce' import Paragraph from 'src/components/layout/Paragraph' const StyledDivider = styled(Divider)` @@ -103,7 +103,7 @@ export const EditTxParametersForm = ({ const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice, ethMaxPrioFee } = txParameters const showSafeTxGas = useSafeTxGas() const safeAddress = extractSafeAddress() - const recommendedNonce = useGetRecommendedNonce(safeAddress) + const recommendedNonce = useRecommendedNonce(safeAddress) const onSubmit = (values: TxParameters) => { onClose(values) diff --git a/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx b/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx index bb1a819854..07b781deaa 100644 --- a/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx +++ b/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx @@ -18,10 +18,13 @@ import useCanTxExecute from 'src/logic/hooks/useCanTxExecute' import { useSelector } from 'react-redux' import { grantedSelector } from 'src/routes/safe/container/selector' import { List } from 'immutable' -import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { providerSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' import { getNativeCurrency } from 'src/config' +import { useEstimateSafeTxGas } from 'src/logic/hooks/useEstimateSafeTxGas' +import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' +import { currentSafe } from 'src/logic/safe/store/selectors' type Props = { children: ReactNode @@ -45,6 +48,19 @@ type Props = { const Container = styled.div` padding: 0 ${lg} ${md}; ` +export const isApproveAndExecute = ( + threshold: number, + txConfirmations: number, + txType?: string, + preApprovingOwner?: string, +): boolean => { + if (txConfirmations === threshold) return false + if (!preApprovingOwner) return false + return txConfirmations + 1 === threshold || isSpendingLimit(txType) +} + +export const isMultisigCreation = (txConfirmations: number, txType?: string): boolean => + txConfirmations === 0 && !isSpendingLimit(txType) /** * Determines which fields are displayed in the TxEditableParameters @@ -75,13 +91,12 @@ export const TxModalWrapper = ({ onClose, submitText, isSubmitDisabled, - isRejectTx, + isRejectTx = false, }: Props): React.ReactElement => { - const [manualSafeTxGas, setManualSafeTxGas] = useState('0') - const [manualGasPrice, setManualGasPrice] = useState() - const [manualMaxPrioFee, setManualMaxPrioFee] = useState() - const [manualGasLimit, setManualGasLimit] = useState() - const [manualSafeNonce, setManualSafeNonce] = useState() + const [manualSafeTxGas, setManualSafeTxGas] = useState('0') + const [manualGasPrice, setManualGasPrice] = useState() + const [manualMaxPrioFee, setManualMaxPrioFee] = useState() + const [manualGasLimit, setManualGasLimit] = useState() const [executionApproved, setExecutionApproved] = useState(true) const isOwner = useSelector(grantedSelector) const userAddress = useSelector(userAccountSelector) @@ -91,34 +106,40 @@ export const TxModalWrapper = ({ const confirmationsLen = Array.from(txConfirmations || []).length const canTxExecute = useCanTxExecute(preApprovingOwner, confirmationsLen, txThreshold, txNonce) const doExecute = executionApproved && canTxExecute + const showCheckbox = !isSpendingLimitTx && canTxExecute && (!txThreshold || txThreshold > confirmationsLen) const nativeCurrency = getNativeCurrency() + const { currentVersion: safeVersion, threshold } = useSelector(currentSafe) ?? {} + const { smartContractWallet } = useSelector(providerSelector) + const isCreation = isMultisigCreation(confirmationsLen, txType) + const isOffChainSignature = checkIfOffChainSignatureIsPossible(doExecute, smartContractWallet, safeVersion) + + const approvalAndExecution = isApproveAndExecute(Number(threshold), confirmationsLen, txType, preApprovingOwner) - const { - gasCostFormatted, - gasPriceFormatted, - gasMaxPrioFeeFormatted, - gasLimit, - gasEstimation, - txEstimationExecutionStatus, + const safeTxGasEstimation = useEstimateSafeTxGas({ isCreation, - isOffChainSignature, - } = useEstimateTransactionGas({ + isRejectTx, txData, txRecipient: txTo || safeAddress, - txType, - txConfirmations, txAmount: txValue, - preApprovingOwner, - safeTxGas: safeTxGas || manualSafeTxGas, - manualGasPrice, - manualMaxPrioFee, - manualGasLimit, - manualSafeNonce, operation, }) + const { gasCostFormatted, gasPriceFormatted, gasMaxPrioFeeFormatted, gasLimit, txEstimationExecutionStatus } = + useEstimateTransactionGas({ + txData, + txRecipient: txTo || safeAddress, + txConfirmations, + txAmount: txValue, + safeTxGas: safeTxGas || manualSafeTxGas, + manualGasPrice, + manualMaxPrioFee, + manualGasLimit, + operation, + isExecution: doExecute, + approvalAndExecution, + }) + const [submitStatus, setSubmitStatus] = useEstimationStatus(txEstimationExecutionStatus) - const showCheckbox = !isSpendingLimitTx && canTxExecute && (!txThreshold || txThreshold > confirmationsLen) const onEditClose = (txParameters: TxParameters) => { const oldGasPrice = gasPriceFormatted @@ -127,9 +148,8 @@ export const TxModalWrapper = ({ const newGasLimit = txParameters.ethGasLimit const oldMaxPrioFee = gasMaxPrioFeeFormatted const newMaxPrioFee = txParameters.ethMaxPrioFee - const oldSafeTxGas = gasEstimation + const oldSafeTxGas = safeTxGasEstimation const newSafeTxGas = txParameters.safeTxGas - const newSafeNonce = txParameters.safeNonce if (oldGasPrice !== newGasPrice) { setManualGasPrice(newGasPrice) @@ -146,11 +166,6 @@ export const TxModalWrapper = ({ if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { setManualSafeTxGas(newSafeTxGas) } - - if (newSafeNonce) { - const newSafeNonceNumber = parseInt(newSafeNonce, 10) - setManualSafeNonce(newSafeNonceNumber) - } } const onSubmitClick = (txParameters: TxParameters) => { @@ -175,7 +190,7 @@ export const TxModalWrapper = ({ ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} ethMaxPrioFee={gasMaxPrioFeeFormatted} - safeTxGas={gasEstimation} + safeTxGas={safeTxGasEstimation} safeNonce={txNonce} parametersStatus={parametersStatus} closeEditModalCallback={onEditClose} @@ -185,7 +200,7 @@ export const TxModalWrapper = ({ {children} - {showCheckbox && } + {showCheckbox && } {!isSpendingLimitTx && doExecute && ( diff --git a/src/store/index.ts b/src/store/index.ts index 4cdd7085d4..ae186e1843 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,8 +26,8 @@ import { PENDING_TRANSACTIONS_ID, } from 'src/logic/safe/store/reducer/pendingTransactions' import tokensReducer, { TokenState, TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' -import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' -import providerReducer, { ProviderState, PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider' +import providerMiddleware from 'src/logic/wallets/store/middleware' +import providerReducer, { ProvidersState, PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer' import notificationsMiddleware from 'src/logic/safe/store/middleware/notificationsMiddleware' import { safeStorageMiddleware } from 'src/logic/safe/store/middleware/safeStorage' import safeReducer, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' @@ -72,7 +72,7 @@ const enhancer = composeEnhancers( save(LS_CONFIG), notificationsMiddleware, safeStorageMiddleware, - providerWatcher, + providerMiddleware, addressBookMiddleware, configMiddleware, gatewayTransactionsMiddleware, @@ -103,7 +103,7 @@ const rootReducer = combineReducers(reducers) // ReturnType // or https://dev.to/svehla/typescript-100-type-safe-react-redux-under-20-lines-4h8n export type AppReduxState = CombinedState<{ - [PROVIDER_REDUCER_ID]: ProviderState + [PROVIDER_REDUCER_ID]: ProvidersState [SAFE_REDUCER_ID]: SafeReducerMap [NFT_ASSETS_REDUCER_ID]: NFTAssets [NFT_TOKENS_REDUCER_ID]: NFTTokens diff --git a/src/theme/mui.ts b/src/theme/mui.ts index d64e4a05d0..db4472ea0d 100644 --- a/src/theme/mui.ts +++ b/src/theme/mui.ts @@ -2,6 +2,8 @@ import { createTheme } from '@material-ui/core/styles' import { alpha } from '@material-ui/core/styles/colorManipulator' import { + alertWarning, + background, boldFont, bolderFont, border, @@ -24,7 +26,6 @@ import { sm, smallFontSize, xs, - alertWarning, } from './variables' const palette = { @@ -492,7 +493,7 @@ export const DropdownListTheme = { }, button: { '&:hover': { - backgroundColor: '#f7f5f5', + backgroundColor: `${background}`, }, }, }, diff --git a/src/theme/variables.js b/src/theme/variables.js index 378f9227f2..5593724ea0 100644 --- a/src/theme/variables.js +++ b/src/theme/variables.js @@ -1,4 +1,3 @@ -const background = '#f7f5f5' const border = '#e8e7e6' const connectedColor = '#008C73' const disabled = '#5D6D74' @@ -10,6 +9,8 @@ const lg = '24px' const marginButtonImg = '12px' const md = '16px' const primary = '#001428' +const primaryLite = '#EFFAF8' +const primaryActive = '#008C73' const secondary = '#008C73' const secondaryTextOrSvg = '#B2B5B2' const secondaryBackground = '#f0efee' @@ -20,13 +21,11 @@ const xl = '32px' const xs = '4px' const xxl = '40px' -const grey500 = '#E2E3E3' -const black400 = '#566976' -const black600 = '#111B22' - module.exports = { - background, + background: '#F6F7F8', black300: '#B2BBC0', + black400: '#566976', + black600: '#111B22', boldFont: 700, bolderFont: 500, border, @@ -43,7 +42,8 @@ module.exports = { fontSizeHeadingMd: 20, fontSizeHeadingSm: 16, fontSizeHeadingXs: 13, - gray500: '#e2e3e3', + grey400: '#EEEFF0', + gray500: '#E2E3E3', headerHeight, largeFontSize: '16px', lg, @@ -54,7 +54,11 @@ module.exports = { mediumFontSize: '14px', orange500: '#e8663d', primary, - primary400: '#008C73', + primaryLite, + primaryActive, + primary200: primaryLite, + primary300: '#92C9BE', + primary400: primaryActive, regularFont: 400, red400: '#C31717', screenLg: 1200, @@ -76,7 +80,4 @@ module.exports = { xs, xxl, xxlFontSize: '32px', - grey500, - black400, - black600, } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 9e5ac62c17..743afa9cf1 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -1,4 +1,4 @@ -$background: #f7f5f5; +$background: #F6F7F8; $border: #e8e7e6; $connectedColor: #008C73; $disabled: #5D6D74; diff --git a/src/utils/storage/Storage.ts b/src/utils/storage/Storage.ts index 3311282496..b08f44dcb7 100644 --- a/src/utils/storage/Storage.ts +++ b/src/utils/storage/Storage.ts @@ -3,6 +3,11 @@ import { LS_NAMESPACE, LS_SEPARATOR } from '../constants' type BrowserStorage = typeof localStorage | typeof sessionStorage +type ItemWithExpiry = { + value: T + expiry: number +} + const DEFAULT_PREFIX = `${LS_NAMESPACE}${LS_SEPARATOR}` class Storage { @@ -53,6 +58,27 @@ class Storage { logError(Errors._702, `key ${key} – ${err.message}`) } } + + public setWithExpiry = (key: string, item: T, expiry: number): void => { + this.setItem>(key, { + value: item, + expiry: new Date().getTime() + expiry, + }) + } + + public getWithExpiry = (key: string): T | undefined => { + const item = this.getItem>(key) + if (!item) { + return + } + + if (new Date().getTime() > item.expiry) { + this.removeItem(key) + return + } + + return item.value + } } export default Storage diff --git a/src/utils/storage/index.ts b/src/utils/storage/index.ts index 02a26c65cd..4afc9be046 100644 --- a/src/utils/storage/index.ts +++ b/src/utils/storage/index.ts @@ -35,3 +35,11 @@ export const saveToStorage = (key: string, value: T): void => { export const removeFromStorage = (key: string): void => { storage.removeItem(`${getStoragePrefix()}${key}`) } + +export const saveToStorageWithExpiry = (key: string, value: T, expiry: number): void => { + storage.setWithExpiry(key, value, expiry) +} + +export const loadFromStorageWithExpiry = (key: string): T | undefined => { + return storage.getWithExpiry(key) +} diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx index ef52428962..e5eefec9f5 100644 --- a/src/utils/test-utils.tsx +++ b/src/utils/test-utils.tsx @@ -6,7 +6,6 @@ import Providers from 'src/components/Providers' import { createPreloadedStore, store } from 'src/store' import { history } from 'src/routes/routes' import theme from 'src/theme/mui' -import { makeProvider } from 'src/logic/wallets/store/model/provider' import { SafeReducerMap } from 'src/logic/safe/store/reducer/types/safe' import makeSafe from 'src/logic/safe/store/models/safe' @@ -14,7 +13,6 @@ import makeSafe from 'src/logic/safe/store/models/safe' function renderWithProviders(Components: ReactElement, customState?: any): RenderResult { const customStore = { ...customState, - providers: makeProvider(customState?.providers), safes: Map({ safes: Map(buildSafesState(customState?.safes?.safes)), latestMasterContractVersion: customState?.safes?.latestMasterContractVersion || '1.3.0', diff --git a/yarn.lock b/yarn.lock index 0c45b57591..f0318c5f46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1501,7 +1501,7 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.3.0", "@ethereumjs/common@^2.4.0", "@ethereumjs/common@^2.5.0": +"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.4.0", "@ethereumjs/common@^2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.5.0.tgz#ec61551b31bef7a69d1dc634d8932468866a4268" integrity sha512-DEHjW6e38o+JmB/NO3GZBpW4lpaiBpkFgXF6jLcJ6gETBYpEyaA5nTimsWBUJR3Vmtm/didUEbNjajskugZORg== @@ -1509,6 +1509,14 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.1" +"@ethereumjs/common@^2.6.1": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.2.tgz#eb006c9329c75c80f634f340dc1719a5258244df" + integrity sha512-vDwye5v0SVeuDky4MtKsu+ogkH2oFUV8pBKzH/eNBzT8oI91pKa8WyzDuYuxOQsgNgv5R34LfFDh2aaw3H4HbQ== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.1.4" + "@ethereumjs/tx@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.0.0.tgz#8dfd91ed6e91e63996e37b3ddc340821ebd48c81" @@ -1517,7 +1525,7 @@ "@ethereumjs/common" "^2.0.0" ethereumjs-util "^7.0.7" -"@ethereumjs/tx@^3.0.0", "@ethereumjs/tx@^3.2.1", "@ethereumjs/tx@^3.3.0": +"@ethereumjs/tx@^3.0.0", "@ethereumjs/tx@^3.3.0": version "3.3.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.2.tgz#348d4624bf248aaab6c44fec2ae67265efe3db00" integrity sha512-6AaJhwg4ucmwTvw/1qLaZUX5miWrwZ4nLOUsKyb/HtzS3BMw/CasKhdi1ims9mBKeK9sOJCH4qGKOBGyJCeeog== @@ -1525,6 +1533,14 @@ "@ethereumjs/common" "^2.5.0" ethereumjs-util "^7.1.2" +"@ethereumjs/tx@^3.3.2": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.0.tgz#783b0aeb08518b9991b23f5155763bbaf930a037" + integrity sha512-/+ZNbnJhQhXC83Xuvy6I9k4jT5sXiV0tMR9C+AzSSpcCV64+NB8dTE1m3x98RYMqb8+TLYWA+HML4F5lfXTlJw== + dependencies: + "@ethereumjs/common" "^2.6.1" + ethereumjs-util "^7.1.4" + "@ethersproject/abi@5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.7.tgz#79e52452bd3ca2956d0e1c964207a58ad1a0ee7b" @@ -1628,7 +1644,7 @@ dependencies: "@ethersproject/bignumber" "^5.5.0" -"@ethersproject/contracts@5.5.0": +"@ethersproject/contracts@5.5.0", "@ethersproject/contracts@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197" integrity sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg== @@ -1792,7 +1808,7 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.5.0": +"@ethersproject/solidity@5.5.0", "@ethersproject/solidity@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f" integrity sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw== @@ -1885,12 +1901,12 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@gnosis.pm/safe-apps-provider@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-provider/-/safe-apps-provider-0.5.0.tgz#e0121553ef22c1458eb95cf0afed14e8c2570ae3" - integrity sha512-c4OuKV+cIW2aDmv0DZfLOelmyNNZz5Dr3OG5TvnCfmYhZtHyOd1x6bd2xnROCuiZU+QAUGJsm65mBe6iy8NAVQ== +"@gnosis.pm/safe-apps-provider@^0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-provider/-/safe-apps-provider-0.9.3.tgz#d8913b0f8abc15fdca229571eefc5f9385c82ea7" + integrity sha512-WzsfEMrOTd7/epEKs7S0QBB+sgw25d1B4SeLCD7q9RYi0vYLaeWT3jTuVXVGqwAlT3tFyedmvXnryLV5SUwiug== dependencies: - "@gnosis.pm/safe-apps-sdk" "3.0.0" + "@gnosis.pm/safe-apps-sdk" "6.2.0" events "^3.3.0" "@gnosis.pm/safe-apps-sdk-v1@npm:@gnosis.pm/safe-apps-sdk@0.4.2": @@ -1898,11 +1914,6 @@ resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-0.4.2.tgz#ae87b2164931c006cb0efdede3d82ff210df1648" integrity sha512-BwA2dyCebPMdi4JhhTkp6EjkhEM6vAIviKdhqHiHnSmL+sDfxtP1jdOuE8ME2/4+5TiLSS8k8qscYjLSlf1LLw== -"@gnosis.pm/safe-apps-sdk@3.0.0", "@gnosis.pm/safe-apps-sdk@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-3.0.0.tgz#0f90185c3693f2683322d275e796e61ff99ce87d" - integrity sha512-dLCSlniYnxEqCglx4XdhByvi7KKuSYRWJKm1lVXAc4oJqwwVkoCwp0bFIejLZ/dnf7cQSBUUVsTGWhvSda511w== - "@gnosis.pm/safe-apps-sdk@6.2.0": version "6.2.0" resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-6.2.0.tgz#05751b4ae4c6cfa7e19839d3655e7d9b5fb72dfe" @@ -1911,25 +1922,35 @@ "@gnosis.pm/safe-react-gateway-sdk" "^2.5.6" ethers "^5.4.7" -"@gnosis.pm/safe-core-sdk-types@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-core-sdk-types/-/safe-core-sdk-types-0.1.1.tgz#908c394cb4660493b4e9c8e01b5a7aa36efafd30" - integrity sha512-PghXGDaI5Foq37nZGmI90U2OKMeGtxh5KqkDqou9aFHwGVa/nf9HRQPxG9/XUzcyfe9OlKttDlJnR3XnC3dSDw== +"@gnosis.pm/safe-apps-sdk@^6.2.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-6.3.0.tgz#19f8bff136bdfdf9003745e4202e1cb85322e493" + integrity sha512-atUiUj1JEGnZwxDrKbuxfkwPsNQtoxnQqNjvB9cVODxSdR9OiLy5XdW2wz3Y/Gq+sjWc6lAUy3M5ovTY7qmbrg== + dependencies: + "@gnosis.pm/safe-react-gateway-sdk" "^2.8.5" + ethers "^5.4.7" -"@gnosis.pm/safe-core-sdk@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-core-sdk/-/safe-core-sdk-1.3.0.tgz#ba21c6163c6a06e3fc51f5c7dac650e46fe8a779" - integrity sha512-laKkyJUv0llPPG5ep2+18v/anIEGi+KjarNoeVAutYzIeAAkSvvVbY8qyfczh5bWQkqvQ3K1l/QLUJ8Kx4hm3g== +"@gnosis.pm/safe-core-sdk-types@1.0.0", "@gnosis.pm/safe-core-sdk-types@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-core-sdk-types/-/safe-core-sdk-types-1.0.0.tgz#8f8f4d262ac8ff0f5b3b5f4dd1198974c80762b3" + integrity sha512-/MiMfXth/5rnCNvBWPmVYXP66/UZqHiwfOc3BwERVRTvUnF95jD4DOT/K/EjCW0oH9PhwXkFs2mXraKkGQL77Q== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/contracts" "^5.5.0" + "@gnosis.pm/safe-deployments" "^1.8.0" + web3-core "^1.7.0" + +"@gnosis.pm/safe-core-sdk@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-core-sdk/-/safe-core-sdk-2.0.0.tgz#9c64dd4e5647e845eff6a5a01bd16e948c1afefd" + integrity sha512-fAUxRrQ/pyTV80S9e5DMCWIGLDDvUsdBzPUZupqe+Uc0FwnbJhMpCofzoHkTEONhnJQJCU3AzPcOBEkmtw8voQ== dependencies: - "@gnosis.pm/safe-core-sdk-types" "^0.1.1" - "@gnosis.pm/safe-deployments" "^1.7.0" + "@ethersproject/solidity" "^5.5.0" + "@gnosis.pm/safe-core-sdk-types" "^1.0.0" + "@gnosis.pm/safe-deployments" "^1.8.0" ethereumjs-util "^7.1.3" semver "^7.3.5" - -"@gnosis.pm/safe-deployments@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-deployments/-/safe-deployments-1.7.0.tgz#dd5fde901595545131d447888e327cc72e41d370" - integrity sha512-OWc73XDBOJzoDk7GAQsasOxHnXKdJdOb28Jkv+JNie2LlRwZe1SMCu0iTozhwpLFp4BC+iDtuVWN7EVpe142ag== + web3-utils "^1.7.0" "@gnosis.pm/safe-deployments@^1.8.0": version "1.8.0" @@ -1958,6 +1979,20 @@ dependencies: isomorphic-unfetch "^3.1.0" +"@gnosis.pm/safe-react-gateway-sdk@^2.8.5": + version "2.8.5" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-gateway-sdk/-/safe-react-gateway-sdk-2.8.5.tgz#3b82b993ae64a5fde1a96a4b0c47a97514839055" + integrity sha512-lrZ3gXzbNzIjIzYDs21d7I3fwwHi01YFnD+2+5Kuy0+fJAiONYXih2Z9Q4h3RS/AgzTHfL+G7WB6XB0V8GMVCQ== + dependencies: + isomorphic-unfetch "^3.1.0" + +"@gnosis.pm/safe-web3-lib@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-web3-lib/-/safe-web3-lib-1.0.0.tgz#4189276d8f953768723b4fb2b653a17696a33530" + integrity sha512-WrC3329UlEr6jN3L6/xkwmGHkjCK1kj3paVnX7zJCocZcHQsoRWhctAr8YPZ/h07nIst2UUUkyIKo2lnaZhzHQ== + dependencies: + "@gnosis.pm/safe-core-sdk-types" "^1.0.0" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -4093,6 +4128,11 @@ dependencies: "@types/jest" "*" +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + "@types/uglify-js@*": version "3.13.1" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea" @@ -4275,35 +4315,35 @@ optionalDependencies: dotenv "^8.2.0" -"@walletconnect/browser-utils@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.6.6.tgz#a985b48c99c65a986a051d66a4910010a10a0c56" - integrity sha512-E29xSHU7Akd4jaPehWVGx7ct+SsUzZbxcGc0fz+Pw6/j4Gh5tlfYZ9XuVixuYI4WPdQ2CmOraj8RrVOu5vba4w== +"@walletconnect/browser-utils@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.7.1.tgz#2a28846cd4d73166debbbf7d470e78ba25616f5e" + integrity sha512-y6KvxPhi52sWzS0/HtA3EhdgmtG8mXcxdc26YURDOVC/BJh3MxV8E16JFrT4InylOqYJs6dcSLWVfcnJaiPtZw== dependencies: "@walletconnect/safe-json" "1.0.0" - "@walletconnect/types" "^1.6.6" + "@walletconnect/types" "^1.7.1" "@walletconnect/window-getters" "1.0.0" "@walletconnect/window-metadata" "1.0.0" detect-browser "5.2.0" -"@walletconnect/client@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.6.6.tgz#ec64575b245bfce25cc0d9150a3c2e919a8a2632" - integrity sha512-DDOrxagSmXCciIEr16hTf4gWZ7PG7GXribYTfOOsjtODLtPEODEEYj/AsmEALjh3ZBG4bN35Vj0F/ZA1D+90GQ== +"@walletconnect/client@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.7.1.tgz#aaa74199bdc0605db9ac2ecdf8a463b271586d3b" + integrity sha512-xD8B8s1hL7Z5vJwb3L0u1bCVAk6cRQfIY9ycymf7KkmIhkAONQJNf2Y0C0xIpbPp2fdn9VwnSfLm5Ed/Ht/1IA== dependencies: - "@walletconnect/core" "^1.6.6" - "@walletconnect/iso-crypto" "^1.6.6" - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" + "@walletconnect/core" "^1.7.1" + "@walletconnect/iso-crypto" "^1.7.1" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" -"@walletconnect/core@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.6.6.tgz#0a35a9b0f91da8958bec27be801a510818f4e142" - integrity sha512-pSftIVPY6mYz2koZPBEYmeFeAjVf2MSnRHOM6+vx+iAsUEcfMZHkgeXX6GtM6Fjza+zSZu1qnmdgURVXpmKwtQ== +"@walletconnect/core@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.7.1.tgz#321c14d63af81241658b028022e0e5fa6dc7f374" + integrity sha512-qO+4wykyRNiq3HEuaAA2pW2PDnMM4y7pyPAgiCwfHiqF4PpWvtcdB301hI0K5am9ghuqKZMy1HlE9LWNOEBvcw== dependencies: - "@walletconnect/socket-transport" "^1.6.6" - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" + "@walletconnect/socket-transport" "^1.7.1" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" "@walletconnect/crypto@^1.0.1": version "1.0.1" @@ -4329,24 +4369,24 @@ resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.0.tgz#c4545869fa9c389ec88c364e1a5f8178e8ab5034" integrity sha512-4BwqyWy6KpSvkocSaV7WR3BlZfrxLbJSLkg+j7Gl6pTDE+U55lLhJvQaMuDVazXYxcjBsG09k7UlH7cGiUI5vQ== -"@walletconnect/http-connection@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.6.6.tgz#d5030bf175f24e57901e5da3acff493a3e7df556" - integrity sha512-V0UEnvMQPYBpD+8LAbuxN+i0dWVVfZ8XtmJymsBh2KyHLgKyHSsT5RwSCst132JGDV4/JP4HrHCs5t8KqSfEPw== +"@walletconnect/http-connection@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.7.1.tgz#fddddccd70a5c659c6e6ac25ba5305290c158705" + integrity sha512-cz3pw2MsTyBT5hy8qhs67NFHTIFOzltdMx9Hy1ftkjXQYtenxIBzAQpZzF6l/lXC3GmMziueYnknZILo1+wgfg== dependencies: - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" eventemitter3 "4.0.7" xhr2-cookies "1.1.0" -"@walletconnect/iso-crypto@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.6.6.tgz#19848bdcd54e9945961bab8a996cbca8a00d7cf1" - integrity sha512-wRYgKvd8K3A9FVLn2c0cDh4+9OUHkqibKtwQJTJsz+ibPGgd+n5j1/FjnzDDRGb9T1+TtlwYF3ZswKyys3diVQ== +"@walletconnect/iso-crypto@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.7.1.tgz#c463bb5874686c2f21344e2c7f3cf4d71c34ca70" + integrity sha512-qMiW0kLN6KCjnLMD50ijIj1lQqjNjGszGUwrSVUiS2/Dp4Ecx+4QEtHbmVwGEkfx4kelYPFpDJV3ZJpQ4Kqg/g== dependencies: "@walletconnect/crypto" "^1.0.1" - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" "@walletconnect/jsonrpc-types@^1.0.0": version "1.0.0" @@ -4368,14 +4408,14 @@ resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.4.0.tgz#502cf8ab87330841d794819081e748ebdef7aee5" integrity sha512-ZtKRio4uCZ1JUF7LIdecmZt7FOLnX72RPSY7aUVu7mj7CSfxDwUn6gBuK6WGtH+NZCldBqDl5DenI5fFSvkKYw== -"@walletconnect/qrcode-modal@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.6.6.tgz#d95d790a4f0eaee4e8bb024b92ed111a0ee716bd" - integrity sha512-wZorjpOIm6OhXKNvyH1YtpxfCUVcnuJxS8YbUeKWckGjS3tDPqUTbXWPlzFdMpNBrpY3j0B2XjLgVVQ2aUDX0w== +"@walletconnect/qrcode-modal@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.7.1.tgz#89b19c2eb6466ec237ccd597388d7a1b1b946067" + integrity sha512-m/4lSx3pgj8V2eHVJcGnxBKUSCNFtyVIcg5tqbSJHi9HjKIBxvRq4D5M4X4yEpgXYtRmTucihxNCrj2zQrmlSQ== dependencies: - "@walletconnect/browser-utils" "^1.6.6" + "@walletconnect/browser-utils" "^1.7.1" "@walletconnect/mobile-registry" "^1.4.0" - "@walletconnect/types" "^1.6.6" + "@walletconnect/types" "^1.7.1" copy-to-clipboard "^3.3.1" preact "10.4.1" qrcode "1.4.4" @@ -4394,43 +4434,43 @@ resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.0.tgz#12eeb11d43795199c045fafde97e3c91646683b2" integrity sha512-QJzp/S/86sUAgWY6eh5MKYmSfZaRpIlmCJdi5uG4DJlKkZrHEF7ye7gA+VtbVzvTtpM/gRwO2plQuiooIeXjfg== -"@walletconnect/socket-transport@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.6.6.tgz#b80974fe3e2a2f93ba1f6b40df5a0ea492b94086" - integrity sha512-mugCEoeKTx75ogb5ROg/+LA3yGTsuRNcrYgrApceo7WNU9Z4dG8l6ycMPqrrFcODcrasq3NmXVWUYDv/CvrzSw== +"@walletconnect/socket-transport@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.7.1.tgz#cc4c8dcf21c40b805812ecb066b2abb156fdb146" + integrity sha512-Gu1RPro0eLe+HHtLhq/1T5TNFfO/HW2z3BnWuUYuJ/F8w1U9iK7+4LMHe+LTgwgWy9Ybcb2k0tiO5e3LgjHBHQ== dependencies: - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" ws "7.5.3" -"@walletconnect/types@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.6.6.tgz#8d644e2a390e494e40424c60272e91b4820bf0d4" - integrity sha512-op77cxexOmQQN36XB1sYouNTlBRV0Rup/2NYK8A1ffdwXa3a6HLHHdhBM7I/I9BVmRXoZ4+XoOnPKGGrYtlS3g== +"@walletconnect/types@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.7.1.tgz#86cc3832e02415dc9f518f3dcb5366722afbfc03" + integrity sha512-X0NunEUgq46ExDcKo7BnnFpFhuZ89bZ04/1FtohNziBWcP2Mblp2yf+FN7iwmZiuZ3bRTb8J1O4oJH2JGP9I7A== -"@walletconnect/utils@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.6.6.tgz#e8e49a5f2c35e4a5f9153b09ad076655f38d8c96" - integrity sha512-s2X/cVXiMDSEoWV6i7HPMbP1obXlzP7KLMrBo9OMabiJKnQEh6HSZ39WLswB2PHnl8Hp1Sr4BdRvhM5kCcYWRw== +"@walletconnect/utils@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.7.1.tgz#f858d5f22425a4c2da2a28ae493bde7f2eecf815" + integrity sha512-7Lig9rruqTMaFuwEhBrArq1QgzIf2NuzO6J3sCUYCZh60EQ7uIZjekaDonQjiQJAbfYcgWUBm8qa0PG1TzYN3Q== dependencies: - "@walletconnect/browser-utils" "^1.6.6" + "@walletconnect/browser-utils" "^1.7.1" "@walletconnect/encoding" "^1.0.0" "@walletconnect/jsonrpc-utils" "^1.0.0" - "@walletconnect/types" "^1.6.6" + "@walletconnect/types" "^1.7.1" bn.js "4.11.8" js-sha3 "0.8.0" query-string "6.13.5" -"@walletconnect/web3-provider@^1.6.2": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.6.6.tgz#7be7b6d6230d6925f8728cdddc226ef24119e602" - integrity sha512-8z4r9JCE0lKuZmVCPSdYnX114ckQ+oMfr9D8osRBtdyhvN9elwITMloUJfACDRelcuet94yEbXuDobQeBDDkkw== - dependencies: - "@walletconnect/client" "^1.6.6" - "@walletconnect/http-connection" "^1.6.6" - "@walletconnect/qrcode-modal" "^1.6.6" - "@walletconnect/types" "^1.6.6" - "@walletconnect/utils" "^1.6.6" +"@walletconnect/web3-provider@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.7.1.tgz#3b7bf41bfd0198b18f5cc5626e1ec28e931667c7" + integrity sha512-dhoYwQaBVbaKIiELNeCF4kW7Dslbf73wDIsxOF9gmjVch1Qi18kNlqbR03u56iBcAsXU0tAwfd9Z7cGHfUX1Fg== + dependencies: + "@walletconnect/client" "^1.7.1" + "@walletconnect/http-connection" "^1.7.1" + "@walletconnect/qrcode-modal" "^1.7.1" + "@walletconnect/types" "^1.7.1" + "@walletconnect/utils" "^1.7.1" web3-provider-engine "16.0.1" "@walletconnect/window-getters@1.0.0", "@walletconnect/window-getters@^1.0.0": @@ -5995,17 +6035,17 @@ bnb-javascript-sdk-nobroadcast@^2.16.14: uuid "^3.3.2" websocket-stream "^5.5.0" -bnc-onboard@~1.35.3: - version "1.35.3" - resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.35.3.tgz#f6e92e4dec1b35bbc808e57032d76c677d7f1ff8" - integrity sha512-9g3Lgfo5Q4VwKdoIDw4Zf+nVMCa16lReb0f+iSmH6CrdiDrN5rRTGMy8qEOxQ/vSNuyHMeL9lVARpLA4s7X5xQ== +bnc-onboard@^1.37.3: + version "1.37.3" + resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.37.3.tgz#e4c3ae94ab44b3053a6dea07057447feb9b9da14" + integrity sha512-G5pHatWvJf/r7gB7A38p7BlQfbzNJhND2nr0tPKqGtpXucTZJ0cl0bPvrh2nRNMs22ajd2wG29N88bWrWU5Vcw== dependencies: "@cvbb/eth-keyring" "^1.1.0" "@ensdomains/ensjs" "^2.0.1" "@ethereumjs/common" "^2.0.0" "@ethereumjs/tx" "^3.0.0" - "@gnosis.pm/safe-apps-provider" "^0.5.0" - "@gnosis.pm/safe-apps-sdk" "^3.0.0" + "@gnosis.pm/safe-apps-provider" "^0.9.3" + "@gnosis.pm/safe-apps-sdk" "^6.2.0" "@keystonehq/eth-keyring" "0.9.0" "@ledgerhq/hw-app-eth" "6.8.1" "@ledgerhq/hw-transport-u2f" "^5.21.0" @@ -6015,7 +6055,7 @@ bnc-onboard@~1.35.3: "@shapeshiftoss/hdwallet-keepkey" "^1.15.2" "@shapeshiftoss/hdwallet-keepkey-webusb" "^1.15.2" "@toruslabs/torus-embed" "^1.10.11" - "@walletconnect/web3-provider" "^1.6.2" + "@walletconnect/web3-provider" "^1.7.1" authereum "^0.1.12" bignumber.js "^9.0.0" bnc-sdk "^3.4.1" @@ -6029,7 +6069,7 @@ bnc-onboard@~1.35.3: hdkey "^2.0.1" regenerator-runtime "^0.13.7" trezor-connect "^8.1.9" - walletlink "^2.1.11" + walletlink "^2.4.6" web3-provider-engine "^15.0.4" bnc-sdk@^3.4.1: @@ -6321,7 +6361,7 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@6.0.3, buffer@^6.0.3: +buffer@6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -9340,6 +9380,17 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.3, ethereumjs-util@^7.0.7, ethereu ethereum-cryptography "^0.1.3" rlp "^2.2.4" +ethereumjs-util@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458" + integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A== + dependencies: + "@types/bn.js" "^5.1.0" + bn.js "^5.1.2" + create-hash "^1.1.2" + ethereum-cryptography "^0.1.3" + rlp "^2.2.4" + ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz#76243ed8de031b408793ac33907fb3407fe400c6" @@ -19322,6 +19373,11 @@ ua-parser-js@^0.7.24: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b" integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg== +ua-parser-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + uglify-js@^2.8.29: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -19504,9 +19560,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" - integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + version "1.5.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a" + integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -19602,7 +19658,7 @@ util@^0.11.0: dependencies: inherits "2.0.3" -util@^0.12.0, util@^0.12.4: +util@^0.12.0: version "0.12.4" resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw== @@ -19763,15 +19819,14 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" -walletlink@^2.1.11: - version "2.2.6" - resolved "https://registry.yarnpkg.com/walletlink/-/walletlink-2.2.6.tgz#cfea3ba94e5ea33e87b0a2f31151a77ee1a59d72" - integrity sha512-4TF1kkpo9aq1QlfKv6jTCEsV8Rc+1RIuXn2EtsTJt9/H02fG3oy7k49sqB4gXZ9CWN48yoXnmSwq1GdkvfYGjw== +walletlink@^2.4.6: + version "2.4.7" + resolved "https://registry.yarnpkg.com/walletlink/-/walletlink-2.4.7.tgz#3dd034f7cd6e9d9f4cc1d677bb951869dc743e20" + integrity sha512-jhLVOMly9oWiSE8mZ4/+uMyVsAKHw71kGbgC1xYp50SQpuLT2pfa6Hiw2VQ0omP/WHsDAPFuBo8hJGxggr768w== dependencies: "@metamask/safe-event-emitter" "2.0.0" bind-decorator "^1.0.11" bn.js "^5.1.1" - buffer "^6.0.3" clsx "^1.1.0" eth-block-tracker "4.4.3" eth-json-rpc-filters "4.2.2" @@ -19782,7 +19837,6 @@ walletlink@^2.1.11: preact "^10.5.9" rxjs "^6.6.3" stream-browserify "^3.0.0" - util "^0.12.4" warning@^4.0.2, warning@^4.0.3: version "4.0.3" @@ -19816,10 +19870,10 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -web3-bzz@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.6.0.tgz#584b51339f21eedff159abc9239b4b7ef6ded840" - integrity sha512-ugYV6BsinwhIi0CsLWINBz4mqN9wR9vNG0WmyEbdECjxcPyr6vkaWt4qi0zqlUxEnYAwGj4EJXNrbjPILntQTQ== +web3-bzz@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.7.0.tgz#0b754d787a1700f0580fa741fc707d19d1447ff4" + integrity sha512-XPhTWUnZa8gnARfiqaag3jJ9+6+a66Li8OikgBUJoMUqPuQTCJPncTbGYqOJIfRFGavEAdlMnfYXx9lvgv2ZPw== dependencies: "@types/node" "^12.12.6" got "9.6.0" @@ -19833,6 +19887,14 @@ web3-core-helpers@1.6.0: web3-eth-iban "1.6.0" web3-utils "1.6.0" +web3-core-helpers@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.7.0.tgz#0eaef7bc55ff7ec5ba726181d0e8529be5d60903" + integrity sha512-kFiqsZFHJliKF8VKZNjt2JvKu3gu7h3N1/ke3EPhdp9Li/rLmiyzFVr6ApryZ1FSjbSx6vyOkibG3m6xQ5EHJA== + dependencies: + web3-eth-iban "1.7.0" + web3-utils "1.7.0" + web3-core-method@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.6.0.tgz#ebe4ea51f5a4fa809bb68185576186359d3982e9" @@ -19845,6 +19907,17 @@ web3-core-method@1.6.0: web3-core-subscriptions "1.6.0" web3-utils "1.6.0" +web3-core-method@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.7.0.tgz#5e98030ac9e0d96c6ff1ba93fde1292a332b1b81" + integrity sha512-43Om+kZX8wU5u1pJ28TltF9e9pSTRph6b8wrOb6wgXAfPHqMulq6UTBJWjXXIRVN46Eiqv0nflw35hp9bbgnbA== + dependencies: + "@ethersproject/transactions" "^5.0.0-beta.135" + web3-core-helpers "1.7.0" + web3-core-promievent "1.7.0" + web3-core-subscriptions "1.7.0" + web3-utils "1.7.0" + web3-core-promievent@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.6.0.tgz#8b6053ae83cb47164540167fc361469fc604d2dd" @@ -19852,6 +19925,13 @@ web3-core-promievent@1.6.0: dependencies: eventemitter3 "4.0.4" +web3-core-promievent@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.7.0.tgz#e2c6c38f29b912cc549a2a3f806636a3393983eb" + integrity sha512-xPH66XeC0K0k29GoRd0vyPQ07yxERPRd4yVPrbMzGAz/e9E4M3XN//XK6+PdfGvGw3fx8VojS+tNIMiw+PujbQ== + dependencies: + eventemitter3 "4.0.4" + web3-core-requestmanager@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.6.0.tgz#8ef3a3b89cd08983bd94574f9c5893f70a8a6aea" @@ -19863,6 +19943,17 @@ web3-core-requestmanager@1.6.0: web3-providers-ipc "1.6.0" web3-providers-ws "1.6.0" +web3-core-requestmanager@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.7.0.tgz#5b62b413471d6d2a789ee33d587d280178979c7e" + integrity sha512-rA3dBTBPrt+eIfTAQ2/oYNTN/2wbZaYNR3pFZGqG8+2oCK03+7oQyz4sWISKy/nYQhURh4GK01rs9sN4o/Tq9w== + dependencies: + util "^0.12.0" + web3-core-helpers "1.7.0" + web3-providers-http "1.7.0" + web3-providers-ipc "1.7.0" + web3-providers-ws "1.7.0" + web3-core-subscriptions@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.6.0.tgz#8c23b15b434a7c9f937652ecca45d7108e2c54df" @@ -19871,7 +19962,15 @@ web3-core-subscriptions@1.6.0: eventemitter3 "4.0.4" web3-core-helpers "1.6.0" -web3-core@1.6.0, web3-core@^1.6.0: +web3-core-subscriptions@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.7.0.tgz#30475d8ed5f51a170e5df02085f721925622a795" + integrity sha512-6giF8pyJrPmWrRpc2WLoVCvQdMMADp20ZpAusEW72axauZCNlW1XfTjs0i4QHQBfdd2lFp65qad9IuATPhuzrQ== + dependencies: + eventemitter3 "4.0.4" + web3-core-helpers "1.7.0" + +web3-core@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.6.0.tgz#144eb00f651c9812faf7176abd7ee99d5f45e212" integrity sha512-o0WsLrJ2yD+HAAc29lGMWJef/MutTyuzpJC0UzLJtIAQJqtpDalzWINEu4j8XYXGk34N/V6vudtzRPo23QEE6g== @@ -19884,6 +19983,19 @@ web3-core@1.6.0, web3-core@^1.6.0: web3-core-requestmanager "1.6.0" web3-utils "1.6.0" +web3-core@1.7.0, web3-core@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.7.0.tgz#67b7839130abd19476e7f614ea6ec4c64d08eb00" + integrity sha512-U/CRL53h3T5KHl8L3njzCBT7fCaHkbE6BGJe3McazvFldRbfTDEHXkUJCyM30ZD0RoLi3aDfTVeFIusmEyCctA== + dependencies: + "@types/bn.js" "^4.11.5" + "@types/node" "^12.12.6" + bignumber.js "^9.0.0" + web3-core-helpers "1.7.0" + web3-core-method "1.7.0" + web3-core-requestmanager "1.7.0" + web3-utils "1.7.0" + web3-eth-abi@1.6.0, web3-eth-abi@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.6.0.tgz#4225608f61ebb0607d80849bb2b20f910780253d" @@ -19892,24 +20004,46 @@ web3-eth-abi@1.6.0, web3-eth-abi@^1.2.1: "@ethersproject/abi" "5.0.7" web3-utils "1.6.0" -web3-eth-accounts@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.6.0.tgz#530927f4c5b78df93b3ea1203abbb467de29cd04" - integrity sha512-2f6HS4KIH4laAsNCOfbNX3dRiQosqSY2TRK86C8jtAA/QKGdx+5qlPfYzbI2RjG81iayb2+mVbHIaEaBGZ8sGw== +web3-eth-abi@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.7.0.tgz#4fac9c7d9e5a62b57f8884b37371f515c766f3f4" + integrity sha512-heqR0bWxgCJwjWIhq2sGyNj9bwun5+Xox/LdZKe+WMyTSy0cXDXEAgv3XKNkXC4JqdDt/ZlbTEx4TWak4TRMSg== + dependencies: + "@ethersproject/abi" "5.0.7" + web3-utils "1.7.0" + +web3-eth-accounts@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.7.0.tgz#d0a6f2cfbd61dd6014224056070b7f8d1d63c0ab" + integrity sha512-Zwm7TlQXdXGRuS6+ib1YsR5fQwpfnFyL6UAZg1zERdrUrs3IkCZSL3yCP/8ZYbAjdTEwWljoott2iSqXNH09ug== dependencies: - "@ethereumjs/common" "^2.3.0" - "@ethereumjs/tx" "^3.2.1" + "@ethereumjs/common" "^2.5.0" + "@ethereumjs/tx" "^3.3.2" crypto-browserify "3.12.0" eth-lib "0.2.8" ethereumjs-util "^7.0.10" scrypt-js "^3.0.1" uuid "3.3.2" - web3-core "1.6.0" - web3-core-helpers "1.6.0" - web3-core-method "1.6.0" - web3-utils "1.6.0" + web3-core "1.7.0" + web3-core-helpers "1.7.0" + web3-core-method "1.7.0" + web3-utils "1.7.0" -web3-eth-contract@1.6.0, web3-eth-contract@^1.6.0: +web3-eth-contract@1.7.0, web3-eth-contract@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.7.0.tgz#3795767a65d7b87bd22baea3e18aafdd928d5313" + integrity sha512-2LY1Xwxu5rx468nqHuhvupQAIpytxIUj3mGL9uexszkhrQf05THVe3i4OnUCzkeN6B2cDztNOqLT3j9SSnVQDg== + dependencies: + "@types/bn.js" "^4.11.5" + web3-core "1.7.0" + web3-core-helpers "1.7.0" + web3-core-method "1.7.0" + web3-core-promievent "1.7.0" + web3-core-subscriptions "1.7.0" + web3-eth-abi "1.7.0" + web3-utils "1.7.0" + +web3-eth-contract@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.6.0.tgz#deb946867ad86d32bcbba899d733b681b25ea674" integrity sha512-ZUtO77zFnxuFtrc+D+iJ3AzNgFXAVcKnhEYN7f1PNz/mFjbtE6dJ+ujO0mvMbxIZF02t9IZv0CIXRpK0rDvZAw== @@ -19923,19 +20057,19 @@ web3-eth-contract@1.6.0, web3-eth-contract@^1.6.0: web3-eth-abi "1.6.0" web3-utils "1.6.0" -web3-eth-ens@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.6.0.tgz#af13852168d56fa71b9198eb097e96fb93831c2a" - integrity sha512-AG24PNv9qbYHSpjHcU2pViOII0jvIR7TeojJ2bxXSDqfcgHuRp3NZGKv6xFvT4uNI4LEQHUhSC7bzHoNF5t8CA== +web3-eth-ens@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.7.0.tgz#49c5300935b026578aaaf9664e5e5529d4c76a68" + integrity sha512-I1bikYJJWQ/FJZIAvwsGOvzAgcRIkosWG4s1L6veRoXaU8OEJFeh4s00KcfHDxg7GWZZGbUSbdbzKpwRbWnvkg== dependencies: content-hash "^2.5.2" eth-ens-namehash "2.0.8" - web3-core "1.6.0" - web3-core-helpers "1.6.0" - web3-core-promievent "1.6.0" - web3-eth-abi "1.6.0" - web3-eth-contract "1.6.0" - web3-utils "1.6.0" + web3-core "1.7.0" + web3-core-helpers "1.7.0" + web3-core-promievent "1.7.0" + web3-eth-abi "1.7.0" + web3-eth-contract "1.7.0" + web3-utils "1.7.0" web3-eth-iban@1.6.0: version "1.6.0" @@ -19945,44 +20079,52 @@ web3-eth-iban@1.6.0: bn.js "^4.11.9" web3-utils "1.6.0" -web3-eth-personal@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.6.0.tgz#b75a61c0737b8b8bcc11d05db2ed7bfce7e4b262" - integrity sha512-8ohf4qAwbShf4RwES2tLHVqa+pHZnS5Q6tV80sU//bivmlZeyO1W4UWyNn59vu9KPpEYvLseOOC6Muxuvr8mFQ== +web3-eth-iban@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.7.0.tgz#b56cd58587457d3339730e0cb42772a37141b434" + integrity sha512-1PFE/Og+sPZaug+M9TqVUtjOtq0HecE+SjDcsOOysXSzslNC2CItBGkcRwbvUcS+LbIkA7MFsuqYxOL0IV/gyA== dependencies: - "@types/node" "^12.12.6" - web3-core "1.6.0" - web3-core-helpers "1.6.0" - web3-core-method "1.6.0" - web3-net "1.6.0" - web3-utils "1.6.0" + bn.js "^4.11.9" + web3-utils "1.7.0" -web3-eth@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.6.0.tgz#4c9d5fb4eccf9f8744828281757e6ea76af58cbd" - integrity sha512-qJMvai//r0be6I9ghU24/152f0zgJfYC23TMszN3Y6jse1JtjCBP2TlTibFcvkUN1RRdIUY5giqO7ZqAYAmp7w== +web3-eth-personal@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.7.0.tgz#260c9b6af6e0bea772c6a9a5d58c8d62c035ed99" + integrity sha512-Dr9RZTNOR80PhrPKGdktDUXpOgExEcCcosBj080lKCJFU1paSPj9Zfnth3u6BtIOXyKsVFTrpqekqUDyAwXnNw== dependencies: - web3-core "1.6.0" - web3-core-helpers "1.6.0" - web3-core-method "1.6.0" - web3-core-subscriptions "1.6.0" - web3-eth-abi "1.6.0" - web3-eth-accounts "1.6.0" - web3-eth-contract "1.6.0" - web3-eth-ens "1.6.0" - web3-eth-iban "1.6.0" - web3-eth-personal "1.6.0" - web3-net "1.6.0" - web3-utils "1.6.0" + "@types/node" "^12.12.6" + web3-core "1.7.0" + web3-core-helpers "1.7.0" + web3-core-method "1.7.0" + web3-net "1.7.0" + web3-utils "1.7.0" -web3-net@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.6.0.tgz#2c28f8787073110a7c2310336889d2dad647e500" - integrity sha512-LFfG95ovTT2sNHkO1TEfsaKpYcxOSUtbuwHQ0K3G0e5nevKDJkPEFIqIcob40yiwcWoqEjENJP9Bjk8CRrZ99Q== +web3-eth@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.7.0.tgz#4adbed9b28ab7f81cb11e3586a12d01ab6e812aa" + integrity sha512-3uYwjMjn/MZjKIzXCt4YL9ja/k9X5shfa4lKparZhZE6uesmu+xmSmrEFXA/e9qcveF50jkV7frjkT8H+cLYtw== + dependencies: + web3-core "1.7.0" + web3-core-helpers "1.7.0" + web3-core-method "1.7.0" + web3-core-subscriptions "1.7.0" + web3-eth-abi "1.7.0" + web3-eth-accounts "1.7.0" + web3-eth-contract "1.7.0" + web3-eth-ens "1.7.0" + web3-eth-iban "1.7.0" + web3-eth-personal "1.7.0" + web3-net "1.7.0" + web3-utils "1.7.0" + +web3-net@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.7.0.tgz#694a0c7988f7efc336bab0ee413eb4522efee3b2" + integrity sha512-8pmfU1Se7DmG40Pu8nOCKlhuI12VsVzCtdFDnLAai0zGVAOUuuOCK71B2aKm6u9amWBJjtOlyrCwvsG+QEd6dw== dependencies: - web3-core "1.6.0" - web3-core-method "1.6.0" - web3-utils "1.6.0" + web3-core "1.7.0" + web3-core-method "1.7.0" + web3-utils "1.7.0" web3-provider-engine@15.0.4: version "15.0.4" @@ -20076,6 +20218,14 @@ web3-providers-http@1.6.0: web3-core-helpers "1.6.0" xhr2-cookies "1.1.0" +web3-providers-http@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.7.0.tgz#0661261eace122a0ed5853f8be5379d575a9130c" + integrity sha512-Y9reeEiApfvQKLUUtrU4Z0c+H6b7BMWcsxjgoXndI1C5NB297mIUfltXxfXsh5C/jk5qn4Q3sJp3SwQTyVjH7Q== + dependencies: + web3-core-helpers "1.7.0" + xhr2-cookies "1.1.0" + web3-providers-ipc@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.6.0.tgz#6a3410fd47a67c4a36719fb97f99534ae12aac98" @@ -20084,6 +20234,14 @@ web3-providers-ipc@1.6.0: oboe "2.1.5" web3-core-helpers "1.6.0" +web3-providers-ipc@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.7.0.tgz#152dc1231eb4f17426498d4d5d973c865eab03d9" + integrity sha512-U5YLXgu6fvAK4nnMYqo9eoml3WywgTym0dgCdVX/n1UegLIQ4nctTubBAuWQEJzmAzwh+a6ValGcE7ZApTRI7Q== + dependencies: + oboe "2.1.5" + web3-core-helpers "1.7.0" + web3-providers-ws@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.6.0.tgz#dc15dc18c30089efda992015fd5254bd2b77af5f" @@ -20093,15 +20251,24 @@ web3-providers-ws@1.6.0: web3-core-helpers "1.6.0" websocket "^1.0.32" -web3-shh@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.6.0.tgz#838a3435dce1039f669a48e53e948062de197931" - integrity sha512-ymN0OFL81WtEeSyb+PFpuUv39fR3frGwsZnIg5EVPZvrOIdaDSFcGSLDmafUt0vKSubvLMVYIBOCskRD6YdtEQ== +web3-providers-ws@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.7.0.tgz#99c2de9f6b5ac56e926794ef9074c7442d937372" + integrity sha512-0a8+lVV3JBf+eYnGOsdzOpftK1kis5X7s35QAdoaG5SDapnEylXFlR4xDSSSU88ZwMwvse8hvng2xW6A7oeWxw== dependencies: - web3-core "1.6.0" - web3-core-method "1.6.0" - web3-core-subscriptions "1.6.0" - web3-net "1.6.0" + eventemitter3 "4.0.4" + web3-core-helpers "1.7.0" + websocket "^1.0.32" + +web3-shh@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.7.0.tgz#ed9d085b670bb5a938f2847393478e33df3ec95c" + integrity sha512-RZhxcevALIPK178VZCpwMBvQeW+IoWtRJ4EMdegpbnETeZaC3aRUcs6vKnrf0jXJjm4J/E2Dt438Y1Ord/1IMw== + dependencies: + web3-core "1.7.0" + web3-core-method "1.7.0" + web3-core-subscriptions "1.7.0" + web3-net "1.7.0" web3-utils@1.2.1: version "1.2.1" @@ -20129,18 +20296,31 @@ web3-utils@1.6.0, web3-utils@^1.0.0-beta.31, web3-utils@^1.2.1, web3-utils@^1.6. randombytes "^2.1.0" utf8 "3.0.0" -web3@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/web3/-/web3-1.6.0.tgz#d8fa0cd9e7bf252f9fe43bb77dc42bc6671affde" - integrity sha512-rWpXnO88MiVX5yTRqMBCVKASxc7QDkXZZUl1D48sKlbX4dt3BAV+nVMVUKCBKiluZ5Bp8pDrVCUdPx/jIYai5Q== +web3-utils@1.7.0, web3-utils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.7.0.tgz#c59f0fd43b2449357296eb54541810b99b1c771c" + integrity sha512-O8Tl4Ky40Sp6pe89Olk2FsaUkgHyb5QAXuaKo38ms3CxZZ4d3rPGfjP9DNKGm5+IUgAZBNpF1VmlSmNCqfDI1w== dependencies: - web3-bzz "1.6.0" - web3-core "1.6.0" - web3-eth "1.6.0" - web3-eth-personal "1.6.0" - web3-net "1.6.0" - web3-shh "1.6.0" - web3-utils "1.6.0" + bn.js "^4.11.9" + ethereum-bloom-filters "^1.0.6" + ethereumjs-util "^7.1.0" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + +web3@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/web3/-/web3-1.7.0.tgz#5867cd10a2bebb5c33fc218368e3f6f826f6897e" + integrity sha512-n39O7QQNkpsjhiHMJ/6JY6TaLbdX+2FT5iGs8tb3HbIWOhPm4+a7UDbr5Lkm+gLa9aRKWesZs5D5hWyEvg4aJA== + dependencies: + web3-bzz "1.7.0" + web3-core "1.7.0" + web3-eth "1.7.0" + web3-eth-personal "1.7.0" + web3-net "1.7.0" + web3-shh "1.7.0" + web3-utils "1.7.0" webidl-conversions@^3.0.0: version "3.0.1"