From 1a50b59404e03952f8e0dc6c03eee2336667729e Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 13 Jan 2021 17:28:10 +0400 Subject: [PATCH 01/20] Bug: Use link tag instead of javascript navigation in apps list (#1770) * Use list instead of programmable navigation * move color style to appcard * use color from theme * add declaration for styled-components theme --- .../Apps/components/AppCard/index.tsx | 1 + .../components/Apps/components/AppsList.tsx | 23 ++++----- src/types/definitions.d.ts | 9 ++++ yarn.lock | 47 ------------------- 4 files changed, 18 insertions(+), 62 deletions(-) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx index a28053d0..234e6116 100644 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ b/src/routes/safe/components/Apps/components/AppCard/index.tsx @@ -15,6 +15,7 @@ const StyledAppCard = styled(Card)` height: 232px !important; box-sizing: border-box; cursor: pointer; + color: ${({ theme }) => theme.colors.secondary}; :hover { box-shadow: 1px 2px 16px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.35)}; diff --git a/src/routes/safe/components/Apps/components/AppsList.tsx b/src/routes/safe/components/Apps/components/AppsList.tsx index 89933d2b..378e4780 100644 --- a/src/routes/safe/components/Apps/components/AppsList.tsx +++ b/src/routes/safe/components/Apps/components/AppsList.tsx @@ -6,7 +6,7 @@ import { GenericModal, IconText, Loader, Menu } from '@gnosis.pm/safe-react-comp import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import AppCard from 'src/routes/safe/components/Apps/components/AppCard' import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' -import { useRouteMatch, useHistory } from 'react-router-dom' +import { useRouteMatch, Link } from 'react-router-dom' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { useAppList } from '../hooks/useAppList' @@ -19,6 +19,10 @@ const Wrapper = styled.div` flex-direction: column; ` +const StyledLink = styled(Link)` + text-decoration: none; +` + const centerCSS = css` display: flex; align-items: center; @@ -53,17 +57,11 @@ const Breadcrumb = styled.div` ` const AppsList = (): React.ReactElement => { - const history = useHistory() const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const safeAddress = useSelector(safeParamAddressFromStateSelector) const { appList } = useAppList() const [isAddAppModalOpen, setIsAddAppModalOpen] = useState(false) - const onAddAppHandler = (url: string) => () => { - const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(url)}` - history.push(goToApp) - } - const openAddAppModal = () => setIsAddAppModalOpen(true) const closeAddAppModal = () => setIsAddAppModalOpen(false) @@ -92,14 +90,9 @@ const AppsList = (): React.ReactElement => { {appList .filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR) .map((a) => ( - + + + ))} diff --git a/src/types/definitions.d.ts b/src/types/definitions.d.ts index e153daf5..c4022c69 100644 --- a/src/types/definitions.d.ts +++ b/src/types/definitions.d.ts @@ -1,3 +1,8 @@ +import 'styled-components' +import { theme } from '@gnosis.pm/safe-react-components' + +type Theme = typeof theme + export {} declare global { interface Window { @@ -10,3 +15,7 @@ declare global { } declare module '@openzeppelin/contracts/build/contracts/ERC721' declare module 'currency-flags/dist/currency-flags.min.css' + +declare module 'styled-components' { + export interface DefaultTheme extends Theme {} // eslint-disable-line +} diff --git a/yarn.lock b/yarn.lock index 74f066ad..86b9265f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3558,18 +3558,6 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b" - integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.12.0" - "@typescript-eslint/types" "4.12.0" - "@typescript-eslint/typescript-estree" "4.12.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - "@typescript-eslint/experimental-utils@4.13.0", "@typescript-eslint/experimental-utils@^4.0.1": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz#9dc9ab375d65603b43d938a0786190a0c72be44e" @@ -3603,14 +3591,6 @@ "@typescript-eslint/typescript-estree" "4.13.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279" - integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg== - dependencies: - "@typescript-eslint/types" "4.12.0" - "@typescript-eslint/visitor-keys" "4.12.0" - "@typescript-eslint/scope-manager@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz#5b45912a9aa26b29603d8fa28f5e09088b947141" @@ -3624,11 +3604,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== -"@typescript-eslint/types@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5" - integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g== - "@typescript-eslint/types@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.13.0.tgz#6a7c6015a59a08fbd70daa8c83dfff86250502f8" @@ -3648,20 +3623,6 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e" - integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w== - dependencies: - "@typescript-eslint/types" "4.12.0" - "@typescript-eslint/visitor-keys" "4.12.0" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" - "@typescript-eslint/typescript-estree@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz#cf6e2207c7d760f5dfd8d18051428fadfc37b45e" @@ -3683,14 +3644,6 @@ dependencies: eslint-visitor-keys "^1.1.0" -"@typescript-eslint/visitor-keys@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a" - integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw== - dependencies: - "@typescript-eslint/types" "4.12.0" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.13.0.tgz#9acb1772d3b3183182b6540d3734143dce9476fe" From 188dc8091289c64552a89b111e343cda29f96ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Fri, 15 Jan 2021 16:06:05 -0300 Subject: [PATCH 02/20] Fix asset value column alignment (#1778) --- src/routes/safe/components/Balances/Coins/styles.ts | 1 - src/routes/safe/components/Balances/dataFetcher.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index 5df6bf51..3c10059d 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -46,7 +46,6 @@ export const styles = createStyles({ marginRight: sm, }, currencyValueRow: { - maxWidth: '125px', textAlign: 'right', }, }) diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index c7a3f2d4..e950560e 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -105,6 +105,7 @@ export const generateColumns = (): List => { const value: TableColumn = { id: BALANCE_TABLE_VALUE_ID, + align: 'right', order: true, label: 'Value', custom: false, From 46274b00ec68fa8040606b65362f42187a17d471 Mon Sep 17 00:00:00 2001 From: francovenica Date: Tue, 19 Jan 2021 15:08:51 -0300 Subject: [PATCH 03/20] fix ids (#1755) add data-testid for sidebar add id for recipient field Co-authored-by: Daniel Sanchez Co-authored-by: Fernando --- src/components/AppLayout/index.tsx | 2 +- .../Balances/SendModal/screens/AddressBookInput/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index 56a0e482..e70a31b4 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -97,7 +97,7 @@ const Layout: React.FC = ({
- + ( Date: Tue, 26 Jan 2021 15:29:28 -0300 Subject: [PATCH 04/20] (Fix) - #1804 Tx gas estimation fail for threshold >= 3 (#1808) * Fix checkIfTxIsExecution method implementation * Add tests for checkIfTxIsExecution/checkIfTxIsCreation/checkIfTxIsApproveAndExecution/ * Minimice number of ifs with same result Co-authored-by: Mati Dastugue Co-authored-by: Daniel Sanchez --- .../useEstimateTransactionGas.test.ts | 148 ++++++++++++++++++ src/logic/hooks/useEstimateTransactionGas.tsx | 19 ++- 2 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts diff --git a/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts new file mode 100644 index 00000000..99b83f03 --- /dev/null +++ b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts @@ -0,0 +1,148 @@ +import { + checkIfTxIsApproveAndExecution, + checkIfTxIsCreation, + checkIfTxIsExecution, +} from 'src/logic/hooks/useEstimateTransactionGas' + +describe('checkIfTxIsExecution', () => { + const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B' + it(`should return true if the safe threshold is 1`, () => { + // given + const threshold = 1 + const preApprovingOwner = undefined + const transactionConfirmations = 0 + const transactionType = '' + + // when + const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return true if the safe threshold is reached for the transaction`, () => { + // given + const threshold = 3 + const preApprovingOwner = mockedEthAccount + const transactionConfirmations = 3 + const transactionType = '' + + // when + const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return true if the transaction is spendingLimit`, () => { + // given + const threshold = 5 + const preApprovingOwner = undefined + const transactionConfirmations = 0 + const transactionType = 'spendingLimit' + + // when + const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return true if the number of confirmations is one bellow the threshold but there is a preApprovingOwner`, () => { + // given + const threshold = 5 + const preApprovingOwner = mockedEthAccount + const transactionConfirmations = 4 + const transactionType = undefined + + // when + const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return false if the number of confirmations is one bellow the threshold and there is no preApprovingOwner`, () => { + // given + const threshold = 5 + const preApprovingOwner = undefined + const transactionConfirmations = 4 + const transactionType = undefined + + // when + const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) +}) +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 = '' + + // when + const result = checkIfTxIsCreation(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 = checkIfTxIsCreation(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 = checkIfTxIsCreation(transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) +}) + +describe('checkIfTxIsApproveAndExecution', () => { + it(`should return true if there is only one confirmation left to reach the safe threshold`, () => { + // given + const transactionConfirmations = 2 + const safeThreshold = 3 + const transactionType = '' + + // when + const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) + + // then + expect(result).toBe(true) + }) + it(`should return true if the transaction is spendingLimit`, () => { + // given + const transactionConfirmations = 0 + const transactionType = 'spendingLimit' + const safeThreshold = 3 + + // when + const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) + + // 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 = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) + + // then + expect(result).toBe(false) + }) +}) diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index f3c33747..fa5cc94e 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -30,18 +30,27 @@ export enum EstimationStatus { SUCCESS = 'SUCCESS', } -const checkIfTxIsExecution = ( +export const checkIfTxIsExecution = ( threshold: number, preApprovingOwner?: string, txConfirmations?: number, txType?: string, -): boolean => - txConfirmations === threshold || !!preApprovingOwner || threshold === 1 || sameString(txType, 'spendingLimit') +): boolean => { + if (threshold === 1 || sameString(txType, 'spendingLimit') || txConfirmations === threshold) { + return true + } -const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean => + if (preApprovingOwner && txConfirmations) { + return txConfirmations + 1 === threshold + } + + return false +} + +export const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean => txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit') -const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean => +export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean => txConfirmations === 0 && !sameString(txType, 'spendingLimit') type TransactionEstimationProps = { From a020e8e0dc27859b766aebb220d3e1d363feb32b Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Tue, 26 Jan 2021 18:06:39 -0300 Subject: [PATCH 05/20] (Fix) - Gas estimation for transaction creation (#1772) * Fix gas estimation for transaction creation * Adds missing logs Co-authored-by: Daniel Sanchez --- src/logic/safe/transactions/gas.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index 37f12655..e808860c 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -131,6 +131,7 @@ const calculateMinimumGasForTransaction = async ( ): Promise => { for (const additionalGas of additionalGasBatches) { const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + additionalGas + console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`) try { await getGasEstimationTxResponse({ to: safeAddress, @@ -139,7 +140,8 @@ const calculateMinimumGasForTransaction = async ( gasPrice: 0, gas: amountOfGasToTryTx, }) - return txGasEstimation + additionalGas + console.info(`Gas estimation successfully finished with gas amount: ${amountOfGasToTryTx}`) + return amountOfGasToTryTx } catch (error) { console.log(`Error trying to estimate gas with amount: ${amountOfGasToTryTx}`) } @@ -165,8 +167,6 @@ export const estimateGasForTransactionCreation = async ( data: estimateData, }) - const txGasEstimation = gasEstimationResponse + 10000 - // 21000 - additional gas costs (e.g. base tx costs, transfer costs) const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000 const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000] @@ -175,7 +175,7 @@ export const estimateGasForTransactionCreation = async ( additionalGasBatches, safeAddress, estimateData, - txGasEstimation, + gasEstimationResponse, dataGasEstimation, ) } catch (error) { From fef88b0bb90023b870a53df47fe4f2fd841b553f Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 27 Jan 2021 15:30:29 +0100 Subject: [PATCH 06/20] Update 1 Inch app to v1.2.3 (#1816) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 4c4a4409..eccc3e59 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -26,7 +26,7 @@ export type StaticAppInfo = { export const staticAppsList: Array = [ // 1inch { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUDTSghr154kCCGguyA3cbG5HRVd2tQgNR7yD69bcsjm5`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 57f7acd26e8b4186d0ebb6211d469dc4e079c0c1 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 27 Jan 2021 16:30:39 +0100 Subject: [PATCH 07/20] Update IDLE Safe app (#1813) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index eccc3e59..bdff6d8c 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -59,7 +59,7 @@ export const staticAppsList: Array = [ { url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] }, // Idle { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZ3oug89a3BaVqdJrJEA8CKmLF4M8snuAnphR6z1yq8V8`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], }, From d6343a9bb1a7b3c097b8f07cc49f58bf9e26116e Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 27 Jan 2021 20:35:14 +0400 Subject: [PATCH 08/20] Feature: Single tx safe apps interactions should not use multisend (#1792) * Don't use multisend if txs.length = 1 * calc txData inside useMemo * fix sending txs from apps, use correct operation/recipient Co-authored-by: Fernando Co-authored-by: Daniel Sanchez --- .../components/ConfirmTransactionModal.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index fd199a37..02c371ae 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useMemo } from 'react' import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components' import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' import styled from 'styled-components' @@ -18,7 +18,7 @@ import { SafeApp } from 'src/routes/safe/components/Apps/types.d' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' -import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { CALL, DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' import GasEstimationInfo from './GasEstimationInfo' @@ -101,6 +101,9 @@ export const ConfirmTransactionModal = ({ onTxReject, }: OwnProps): React.ReactElement | null => { const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) + const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs]) + const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs]) + const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs]) const { gasEstimation, @@ -110,9 +113,9 @@ export const ConfirmTransactionModal = ({ gasCostFormatted, txEstimationExecutionStatus, } = useEstimateTransactionGas({ - txData: encodeMultiSendCall(txs), - txRecipient: MULTI_SEND_ADDRESS, - operation: DELEGATE_CALL, + txData: txData || '', + txRecipient, + operation, }) useEffect(() => { @@ -137,16 +140,14 @@ export const ConfirmTransactionModal = ({ } const confirmTransactions = async () => { - const txData = encodeMultiSendCall(txs) - await dispatch( createTransaction( { safeAddress, - to: MULTI_SEND_ADDRESS, + to: txRecipient, valueInWei: '0', txData, - operation: DELEGATE_CALL, + operation, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, origin: app.id, navigateToTransactionsTab: false, From 621401bc82302829a0fa316d0850fa258f3982b8 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 27 Jan 2021 19:15:17 +0100 Subject: [PATCH 09/20] Add dHedge safe app (#1821) * Add dHedge safe app * Disable volta build for all PRs and leave only for release branch and master --- .travis.yml | 1 + src/routes/safe/components/Apps/utils.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index c457b9d3..249b42bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ matrix: - REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA} - SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA} - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA} + if: (branch = master) OR tag IS present - env: - REACT_APP_NETWORK='energy_web_chain' - STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME} diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index bdff6d8c..e6115af4 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -57,6 +57,12 @@ export const staticAppsList: Array = [ }, // Compound { url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] }, + // dHedge + { + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`, + disabled: false, + networks: [ETHEREUM_NETWORK.MAINNET], + }, // Idle { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`, From e3adb248252ff608423c87b23738595654630c9d Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 27 Jan 2021 20:19:32 +0100 Subject: [PATCH 10/20] Update balancer pool management app with new keys (#1819) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index e6115af4..02111b2d 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -44,7 +44,7 @@ export const staticAppsList: Array = [ }, // Balancer Pool { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaTucdZYLKTqaewwJduVMM8qfCDhyaEqjd8tBNae26K1J`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQsmxUVtcEWmKcXxKwYsZFKJ2kDdqqjqdExujiGY1g3tV`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 3d43db039b56046538bc9dbfe5c54e3cbaec009f Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Wed, 27 Jan 2021 19:44:02 -0300 Subject: [PATCH 11/20] (Fix) Contract interaction tuples (#1787) * Adds isTupleParameter validation to the extractMethodArgs * Improves tuples parsing * Show examples for txs inputs * Remove MethodsInputExamples Co-authored-by: Mati Dastugue --- .../ContractInteraction/Header/index.tsx | 6 ++--- .../MethodsDropdown/index.tsx | 27 ++++++++++--------- .../InputComponent/index.tsx | 6 ++--- .../RenderInputParams/index.tsx | 8 +++--- .../RenderOutputParams/index.tsx | 14 +++++----- .../ContractInteraction/Review/index.tsx | 2 +- .../screens/ContractInteraction/index.tsx | 8 +++--- .../ContractInteraction/utils/index.ts | 13 +++++---- 8 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header/index.tsx index b8a8dc5c..268206cd 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header/index.tsx @@ -1,7 +1,7 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import React from 'react' +import React, { ReactElement } from 'react' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' @@ -15,7 +15,7 @@ interface HeaderProps { title: string } -const Header = ({ onClose, subTitle, title }: HeaderProps) => { +export const Header = ({ onClose, subTitle, title }: HeaderProps): ReactElement => { const classes = useStyles() return ( @@ -30,5 +30,3 @@ const Header = ({ onClose, subTitle, title }: HeaderProps) => { ) } - -export default Header diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx index 82ebacdf..ff5374bd 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx @@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem' import { MuiThemeProvider } from '@material-ui/core/styles' import SearchIcon from '@material-ui/icons/Search' import classNames from 'classnames' -import React from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import { useField, useFormState } from 'react-final-form' import { AbiItem } from 'web3-utils' @@ -24,7 +24,7 @@ interface MethodsDropdownProps { onChange: (method: AbiItem) => void } -const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement | null => { +export const MethodsDropdown = ({ onChange }: MethodsDropdownProps): ReactElement | null => { const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH }) const { input: { value: abi }, @@ -33,13 +33,14 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement const { initialValues: { selectedMethod: selectedMethodByDefault }, } = useFormState({ subscription: { initialValues: true } }) - const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {}) - const [methodsList, setMethodsList] = React.useState([]) - const [methodsListFiltered, setMethodsListFiltered] = React.useState([]) - const [anchorEl, setAnchorEl] = React.useState(null) - const [searchParams, setSearchParams] = React.useState('') + const [selectedMethod, setSelectedMethod] = useState(selectedMethodByDefault ? selectedMethodByDefault : {}) + const [methodsList, setMethodsList] = useState([]) + const [methodsListFiltered, setMethodsListFiltered] = useState([]) - React.useEffect(() => { + const [anchorEl, setAnchorEl] = useState(null) + const [searchParams, setSearchParams] = useState('') + + useEffect(() => { if (abi) { try { setMethodsList(extractUsefulMethods(JSON.parse(abi))) @@ -49,7 +50,7 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement } }, [abi]) - React.useEffect(() => { + useEffect(() => { setMethodsListFiltered(methodsList.filter(({ name }) => name?.toLowerCase().includes(searchParams.toLowerCase()))) }, [methodsList, searchParams]) @@ -67,7 +68,11 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement handleClose() } - return !valid || !abi || abi === NO_CONTRACT ? null : ( + if (!valid || !abi || abi === NO_CONTRACT) { + return null + } + + return ( @@ -145,5 +150,3 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement ) } - -export default MethodsDropdown diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx index 9c8af66c..548f3c37 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx @@ -1,5 +1,5 @@ import { Checkbox } from '@gnosis.pm/safe-react-components' -import React from 'react' +import React, { ReactElement } from 'react' import Col from 'src/components/layout/Col' import Field from 'src/components/forms/Field' @@ -15,7 +15,7 @@ type Props = { placeholder: string } -const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement | null => { +export const InputComponent = ({ type, keyValue, placeholder }: Props): ReactElement | null => { if (!type) { return null } @@ -67,5 +67,3 @@ const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElem } } } - -export default InputComponent diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx index 47b3b6c6..cc0f561a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx @@ -1,13 +1,13 @@ -import React from 'react' +import React, { ReactElement } from 'react' import { useField } from 'react-final-form' import Row from 'src/components/layout/Row' -import InputComponent from './InputComponent' +import { InputComponent } from './InputComponent' import { generateFormFieldKey } from '../utils' import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' -const RenderInputParams = (): React.ReactElement | null => { +export const RenderInputParams = (): ReactElement | null => { const { meta: { valid: validABI }, } = useField('abi', { subscription: { valid: true, value: true } }) @@ -31,5 +31,3 @@ const RenderInputParams = (): React.ReactElement | null => { ) } - -export default RenderInputParams diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams/index.tsx index 339935f6..ef2d4ee7 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactElement } from 'react' import { useField } from 'react-final-form' import { makeStyles } from '@material-ui/core/styles' import TextField from 'src/components/forms/TextField' @@ -17,7 +17,7 @@ const useStyles = makeStyles({ }, }) -const RenderOutputParams = () => { +export const RenderOutputParams = (): ReactElement | null => { const classes = useStyles() const { input: { value: method }, @@ -27,7 +27,11 @@ const RenderOutputParams = () => { }: any = useField('callResults', { subscription: { value: true } }) const multipleResults = !!method && method.outputs.length > 1 - return results ? ( + if (!results) { + return null + } + + return ( <> @@ -57,7 +61,5 @@ const RenderOutputParams = () => { ) })} - ) : null + ) } - -export default RenderOutputParams diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index 532bf33b..ff9cbc6b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -16,7 +16,7 @@ import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIServic import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style' -import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header' +import { Header } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx index c3ea4a7d..374587a5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx @@ -14,10 +14,10 @@ import ContractABI from './ContractABI' import { EthAddressInput } from './EthAddressInput' import FormDivisor from './FormDivisor' import FormErrorMessage from './FormErrorMessage' -import Header from './Header' -import MethodsDropdown from './MethodsDropdown' -import RenderInputParams from './RenderInputParams' -import RenderOutputParams from './RenderOutputParams' +import { Header } from './Header' +import { MethodsDropdown } from './MethodsDropdown' +import { RenderInputParams } from './RenderInputParams' +import { RenderOutputParams } from './RenderOutputParams' import { createTxObject, formMutators, handleSubmitError, isReadMethod, ensResolver } from './utils' import { TransactionReviewType } from './Review' import { NativeCoinValue } from './NativeCoinValue' diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts index c30e1eaa..33cda4bd 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts @@ -63,6 +63,13 @@ export const isInt = (type: string): boolean => type.indexOf('int') === 0 export const isByte = (type: string): boolean => type.indexOf('byte') === 0 export const isArrayParameter = (parameter: string): boolean => /(\[\d*])+$/.test(parameter) +export const getParsedJSONOrArrayFromString = (parameter: string): (string | number)[] | null => { + try { + return JSON.parse(parameter) + } catch (err) { + return null + } +} export const handleSubmitError = (error: SubmissionErrors, values: Record): Record => { for (const key in values) { @@ -83,11 +90,7 @@ export const generateFormFieldKey = (type: string, signatureHash: string, index: const extractMethodArgs = (signatureHash: string, values: Record) => ({ type }, index) => { const key = generateFormFieldKey(type, signatureHash, index) - if (isArrayParameter(type)) { - return JSON.parse(values[key]) - } - - return values[key] + return getParsedJSONOrArrayFromString(values[key]) || values[key] } export const createTxObject = ( From 758fc3c0c06a6414bb6516bd936de0ea95d89a52 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Thu, 28 Jan 2021 05:12:02 -0300 Subject: [PATCH 12/20] (Fix) Estimate gas for wallet connect (#1806) * Guard for empty result * Type TextField * Fix warning of InputAdornment in SendFunds modal * Re-enable gas estimation for wallet connect * Replace web3.call on parseRequiredTxGasResponse with axios post to infura * Adds estimateGasWithInfura and estimateGasWithWeb3Provider for changing the estimation method if we are in a non-infura-supported network * Revert calculateMinimumGasForTransaction change to leave the change for the already-open pr * Renames estimateGasWithInfura with estimateGasWithRPCCall Replaces web3 with web3ReadOnly in estimateGasWithRPCCall Co-authored-by: Mati Dastugue Co-authored-by: Fernando Co-authored-by: Daniel Sanchez --- src/components/TransactionsFees/index.tsx | 11 -- src/components/forms/TextField/index.tsx | 144 +++++++++++------- src/config/index.ts | 13 +- src/logic/hooks/useEstimateTransactionGas.tsx | 5 - src/logic/safe/transactions/gas.ts | 58 ++++++- .../SendModal/screens/SendFunds/index.tsx | 10 +- 6 files changed, 151 insertions(+), 90 deletions(-) diff --git a/src/components/TransactionsFees/index.tsx b/src/components/TransactionsFees/index.tsx index 372c8b52..c5e54f19 100644 --- a/src/components/TransactionsFees/index.tsx +++ b/src/components/TransactionsFees/index.tsx @@ -3,10 +3,6 @@ import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' import Paragraph from 'src/components/layout/Paragraph' import { getNetworkInfo } from 'src/config' import { TransactionFailText } from 'src/components/TransactionFailText' -import { useSelector } from 'react-redux' -import { providerNameSelector } from 'src/logic/wallets/store/selectors' -import { sameString } from 'src/utils/strings' -import { WALLETS } from 'src/config/networks/network.d' type TransactionFailTextProps = { txEstimationExecutionStatus: EstimationStatus @@ -24,8 +20,6 @@ export const TransactionFees = ({ isOffChainSignature, txEstimationExecutionStatus, }: TransactionFailTextProps): React.ReactElement | null => { - const providerName = useSelector(providerNameSelector) - let transactionAction if (isCreation) { transactionAction = 'create' @@ -35,11 +29,6 @@ export const TransactionFees = ({ transactionAction = 'approve' } - // FIXME this should be removed when estimating with WalletConnect correctly - if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) { - return null - } - return ( <> diff --git a/src/components/forms/TextField/index.tsx b/src/components/forms/TextField/index.tsx index c1998d63..19123e33 100644 --- a/src/components/forms/TextField/index.tsx +++ b/src/components/forms/TextField/index.tsx @@ -1,5 +1,5 @@ import MuiTextField from '@material-ui/core/TextField' -import { withStyles } from '@material-ui/core/styles' +import { createStyles, makeStyles } from '@material-ui/core/styles' import React from 'react' import { lg } from 'src/theme/variables' @@ -10,65 +10,93 @@ const overflowStyle = { width: '100%', } -const styles = () => ({ - root: { - paddingTop: lg, - paddingBottom: '12px', - lineHeight: 0, - }, -}) +const styles = () => + createStyles({ + root: { + paddingTop: lg, + paddingBottom: '12px', + lineHeight: 0, + }, + }) -class TextField extends React.PureComponent { - render() { - const { - classes, - input: { name, onChange, value, ...restInput }, - inputAdornment, - meta, - multiline, - rows, - testId, - text, - ...rest - } = this.props - const helperText = value ? text : undefined - const showError = (meta.touched || !meta.pristine) && !meta.valid - const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError) - const errorMessage = meta.error || meta.submitError - const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched) - const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched +const useStyles = makeStyles(styles) - const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched - - const inputRoot = helperText ? classes.root : '' - const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : '' - const inputProps = { - ...restInput, - autoComplete: 'off', - 'data-testid': testId, - } - const inputRootProps = { - ...inputAdornment, - className: `${inputRoot} ${statusClasses}`, - disableUnderline: disableUnderline, - } - - return ( - - ) +type Props = { + input: { + name: string + onChange?: () => void + value: string + placeholder: string + type: string } + meta: { + touched?: boolean + pristine?: boolean + valid?: boolean + error?: string + modifiedSinceLastSubmit?: boolean + submitError?: boolean + active?: boolean + } + inputAdornment?: { endAdornment: React.ReactElement } | undefined + multiline: boolean + rows?: string + testId: string + text: string + disabled?: boolean + rowsMax?: number + className?: string } -export default withStyles(styles as any)(TextField) +const TextField = (props: Props): React.ReactElement => { + const { + input: { name, onChange, value, ...restInput }, + inputAdornment, + meta, + multiline, + rows, + testId, + text, + ...rest + } = props + const classes = useStyles() + const helperText = value ? text : undefined + const showError = (meta.touched || !meta.pristine) && !meta.valid + const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError) + const errorMessage = meta.error || meta.submitError + const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched) + const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched + + const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched + + const inputRoot = helperText ? classes.root : '' + const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : '' + const inputProps = { + ...restInput, + autoComplete: 'off', + 'data-testid': testId, + } + const inputRootProps = { + ...inputAdornment, + className: `${inputRoot} ${statusClasses}`, + disableUnderline: disableUnderline, + } + + return ( + + ) +} + +export default TextField diff --git a/src/config/index.ts b/src/config/index.ts index d8660603..e1affc98 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -17,6 +17,8 @@ export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK] export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()] +export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId()) + const getCurrentEnvironment = (): string => { switch (NODE_ENV) { case 'test': { @@ -76,15 +78,8 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle -export const getRpcServiceUrl = (): string => { - const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId()) - - if (usesInfuraRPC) { - return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` - } - - return getConfig().rpcServiceUrl -} +export const getRpcServiceUrl = (): string => + usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}` diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index fa5cc94e..77bce100 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -22,7 +22,6 @@ 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 { sameString } from 'src/utils/strings' -import { WALLETS } from 'src/config/networks/network.d' export enum EstimationStatus { LOADING = 'LOADING', @@ -177,10 +176,6 @@ export const useEstimateTransactionGas = ({ if (!txData.length) { return } - // FIXME this should be removed when estimating with WalletConnect correctly - if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) { - return null - } const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType) const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType) diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index e808860c..3edb1b90 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -1,12 +1,14 @@ import { BigNumber } from 'bignumber.js' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' -import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { sameString } from 'src/utils/strings' +import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' import { List } from 'immutable' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' +import axios from 'axios' +import { getRpcServiceUrl, usesInfuraRPC } from 'src/config' +import { sameString } from 'src/utils/strings' // Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount const parseRequiredTxGasResponse = (data: string): number => { @@ -88,7 +90,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde } } -export const getGasEstimationTxResponse = async (txConfig: { +const estimateGasWithWeb3Provider = async (txConfig: { to: string from: string data: string @@ -116,12 +118,56 @@ export const getGasEstimationTxResponse = async (txConfig: { return new BigNumber(estimationData.substring(138), 16).toNumber() } - - // This will fail in case that we receive an EMPTY_DATA on the GETH node gas estimation (for version < v1.9.24 of geth nodes) - // We cannot throw this error above because it will be captured again on the catch block bellow throw new Error('Error while estimating the gas required for tx') } +const estimateGasWithRPCCall = async (txConfig: { + to: string + from: string + data: string + gasPrice?: number + gas?: number +}): Promise => { + try { + const { data } = await axios.post(getRpcServiceUrl(), { + jsonrpc: '2.0', + method: 'eth_call', + id: 1, + params: [ + { + ...txConfig, + gasPrice: web3ReadOnly.utils.toHex(txConfig.gasPrice || 0), + gas: txConfig.gas ? web3ReadOnly.utils.toHex(txConfig.gas) : undefined, + }, + 'latest', + ], + }) + + const { error } = data + if (error?.data) { + return new BigNumber(data.error.data.substring(138), 16).toNumber() + } + } catch (error) { + console.log('Gas estimation endpoint errored: ', error.message) + } + throw new Error('Error while estimating the gas required for tx') +} + +export const getGasEstimationTxResponse = async (txConfig: { + to: string + from: string + data: string + gasPrice?: number + gas?: number +}): Promise => { + // If we are in a infura supported network we estimate using infura + if (usesInfuraRPC) { + return estimateGasWithRPCCall(txConfig) + } + // Otherwise we estimate using the current connected provider + return estimateGasWithWeb3Provider(txConfig) +} + const calculateMinimumGasForTransaction = async ( additionalGasBatches: number[], safeAddress: string, diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index f8b3c7ce..f39e9ab2 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -75,6 +75,10 @@ type SendFundsProps = { amount?: string } +const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElement => { + return <>{symbol} +} + const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amount }: SendFundsProps): ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) @@ -295,7 +299,11 @@ const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amou {selectedToken?.symbol}, + endAdornment: ( + + + + ), }} name="amount" placeholder="Amount*" From 6b4b156dafae8392d382c0e6b80b9491e76f25ff Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 28 Jan 2021 15:22:48 +0100 Subject: [PATCH 13/20] Update balancer exchange app (#1823) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 02111b2d..9203b45e 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -38,7 +38,7 @@ export const staticAppsList: Array = [ }, //Balancer Exchange { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmfPLXne1UrY399RQAcjD1dmBhQrPGZWgp311CDLLW3VTn`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 012b41a3f793c898d8798d13982e78a2e4c9f707 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Fri, 29 Jan 2021 13:07:47 +0100 Subject: [PATCH 14/20] Feature - Add .crypto domain resolution (#1764) * (Feature) - Support for .crypto resolution * Fix EthereumAddress validator error msg * Extract unstoppable domains logic to an util * Add singleton to UnstoppableDomains instance * Shorten address or domain validation error message Co-authored-by: sudoryan --- docs/networks.md | 2 +- package.json | 1 + src/components/forms/AddressInput/index.tsx | 8 +- src/components/forms/validator.test.ts | 2 +- src/components/forms/validator.ts | 8 +- src/config/networks/network.d.ts | 2 +- src/config/networks/xdai.ts | 2 +- src/logic/wallets/ethAddresses.ts | 2 + src/logic/wallets/getWeb3.ts | 10 +- src/logic/wallets/utils/unstoppableDomains.ts | 18 + .../screens/AddressBookInput/index.tsx | 11 +- .../ContractInteraction/utils/index.ts | 8 +- yarn.lock | 361 +++++++++++++++++- 13 files changed, 403 insertions(+), 32 deletions(-) create mode 100644 src/logic/wallets/utils/unstoppableDomains.ts diff --git a/docs/networks.md b/docs/networks.md index a048fa86..1990b7e6 100644 --- a/docs/networks.md +++ b/docs/networks.md @@ -73,7 +73,7 @@ export enum FEATURES { ERC1155 = 'ERC1155', SAFE_APPS = 'SAFE_APPS', CONTRACT_INTERACTION = 'CONTRACT_INTERACTION', - ENS_LOOKUP = 'ENS_LOOKUP' + DOMAIN_LOOKUP = 'DOMAIN_LOOKUP' } ``` diff --git a/package.json b/package.json index 6b11f0db..cc73f75a 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "@openzeppelin/contracts": "3.1.0", "@sentry/react": "^5.28.0", "@sentry/tracing": "^5.28.0", + "@unstoppabledomains/resolution": "^1.11.1", "@truffle/contract": "^4.3.0", "async-sema": "^3.1.0", "axios": "0.21.1", diff --git a/src/components/forms/AddressInput/index.tsx b/src/components/forms/AddressInput/index.tsx index 8655d259..46168ba9 100644 --- a/src/components/forms/AddressInput/index.tsx +++ b/src/components/forms/AddressInput/index.tsx @@ -5,8 +5,8 @@ import { OnChange } from 'react-final-form-listeners' import TextField from 'src/components/forms/TextField' import { Validator, composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator' import { trimSpaces } from 'src/utils/strings' -import { getAddressFromENS } from 'src/logic/wallets/getWeb3' -import { isValidEnsName } from 'src/logic/wallets/ethAddresses' +import { getAddressFromDomain } from 'src/logic/wallets/getWeb3' +import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses' import { checksumAddress } from 'src/utils/checksumAddress' // an idea for second field was taken from here @@ -54,9 +54,9 @@ const AddressInput = ({ {async (value) => { const address = trimSpaces(value) - if (isValidEnsName(address)) { + if (isValidEnsName(address) || isValidCryptoDomainName(address)) { try { - const resolverAddr = await getAddressFromENS(address) + const resolverAddr = await getAddressFromDomain(address) const formattedAddress = checksumAddress(resolverAddr) fieldMutator(formattedAddress) } catch (err) { diff --git a/src/components/forms/validator.test.ts b/src/components/forms/validator.test.ts index 6362722a..38264deb 100644 --- a/src/components/forms/validator.test.ts +++ b/src/components/forms/validator.test.ts @@ -128,7 +128,7 @@ describe('Forms > Validators', () => { }) describe('mustBeEthereumAddress validator', () => { - const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Address should be a valid Ethereum address or ENS name' + const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Input must be a valid Ethereum address, ENS or Unstoppable domain' it('Returns undefined for a valid ethereum address', async () => { expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined() diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 1f356a53..caa7badb 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -64,8 +64,8 @@ export const mustBeEthereumAddress = memoize( const startsWith0x = address?.startsWith('0x') const isAddress = getWeb3().utils.isAddress(address) - const errorMessage = `Address should be a valid Ethereum address${ - isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : '' + const errorMessage = `Input must be a valid Ethereum address${ + isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : '' }` return startsWith0x && isAddress ? undefined : errorMessage @@ -76,8 +76,8 @@ export const mustBeEthereumContractAddress = memoize( async (address: string): Promise => { const contractCode = await getWeb3().eth.getCode(address) - const errorMessage = `Address should be a valid Ethereum contract address${ - isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : '' + const errorMessage = `Input must be a valid Ethereum contract address${ + isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : '' }` return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined diff --git a/src/config/networks/network.d.ts b/src/config/networks/network.d.ts index 2cd2f1a7..c88a076e 100644 --- a/src/config/networks/network.d.ts +++ b/src/config/networks/network.d.ts @@ -24,7 +24,7 @@ export enum FEATURES { ERC1155 = 'ERC1155', SAFE_APPS = 'SAFE_APPS', CONTRACT_INTERACTION = 'CONTRACT_INTERACTION', - ENS_LOOKUP = 'ENS_LOOKUP', + DOMAIN_LOOKUP = 'DOMAIN_LOOKUP', } type Token = { diff --git a/src/config/networks/xdai.ts b/src/config/networks/xdai.ts index 743f85fe..7f218e5c 100644 --- a/src/config/networks/xdai.ts +++ b/src/config/networks/xdai.ts @@ -51,7 +51,7 @@ const xDai: NetworkConfig = { WALLETS.AUTHEREUM, WALLETS.LATTICE, ], - disabledFeatures: [FEATURES.ENS_LOOKUP], + disabledFeatures: [FEATURES.DOMAIN_LOOKUP], } export default xDai diff --git a/src/logic/wallets/ethAddresses.ts b/src/logic/wallets/ethAddresses.ts index 891da66a..79ecfd43 100644 --- a/src/logic/wallets/ethAddresses.ts +++ b/src/logic/wallets/ethAddresses.ts @@ -47,3 +47,5 @@ export const isUserAnOwnerOfAnySafe = (safes: List | SafeRecord[], u safes.some((safe: SafeRecord) => isUserAnOwner(safe, userAccount)) export const isValidEnsName = (name: string): boolean => /^([\w-]+\.)+(eth|test|xyz|luxe|ewc)$/.test(name) + +export const isValidCryptoDomainName = (name: string): boolean => /^([\w-]+\.)+(crypto)$/.test(name) diff --git a/src/logic/wallets/getWeb3.ts b/src/logic/wallets/getWeb3.ts index 1427444b..c1738032 100644 --- a/src/logic/wallets/getWeb3.ts +++ b/src/logic/wallets/getWeb3.ts @@ -1,12 +1,13 @@ import Web3 from 'web3' import { provider as Provider } from 'web3-core' import { ContentHash } from 'web3-eth-ens' - import { sameAddress } from './ethAddresses' import { EMPTY_DATA } from './ethTransactions' import { ProviderProps } from './store/model/provider' import { NODE_ENV } from 'src/utils/constants' import { getRpcServiceUrl } from 'src/config' +import { isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses' +import { getAddressFromUnstoppableDomain } from './utils/unstoppableDomains' export const WALLET_PROVIDER = { SAFE: 'SAFE', @@ -85,7 +86,12 @@ export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet } } -export const getAddressFromENS = (name: string): Promise => web3.eth.ens.getAddress(name) +export const getAddressFromDomain = (name: string): Promise => { + if (isValidCryptoDomainName(name)) { + return getAddressFromUnstoppableDomain(name) + } + return web3.eth.ens.getAddress(name) +} export const getContentFromENS = (name: string): Promise => web3.eth.ens.getContenthash(name) diff --git a/src/logic/wallets/utils/unstoppableDomains.ts b/src/logic/wallets/utils/unstoppableDomains.ts new file mode 100644 index 00000000..7d615875 --- /dev/null +++ b/src/logic/wallets/utils/unstoppableDomains.ts @@ -0,0 +1,18 @@ +import UnstoppableResolution from '@unstoppabledomains/resolution' +import { getRpcServiceUrl } from 'src/config' + +let unstoppableResolver + +export const getAddressFromUnstoppableDomain = (name: string) => { + if (!unstoppableResolver) { + unstoppableResolver = new UnstoppableResolution({ + blockchain: { + cns: { + url: getRpcServiceUrl(), + }, + }, + }) + } + + return unstoppableResolver.addr(name, 'ETH') +} diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index 6c237aae..0bc42e7f 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -10,8 +10,8 @@ import { FEATURES } from 'src/config/networks/network.d' import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils' -import { isValidEnsName } from 'src/logic/wallets/ethAddresses' -import { getAddressFromENS } from 'src/logic/wallets/getWeb3' +import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses' +import { getAddressFromDomain } from 'src/logic/wallets/getWeb3' import { useTextFieldInputStyle, useTextFieldLabelStyle, @@ -85,8 +85,11 @@ const BaseAddressBookInput = ({ } // ENS-enabled resolve/validation - if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) { - const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue) + if ( + isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) && + (isValidEnsName(normalizedValue) || isValidCryptoDomainName(normalizedValue)) + ) { + const address = await getAddressFromDomain(normalizedValue) const validatedAddress = validateAddress(address) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts index 33cda4bd..354bf199 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts @@ -3,9 +3,9 @@ import createDecorator from 'final-form-calculate' import { ContractSendMethod } from 'web3-eth-contract' import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' -import { getAddressFromENS, getWeb3 } from 'src/logic/wallets/getWeb3' +import { getAddressFromDomain, getWeb3 } from 'src/logic/wallets/getWeb3' import { TransactionReviewType } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review' -import { isValidEnsName } from 'src/logic/wallets/ethAddresses' +import { isValidCryptoDomainName, isValidEnsName } from 'src/logic/wallets/ethAddresses' export const NO_CONTRACT = 'no contract' @@ -14,7 +14,9 @@ export const ensResolver = createDecorator({ updates: { contractAddress: async (contractAddress) => { try { - const resolvedAddress = isValidEnsName(contractAddress) && (await getAddressFromENS(contractAddress)) + const resolvedAddress = + (isValidEnsName(contractAddress) || isValidCryptoDomainName(contractAddress)) && + (await getAddressFromDomain(contractAddress)) if (resolvedAddress) { return resolvedAddress diff --git a/yarn.lock b/yarn.lock index 3c77ac22..c6eea477 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1299,6 +1299,15 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@ensdomains/address-encoder@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@ensdomains/address-encoder/-/address-encoder-0.1.8.tgz#be022a706cfd39327caaca2fa7f59d19ea2787d9" + integrity sha512-0YwXk/cX7G/qM7YHrOybF9Ht7cwR0/XH37yh0cdPdIOPJ6Y839yymyAWGVC7Sl2ESAhqj4hfoHpb2+QllB7KAQ== + dependencies: + bech32 "^1.1.3" + crypto-addr-codec "^0.1.7" + eztz-lib "^0.1.2" + "@eslint/eslintrc@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76" @@ -1345,6 +1354,21 @@ "@ethersproject/properties" "^5.0.3" "@ethersproject/strings" "^5.0.4" +"@ethersproject/abi@^5.0.1": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.9.tgz#738c1c557e56d8f395a5a27caef9b0449bc85a10" + integrity sha512-ily2OufA2DTrxkiHQw5GqbkMSnNKuwZBqKsajtT0ERhZy1r9w2CpW1bmtRMIGzaqQxCdn/GEoFogexk72cBBZQ== + dependencies: + "@ethersproject/address" "^5.0.4" + "@ethersproject/bignumber" "^5.0.7" + "@ethersproject/bytes" "^5.0.4" + "@ethersproject/constants" "^5.0.4" + "@ethersproject/hash" "^5.0.4" + "@ethersproject/keccak256" "^5.0.3" + "@ethersproject/logger" "^5.0.5" + "@ethersproject/properties" "^5.0.3" + "@ethersproject/strings" "^5.0.4" + "@ethersproject/abstract-provider@^5.0.8": version "5.0.8" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.8.tgz#880793c29bfed33dff4c2b2be7ecb9ba966d52c0" @@ -3660,6 +3684,24 @@ "@restless/sanitizers" "^0.2.5" reactive-properties "^0.1.11" +"@unstoppabledomains/resolution@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unstoppabledomains/resolution/-/resolution-1.11.1.tgz#88ac10b195104a396f4700287b9bbbc1a202b07b" + integrity sha512-BLLKRCYg3ouDbG9HlzJS3cn1W1OY5rdCMiFChqLjWUBvRjsNO7KdREa6SqXsWwm7zQZ35HDjX1aRdiypQ50WUA== + dependencies: + "@ensdomains/address-encoder" "0.1.8" + "@ethersproject/abi" "^5.0.1" + bip44-constants "^8.0.5" + bn.js "^4.4.0" + commander "^4.1.1" + content-hash "^2.5.2" + ethereum-ens-network-map "^1.0.2" + hash.js "^1.1.7" + js-sha3 "^0.8.0" + node-fetch "^2.6.0" + optionalDependencies: + dotenv "^8.2.0" + "@walletconnect/client@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.3.2.tgz#8f67ac53bc3f7dfa0cff7a86ea6e43b11178d4f4" @@ -4246,6 +4288,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argv@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab" + integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas= + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -4423,6 +4470,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= + assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" @@ -4562,12 +4614,17 @@ await-semaphore@^0.1.3: resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q== +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.8.0: +aws4@^1.2.1, aws4@^1.8.0: version "1.11.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== @@ -5565,6 +5622,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bech32@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + bfj@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -5575,6 +5637,11 @@ bfj@^7.0.2: hoopy "^0.1.4" tryer "^1.0.1" +big-integer@1.6.36: + version "1.6.36" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" + integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -5612,6 +5679,22 @@ bindings@^1.2.1, bindings@^1.3.0, bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bip39@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.6.0.tgz#9e3a720b42ec8b3fbe4038f1e445317b6a99321c" + integrity sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg== + dependencies: + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + safe-buffer "^5.0.1" + unorm "^1.3.3" + +bip44-constants@^8.0.5: + version "8.0.84" + resolved "https://registry.yarnpkg.com/bip44-constants/-/bip44-constants-8.0.84.tgz#d9576385714fdafee44e83e41a6bd256f58fdc80" + integrity sha512-GihFb+wM+fZmi+UEh4tYdNbw3gsZZ22aKoaCxadhmyzDjXQAZ5x/FZiEw5cTs1y6vNZ94JnrTJOIvDsMSGw+1A== + bip66@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" @@ -5747,6 +5830,13 @@ boolean@^3.0.1: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.2.tgz#df1baa18b6a2b0e70840475e1d93ec8fe75b2570" integrity sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g== +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= + dependencies: + hoek "2.x.x" + bowser@^2.10.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -5927,7 +6017,7 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" -bs58check@^2.1.2: +bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -5991,6 +6081,14 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + buffer@^4.3.0: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -6000,7 +6098,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.5, buffer@^5.2.1, buffer@^5.4.2, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.0.5, buffer@^5.1.0, buffer@^5.2.1, buffer@^5.4.2, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -6265,6 +6363,11 @@ case-sensitive-paths-webpack-plugin@2.3.0, case-sensitive-paths-webpack-plugin@^ resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c= + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -6287,7 +6390,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^1.1.3: +chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -6637,6 +6740,15 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codecov@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/codecov/-/codecov-2.3.1.tgz#7dda945cd58a1f6081025b5b03ee01a2ef20f86e" + integrity sha1-fdqUXNWKH2CBAltbA+4Bou8g+G4= + dependencies: + argv "0.0.2" + request "2.77.0" + urlgrey "0.4.4" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -6700,7 +6812,7 @@ colors@^1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -6731,7 +6843,7 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@^2.19.0, commander@^2.20.0: +commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -7121,6 +7233,26 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= + dependencies: + boom "2.x.x" + +crypto-addr-codec@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/crypto-addr-codec/-/crypto-addr-codec-0.1.7.tgz#e16cea892730178fe25a38f6d15b680cab3124ae" + integrity sha512-X4hzfBzNhy4mAc3UpiXEC/L0jo5E8wAa9unsnA8nNXYzXjCcGk83hfC5avJWCSGT8V91xMnAS9AKMHmjw5+XCg== + dependencies: + base-x "^3.0.8" + big-integer "1.6.36" + blakejs "^1.1.0" + bs58 "^4.0.1" + ripemd160-min "0.0.6" + safe-buffer "^5.2.0" + sha3 "^2.1.1" + crypto-browserify@3.12.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -9043,6 +9175,11 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-ens-network-map@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.2.tgz#4e27bad18dae7bd95d84edbcac2c9e739fc959b9" + integrity sha512-5qwJ5n3YhjSpE6O/WEBXCAb2nagUgyagJ6C0lGUBWC4LjKp/rRzD+pwtDJ6KCiITFEAoX4eIrWOjRy0Sylq5Hg== + ethereum-ens@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/ethereum-ens/-/ethereum-ens-0.8.0.tgz#6d0f79acaa61fdbc87d2821779c4e550243d4c57" @@ -9483,7 +9620,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@~3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -9531,6 +9668,21 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +eztz-lib@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/eztz-lib/-/eztz-lib-0.1.2.tgz#6e00037f85862f06f9997108302cfa673fe2e703" + integrity sha512-77pr673PMEPvbcHylbRuHwd5P5D3Myc3VC54AC2ovaKTdIVbW/vc6HIrf8D44/a5q9eZzlUpunfKrkBug02OBw== + dependencies: + bignumber.js "^7.2.1" + bip39 "^2.5.0" + bs58check "^2.1.1" + buffer "^5.1.0" + codecov "^2.3.1" + libsodium-wrappers "^0.5.4" + pbkdf2 "^3.0.14" + xhr2 "^0.1.4" + xmlhttprequest "^1.8.0" + fake-merkle-patricia-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" @@ -9959,6 +10111,15 @@ form-data@^2.3.1: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE= + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -10152,6 +10313,20 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= + dependencies: + is-property "^1.0.0" + gensync@^1.0.0-beta.1: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -10499,6 +10674,16 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + integrity sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0= + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + har-validator@~5.1.3: version "5.1.5" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" @@ -10634,6 +10819,16 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + hdkey@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-2.0.1.tgz#0a211d0c510bfc44fa3ec9d44b13b634641cad74" @@ -10689,6 +10884,11 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= + hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -10884,6 +11084,15 @@ http-proxy@^1.17.0: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -11534,6 +11743,22 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== + +is-my-json-valid@^2.12.4: + version "2.20.5" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz#5eca6a8232a687f68869b7361be1612e7512e5df" + integrity sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A== + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + is-negative-zero@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -11617,6 +11842,11 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= + is-regex@^1.0.4, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" @@ -12553,6 +12783,11 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonpointer@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" + integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg== + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -12874,6 +13109,18 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libsodium-wrappers@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.5.4.tgz#0059bf219c3a37b228f823342399b1607a4c5ba4" + integrity sha512-dAYsfQgh6XwR4I65y7T5qDgb2XNKDpzXEXz229sDplaJfnAuIBTHBYlQ44jL5DIS4cCUspE3+g0kF4/Xe8286A== + dependencies: + libsodium "0.5.4" + +libsodium@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.5.4.tgz#874413435ee40c512dd59ea6db9b75970f8aec02" + integrity sha512-s1TQ2V23JvGby1gnCQEQncTNTGck/rtJPPA8c0TiBo9z9TpT4eUk5zThte8H1TkdoKQznneqZqyoqdrwu2btWw== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -13453,7 +13700,7 @@ mime-db@1.45.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.7: version "2.1.28" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== @@ -13997,6 +14244,11 @@ node-releases@^1.1.29, node-releases@^1.1.3, node-releases@^1.1.61, node-release resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== +node-uuid@~1.4.7: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= + nofilter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e" @@ -14139,6 +14391,11 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -14766,7 +15023,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.17, pbkdf2@^3.0.3: +pbkdf2@^3.0.14, pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.1" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== @@ -15925,7 +16182,7 @@ punycode@2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" integrity sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0= -punycode@^1.2.4: +punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -15984,6 +16241,11 @@ qs@^6.5.1, qs@^6.6.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + integrity sha1-51vV9uJoEioqDgvaYwslUMFmUCw= + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -16995,6 +17257,32 @@ request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" +request@2.77.0: + version "2.77.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.77.0.tgz#2b00d82030ededcc97089ffa5d8810a9c2aa314b" + integrity sha1-KwDYIDDt7cyXCJ/6XYgQqcKqMUs= + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + request@^2.79.0, request@^2.85.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -17211,6 +17499,11 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +ripemd160-min@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/ripemd160-min/-/ripemd160-min-0.0.6.tgz#a904b77658114474d02503e819dcc55853b67e62" + integrity sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A== + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -17706,6 +17999,13 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +sha3@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/sha3/-/sha3-2.1.3.tgz#ab05b841b2bce347765db31f57fe2a3134b9fb48" + integrity sha512-Io53D4o9qOmf3Ow9p/DoGLQiQHhtuR0ulbyambvRSG+OX5yXExk2yYfvjHtb7AtOyk6K6+sPeK/qaowWc/E/GA== + dependencies: + buffer "5.6.0" + shallow-clone@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" @@ -17911,6 +18211,13 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= + dependencies: + hoek "2.x.x" + sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -18417,6 +18724,11 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" +stringstream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" + integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== + strip-ansi@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" @@ -19077,6 +19389,13 @@ tough-cookie@^3.0.1: psl "^1.1.28" punycode "^2.1.1" +tough-cookie@~2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== + dependencies: + punycode "^1.4.1" + tr46@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" @@ -19203,6 +19522,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + integrity sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us= + tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" @@ -19441,6 +19765,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unorm@^1.3.3: + version "1.6.0" + resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.6.0.tgz#029b289661fba714f1a9af439eb51d9b16c205af" + integrity sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -19553,6 +19882,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urlgrey@0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f" + integrity sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8= + usb-detection@^4.10.0: version "4.10.0" resolved "https://registry.yarnpkg.com/usb-detection/-/usb-detection-4.10.0.tgz#0f8a3b8965a5e4e7fbee1667971ca97e455ed11f" @@ -21191,6 +21525,11 @@ xhr2-cookies@1.1.0: dependencies: cookiejar "^2.1.1" +xhr2@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" + integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8= + xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" @@ -21211,7 +21550,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmlhttprequest@1.8.0: +xmlhttprequest@1.8.0, xmlhttprequest@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= From 3415b73ef06b441dc75e90003aedfcec39cb43a0 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Fri, 29 Jan 2021 20:24:00 +0400 Subject: [PATCH 15/20] Update WalletConnect app url (#1822) Co-authored-by: Daniel Sanchez --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 9203b45e..b6bd98aa 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -113,7 +113,7 @@ export const staticAppsList: Array = [ }, // Wallet-Connect { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmWwSuByB3B3hLU5ita3RQgiSEDYtBr5LjjDCRGb8YqLKF`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT3VxxfFtfEcvq8AeaoFAyUGxePRa2zisvnxLTrQXU5Uf`, disabled: false, networks: [ ETHEREUM_NETWORK.MAINNET, From 2ea67b51374fcb0d9f867881f3438b4ccefe5765 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Jan 2021 17:21:42 -0300 Subject: [PATCH 16/20] Set transaction parameters manually - Advanced options (#1760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Makes getGasEstimationTxResponse exportable * Add check for failing txs on approveTxModal * Adds styles for reviewTx * Adds useTxSuccessCheck hook * Remove hook * Adds checkIfTxWillFail function * Uses checkIfTxWillFailAsync on reviewTx modal * Improves approveTx modal * Add check for failing transaction in contract interaction modal * Add check for reviewCollectible * Fix check on sendFunds reviewTx * Adds styling for contractInteraction modal * Fix gas calculation for native token transfers * Remove logs * Rename estimateDataGasCosts to parseRequiredTxGasResponse Adds getPreValidatedSignatures Refactor estimateTxGasCosts Refactor checkIfExecTxWillFail * Refactor checkIfExecTxWill usage * Refactor checkIfTxWillFailAsync in ReviewTx * Use getPreValidatedSignatures in createTransaction() * Refactor estimateTxGasCosts Rename estimateSafeTxGas to estimateExecTransactionGas * Refactor ReviewTx: extract useEffects to hooks * Remove unnecessary gas transfer amount * Refactor estimateTxGasCosts: extract checkIfTxIsExecution and estimateTxGas * Fix tx amount Remove console log * Moves useCheckIfTransactionWillFail to logic/hooks folder * Replaces useEffect usage with useCheckIfTransactionWillFail hook Also fix how some modals fetch the safeAddress * Improves modal's wording * Remove comment * Fix error parsing the cancel transaction error message from GETH nodes * Remove useCheckIfTransactionWillFail Adds useEstimateTransactionGas Renames estimateTxGas to estimateTransactionGas Removes estimateTxGasCosts Removes checkIfExecTxWillFail * Replace useCheckIfTransactionWillFail from modals with useEstimateTransactionGas * Replace estimateGasCosts from every review tx modal with useEstimateTransactionGas * Replace estimateGasCosts from every review tx modal with useEstimateTransactionGas * Extract isExecution calculation to useEstimateTransactionGas * Creates TransactionFailText * Uses TransactionFailText in the review modals * Remove unnecessary styles * Fix imports * Remove css * Fix missing style * Remove duplicated function * Fix modal height * Fix wrong selector usage * Fix missing null check on cancel tx confirmations * Add guard for CLOSE_SNACKBAR action when tx was already dismissed * Improves useEstimateTransactionGas in review custom tx and contract interaction review * Fix hook dependency * Add active screen types in send modal * SafeInfo: replace AddressInfo by EthHashInfo * Fix review replace/remove/add owner modals styling * AdvancedOptions without funcionality * change button text * fix margins and texts * fix close button margin * rename ReviewTx by ReviewSendFundsTx * safe params: partial implementation * make use of useTransactionParameters hook * Refactor response of useEstimateTransactionGas * Remove safeAddress as param to the useEstimateTransactionGas * Improves how threshold is obtained in useEstimateTransactionGas.tsx * Rename gasCostHumanReadable to gasCostFormatted * Add operation to useEstimateTransactionGas * Refactor ConfirmTransactionModal to use useEstimateTransactionGas * Refactor proccessTransaction to use getPreValidatedSignatures method * Fix default export of ApproveTxModal * Rename estimateExecTransactionGas to estimateGasForTransactionCreation Remove estimateTransactionGas from gas.ts * form, accordion, validations * Make estimateGasForTransactionCreation throw error instead of 0 gas * Adds estimateGasForTransactionExecution and estimateGasForTransactionApproval to gas.ts * Move estimateTransactionGas to useEstimateTransactionGas Refactors useEstimateTransactionGas to return isCreation and isOffChainSignature * Add in contractInteraction * review custom TX * review collectible * Type and refactor generateSignaturesFromTxConfirmations Moves getPreValidatedSignatures to safeTxSigner.ts * Uses confirmations to estimateGasForTransactionExecution * Adds TransactionFeesText component Uses TransactionFeesText on ApproveTxModal * Update text * Update text * Remove unnecessary condition * Pass more parameters to estimateGasForTransactionExecution * Removes unnecessary parameter in getNewTxNonce * Moves checkIfOffChainSignatureIsPossible to safeTxSigner.ts * rename AdvancedOptions and add parameters in add owner * replace owner - remove owner * Fix check for null confirmations * Uses checkIfOffChainSignatureIsPossible on createTransaction.ts * Policies * Move TransactionFailText inside TransactionFees component * spending limit * fix textfields gap and margins * fix margins, text size and textfields disposition * Remove unnecessary awaits * Update Accordion comp * Pass safeTxGas to useEstimateTransactionGas.tsx Improves how we use default params * Fix gas iteration on estimateGasForTransactionExecution * Fix estimateGasForTransactionExecution calculation * Fix generateSignaturesFromTxConfirmations calculation * Remove unnecessary Promise and await * Fix estimateGasForTransactionExecution for preApproving owner case * Improve logging * Remove log * Fix typo * merge fixes * Uses operation in useEstimateTransactionGas * Uses operation in useEstimateTransactionGas * add txParameters in ApproveTx * useEstimateTransactionGas add gasPriceFormatted * reviewSendFundsTx gasFormatted * add gasLimit to useEstimateTransactionGas * Files movement - EditParameters refactor * tx parameters in creation * set correct gasPrice * createTx from contractInteraction * Fix gas estimation for transaction creation * disable button while calculating gas * Add owner - reactor and creation options * Add owner: remove unused code * Threshold settings * fix modal ui bug (tx) * fix modal ui bug (settings) * fix modal ui bug (Settings: Policies ) * fix ui bug modal (Spending Limit) * (Fix) - Calculates gas for SpendingLimit transactions (#1773) * Bug: Use link tag instead of javascript navigation in apps list (#1770) * Use list instead of programmable navigation * add declaration for styled-components theme * (Fix) - Calculates gas for SpendingLimit transactions (#1773) * Calculates gas for spendingLimit transactions * Adds TransactionFees component inside UpdateSafeModal * Fix send collectible gas calculation Co-authored-by: Mikhail Mikheev * speding limits * Remove default exports for spendingLimits * Fix warning in TxParametersDetail * Extracts calculateSpendingLimitsTxData logic and add types * Remove txParameters from setSpendingLimitMultiSendTx/spendingLimitMultiSendTx Adds log for transaction creation success * Refactor handleSubmit, now uses calculateSpendingLimitsTxData to calculate the spendingLimit transaction data before submitting Also calculateSpendingLimitsTxData is used to estimate the amount of gas that the transaction will cost * disable parameters when needed * fix parametersStatus condition * Some fixes * more fixes * send directly when tx nonce == safeNonce * fix tests * parameters in replace owner and remove owner * Fix gas estimation issues for Metamask * FIx Edit transaction parameters src route * Remove default exports RemoveModuleModal * Adds advanced parameters in RemoveModuleModal * Fix css styling of UpdateSafeModal * Adds EditableTxParameters within UpdateSafeModal * Fix warning in for compact parameter * Add advanced settings for RemoveLimitModal.tsx * Fix txNonce usage in RemoveModuleModal.tsx * Remove logs * Fix txNonce param in RemoveLimitModal.tsx * Remove TODOS for refactoring default case of getParametersStatus in EditableTxParameters * Add advanced parameters to Safe apps modal * Force gasPrice and safeTxGas 0 for cancelling transaction * Invert validation of safeTxGas > ethGasLimit, safeTxGas should be < than ethGasLimit * Add missing validation for approveTxModal submit button * Fix checkIfTxIsApproveAndExecution method Add tests * Remove duplication of gas value for calculateGasOf * Fix gas estimation for execution & approval * Adds EXTRA_NODE_GAS for the gasLimit calculation * Revert duplicate of web3.eth.estimateGas * Revert fix of execution & approval * Adds closeEditModalCallback in EditableTxParameters * Accept manual gas price in useEstimateTransactionGas.tsx * Use manualPrice in sendFunds gas estimation * Add safeTxGas estimation to Editable TX Parameters * Set safeTx gasPrice to 0 on tx creation * Refactor useTransactionParameters with initialValues support * Refactor EditableTxParameters.tsx, fixs changing gasLimit or gasPrice * Fix safeTxGas * Add naming to minimum gas required for a transaction * Remove log * Fix nonce calculation for cancel transaction * Update gas estimation to be more precise and avoid reverts * Check that only the creator of the transaction or the executor could modify the advanced parameters * Add validation to submit buttons on modals when the gas estimation is loading * Fix advanced parameters display in change threshold * Fix wrong commit Co-authored-by: Agustin Pane Co-authored-by: Daniel Sanchez Co-authored-by: Fernando Co-authored-by: Agustín Longoni Co-authored-by: Mikhail Mikheev --- package.json | 2 +- src/components/Modal/index.tsx | 9 +- src/components/ModalTitle/index.tsx | 44 ++- src/components/forms/validator.ts | 4 + .../useEstimateTransactionGas.test.ts | 51 ++- src/logic/hooks/useEstimateTransactionGas.tsx | 43 ++- .../store/actions/__tests__/utils.test.ts | 41 ++- .../safe/store/actions/createTransaction.ts | 18 +- .../safe/store/actions/processTransaction.ts | 15 +- src/logic/safe/store/actions/utils.ts | 32 +- src/logic/safe/transactions/gas.ts | 6 +- src/logic/safe/utils/spendingLimits.ts | 6 +- src/logic/wallets/ethTransactions.ts | 9 + .../components/ConfirmTransactionModal.tsx | 193 +++++++---- .../Balances/SendModal/SafeInfo/index.tsx | 43 ++- .../components/Balances/SendModal/index.tsx | 35 +- .../ContractInteraction/Review/index.tsx | 247 ++++++++------ .../ReviewCustomTx/index.tsx | 214 ++++++------ .../screens/ReviewCollectible/index.tsx | 187 +++++----- .../{ReviewTx => ReviewSendFundsTx}/index.tsx | 237 +++++++------ .../{ReviewTx => ReviewSendFundsTx}/style.ts | 0 .../screens/SendCollectible/style.ts | 2 +- .../SendModal/screens/SendFunds/index.tsx | 12 +- .../SendModal/screens/SendFunds/style.ts | 4 +- .../Settings/Advanced/ModulesTable.tsx | 6 +- .../Settings/Advanced/RemoveModuleModal.tsx | 181 ++++++---- .../components/Settings/Advanced/index.tsx | 6 +- .../ManageOwners/AddOwnerModal/index.tsx | 30 +- .../AddOwnerModal/screens/Review/index.tsx | 263 +++++++------- .../AddOwnerModal/screens/Review/style.ts | 4 +- .../ManageOwners/RemoveOwnerModal/index.tsx | 12 +- .../RemoveOwnerModal/screens/Review/index.tsx | 284 ++++++++------- .../ManageOwners/ReplaceOwnerModal/index.tsx | 15 +- .../screens/Review/index.tsx | 323 ++++++++++-------- .../LimitsTable/SpentVsAmount.tsx | 4 +- .../SpendingLimit/LimitsTable/index.tsx | 8 +- .../SpendingLimit/NewLimitModal/Review.tsx | 281 ++++++++++----- .../SpendingLimit/NewLimitModal/index.tsx | 8 +- .../Settings/SpendingLimit/NewLimitSteps.tsx | 4 +- .../SpendingLimit/RemoveLimitModal.tsx | 132 +++++-- .../Settings/SpendingLimit/index.tsx | 10 +- .../Settings/SpendingLimit/style.ts | 3 +- .../ChangeThreshold/index.tsx | 174 ++++++---- .../ChangeThreshold/style.ts | 4 +- .../Settings/ThresholdSettings/index.tsx | 42 ++- .../Settings/UpdateSafeModal/index.tsx | 137 +++++--- .../Settings/UpdateSafeModal/style.ts | 3 - src/routes/safe/components/Settings/index.tsx | 4 +- .../ExpandedTx/ApproveTxModal/index.tsx | 184 ++++++---- .../ExpandedTx/ApproveTxModal/style.ts | 4 +- .../ExpandedTx/RejectTxModal/index.tsx | 134 +++++--- .../ExpandedTx/RejectTxModal/style.ts | 4 +- .../TxsTable/ExpandedTx/index.tsx | 26 +- .../helpers/EditTxParametersForm/index.tsx | 240 +++++++++++++ .../helpers/EditTxParametersForm/style.ts | 45 +++ .../helpers/EditableTxParameters.tsx | 81 +++++ .../helpers/TxParametersDetail/index.tsx | 156 +++++++++ .../components/Transactions/helpers/utils.ts | 14 + .../hooks/useTransactionParameters.ts | 110 ++++++ .../transactions/__tests__/utils.test.ts | 6 +- yarn.lock | 35 +- 61 files changed, 2969 insertions(+), 1462 deletions(-) rename src/routes/safe/components/Balances/SendModal/screens/{ReviewTx => ReviewSendFundsTx}/index.tsx (50%) rename src/routes/safe/components/Balances/SendModal/screens/{ReviewTx => ReviewSendFundsTx}/style.ts (100%) create mode 100644 src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx create mode 100644 src/routes/safe/components/Transactions/helpers/EditTxParametersForm/style.ts create mode 100644 src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx create mode 100644 src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx create mode 100644 src/routes/safe/components/Transactions/helpers/utils.ts create mode 100644 src/routes/safe/container/hooks/useTransactionParameters.ts diff --git a/package.json b/package.json index cc73f75a..eb592303 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.2", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#2e7574f", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.38.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 69ff6ecc..d9a2ea46 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -9,15 +9,14 @@ const useStyles = makeStyles( createStyles({ root: { alignItems: 'center', - justifyContent: 'center', + flexDirection: 'column', display: 'flex', overflowY: 'scroll', }, paper: { - position: 'absolute', - top: '120px', + position: 'relative', + top: '68px', width: '500px', - height: '580px', borderRadius: sm, backgroundColor: '#ffffff', boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)', @@ -62,7 +61,7 @@ const GnoModal = ({ onClose={handleClose} open={open} > -
{children}
+
{children}
) } diff --git a/src/components/ModalTitle/index.tsx b/src/components/ModalTitle/index.tsx index 707680ae..f2af9051 100644 --- a/src/components/ModalTitle/index.tsx +++ b/src/components/ModalTitle/index.tsx @@ -1,8 +1,11 @@ import React from 'react' import styled from 'styled-components' +import IconButton from '@material-ui/core/IconButton' +import Close from '@material-ui/icons/Close' import Paragraph from 'src/components/layout/Paragraph' -import { lg } from 'src/theme/variables' +import { md, lg } from 'src/theme/variables' +import Row from 'src/components/layout/Row' const StyledParagraph = styled(Paragraph)` && { @@ -18,14 +21,39 @@ const TitleWrapper = styled.div` align-items: center; ` -const ModalTitle = ({ iconUrl, title }: { title: string; iconUrl: string }) => { +const StyledRow = styled(Row)` + padding: ${md} ${lg}; + justify-content: space-between; + box-sizing: border-box; + max-height: 75px; +` + +const StyledClose = styled(Close)` + height: 35px; + width: 35px; +` + +const ModalTitle = ({ + iconUrl, + title, + onClose, +}: { + title: string + iconUrl: string + onClose?: () => void +}): React.ReactElement => { return ( - - {iconUrl && } - - {title} - - + + + {iconUrl && } + + {title} + + + + + + ) } diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index caa7badb..09db0431 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -42,6 +42,10 @@ export const mustBeUrl = (value: string): ValidatorReturnType => { } export const minValue = (min: number | string, inclusive = true) => (value: string): ValidatorReturnType => { + if (!value) { + return undefined + } + if (Number.parseFloat(value) > Number(min) || (inclusive && Number.parseFloat(value) >= Number(min))) { return undefined } diff --git a/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts index 99b83f03..d7a613d7 100644 --- a/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts +++ b/src/logic/hooks/__tests__/useEstimateTransactionGas.test.ts @@ -109,7 +109,26 @@ describe('checkIfTxIsCreation', () => { }) describe('checkIfTxIsApproveAndExecution', () => { - it(`should return true if there is only one confirmation left to reach the safe threshold`, () => { + 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, + ) + + // 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 @@ -119,16 +138,40 @@ describe('checkIfTxIsApproveAndExecution', () => { const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType) // then - expect(result).toBe(true) + expect(result).toBe(false) }) - it(`should return true if the transaction is spendingLimit`, () => { + 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) + const result = checkIfTxIsApproveAndExecution( + 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 = checkIfTxIsApproveAndExecution( + safeThreshold, + transactionConfirmations, + transactionType, + preApprovingOwner, + ) // then expect(result).toBe(true) diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 77bce100..edc07050 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react' + import { estimateGasForTransactionApproval, estimateGasForTransactionCreation, estimateGasForTransactionExecution, + MINIMUM_TRANSACTION_GAS, } from 'src/logic/safe/transactions/gas' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' @@ -15,7 +17,8 @@ import { safeThresholdSelector, } from 'src/logic/safe/store/selectors' import { CALL } from 'src/logic/safe/transactions' -import { providerSelector } from '../wallets/store/selectors' +import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' +import { providerSelector } from 'src/logic/wallets/store/selectors' import { List } from 'immutable' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' @@ -46,8 +49,18 @@ export const checkIfTxIsExecution = ( return false } -export const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean => - txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit') +export const checkIfTxIsApproveAndExecution = ( + threshold: number, + txConfirmations: number, + txType?: string, + preApprovingOwner?: string, +): boolean => { + if (preApprovingOwner) { + return txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit') + } + + return false +} export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean => txConfirmations === 0 && !sameString(txType, 'spendingLimit') @@ -132,6 +145,7 @@ type UseEstimateTransactionGasProps = { operation?: number safeTxGas?: number txType?: string + manualGasPrice?: string } type TransactionGasEstimationResult = { @@ -140,6 +154,8 @@ type TransactionGasEstimationResult = { 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 + gasPriceFormatted: string // Current gas price formatted + gasLimit: string // Minimum gas requited to execute the Tx isExecution: boolean // Returns true if the user will execute the tx or false if it just signs it isCreation: boolean // Returns true if the transaction is a creation transaction isOffChainSignature: boolean // Returns true if offChainSignature is available @@ -154,6 +170,7 @@ export const useEstimateTransactionGas = ({ operation, safeTxGas, txType, + manualGasPrice, }: UseEstimateTransactionGasProps): TransactionGasEstimationResult => { const [gasEstimation, setGasEstimation] = useState({ txEstimationExecutionStatus: EstimationStatus.LOADING, @@ -161,6 +178,8 @@ export const useEstimateTransactionGas = ({ gasCost: '0', gasCostFormatted: '< 0.001', gasPrice: '0', + gasPriceFormatted: '0', + gasLimit: '0', isExecution: false, isCreation: false, isOffChainSignature: false, @@ -179,7 +198,12 @@ export const useEstimateTransactionGas = ({ const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType) const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType) - const approvalAndExecution = checkIfTxIsApproveAndExecution(Number(threshold), txConfirmations?.size || 0, txType) + const approvalAndExecution = checkIfTxIsApproveAndExecution( + Number(threshold), + txConfirmations?.size || 0, + txType, + preApprovingOwner, + ) try { const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) @@ -198,10 +222,12 @@ export const useEstimateTransactionGas = ({ safeTxGas, approvalAndExecution, }) - const gasPrice = await calculateGasPrice() + const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice() + const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei') const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) const gasCostFormatted = formatAmount(gasCost) + const gasLimit = (gasEstimation * 2 + MINIMUM_TRANSACTION_GAS).toString() let txEstimationExecutionStatus = EstimationStatus.SUCCESS @@ -215,6 +241,8 @@ export const useEstimateTransactionGas = ({ gasCost, gasCostFormatted, gasPrice, + gasPriceFormatted, + gasLimit, isExecution, isCreation, isOffChainSignature, @@ -222,7 +250,7 @@ export const useEstimateTransactionGas = ({ } catch (error) { console.warn(error.message) // We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail - const gasEstimation = 10000 + const gasEstimation = MINIMUM_TRANSACTION_GAS const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals) const gasCostFormatted = formatAmount(gasCost) setGasEstimation({ @@ -231,6 +259,8 @@ export const useEstimateTransactionGas = ({ gasCost, gasCostFormatted, gasPrice: '1', + gasPriceFormatted: '1', + gasLimit: '0', isExecution, isCreation, isOffChainSignature: false, @@ -255,6 +285,7 @@ export const useEstimateTransactionGas = ({ safeTxGas, txType, providerName, + manualGasPrice, ]) return gasEstimation diff --git a/src/logic/safe/store/actions/__tests__/utils.test.ts b/src/logic/safe/store/actions/__tests__/utils.test.ts index b11ebcb3..549b93eb 100644 --- a/src/logic/safe/store/actions/__tests__/utils.test.ts +++ b/src/logic/safe/store/actions/__tests__/utils.test.ts @@ -1,6 +1,8 @@ import { getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { getMockedSafeInstance } from 'src/test/utils/safeHelper' +import { NonPayableTransactionObject } from 'src/types/contracts/types' describe('Store actions utils > getNewTxNonce', () => { it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => { @@ -43,13 +45,12 @@ describe('Store actions utils > getNewTxNonce', () => { describe('Store actions utils > shouldExecuteTransaction', () => { it(`should return false if there's a previous tx pending to be executed`, async () => { // Given - const safeInstance = { - methods: { - getThreshold: () => ({ - call: () => Promise.resolve('1'), - }), - }, - } + const safeInstance = getMockedSafeInstance({}) + safeInstance.methods.getThreshold = () => + ({ + call: () => Promise.resolve('1'), + } as NonPayableTransactionObject) + const nonce = '1' const lastTx = { isExecuted: false } as TxServiceModel @@ -62,13 +63,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => { it(`should return false if threshold is greater than 1`, async () => { // Given - const safeInstance = { - methods: { - getThreshold: () => ({ - call: () => Promise.resolve('2'), - }), - }, - } + const safeInstance = getMockedSafeInstance({}) + safeInstance.methods.getThreshold = () => + ({ + call: () => Promise.resolve('2'), + } as NonPayableTransactionObject) + const nonce = '1' const lastTx = { isExecuted: true } as TxServiceModel @@ -81,13 +81,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => { it(`should return true is threshold is 1 and previous tx is executed`, async () => { // Given - const safeInstance = { - methods: { - getThreshold: () => ({ - call: () => Promise.resolve('1'), - }), - }, - } + const safeInstance = getMockedSafeInstance({ nonce: '1' }) + safeInstance.methods.getThreshold = () => + ({ + call: () => Promise.resolve('1'), + } as NonPayableTransactionObject) + const nonce = '1' const lastTx = { isExecuted: true } as TxServiceModel diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 0e318e33..0efccdae 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -39,6 +39,7 @@ import { PayableTx } from 'src/types/contracts/types.d' 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' export interface CreateTransactionArgs { navigateToTransactionsTab?: boolean @@ -51,6 +52,7 @@ export interface CreateTransactionArgs { txNonce?: number | string valueInWei: string safeTxGas?: number + ethParameters?: Pick } type CreateTransactionAction = ThunkAction, AppReduxState, DispatchReturn, AnyAction> @@ -70,6 +72,7 @@ const createTransaction = ( navigateToTransactionsTab = true, origin = null, safeTxGas: safeTxGasArg, + ethParameters, }: CreateTransactionArgs, onUserConfirm?: ConfirmEventHandler, onError?: ErrorEventHandler, @@ -86,7 +89,8 @@ const createTransaction = ( const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state) const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const lastTx = await getLastTx(safeAddress) - const nonce = txNonce ? txNonce.toString() : await getNewTxNonce(lastTx, safeInstance) + const nextNonce = await getNewTxNonce(lastTx, safeInstance) + const nonce = txNonce ? txNonce.toString() : nextNonce const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const safeVersion = await getCurrentSafeVersion(safeInstance) let safeTxGas @@ -120,7 +124,6 @@ const createTransaction = ( sigs, } const safeTxHash = generateSafeTxHash(safeAddress, txArgs) - try { if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) { const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) @@ -137,11 +140,12 @@ const createTransaction = ( } const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash) - const sendParams: PayableTx = { from, value: 0 } - - // if not set owner management tests will fail on ganache - if (process.env.NODE_ENV === 'test') { - sendParams.gas = '7000000' + const sendParams: PayableTx = { + from, + value: 0, + gas: ethParameters?.ethGasLimit, + gasPrice: ethParameters?.ethGasPriceInGWei, + nonce: ethParameters?.ethNonce, } const txToMock: TxToMock = { diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index a8a352bf..5062e487 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -23,8 +23,10 @@ import { AppReduxState } from 'src/store' import { getErrorMessage } from 'src/test/utils/ethereumErrors' import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions' import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { Dispatch, DispatchReturn } from './types' +import { PayableTx } from 'src/types/contracts/types' interface ProcessTransactionArgs { approveAndExecute: boolean @@ -32,6 +34,7 @@ interface ProcessTransactionArgs { safeAddress: string tx: Transaction userAddress: string + ethParameters?: Pick thresholdReached: boolean } @@ -43,6 +46,7 @@ export const processTransaction = ({ safeAddress, tx, userAddress, + ethParameters, thresholdReached, }: ProcessTransactionArgs): ProcessTransactionAction => async ( dispatch: Dispatch, @@ -107,11 +111,12 @@ export const processTransaction = ({ transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash) - const sendParams: any = { from, value: 0 } - - // if not set owner management tests will fail on ganache - if (process.env.NODE_ENV === 'test') { - sendParams.gas = '7000000' + const sendParams: PayableTx = { + from, + value: 0, + gas: ethParameters?.ethGasLimit, + gasPrice: ethParameters?.ethGasPriceInGWei, + nonce: ethParameters?.ethNonce, } const txToMock: TxToMock = { diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index 828c6e92..2943bd31 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -26,17 +26,31 @@ export const shouldExecuteTransaction = async ( nonce: string, lastTx: TxServiceModel | null, ): Promise => { - const threshold = await safeInstance.methods.getThreshold().call() + const safeNonce = (await safeInstance.methods.nonce().call()).toString() + const thresholdAsString = await safeInstance.methods.getThreshold().call() + const threshold = Number(thresholdAsString) - // Tx will automatically be executed if and only if the threshold is 1 - if (Number.parseInt(threshold) === 1) { - const isFirstTransaction = Number.parseInt(nonce) === 0 - // if the previous tx is not executed, it's delayed using the approval mechanisms, - // once the previous tx is executed, the current tx will be available to be executed - // by the user using the exec button. - const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted + // Needs to collect owners signatures + if (threshold > 1) { + return false + } - return isFirstTransaction || !!canExecuteCurrentTransaction + // Allow first tx. + if (Number(nonce) === 0) { + return true + } + + // Allow if nonce === safeNonce and threshold === 1 + if (nonce === safeNonce) { + return true + } + + // If the previous tx is not executed or the different between lastTx.nonce and nonce is > 1 + // it's delayed using the approval mechanisms. + // Once the previous tx is executed, the current tx will be available to be executed + // by the user using the exec button. + if (lastTx) { + return lastTx.isExecuted && lastTx.nonce + 1 === Number(nonce) } return false diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index 3edb1b90..bcee4537 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -10,6 +10,9 @@ import axios from 'axios' import { getRpcServiceUrl, usesInfuraRPC } from 'src/config' import { sameString } from 'src/utils/strings' +// 21000 - additional gas costs (e.g. base tx costs, transfer costs) +export const MINIMUM_TRANSACTION_GAS = 21000 + // Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount const parseRequiredTxGasResponse = (data: string): number => { const reducer = (accumulator, currentValue) => { @@ -213,8 +216,7 @@ export const estimateGasForTransactionCreation = async ( data: estimateData, }) - // 21000 - additional gas costs (e.g. base tx costs, transfer costs) - const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000 + const dataGasEstimation = parseRequiredTxGasResponse(estimateData) const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000] return await calculateMinimumGasForTransaction( diff --git a/src/logic/safe/utils/spendingLimits.ts b/src/logic/safe/utils/spendingLimits.ts index c23d639e..1c20e890 100644 --- a/src/logic/safe/utils/spendingLimits.ts +++ b/src/logic/safe/utils/spendingLimits.ts @@ -180,7 +180,7 @@ type SpendingLimitTxParams = { resetTimeMin: number resetBaseMin: number } - safeAddress + safeAddress: string } export const setSpendingLimitTx = ({ @@ -190,7 +190,7 @@ export const setSpendingLimitTx = ({ const spendingLimitContract = getSpendingLimitContract() const { nativeCoin } = getNetworkInfo() - return { + const txArgs: CreateTransactionArgs = { safeAddress, to: SPENDING_LIMIT_MODULE_ADDRESS, valueInWei: ZERO_VALUE, @@ -206,6 +206,8 @@ export const setSpendingLimitTx = ({ operation: CALL, notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, } + + return txArgs } export const setSpendingLimitMultiSendTx = (args: SpendingLimitTxParams): MultiSendTx => { diff --git a/src/logic/wallets/ethTransactions.ts b/src/logic/wallets/ethTransactions.ts index 87a702f2..bfedd1a0 100644 --- a/src/logic/wallets/ethTransactions.ts +++ b/src/logic/wallets/ethTransactions.ts @@ -66,3 +66,12 @@ export const calculateGasOf = async (txConfig: { return Promise.reject(err) } } + +export const getUserNonce = async (userAddress: string): Promise => { + const web3 = getWeb3() + try { + return await web3.eth.getTransactionCount(userAddress, 'pending') + } catch (error) { + return Promise.reject(error) + } +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 02c371ae..b2bf1d62 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState, useMemo } from 'react' -import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components' +import React, { useEffect, useMemo, useState } from 'react' +import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components' import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' import styled from 'styled-components' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import AddressInfo from 'src/components/AddressInfo' import DividerLine from 'src/components/DividerLine' @@ -25,8 +25,14 @@ import GasEstimationInfo from './GasEstimationInfo' import { getNetworkInfo } from 'src/config' import { TransactionParams } from './AppFrame' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { safeThresholdSelector } from 'src/logic/safe/store/selectors' +import Modal from 'src/components/Modal' import Row from 'src/components/layout/Row' +import Hairline from 'src/components/layout/Hairline' import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { md, lg } from 'src/theme/variables' const isTxValid = (t: Transaction): boolean => { if (!['string', 'number'].includes(typeof t.value)) { @@ -71,6 +77,12 @@ const StyledTextBox = styled(TextBox)` const Container = styled.div` max-width: 480px; + padding: ${md} ${lg}; +` + +const ModalFooter = styled(Row)` + padding: ${md} ${lg}; + justify-content: center; ` type OwnProps = { @@ -101,11 +113,14 @@ export const ConfirmTransactionModal = ({ onTxReject, }: OwnProps): React.ReactElement | null => { const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) + const threshold = useSelector(safeThresholdSelector) || 1 const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs]) const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs]) const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs]) const { + gasLimit, + gasPriceFormatted, gasEstimation, isOffChainSignature, isCreation, @@ -139,6 +154,8 @@ export const ConfirmTransactionModal = ({ onClose() } + const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') + const confirmTransactions = async () => { await dispatch( createTransaction( @@ -161,80 +178,108 @@ export const ConfirmTransactionModal = ({ const areTxsMalformed = txs.some((t) => !isTxValid(t)) - const body = areTxsMalformed ? ( - <> - - - Transaction error - - - This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this - Safe App for more information. - - - ) : ( - - - - {txs.map((tx, index) => ( - - } title={`Transaction ${index + 1}`}> - + const body = areTxsMalformed + ? () => ( + <> + + + Transaction error + + + This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of + this Safe App for more information. + + + ) + : (txParameters, toggleEditMode) => { + return ( + + + + {txs.map((tx, index) => ( + + } title={`Transaction ${index + 1}`}> + +
+ Value +
+ Ether + + {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} + +
+
+
+ Data (hex encoded)* + {tx.data} +
+
+
+
+ ))} + + {params?.safeTxGas && (
- Value -
- Ether - - {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} - -
+ SafeTxGas + {params?.safeTxGas} +
-
- Data (hex encoded)* - {tx.data} -
-
-
-
- ))} - - {params?.safeTxGas && ( -
- SafeTxGas - {params?.safeTxGas} - -
- )} - - - -
- ) + )} + + {/* Tx Parameters */} + + + + + + ) + } return ( - } - body={body} - footer={ - - } - onClose={handleTxRejection} - /> + + + {(txParameters, toggleEditMode) => ( + <> + + + + + {body(txParameters, toggleEditMode)} + + + + + + + )} + + ) } diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index d2b49300..0f544e04 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -1,12 +1,49 @@ import React from 'react' import { useSelector } from 'react-redux' +import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' -import AddressInfo from 'src/components/AddressInfo' +import { getExplorerInfo, getNetworkInfo } from 'src/config' import { safeSelector } from 'src/logic/safe/store/selectors' +import Paragraph from 'src/components/layout/Paragraph' +import Bold from 'src/components/layout/Bold' +import { border, xs } from 'src/theme/variables' +import Block from 'src/components/layout/Block' +const { nativeCoin } = getNetworkInfo() -const SafeInfo = () => { +const StyledBlock = styled(Block)` + font-size: 12px; + line-height: 1.08; + letter-spacing: -0.5; + background-color: ${border}; + width: fit-content; + padding: 5px 10px; + margin-top: ${xs}; + margin-left: 40px; + border-radius: 3px; +` + +const SafeInfo = (): React.ReactElement => { const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {} - return + + return ( + <> + + {ethBalance && ( + + + Balance: {`${ethBalance} ${nativeCoin.symbol}`} + + + )} + + ) } export default SafeInfo diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 71e6823e..84876785 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -8,7 +8,7 @@ import { CollectibleTx } from './screens/ReviewCollectible' import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx' import { ContractInteractionTx } from './screens/ContractInteraction' import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx' -import { ReviewTxProp } from './screens/ReviewTx' +import { ReviewTxProp } from './screens/ReviewSendFundsTx' import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d' import { SendCollectibleTxInfo } from './screens/SendCollectible' @@ -20,7 +20,7 @@ const SendCollectible = React.lazy(() => import('./screens/SendCollectible')) const ReviewCollectible = React.lazy(() => import('./screens/ReviewCollectible')) -const ReviewTx = React.lazy(() => import('./screens/ReviewTx')) +const ReviewSendFundsTx = React.lazy(() => import('./screens/ReviewSendFundsTx')) const ContractInteraction = React.lazy(() => import('./screens/ContractInteraction')) @@ -46,8 +46,19 @@ const useStyles = makeStyles({ }, }) +export type TxType = + | 'chooseTxType' + | 'sendFunds' + | 'sendFundsReviewTx' + | 'contractInteraction' + | 'contractInteractionReview' + | 'reviewCustomTx' + | 'sendCollectible' + | 'reviewCollectible' + | '' + type Props = { - activeScreenType: string + activeScreenType: TxType isOpen: boolean onClose: () => void recipientAddress?: string @@ -64,7 +75,7 @@ const SendModal = ({ tokenAmount, }: Props): React.ReactElement => { const classes = useStyles() - const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') + const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') const [tx, setTx] = useState({}) const [isABI, setIsABI] = useState(true) @@ -77,7 +88,7 @@ const SendModal = ({ const scalableModalSize = activeScreen === 'chooseTxType' const handleTxCreation = (txInfo: SendCollectibleTxInfo) => { - setActiveScreen('reviewTx') + setActiveScreen('sendFundsReviewTx') setTx(txInfo) } @@ -118,18 +129,21 @@ const SendModal = ({ {activeScreen === 'chooseTxType' && ( )} + {activeScreen === 'sendFunds' && ( )} - {activeScreen === 'reviewTx' && ( - setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} /> + + {activeScreen === 'sendFundsReviewTx' && ( + setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} /> )} + {activeScreen === 'contractInteraction' && isABI && ( )} + {activeScreen === 'contractInteractionReview' && isABI && tx && ( setActiveScreen('contractInteraction')} tx={tx} /> )} + {activeScreen === 'contractInteraction' && !isABI && ( )} + {activeScreen === 'reviewCustomTx' && ( setActiveScreen('contractInteraction')} tx={tx as CustomTx} /> )} + {activeScreen === 'sendCollectible' && ( )} + {activeScreen === 'reviewCollectible' && ( void onPrev: () => void + onEditTxParameters: () => void tx: TransactionReviewType + txParameters: TxParameters } const { nativeCoin } = getNetworkInfo() @@ -46,40 +55,47 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE const classes = useStyles() const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [txParameters, setTxParameters] = useState<{ + + const [txInfo, setTxInfo] = useState<{ txRecipient: string txData: string txAmount: string }>({ txData: '', txAmount: '', txRecipient: '' }) const { + gasLimit, + gasEstimation, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, isOffChainSignature, isCreation, } = useEstimateTransactionGas({ - txRecipient: txParameters?.txRecipient, - txAmount: txParameters?.txAmount, - txData: txParameters?.txData, + txRecipient: txInfo?.txRecipient, + txAmount: txInfo?.txAmount, + txData: txInfo?.txData, }) useEffect(() => { - setTxParameters({ + setTxInfo({ txRecipient: tx.contractAddress as string, txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0', txData: tx.data ? tx.data.trim() : '', }) }, [tx.contractAddress, tx.value, tx.data, safeAddress]) - const submitTx = async () => { - if (safeAddress && txParameters) { + const submitTx = async (txParameters: TxParameters) => { + if (safeAddress && txInfo) { dispatch( createTransaction({ safeAddress, - to: txParameters?.txRecipient, - valueInWei: txParameters?.txAmount, - txData: txParameters?.txData, + to: txInfo?.txRecipient, + valueInWei: txInfo?.txAmount, + txData: txInfo?.txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, }), ) @@ -90,105 +106,118 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE } return ( - <> -
- - - - - Contract Address - - - - - - - - Value - - - - - Ether - - - - - {tx.value || 0} - {' ' + nativeCoin.name} + + {(txParameters, toggleEditMode) => ( + <> +
+ + + + + Contract Address - - - - - - Method - - - - - {tx.selectedMethod?.name} - - - {tx.selectedMethod?.inputs?.map(({ name, type }, index) => { - const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index) - const value: string = getValueFromTxInputs(key, type, tx) - - return ( - - - - {name} ({type}) - - - - - {value} - - - - ) - })} - - - Data (hex encoded) - - - - - - {tx.data} - - - - - - - - - - - - + + + + + + Value + + + + + Ether + + + + + {tx.value || 0} + {' ' + nativeCoin.name} + + + + + + + Method + + + + + {tx.selectedMethod?.name} + + + {tx.selectedMethod?.inputs?.map(({ name, type }, index) => { + const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index) + const value: string = getValueFromTxInputs(key, type, tx) + + return ( + + + + {name} ({type}) + + + + + {value} + + + + ) + })} + + + Data (hex encoded) + + + + + + {tx.data} + + + + + {/* Tx Parameters */} + + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index cda1aead..9a0b17fe 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' +import { ExplorerButton } from '@gnosis.pm/safe-react-components' import { getExplorerInfo, getNetworkInfo } from 'src/config' import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' @@ -22,13 +23,14 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { sm } from 'src/theme/variables' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' import ArrowDown from '../../assets/arrow-down.svg' - import { styles } from './style' -import { ExplorerButton } from '@gnosis.pm/safe-react-components' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -import { TransactionFees } from 'src/components/TransactionsFees' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' export type CustomTx = { contractAddress?: string @@ -52,6 +54,9 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const safeAddress = useSelector(safeParamAddressFromStateSelector) const { + gasLimit, + gasEstimation, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, @@ -63,7 +68,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0', }) - const submitTx = async (): Promise => { + const submitTx = async (txParameters: TxParameters): Promise => { const txRecipient = tx.contractAddress const txData = tx.data ? tx.data.trim() : '' const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' @@ -75,6 +80,9 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { to: txRecipient as string, valueInWei: txValue, txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, }), ) @@ -86,98 +94,112 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { } return ( - <> - - - Send Custom Tx - - 2 of 2 - - - - - - - - - - Arrow Down - - - - - - - - Recipient - - - - - - - - - - {tx.contractAddress} - - - - - - - - - Value - - - - Ether - - {tx.value || 0} - {' ' + nativeCoin.name} - - - - - Data (hex encoded) - - - - - - {tx.data} + + {(txParameters, toggleEditMode) => ( + <> + + + Send Custom Tx + + 2 of 2 + + + + + + + + + + Arrow Down + + + + - - - - - - - - - - - - + + + Recipient + + + + + + + + + + + {tx.contractAddress} + + + + + + + + + Value + + + + Ether + + {tx.value || 0} + {' ' + nativeCoin.name} + + + + + Data (hex encoded) + + + + + + {tx.data} + + + + + {/* Tx Parameters */} + + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index a5cf3a00..830c73ff 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -27,8 +27,11 @@ import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const useStyles = makeStyles(styles) @@ -51,12 +54,16 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) const nftTokens = useSelector(nftTokensSelector) + const txToken = nftTokens.find( ({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId, ) const [data, setData] = useState('') const { + gasLimit, + gasEstimation, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, @@ -87,7 +94,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = } }, [safeAddress, tx]) - const submitTx = async () => { + const submitTx = async (txParameters: TxParameters) => { try { if (safeAddress) { dispatch( @@ -96,6 +103,9 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = to: tx.assetAddress, valueInWei: '0', txData: data, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, }), ) @@ -110,88 +120,101 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = } return ( - <> - - - Send Funds - - 2 of 2 - - - - - - - - - - Arrow Down - - - - - - - - Recipient - - - - - - - - - - {tx.recipientAddress} - - - - - - - - - {textShortener({ charsStart: 40, charsEnd: 0 })(tx.assetName)} - - - {txToken && ( - - {txToken.name} - - {shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)}) + + {(txParameters, toggleEditMode) => ( + <> + + + Send Collectible + 2 of 2 + + + - )} - - - - - - - - - - + + + + + + Arrow Down + + + + + + + + Recipient + + + + + + + + + + {tx.recipientAddress} + + + + + + + + + {textShortener({ charsStart: 40, charsEnd: 0 })(tx.assetName)} + + + {txToken && ( + + {txToken.name} + + {shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)}) + + + )} + + {/* Tx Parameters */} + + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx similarity index 50% rename from src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx rename to src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index d3e389a6..b8edc9d4 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -3,13 +3,13 @@ import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { ExplorerButton, Button } from '@gnosis.pm/safe-react-components' + import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { getExplorerInfo, getNetworkInfo } from 'src/config' - import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' -import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Img from 'src/components/layout/Img' @@ -28,15 +28,17 @@ import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import { SpendingLimit } from 'src/logic/safe/store/models/safe' import { sm } from 'src/theme/variables' import { sameString } from 'src/utils/strings' - -import ArrowDown from '../assets/arrow-down.svg' - -import { styles } from './style' -import { ExplorerButton } from '@gnosis.pm/safe-react-components' import { TokenProps } from 'src/logic/tokens/store/model/token' import { RecordOf } from 'immutable' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' + +import ArrowDown from '../assets/arrow-down.svg' +import { styles } from './style' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' + const useStyles = makeStyles(styles) const { nativeCoin } = getNetworkInfo() @@ -86,19 +88,23 @@ const useTxData = ( return data } -const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => { +const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => { const classes = useStyles() const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const tokens = useSelector(extendedSafeTokensSelector) + const tokens: any = useSelector(extendedSafeTokensSelector) const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token]) - const isSendingNativeToken = sameAddress(txToken?.address, nativeCoin.address) + const isSendingNativeToken = useMemo(() => sameAddress(txToken?.address, nativeCoin.address), [txToken]) const txRecipient = isSendingNativeToken ? tx.recipientAddress : txToken?.address || '' const txValue = isSendingNativeToken ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0' const data = useTxData(isSendingNativeToken, tx.amount, tx.recipientAddress, txToken) + /* Get GasInfo */ const { gasCostFormatted, + gasPriceFormatted, + gasLimit, + gasEstimation, txEstimationExecutionStatus, isExecution, isCreation, @@ -109,7 +115,7 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => txType: tx.txType, }) - const submitTx = async () => { + const submitTx = async (txParameters: TxParameters) => { const isSpendingLimit = sameString(tx.txType, 'spendingLimit') if (!safeAddress) { @@ -141,6 +147,9 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => to: txRecipient as string, valueInWei: txValue, txData: data, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, }), ) @@ -149,97 +158,121 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => } return ( - <> - - - Send Funds - - 2 of 2 - - - - - - - - - - Arrow Down - - - - - - - - Recipient - - - - - - - - - - {tx.recipientAddress} + + {(txParameters, toggleEditMode) => ( + <> + {/* Header */} + + + Send Funds + + 2 of 2 + + + + + + + + + {/* SafeInfo */} + + + + Arrow Down + + + + + + + {/* Recipient */} + + + Recipient - - - - - - - - Amount - - - - {txToken?.name - - {tx.amount} {txToken?.symbol} - - - - - - - - - - - - + + + + + + + + + {tx.recipientAddress} + + + + + + + + {/* Amount */} + + + Amount + + + + {txToken?.name + + {tx.amount} {txToken?.symbol} + + + + {/* Tx Parameters */} + + + {/* Disclaimer */} + + + + + + + + {/* Footer */} + + + + + + )} + ) } -export default ReviewTx +export default ReviewSendFundsTx diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.ts b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/style.ts similarity index 100% rename from src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.ts rename to src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/style.ts diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts index 776dea04..f5e3df7a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts @@ -6,7 +6,7 @@ export const styles = createStyles({ padding: `${md} ${lg}`, justifyContent: 'flex-start', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, annotation: { letterSpacing: '-1px', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index f39e9ab2..4e58f0dd 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -69,7 +69,7 @@ export type SendFundsTx = { type SendFundsProps = { onClose: () => void - onNext: (txInfo: unknown) => void + onReview: (txInfo: unknown) => void recipientAddress?: string selectedToken?: string amount?: string @@ -79,7 +79,13 @@ const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElemen return <>{symbol} } -const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amount }: SendFundsProps): ReactElement => { +const SendFunds = ({ + onClose, + onReview, + recipientAddress, + selectedToken = '', + amount, +}: SendFundsProps): ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) const addressBook = useSelector(addressBookSelector) @@ -119,7 +125,7 @@ const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amou if (!values.recipientAddress) { submitValues.recipientAddress = selectedEntry?.address } - onNext({ ...submitValues, tokenSpendingLimit }) + onReview({ ...submitValues, tokenSpendingLimit }) } const spendingLimits = useSelector(safeSpendingLimitsSelector) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts index 776dea04..e5b663cf 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts @@ -6,7 +6,7 @@ export const styles = createStyles({ padding: `${md} ${lg}`, justifyContent: 'flex-start', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, annotation: { letterSpacing: '-1px', @@ -26,10 +26,12 @@ export const styles = createStyles({ }, formContainer: { padding: `${md} ${lg}`, + minHeight: '216px', }, buttonRow: { height: '84px', justifyContent: 'center', + position: 'relative', '& > button': { fontFamily: 'Averta', fontSize: md, diff --git a/src/routes/safe/components/Settings/Advanced/ModulesTable.tsx b/src/routes/safe/components/Settings/Advanced/ModulesTable.tsx index 7e67d8bf..09003fba 100644 --- a/src/routes/safe/components/Settings/Advanced/ModulesTable.tsx +++ b/src/routes/safe/components/Settings/Advanced/ModulesTable.tsx @@ -7,7 +7,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { generateColumns, ModuleAddressColumn, MODULES_TABLE_ADDRESS_ID } from './dataFetcher' -import RemoveModuleModal from './RemoveModuleModal' +import { RemoveModuleModal } from './RemoveModuleModal' import { styles } from './style' import { grantedSelector } from 'src/routes/safe/container/selector' @@ -39,7 +39,7 @@ interface ModulesTableProps { moduleData: ModuleAddressColumn | null } -const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => { +export const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => { const classes = useStyles() const columns = generateColumns() @@ -124,5 +124,3 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => ) } - -export default ModulesTable diff --git a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx index 53fe9075..f38fba22 100644 --- a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx +++ b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx @@ -4,7 +4,7 @@ import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import OpenInNew from '@material-ui/icons/OpenInNew' import cn from 'classnames' -import React from 'react' +import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -26,6 +26,11 @@ import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { md, secondary } from 'src/theme/variables' import { styles } from './style' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const useStyles = makeStyles(styles) @@ -44,26 +49,48 @@ interface RemoveModuleModalProps { selectedModulePair: ModulePair } -const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): React.ReactElement => { +export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): React.ReactElement => { const classes = useStyles() const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [txData, setTxData] = useState('') const dispatch = useDispatch() const [, moduleAddress] = selectedModulePair const explorerInfo = getExplorerInfo(moduleAddress) const { url } = explorerInfo() - const removeSelectedModule = async (): Promise => { - try { - const txData = getDisableModuleTxData(selectedModulePair, safeAddress) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + gasLimit, + gasEstimation, + gasPriceFormatted, + } = useEstimateTransactionGas({ + txData, + txRecipient: safeAddress, + txAmount: '0', + }) + useEffect(() => { + const txData = getDisableModuleTxData(selectedModulePair, safeAddress) + setTxData(txData) + }, [selectedModulePair, safeAddress]) + + const removeSelectedModule = async (txParameters: TxParameters): Promise => { + try { dispatch( createTransaction({ safeAddress, to: safeAddress, valueInWei: '0', txData, + txNonce: txParameters.ethNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) @@ -73,66 +100,94 @@ const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalPro } return ( - <> - - - - Remove Module - - - - - - - - - - - - - - - {moduleAddress} + + + {(txParameters, toggleEditMode) => { + return ( + <> + + + Remove Module - - - {moduleAddress} + + + + + + + + + + + + + + {moduleAddress} + + + + {moduleAddress} + + + + + + + + + + + + After removing this module, any feature or app that uses this module might no longer work. If this + Safe requires more then one signature, the module removal will have to be confirmed by other owners + as well. - - - - + + {/* Tx Parameters */} + + + + - - - - - - After removing this module, any feature or app that uses this module might no longer work. If this Safe - requires more then one signature, the module removal will have to be confirmed by other owners as well. - - - - - - - - - - - - + + + + + + + + + ) + }} + + ) } - -export default RemoveModuleModal diff --git a/src/routes/safe/components/Settings/Advanced/index.tsx b/src/routes/safe/components/Settings/Advanced/index.tsx index d2740777..f9d98d38 100644 --- a/src/routes/safe/components/Settings/Advanced/index.tsx +++ b/src/routes/safe/components/Settings/Advanced/index.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components' import { getModuleData } from './dataFetcher' import { styles } from './style' -import ModulesTable from './ModulesTable' +import { ModulesTable } from './ModulesTable' import Block from 'src/components/layout/Block' import { safeModulesSelector, safeNonceSelector } from 'src/logic/safe/store/selectors' @@ -38,7 +38,7 @@ const LoadingModules = (): React.ReactElement => { ) } -const Advanced = (): React.ReactElement => { +export const Advanced = (): React.ReactElement => { const classes = useStyles() const nonce = useSelector(safeNonceSelector) const modules = useSelector(safeModulesSelector) @@ -94,5 +94,3 @@ const Advanced = (): React.ReactElement => { ) } - -export default Advanced diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 23f1f973..c3121921 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -2,21 +2,21 @@ import { createStyles, makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { OwnerForm } from './screens/OwnerForm' -import { ReviewAddOwner } from './screens/Review' -import ThresholdForm from './screens/ThresholdForm' - import Modal from 'src/components/Modal' import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner' import createTransaction from 'src/logic/safe/store/actions/createTransaction' - import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { Dispatch } from 'src/logic/safe/store/actions/types.d' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' + +import { OwnerForm } from './screens/OwnerForm' +import { ReviewAddOwner } from './screens/Review' +import ThresholdForm from './screens/ThresholdForm' const styles = createStyles({ biggerModalWindow: { @@ -28,13 +28,18 @@ const styles = createStyles({ const useStyles = makeStyles(styles) -type OwnerValues = { +export type OwnerValues = { ownerAddress: string ownerName: string threshold: string } -export const sendAddOwner = async (values: OwnerValues, safeAddress: string, dispatch: Dispatch): Promise => { +export const sendAddOwner = async ( + values: OwnerValues, + safeAddress: string, + txParameters: TxParameters, + dispatch: Dispatch, +): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() @@ -44,6 +49,9 @@ export const sendAddOwner = async (values: OwnerValues, safeAddress: string, dis to: safeAddress, valueInWei: '0', txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) @@ -61,14 +69,14 @@ type Props = { const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => { const classes = useStyles() const [activeScreen, setActiveScreen] = useState('selectOwner') - const [values, setValues] = useState({}) + const [values, setValues] = useState({ ownerName: '', ownerAddress: '', threshold: '' }) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) useEffect( () => () => { setActiveScreen('selectOwner') - setValues({}) + setValues({ ownerName: '', ownerAddress: '', threshold: '' }) }, [isOpen], ) @@ -98,11 +106,11 @@ const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => { setActiveScreen('reviewAddOwner') } - const onAddOwner = async () => { + const onAddOwner = async (txParameters: TxParameters) => { onClose() try { - await sendAddOwner(values, safeAddress, dispatch) + await sendAddOwner(values, safeAddress, txParameters, dispatch) dispatch( addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })), ) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index 9329feeb..a465f126 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -4,6 +4,8 @@ import Close from '@material-ui/icons/Close' import classNames from 'classnames' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' +import { ExplorerButton } from '@gnosis.pm/safe-react-components' + import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' @@ -15,12 +17,15 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' - -import { styles } from './style' -import { ExplorerButton } from '@gnosis.pm/safe-react-components' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { OwnerValues } from '../..' +import { styles } from './style' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' + export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' const useStyles = makeStyles(styles) @@ -28,23 +33,22 @@ const useStyles = makeStyles(styles) type ReviewAddOwnerProps = { onClickBack: () => void onClose: () => void - onSubmit: () => void - values: { - ownerAddress: string - threshold: string - ownerName: string - } + onSubmit: (txParameters: TxParameters) => void + values: OwnerValues } export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => { const classes = useStyles() const [data, setData] = useState('') - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const safeName = useSelector(safeNameSelector) const owners = useSelector(safeOwnersSelector) const { + gasLimit, + gasEstimation, gasCostFormatted, + gasPriceFormatted, txEstimationExecutionStatus, isExecution, isOffChainSignature, @@ -76,135 +80,146 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie } }, [safeAddress, values.ownerAddress, values.threshold]) - const handleSubmit = () => { - onSubmit() - } return ( - <> - - - Add new owner - - 3 of 3 - - - - - - - - - - - - Details - - - - - Safe name - - - {safeName} - - - - - Any transaction requires the confirmation of: - - - {`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`} - - - - - - - - {`${(owners?.size || 0) + 1} Safe owner(s)`} - - - - {owners?.map((owner) => ( - - + + {(txParameters, toggleEditMode) => ( + <> + + + Add new owner + + 3 of 3 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`} + + + + + + + + {`${(owners?.size || 0) + 1} Safe owner(s)`} + + + + {owners?.map((owner) => ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + ))} + + + ADDING NEW OWNER ↓ + + + + - + - {owner.name} + {values.ownerName} - {owner.address} + {values.ownerAddress} - - + + - - ))} - - - ADDING NEW OWNER ↓ - - - - - - - - - - - {values.ownerName} - - - - {values.ownerAddress} - - - - - - - - - - - - - - - - - - - + + + + {/* Tx Parameters */} + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts index 6b8f83c3..d3d92d98 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts @@ -1,5 +1,5 @@ import { background, border, lg, secondaryText, sm } from 'src/theme/variables' -import { createStyles } from '@material-ui/core' +import { createStyles } from '@material-ui/core/styles' export const styles = createStyles({ root: { @@ -9,7 +9,7 @@ export const styles = createStyles({ padding: `${sm} ${lg}`, justifyContent: 'flex-start', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, annotation: { color: secondaryText, diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 3fe84603..575046d3 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -11,9 +11,9 @@ import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/s import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner' - import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { Dispatch } from 'src/logic/safe/store/actions/types.d' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const styles = createStyles({ biggerModalWindow: { @@ -37,6 +37,7 @@ export const sendRemoveOwner = async ( ownerAddressToRemove: string, ownerNameToRemove: string, dispatch: Dispatch, + txParameters: TxParameters, threshold?: number, ): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) @@ -53,6 +54,9 @@ export const sendRemoveOwner = async ( to: safeAddress, valueInWei: '0', txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) @@ -80,7 +84,7 @@ export const RemoveOwnerModal = ({ const [values, setValues] = useState({}) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const threshold = useSelector(safeThresholdSelector) + const threshold = useSelector(safeThresholdSelector) || 1 useEffect( () => () => { @@ -108,9 +112,9 @@ export const RemoveOwnerModal = ({ setActiveScreen('reviewRemoveOwner') } - const onRemoveOwner = () => { + const onRemoveOwner = (txParameters: TxParameters) => { onClose() - sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, threshold) + sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters, threshold) } return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index 191e7c89..ece059c9 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -4,6 +4,9 @@ import Close from '@material-ui/icons/Close' import classNames from 'classnames' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' +import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { List } from 'immutable' + import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' @@ -15,14 +18,15 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { styles } from './style' -import { ExplorerButton } from '@gnosis.pm/safe-react-components' -import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' -import { List } from 'immutable' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' @@ -31,7 +35,7 @@ const useStyles = makeStyles(styles) type ReviewRemoveOwnerProps = { onClickBack: () => void onClose: () => void - onSubmit: () => void + onSubmit: (txParameters: TxParameters) => void ownerAddress: string ownerName: string threshold?: number @@ -43,7 +47,7 @@ export const ReviewRemoveOwnerModal = ({ onSubmit, ownerAddress, ownerName, - threshold, + threshold = 1, }: ReviewRemoveOwnerProps): React.ReactElement => { const classes = useStyles() const [data, setData] = useState('') @@ -54,6 +58,9 @@ export const ReviewRemoveOwnerModal = ({ const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) const { + gasLimit, + gasEstimation, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, @@ -95,134 +102,149 @@ export const ReviewRemoveOwnerModal = ({ }, [safeAddress, ownerAddress, threshold]) return ( - <> - - - Remove owner - - 3 of 3 - - - - - - - - - - - - Details - - - - - Safe name - - - {safeName} - - - - - Any transaction requires the confirmation of: - - - {`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`} - - - - - - - - {`${owners ? owners.size - 1 : 0} Safe owner(s)`} - - - - {ownersWithAddressBookName?.map( - (owner) => - owner.address !== ownerAddress && ( - - - - - - - - - {owner.name} - - - - {owner.address} - - - - - - - - - - ), - )} - - - REMOVING OWNER ↓ - - - - - - - - - - - {ownerName} - - - - {ownerAddress} + + {(txParameters, toggleEditMode) => ( + <> + + + Remove owner + + 3 of 3 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`} - - + + + + {`${owners ? owners.size - 1 : 0} Safe owner(s)`} + + + + {ownersWithAddressBookName?.map( + (owner) => + owner.address !== ownerAddress && ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + ), + )} + + + REMOVING OWNER ↓ + + + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + - - - - - - - - - - - - - - + + + + {/* Tx Parameters */} + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 10266f78..6e0e9600 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -2,8 +2,6 @@ import { createStyles, makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import OwnerForm from './screens/OwnerForm' - import Modal from 'src/components/Modal' import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' @@ -15,7 +13,10 @@ import { checksumAddress } from 'src/utils/checksumAddress' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { Dispatch } from 'src/logic/safe/store/actions/types.d' + +import OwnerForm from './screens/OwnerForm' import { ReviewReplaceOwnerModal } from './screens/Review' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const styles = createStyles({ biggerModalWindow: { @@ -37,6 +38,7 @@ export const sendReplaceOwner = async ( safeAddress: string, ownerAddressToRemove: string, dispatch: Dispatch, + txParameters: TxParameters, threshold?: number, ): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) @@ -51,6 +53,9 @@ export const sendReplaceOwner = async ( to: safeAddress, valueInWei: '0', txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) @@ -88,7 +93,7 @@ export const ReplaceOwnerModal = ({ }) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const threshold = useSelector(safeThresholdSelector) + const threshold = useSelector(safeThresholdSelector) || 1 useEffect( () => () => { @@ -113,10 +118,10 @@ export const ReplaceOwnerModal = ({ setActiveScreen('reviewReplaceOwner') } - const onReplaceOwner = async () => { + const onReplaceOwner = async (txParameters: TxParameters) => { onClose() try { - await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold) + await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters, threshold) dispatch( addOrUpdateAddressBookEntry( diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index a56a069f..9d987f85 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -25,10 +25,13 @@ import { } from 'src/logic/safe/store/selectors' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' import { styles } from './style' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -import { TransactionFees } from 'src/components/TransactionsFees' export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' @@ -37,7 +40,7 @@ const useStyles = makeStyles(styles) type ReplaceOwnerProps = { onClose: () => void onClickBack: () => void - onSubmit: () => void + onSubmit: (txParameters: TxParameters) => void ownerAddress: string ownerName: string values: { @@ -59,11 +62,14 @@ export const ReviewReplaceOwnerModal = ({ const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) const owners = useSelector(safeOwnersSelector) - const threshold = useSelector(safeThresholdSelector) + const threshold = useSelector(safeThresholdSelector) || 1 const addressBook = useSelector(addressBookSelector) const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) const { + gasLimit, + gasEstimation, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, @@ -94,159 +100,174 @@ export const ReviewReplaceOwnerModal = ({ }, [ownerAddress, safeAddress, values.newOwnerAddress]) return ( - <> - - - Replace owner - - 2 of 2 - - - - - - - - - - - - Details - - - - - Safe name - - - {safeName} - - - - - Any transaction requires the confirmation of: - - - {`${threshold} out of ${owners?.size || 0} owner(s)`} - - - - - - - - {`${owners?.size || 0} Safe owner(s)`} - - - - {ownersWithAddressBookName?.map( - (owner) => - owner.address !== ownerAddress && ( - - - - - - - - - {owner.name} - - - - {owner.address} - - - - - - - - - - ), - )} - - - REMOVING OWNER ↓ - - - - - - - - - - - {ownerName} - - - - {ownerAddress} + + {(txParameters, toggleEditMode) => ( + <> + + + Replace owner + + 2 of 2 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {`${threshold} out of ${owners?.size || 0} owner(s)`} - - - - - - ADDING NEW OWNER ↓ - - - - - - - - - - - {values.newOwnerName} + + + + {`${owners?.size || 0} Safe owner(s)`} - - - {values.newOwnerAddress} - - - - - + + + {ownersWithAddressBookName?.map( + (owner) => + owner.address !== ownerAddress && ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + ), + )} + + + REMOVING OWNER ↓ + + + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + + ADDING NEW OWNER ↓ + + + + + + + + + + + {values.newOwnerName} + + + + {values.newOwnerAddress} + + + + + + + + - - - - - - - - - - - - - - + + + + {/* Tx Parameters */} + + + + + + + + + + + + )} + ) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx index eb2f849a..2fe1f2b6 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx @@ -41,7 +41,7 @@ interface SpentVsAmountProps { tokenAddress: string } -const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): ReactElement | null => { +export const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): ReactElement | null => { const { width } = useWindowDimensions() const showIcon = useMemo(() => width > 1024, [width]) @@ -55,5 +55,3 @@ const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): Rea ) : null } - -export default SpentVsAmount diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx index 6717bfb1..f9b06b21 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx @@ -9,7 +9,7 @@ import Row from 'src/components/layout/Row' import { TableCell, TableRow } from 'src/components/layout/Table' import Table from 'src/components/Table' import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' -import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' +import { RemoveLimitModal } from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import { grantedSelector } from 'src/routes/safe/container/selector' @@ -20,7 +20,7 @@ import { SPENDING_LIMIT_TABLE_SPENT_ID, SpendingLimitTable, } from './dataFetcher' -import SpentVsAmount from './SpentVsAmount' +import { SpentVsAmount } from './SpentVsAmount' const TableActionButton = styled(Button)` background-color: transparent; @@ -35,7 +35,7 @@ interface SpendingLimitTableProps { data?: SpendingLimitTable[] } -const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => { +export const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) @@ -106,5 +106,3 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => { ) } - -export default LimitsTable diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index a464f83e..8e62a497 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -1,12 +1,12 @@ import { Button, Text } from '@gnosis.pm/safe-react-components' -import React, { ReactElement, useMemo } from 'react' +import React, { ReactElement, useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' import { getNetworkInfo } from 'src/config' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import createTransaction, { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction' import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe' import { addSpendingLimitBeneficiaryMultiSendTx, @@ -26,8 +26,13 @@ import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/component import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' import { ActionCallback, CREATE } from '.' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TransactionFees } from 'src/components/TransactionsFees' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' const { nativeCoin } = getNetworkInfo() @@ -63,6 +68,74 @@ const useExistentSpendingLimit = ({ }, [spendingLimits, txToken.decimals, values.beneficiary, values.token]) } +const calculateSpendingLimitsTxData = ( + safeAddress: string, + spendingLimits: SpendingLimit[] | null | undefined, + existentSpendingLimit: SpendingLimit | null, + txToken: Token, + values: Record, + txParameters?: TxParameters, +): { + spendingLimitTxData: CreateTransactionArgs + transactions: MultiSendTx[] + spendingLimitArgs: { + beneficiary: string + token: string + spendingLimitInWei: string + resetTimeMin: number + resetBaseMin: number + } +} => { + const isSpendingLimitEnabled = spendingLimits !== null + const transactions: MultiSendTx[] = [] + + // is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it + if (!isSpendingLimitEnabled && safeAddress) { + transactions.push(enableSpendingLimitModuleMultiSendTx(safeAddress)) + } + + // does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??)) + // ^ - shall we rely on this or query the list of delegates once again? + const isDelegateAlreadyAdded = + spendingLimits?.some(({ delegate }) => sameAddress(delegate, values?.beneficiary)) ?? false + + // if `delegate` does not exist, add it by calling `addDelegate(beneficiary)` + if (!isDelegateAlreadyAdded && values?.beneficiary) { + transactions.push(addSpendingLimitBeneficiaryMultiSendTx(values.beneficiary)) + } + + // prepare the setAllowance tx + const startTime = currentMinutes() - 30 + const spendingLimitArgs = { + beneficiary: values.beneficiary, + token: values.token, + spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals), + resetTimeMin: values.withResetTime ? +values.resetTime * 60 * 24 : 0, + resetBaseMin: values.withResetTime ? startTime : 0, + } + + let spendingLimitTxData + if (safeAddress) { + // if there's no tx for enable module or adding a delegate, then we avoid using multiSend Tx + if (transactions.length === 0) { + spendingLimitTxData = setSpendingLimitTx({ spendingLimitArgs, safeAddress }) + } else { + spendingLimitTxData = spendingLimitMultiSendTx({ transactions, safeAddress }) + } + + if (txParameters) { + spendingLimitTxData.txNonce = txParameters.safeNonce + spendingLimitTxData.safeTxGas = txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined + spendingLimitTxData.ethParameters = txParameters + } + } + return { + spendingLimitTxData, + transactions, + spendingLimitArgs, + } +} + interface ReviewSpendingLimitProps { onBack: ActionCallback onClose: () => void @@ -71,7 +144,7 @@ interface ReviewSpendingLimitProps { existentSpendingLimit?: SpendingLimitRow } -const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): ReactElement => { +export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): ReactElement => { const classes = useStyles() const dispatch = useDispatch() @@ -79,44 +152,62 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): const safeAddress = useSelector(safeParamAddressFromStateSelector) const spendingLimits = useSelector(safeSpendingLimitsSelector) const existentSpendingLimit = useExistentSpendingLimit({ spendingLimits, txToken, values }) + const [estimateGasArgs, setEstimateGasArgs] = useState>({ + to: '', + txData: '', + }) - const handleSubmit = () => { - const isSpendingLimitEnabled = spendingLimits !== null - const transactions: MultiSendTx[] = [] + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isCreation, + isOffChainSignature, + gasPrice, + gasPriceFormatted, + gasLimit, + gasEstimation, + } = useEstimateTransactionGas({ + txData: estimateGasArgs.txData as string, + txRecipient: estimateGasArgs.to as string, + operation: estimateGasArgs.operation, + }) - // is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it - if (!isSpendingLimitEnabled && safeAddress) { - transactions.push(enableSpendingLimitModuleMultiSendTx(safeAddress)) + useEffect(() => { + const { spendingLimitTxData } = calculateSpendingLimitsTxData( + safeAddress, + spendingLimits, + existentSpendingLimit, + txToken, + values, + ) + setEstimateGasArgs(spendingLimitTxData) + }, [safeAddress, spendingLimits, existentSpendingLimit, txToken, values]) + + const handleSubmit = (txParameters: TxParameters): void => { + const { ethGasPrice, ethGasLimit, ethGasPriceInGWei } = txParameters + const advancedOptionsTxParameters = { + ...txParameters, + ethGasPrice: ethGasPrice || gasPrice, + ethGasPriceInGWei: ethGasPriceInGWei || gasPriceFormatted, + ethGasLimit: ethGasLimit || gasLimit, } - - // does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??)) - // ^ - shall we rely on this or query the list of delegates once again? - const isDelegateAlreadyAdded = - spendingLimits?.some(({ delegate }) => sameAddress(delegate, values?.beneficiary)) ?? false - - // if `delegate` does not exist, add it by calling `addDelegate(beneficiary)` - if (!isDelegateAlreadyAdded && values?.beneficiary) { - transactions.push(addSpendingLimitBeneficiaryMultiSendTx(values.beneficiary)) - } - - // prepare the setAllowance tx - const startTime = currentMinutes() - 30 - const spendingLimitArgs = { - beneficiary: values.beneficiary, - token: values.token, - spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals), - resetTimeMin: values.withResetTime ? +values.resetTime * 60 * 24 : 0, - resetBaseMin: values.withResetTime ? startTime : 0, - } - if (safeAddress) { + const { spendingLimitTxData, transactions, spendingLimitArgs } = calculateSpendingLimitsTxData( + safeAddress, + spendingLimits, + existentSpendingLimit, + txToken, + values, + advancedOptionsTxParameters, + ) // if there's no tx for enable module or adding a delegate, then we avoid using multiSend Tx if (transactions.length === 0) { - dispatch(createTransaction(setSpendingLimitTx({ spendingLimitArgs, safeAddress }))) - } else { - transactions.push(setSpendingLimitMultiSendTx({ spendingLimitArgs, safeAddress })) - dispatch(createTransaction(spendingLimitMultiSendTx({ transactions, safeAddress }))) + dispatch(createTransaction(spendingLimitTxData)) + return } + transactions.push(setSpendingLimitMultiSendTx({ spendingLimitArgs, safeAddress })) + dispatch(createTransaction(spendingLimitTxData)) } } @@ -130,60 +221,82 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): ?.label ?? 'One-time spending limit' return ( - <> - + + {(txParameters, toggleEditMode) => ( + <> + - - - - - - - {existentSpendingLimit && ( - - Previous Amount: {existentSpendingLimit.amount} - - )} - - - - {existentSpendingLimit && ( - - - Previous Reset Time: {previousResetTime(existentSpendingLimit)} + + + + + + + {existentSpendingLimit && ( + + Previous Amount: {existentSpendingLimit.amount} + + )} + + + + {existentSpendingLimit && ( + + + Previous Reset Time: {previousResetTime(existentSpendingLimit)} + + + )} + + + {existentSpendingLimit && ( + + You are about to replace an existent spending limit + )} + {/* Tx Parameters */} + + + - )} - + - {existentSpendingLimit && ( - - You are about to replace an existent spending limit - - )} - + + - - - - - - + + + + )} + ) } - -export default Review diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx index 29e7a60f..7cb5b9a5 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx @@ -8,7 +8,7 @@ import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' import Create from './Create' -import Review from './Review' +import { ReviewSpendingLimits } from './Review' export const CREATE = 'CREATE' as const export const REVIEW = 'REVIEW' as const @@ -81,7 +81,7 @@ interface SpendingLimitModalProps { open: boolean } -const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement => { +export const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement => { // state and dispatch const [{ step, txToken, values }, { create, review }] = useNewLimitModal(CREATE) @@ -98,9 +98,7 @@ const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement = description="set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures" > {step === CREATE && } - {step === REVIEW && } + {step === REVIEW && } ) } - -export default NewLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx index 765a8e0b..29d0a2bb 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx @@ -28,7 +28,7 @@ const StepsLine = styled.div` margin: 46px 0; ` -const NewLimitSteps = (): ReactElement => ( +export const NewLimitSteps = (): ReactElement => ( Select Beneficiary @@ -75,5 +75,3 @@ const NewLimitSteps = (): ReactElement => ( ) - -export default NewLimitSteps diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index c6b89200..1905c42f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -1,5 +1,5 @@ import { Button } from '@gnosis.pm/safe-react-components' -import React, { ReactElement } from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' @@ -17,6 +17,12 @@ import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' import { SpendingLimitTable } from './LimitsTable/dataFetcher' import Modal from './Modal' import { useStyles } from './style' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import Row from 'src/components/layout/Row' +import { TransactionFees } from 'src/components/TransactionsFees' interface RemoveSpendingLimitModalProps { onClose: () => void @@ -24,28 +30,50 @@ interface RemoveSpendingLimitModalProps { open: boolean } -const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): ReactElement => { +export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): ReactElement => { const classes = useStyles() const tokenInfo = useTokenInfo(spendingLimit.spent.tokenAddress) const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [txData, setTxData] = useState('') const dispatch = useDispatch() - const removeSelectedSpendingLimit = async (): Promise => { - try { - const { - beneficiary, - spent: { tokenAddress }, - } = spendingLimit - const txData = getDeleteAllowanceTxData({ beneficiary, tokenAddress }) + useEffect(() => { + const { + beneficiary, + spent: { tokenAddress }, + } = spendingLimit + const txData = getDeleteAllowanceTxData({ beneficiary, tokenAddress }) + setTxData(txData) + }, [spendingLimit]) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + gasLimit, + gasEstimation, + gasPriceFormatted, + } = useEstimateTransactionGas({ + txData, + txRecipient: SPENDING_LIMIT_MODULE_ADDRESS, + txAmount: '0', + }) + + const removeSelectedSpendingLimit = async (txParameters: TxParameters): Promise => { + try { dispatch( createTransaction({ safeAddress, to: SPENDING_LIMIT_MODULE_ADDRESS, valueInWei: '0', txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.REMOVE_SPENDING_LIMIT_TX, }), ) @@ -67,36 +95,66 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM title="Remove Spending Limit" description="Remove the selected Spending Limit" > - + + {(txParameters, toggleEditMode) => { + return ( + <> + - - - - - - {tokenInfo && ( - - )} - - - - - + + + + + + {tokenInfo && ( + + )} + + + + + - - - - + {/* Tx Parameters */} + + + + + + + + + + + ) + }} + ) } - -export default RemoveLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index fe34b6f9..7666e895 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -9,17 +9,17 @@ import Row from 'src/components/layout/Row' import { safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' -import LimitsTable from './LimitsTable' +import { LimitsTable } from './LimitsTable' import { getSpendingLimitData } from './LimitsTable/dataFetcher' -import NewLimitModal from './NewLimitModal' -import NewLimitSteps from './NewLimitSteps' +import { NewLimitModal } from './NewLimitModal' +import { NewLimitSteps } from './NewLimitSteps' import { useStyles } from './style' const InfoText = styled(Text)` margin-top: 16px; ` -const SpendingLimitSettings = (): ReactElement => { +export const SpendingLimitSettings = (): ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) const allowances = useSelector(safeSpendingLimitsSelector) @@ -68,5 +68,3 @@ const SpendingLimitSettings = (): ReactElement => { ) } - -export default SpendingLimitSettings diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 35b2f9c2..20ff38ae 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -73,7 +73,7 @@ export const useStyles = makeStyles( modalHeading: { boxSizing: 'border-box', justifyContent: 'space-between', - maxHeight: '75px', + height: '74px', padding: `${sm} ${lg}`, }, modalContainer: { @@ -123,7 +123,6 @@ export const useStyles = makeStyles( modal: { height: 'auto', maxWidth: 'calc(100% - 30px)', - overflow: 'hidden', }, amountInput: { width: '100% !important', diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index d28cbb5d..7ec1389e 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -3,7 +3,7 @@ import MenuItem from '@material-ui/core/MenuItem' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useEffect, useState } from 'react' -import { styles } from './style' +import { List } from 'immutable' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' @@ -17,10 +17,12 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { SafeOwner } from 'src/logic/safe/store/models/safe' -import { List } from 'immutable' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' - +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' + +import { styles } from './style' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' const THRESHOLD_FIELD_NAME = 'threshold' @@ -50,6 +52,9 @@ export const ChangeThresholdModal = ({ isCreation, isExecution, isOffChainSignature, + gasLimit, + gasPriceFormatted, + gasEstimation, } = useEstimateTransactionGas({ txData: data, txRecipient: safeAddress, @@ -71,6 +76,8 @@ export const ChangeThresholdModal = ({ } }, [safeAddress, threshold]) + const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') + const handleSubmit = (values) => { const newThreshold = values[THRESHOLD_FIELD_NAME] @@ -79,75 +86,98 @@ export const ChangeThresholdModal = ({ } return ( - <> - - - Change required confirmations - - - - - - - - {() => ( - <> - - - Any transaction requires the confirmation of: - - - - ( - <> - - {[...Array(Number(owners?.size))].map((x, index) => ( - - {index + 1} - - ))} - - {props.meta.error && props.meta.touched && ( - - {props.meta.error} - + + {(txParameters, toggleEditMode) => ( + <> + + + Change required confirmations + + + + + + + + {() => ( + <> + + + Any transaction requires the confirmation of: + + + + ( + <> + + {[...Array(Number(owners?.size))].map((x, index) => ( + + {index + 1} + + ))} + + {props.meta.error && props.meta.touched && ( + + {props.meta.error} + + )} + )} - - )} - validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))} + validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))} + /> + + + + {`out of ${owners?.size} owner(s)`} + + + + + {/* Tx Parameters */} + - - - - {`out of ${owners?.size} owner(s)`} - - - - - - - - - - - - - - )} - - + + + + + + + + + + + + + + )} + + + )} + ) } diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts index e3f31c05..b3223590 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts @@ -6,7 +6,7 @@ export const styles = createStyles({ padding: `${sm} ${lg}`, justifyContent: 'space-between', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, annotation: { letterSpacing: '-1px', @@ -30,7 +30,7 @@ export const styles = createStyles({ buttonRow: { height: '84px', justifyContent: 'center', - position: 'absolute', + position: 'relative', bottom: 0, width: '100%', }, diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index 753f758c..1d498058 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -2,9 +2,6 @@ import { makeStyles } from '@material-ui/core/styles' import React, { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { ChangeThresholdModal } from './ChangeThreshold' -import { styles } from './style' - import Modal from 'src/components/Modal' import Block from 'src/components/layout/Block' import Bold from 'src/components/layout/Bold' @@ -22,6 +19,11 @@ import { safeThresholdSelector, } from 'src/logic/safe/store/selectors' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { EditTxParametersForm } from 'src/routes/safe/components/Transactions/helpers/EditTxParametersForm' + +import { ChangeThresholdModal } from './ChangeThreshold' +import { styles } from './style' const useStyles = makeStyles(styles) @@ -29,10 +31,12 @@ const ThresholdSettings = (): React.ReactElement => { const classes = useStyles() const [isModalOpen, setModalOpen] = useState(false) const dispatch = useDispatch() - const threshold = useSelector(safeThresholdSelector) + const threshold = useSelector(safeThresholdSelector) || 1 const safeAddress = useSelector(safeParamAddressFromStateSelector) const owners = useSelector(safeOwnersSelector) const granted = useSelector(grantedSelector) + const txParameters = useTransactionParameters() + const [activeScreen, setActiveScreen] = useState('form') const toggleModal = () => { setModalOpen((prevOpen) => !prevOpen) @@ -48,6 +52,9 @@ const ThresholdSettings = (): React.ReactElement => { to: safeAddress, valueInWei: '0', txData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) @@ -59,6 +66,10 @@ const ThresholdSettings = (): React.ReactElement => { trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) }, [trackEvent]) + const closeEditTxParameters = () => setActiveScreen('form') + + const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') + return ( <> @@ -87,13 +98,22 @@ const ThresholdSettings = (): React.ReactElement => { open={isModalOpen} title="Change Required Confirmations" > - + {activeScreen === 'form' && ( + + )} + {activeScreen === 'editTxParameters' && ( + + )} ) diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx index d0023166..ebed303d 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx @@ -15,10 +15,13 @@ import { getUpgradeSafeTransactionHash } from 'src/logic/safe/utils/upgradeSafe' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { makeStyles } from '@material-ui/core' import { TransactionFees } from 'src/components/TransactionsFees' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' import { DELEGATE_CALL } from 'src/logic/safe/transactions' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const useStyles = makeStyles(styles) @@ -40,7 +43,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle calculateUpgradeSafeModal() }, [safeAddress]) - const handleSubmit = async () => { + const handleSubmit = async (txParameters: TxParameters) => { // Call the update safe method dispatch( createTransaction({ @@ -48,6 +51,9 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle to: MULTI_SEND_ADDRESS, valueInWei: '0', txData: multiSendCallData, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, notifiedTransaction: 'STANDARD_TX', operation: DELEGATE_CALL, }), @@ -61,65 +67,86 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle isExecution, isCreation, isOffChainSignature, + gasPriceFormatted, + gasLimit, + gasEstimation, } = useEstimateTransactionGas({ txData: multiSendCallData, txRecipient: safeAddress, }) return ( - <> - - - Update to new Safe version - - - - - - - - {() => ( - <> - - - - Update now to take advantage of new features and the highest security standards available. - - - This update includes: -
    -
  • Compatibility with new asset types (ERC-721 / ERC-1155)
  • -
  • Improved interoperability with modules
  • -
  • Minor security improvements
  • -
+ + {(txParameters, toggleEditMode) => ( + <> + + + Update to new Safe version + + + + + + + handleSubmit(txParameters)}> + {() => ( + <> + + + + Update now to take advantage of new features and the highest security standards available. + + + This update includes: +
    +
  • Compatibility with new asset types (ERC-721 / ERC-1155)
  • +
  • Improved interoperability with modules
  • +
  • Minor security improvements
  • +
+
+ + You will need to confirm this update just like any other transaction. This means other owners will + have to confirm the update in case more than one confirmation is required for this Safe. + +
+ {/* Tx Parameters */} + + + +
- - You will need to confirm this update just like any other transaction. This means other owners will - have to confirm the update in case more than one confirmation is required for this Safe. - -
- - - -
- - - - - - - )} -
- + + + + + + + )} + + + )} + ) } diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/style.ts b/src/routes/safe/components/Settings/UpdateSafeModal/style.ts index e3f31c05..cafec0a0 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/style.ts +++ b/src/routes/safe/components/Settings/UpdateSafeModal/style.ts @@ -30,9 +30,6 @@ export const styles = createStyles({ buttonRow: { height: '84px', justifyContent: 'center', - position: 'absolute', - bottom: 0, - width: '100%', }, inputRow: { position: 'relative', diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 70a258fe..e3b2f703 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -6,8 +6,8 @@ import * as React from 'react' import { useState } from 'react' import { useSelector } from 'react-redux' -import Advanced from './Advanced' -import SpendingLimitSettings from './SpendingLimit' +import { Advanced } from './Advanced' +import { SpendingLimitSettings } from './SpendingLimit' import ManageOwners from './ManageOwners' import { RemoveSafeModal } from './RemoveSafeModal' import SafeDetails from './SafeDetails' diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx index f09395b1..c7ea32b2 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx @@ -21,8 +21,11 @@ import { processTransaction } from 'src/logic/safe/store/actions/processTransact import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { Transaction } from 'src/logic/safe/store/models/types/transaction' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' const useStyles = makeStyles(styles) @@ -53,33 +56,37 @@ const getModalTitleAndDescription = (thresholdReached, isCancelTx) => { } type Props = { + onClose: () => void canExecute: boolean isCancelTx?: boolean isOpen: boolean - onClose: () => void thresholdReached: boolean tx: Transaction + txParameters: TxParameters } export const ApproveTxModal = ({ + onClose, canExecute, isCancelTx = false, isOpen, - onClose, thresholdReached, tx, }: Props): React.ReactElement => { const dispatch = useDispatch() const userAddress = useSelector(userAccountSelector) const classes = useStyles() - const threshold = useSelector(safeThresholdSelector) + const threshold = useSelector(safeThresholdSelector) || 1 const safeAddress = useSelector(safeParamAddressFromStateSelector) const [approveAndExecute, setApproveAndExecute] = useState(canExecute) const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx) const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached + const [manualGasPrice, setManualGasPrice] = useState() const { + gasLimit, + gasPriceFormatted, gasCostFormatted, txEstimationExecutionStatus, isExecution, @@ -93,11 +100,12 @@ export const ApproveTxModal = ({ preApprovingOwner: approveAndExecute ? userAddress : undefined, safeTxGas: tx.safeTxGas, operation: tx.operation, + manualGasPrice: manualGasPrice, }) const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) - const approveTx = () => { + const approveTx = (txParameters: TxParameters) => { dispatch( processTransaction({ safeAddress, @@ -105,72 +113,128 @@ export const ApproveTxModal = ({ userAddress, notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX, approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted, + ethParameters: txParameters, thresholdReached, }), ) onClose() } + const getParametersStatus = () => { + if (canExecute || approveAndExecute) { + return 'SAFE_DISABLED' + } + + return 'DISABLED' + } + + const closeEditModalCallback = (txParameters: TxParameters) => { + const oldGasPrice = Number(gasPriceFormatted) + const newGasPrice = Number(txParameters.ethGasPrice) + + if (newGasPrice && oldGasPrice !== newGasPrice) { + setManualGasPrice(newGasPrice.toString()) + } + } + return ( - - - {title} - - - - - - - - - {description} - - Transaction nonce: -
- {tx.nonce} -
- {oneConfirmationLeft && canExecute && ( + + {(txParameters, toggleEditMode) => { + return ( <> - - Approving this transaction executes it right away. - {!isCancelTx && - ' If you want approve but execute the transaction manually later, click on the checkbox below.'} - - {!isCancelTx && ( - } - label="Execute transaction" - data-testid="execute-checkbox" + {/* Header */} + + + {title} + + + + + + + + + {/* Tx info */} + + + {description} + + Transaction nonce: +
+ {tx.nonce} +
+ + {oneConfirmationLeft && canExecute && ( + <> + + Approving this transaction executes it right away. + {!isCancelTx && + ' If you want approve but execute the transaction manually later, click on the checkbox below.'} + + + {!isCancelTx && ( + + } + label="Execute transaction" + data-testid="execute-checkbox" + /> + )} + + )} + + {/* Tx Parameters */} + {approveAndExecute && ( + + )} +
+ + - )} +
+ + {/* Footer */} + + + + - )} -
- -
- - - - + ) + }} +
) } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts index 41ce3885..a4440670 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts @@ -6,7 +6,7 @@ export const styles = createStyles({ padding: `${sm} ${lg}`, justifyContent: 'space-between', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, headingText: { fontSize: lg, @@ -21,7 +21,7 @@ export const styles = createStyles({ buttonRow: { height: '84px', justifyContent: 'center', - position: 'absolute', + position: 'relative', bottom: 0, width: '100%', borderTop: `1px solid ${border}`, diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx index 8161ac4b..e7d59631 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx @@ -19,8 +19,12 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { Transaction } from 'src/logic/safe/store/models/types/transaction' -import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils' const useStyles = makeStyles(styles) @@ -41,72 +45,106 @@ export const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElemen isExecution, isOffChainSignature, isCreation, + gasLimit, + gasEstimation, + gasPriceFormatted, } = useEstimateTransactionGas({ txData: EMPTY_DATA, txRecipient: safeAddress, }) - const sendReplacementTransaction = () => { + const sendReplacementTransaction = (txParameters: TxParameters) => { dispatch( createTransaction({ safeAddress, to: safeAddress, valueInWei: '0', - notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX, txNonce: tx.nonce, origin: tx.origin, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, + notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX, }), ) onClose() } + const getParametersStatus = (): ParametersStatus => { + return 'CANCEL_TRANSACTION' + } + return ( - - - Reject transaction - - - - - - - - - - This action will cancel this transaction. A separate transaction will be performed to submit the rejection. - - - Transaction nonce: -
- {tx.nonce} -
-
- - - -
- - - - + + {(txParameters, toggleEditMode) => { + return ( + <> + + + Reject transaction + + + + + + + + + + This action will cancel this transaction. A separate transaction will be performed to submit the + rejection. + + + Transaction nonce: +
+ {tx.nonce} +
+
+ {/* Tx Parameters */} + + + + +
+ + + + + + ) + }} +
) } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts index 41ce3885..a4440670 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts @@ -6,7 +6,7 @@ export const styles = createStyles({ padding: `${sm} ${lg}`, justifyContent: 'space-between', boxSizing: 'border-box', - maxHeight: '75px', + height: '74px', }, headingText: { fontSize: lg, @@ -21,7 +21,7 @@ export const styles = createStyles({ buttonRow: { height: '84px', justifyContent: 'center', - position: 'absolute', + position: 'relative', bottom: 0, width: '100%', borderTop: `1px solid ${border}`, diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx index 274acf47..31c6458c 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx @@ -34,6 +34,7 @@ import { getExplorerInfo, getNetworkInfo } from 'src/config' import TransferDescription from './TxDescription/TransferDescription' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { safeNonceSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' +import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const ExpandedModuleTx = ({ tx }: { tx: SafeModuleTransaction }): ReactElement => { const classes = useStyles() @@ -92,22 +93,27 @@ interface ExpandedSafeTxProps { const { nativeCoin } = getNetworkInfo() +type ScreenType = 'approveTx' | 'executeRejectTx' | 'rejectTx' + const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement => { const { fromWei, toBN } = getWeb3().utils const classes = useStyles() const nonce = useSelector(safeNonceSelector) const threshold = useSelector(safeThresholdSelector) as number - const [openModal, setOpenModal] = useState<'approveTx' | 'executeRejectTx' | 'rejectTx'>() - const openApproveModal = () => setOpenModal('approveTx') - const closeModal = () => setOpenModal(undefined) + const [openModal, setOpenModal] = useState() + const txParameters = useTransactionParameters() + const isIncomingTx = !!INCOMING_TX_TYPES[tx.type] const isCreationTx = tx.type === TransactionTypes.CREATION - const thresholdReached = !isIncomingTx && threshold <= tx.confirmations.size const canExecute = !isIncomingTx && nonce === tx.nonce const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations?.size const canExecuteCancel = nonce === tx.nonce + const openApproveModal = () => setOpenModal('approveTx') + + const closeModal = () => setOpenModal(undefined) + const openRejectModal = () => { if (!!cancelTx && nonce === cancelTx.nonce) { setOpenModal('executeRejectTx') @@ -168,6 +174,8 @@ const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement => )}
+ + {/* Approve TX */} {openModal === 'approveTx' && ( onClose={closeModal} thresholdReached={thresholdReached} tx={tx} + txParameters={txParameters} /> )} + + {/* Reject TX */} {openModal === 'rejectTx' && } + + {/* Execute the rejection TX */} {openModal === 'executeRejectTx' && cancelTx && ( )} diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx new file mode 100644 index 00000000..9485de0c --- /dev/null +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx @@ -0,0 +1,240 @@ +import React from 'react' +import IconButton from '@material-ui/core/IconButton' +import Close from '@material-ui/icons/Close' +import { makeStyles } from '@material-ui/core/styles' +import { Title, Text, Divider, Link, Icon } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' + +import Field from 'src/components/forms/Field' +import TextField from 'src/components/forms/TextField' +import Block from 'src/components/layout/Block' +import Button from 'src/components/layout/Button' +import Row from 'src/components/layout/Row' +import { styles } from './style' +import GnoForm from 'src/components/forms/GnoForm' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { composeValidators, minValue } from 'src/components/forms/validator' + +import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsEnabled } from '../utils' + +const StyledDivider = styled(Divider)` + margin: 0px; +` + +const SafeOptions = styled.div` + display: flex; + justify-content: space-between; + gap: 20px; +` + +const EthereumOptions = styled.div` + display: flex; + /* justify-content: space-between; */ + flex-wrap: wrap; + gap: 10px 20px; + + div { + width: 216px !important; + } +` +const StyledLink = styled(Link)` + margin: 16px 0; + display: inline-flex; + align-items: center; + + > :first-of-type { + margin-right: 5px; + } +` +const StyledIconButton = styled(IconButton)` + margin: 10px 0 0 0; +` +const StyledText = styled(Text)` + margin: 0 0 4px 0; +` +const StyledTextMt = styled(Text)` + margin: 16px 0 4px 0; +` + +const useStyles = makeStyles(styles) + +interface Props { + txParameters: TxParameters + onClose: (txParameters?: TxParameters) => void + parametersStatus: ParametersStatus +} + +const formValidation = (values) => { + const { ethGasLimit, ethGasPrice, ethNonce, safeNonce, safeTxGas } = values ?? {} + + const ethGasLimitValidation = minValue(0, true)(ethGasLimit) + + const ethGasPriceValidation = minValue(0, true)(ethGasPrice) + + const ethNonceValidation = minValue(0, true)(ethNonce) + + const safeNonceValidation = minValue(0, true)(safeNonce) + + const safeTxGasValidation = composeValidators(minValue(0, true), (value: string) => { + if (!value) { + return + } + + if (Number(value) > Number(ethGasLimit)) { + return 'Bigger than Ethereum gas limit.' + } + })(safeTxGas) + + return { + ethGasLimit: ethGasLimitValidation, + ethGasPrice: ethGasPriceValidation, + ethNonce: ethNonceValidation, + safeNonce: safeNonceValidation, + safeTxGas: safeTxGasValidation, + } +} + +export const EditTxParametersForm = ({ + onClose, + txParameters, + parametersStatus = 'ENABLED', +}: Props): React.ReactElement => { + const classes = useStyles() + const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice } = txParameters + + const onSubmit = (values: TxParameters) => { + onClose(values) + } + + const onCloseFormHandler = () => { + onClose() + } + + return ( + <> + {/* Header */} + + + Advanced options + + + + + + + + + + + {() => ( + <> + + Safe transactions parameters + + + + + + + + + Ethereum transactions parameters + + + + + + + + + + + How can I configure the gas price manually? + + + + + + + {/* Footer */} + + + + + + )} + + + + ) +} diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/style.ts b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/style.ts new file mode 100644 index 00000000..e76197f4 --- /dev/null +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/style.ts @@ -0,0 +1,45 @@ +import { lg, md, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' + +export const styles = createStyles({ + heading: { + padding: `${md} ${lg}`, + justifyContent: 'space-between', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: secondaryText, + marginRight: 'auto', + marginLeft: '20px', + }, + headingText: { + fontSize: lg, + }, + closeIcon: { + height: '35px', + width: '35px', + }, + container: { + padding: `${md} ${lg}`, + }, + amount: { + marginLeft: sm, + }, + address: { + marginRight: sm, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + '& > button': { + fontFamily: 'Averta', + fontSize: md, + }, + }, + submitButton: { + boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', + marginLeft: '15px', + }, +}) diff --git a/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx new file mode 100644 index 00000000..4bca38e8 --- /dev/null +++ b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from 'react' +import { TxParameters, useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { EditTxParametersForm } from 'src/routes/safe/components/Transactions/helpers/EditTxParametersForm' +import { ParametersStatus } from './utils' +import { useSelector } from 'react-redux' +import { safeThresholdSelector } from 'src/logic/safe/store/selectors' + +type Props = { + children: (txParameters: TxParameters, toggleStatus: (txParameters?: TxParameters) => void) => any + parametersStatus?: ParametersStatus + ethGasLimit?: TxParameters['ethGasLimit'] + ethGasPrice?: TxParameters['ethGasPrice'] + safeNonce?: TxParameters['safeNonce'] + safeTxGas?: TxParameters['safeTxGas'] + closeEditModalCallback?: (txParameters: TxParameters) => void +} + +export const EditableTxParameters = ({ + children, + parametersStatus, + ethGasLimit, + ethGasPrice, + safeNonce, + safeTxGas, + closeEditModalCallback, +}: Props): React.ReactElement => { + const [isEditMode, toggleEditMode] = useState(false) + const [useManualValues, setUseManualValues] = useState(false) + const threshold = useSelector(safeThresholdSelector) || 1 + const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + const txParameters = useTransactionParameters({ + parameterStatus: parametersStatus || defaultParameterStatus, + initialEthGasLimit: ethGasLimit, + initialEthGasPrice: ethGasPrice, + initialSafeNonce: safeNonce, + initialSafeTxGas: safeTxGas, + }) + const { setEthGasPrice, setEthGasLimit, setSafeNonce, setSafeTxGas, setEthNonce } = txParameters + + // Update TxParameters + useEffect(() => { + if (!useManualValues) { + setEthGasLimit(ethGasLimit) + setEthGasPrice(ethGasPrice) + setSafeTxGas(safeTxGas) + } + }, [ethGasLimit, setEthGasLimit, ethGasPrice, setEthGasPrice, useManualValues, safeTxGas, setSafeTxGas]) + + const toggleStatus = () => { + toggleEditMode((prev) => !prev) + } + + // Sends a callback with the last values of txParameters + useEffect(() => { + if (!isEditMode && closeEditModalCallback) { + closeEditModalCallback(txParameters) + } + }, [isEditMode, closeEditModalCallback, txParameters]) + + const closeEditFormHandler = (txParameters?: TxParameters) => { + if (txParameters) { + setUseManualValues(true) + setSafeNonce(txParameters.safeNonce) + setSafeTxGas(txParameters.safeTxGas) + setEthGasLimit(txParameters.ethGasLimit) + setEthGasPrice(txParameters.ethGasPrice) + setEthNonce(txParameters.ethNonce) + } + toggleStatus() + } + + return isEditMode ? ( + + ) : ( + children(txParameters, toggleStatus) + ) +} diff --git a/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx b/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx new file mode 100644 index 00000000..bb556cf9 --- /dev/null +++ b/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx @@ -0,0 +1,156 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' +import { Text, ButtonLink, Accordion, AccordionSummary, AccordionDetails } from '@gnosis.pm/safe-react-components' + +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { ParametersStatus, areEthereumParamsEnabled, areSafeParamsEnabled } from '../utils' +import { useSelector } from 'react-redux' +import { safeThresholdSelector } from 'src/logic/safe/store/selectors' + +const TxParameterWrapper = styled.div` + display: flex; + justify-content: space-between; +` + +const AccordionDetailsWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; +` +const StyledText = styled(Text)` + margin: 8px 0 0 0; +` + +const StyledButtonLink = styled(ButtonLink)` + padding-left: 0; + margin: 8px 0 0 0; + + > p { + margin-left: 0; + } +` + +type Props = { + txParameters: TxParameters + onEdit: () => void + compact?: boolean + parametersStatus?: ParametersStatus + isTransactionExecution: boolean + isTransactionCreation: boolean +} + +export const TxParametersDetail = ({ + onEdit, + txParameters, + compact = true, + parametersStatus, + isTransactionCreation, + isTransactionExecution, +}: Props): ReactElement | null => { + const threshold = useSelector(safeThresholdSelector) || 1 + const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + + if (!isTransactionExecution && !isTransactionCreation) { + return null + } + + return ( + + + Advanced options + + + + + Safe transactions parameters + + + + + Safe nonce + + + {txParameters.safeNonce} + + + + + + SafeTxGas + + + {txParameters.safeTxGas} + + + + + + Ethereum transaction parameters + + + + + + Ethereum nonce + + + {txParameters.ethNonce} + + + + + + Ethereum gas limit + + + {txParameters.ethGasLimit} + + + + + + Ethereum gas price + + + {txParameters.ethGasPrice} + + + + + Edit + + + + + ) +} diff --git a/src/routes/safe/components/Transactions/helpers/utils.ts b/src/routes/safe/components/Transactions/helpers/utils.ts new file mode 100644 index 00000000..ce1564e6 --- /dev/null +++ b/src/routes/safe/components/Transactions/helpers/utils.ts @@ -0,0 +1,14 @@ +export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_DISABLED' | 'CANCEL_TRANSACTION' + +export const areEthereumParamsEnabled = (parametersStatus: ParametersStatus): boolean => { + return ( + parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') + ) +} + +export const areSafeParamsEnabled = (parametersStatus: ParametersStatus): boolean => { + return ( + parametersStatus === 'ENABLED' || + (parametersStatus !== 'SAFE_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') + ) +} diff --git a/src/routes/safe/container/hooks/useTransactionParameters.ts b/src/routes/safe/container/hooks/useTransactionParameters.ts new file mode 100644 index 00000000..28250406 --- /dev/null +++ b/src/routes/safe/container/hooks/useTransactionParameters.ts @@ -0,0 +1,110 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { getUserNonce } from 'src/logic/wallets/ethTransactions' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { getLastTx, getNewTxNonce } from 'src/logic/safe/store/actions/utils' +import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' +import { safeSelector } from 'src/logic/safe/store/selectors' +import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' +import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils' +import { sameString } from 'src/utils/strings' + +export type TxParameters = { + safeNonce: string | undefined + setSafeNonce: (safeNonce: string | undefined) => void + safeTxGas: string | undefined + setSafeTxGas: (gas: string | undefined) => void + ethNonce: string | undefined + setEthNonce: (ethNonce: string | undefined) => void + ethGasLimit: string | undefined + setEthGasLimit: (ethGasLimit: string | undefined) => void + ethGasPrice: string | undefined + setEthGasPrice: (ethGasPrice: string | undefined) => void + ethGasPriceInGWei: string | undefined +} + +type Props = { + parameterStatus?: ParametersStatus + initialSafeNonce?: string + initialSafeTxGas?: string + initialEthGasLimit?: string + initialEthGasPrice?: string +} + +/** + * This hooks is used to store tx parameter + * It needs to be initialized calling setGasEstimation. + */ +export const useTransactionParameters = (props?: Props): TxParameters => { + const isCancelTransaction = sameString(props?.parameterStatus || 'ENABLED', 'CANCEL_TRANSACTION') + const connectedWalletAddress = useSelector(userAccountSelector) + const { address: safeAddress } = useSelector(safeSelector) || {} + + // Safe Params + const [safeNonce, setSafeNonce] = useState(props?.initialSafeNonce) + // SafeTxGas: for a new Tx call requiredTxGas, for an existing tx get it from the backend. + const [safeTxGas, setSafeTxGas] = useState(isCancelTransaction ? '0' : props?.initialSafeTxGas) + + // ETH Params + const [ethNonce, setEthNonce] = useState() // we delegate it to the wallet + const [ethGasLimit, setEthGasLimit] = useState(props?.initialEthGasLimit) // call execTx until it returns a number > 0 + const [ethGasPrice, setEthGasPrice] = useState(props?.initialEthGasPrice) // get fast gas price + const [ethGasPriceInGWei, setEthGasPriceInGWei] = useState() // get fast gas price + + // Get nonce for connected wallet + useEffect(() => { + const getNonce = async () => { + const res = await getUserNonce(connectedWalletAddress) + setEthNonce(res.toString()) + } + + if (connectedWalletAddress) { + getNonce() + } + }, [connectedWalletAddress]) + + // Get ETH gas price + useEffect(() => { + if (!ethGasPrice) { + setEthGasPriceInGWei(undefined) + return + } + if (isCancelTransaction) { + setEthGasPrice('0') + return + } + setEthGasPriceInGWei(web3.utils.toWei(ethGasPrice, 'Gwei')) + }, [ethGasPrice, isCancelTransaction]) + + // Calc safe nonce + useEffect(() => { + const getSafeNonce = async () => { + if (safeAddress) { + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + const lastTx = await getLastTx(safeAddress) + const nonce = await getNewTxNonce(lastTx, safeInstance) + setSafeNonce(nonce) + } + } + + const safeNonce = Number(props?.initialSafeNonce || 0) + if (!safeNonce) { + getSafeNonce() + } + }, [safeAddress, props?.initialSafeNonce]) + + return { + safeNonce, + setSafeNonce, + safeTxGas, + setSafeTxGas, + ethNonce, + setEthNonce, + ethGasLimit, + setEthGasLimit, + ethGasPrice, + setEthGasPrice, + ethGasPriceInGWei, + } +} diff --git a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts index c10615b2..82579f99 100644 --- a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts +++ b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts @@ -19,9 +19,9 @@ describe('shouldExecuteTransaction', () => { }) it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => { // given - const nonce = '0' + const nonce = '1' const threshold = '1' - const safeInstance = getMockedSafeInstance({ threshold }) + const safeInstance = getMockedSafeInstance({ threshold, nonce }) const lastTx = getMockedTxServiceModel({}) // when @@ -34,7 +34,7 @@ describe('shouldExecuteTransaction', () => { // given const nonce = '10' const threshold = '1' - const safeInstance = getMockedSafeInstance({ threshold }) + const safeInstance = getMockedSafeInstance({ threshold, nonce }) const lastTx = getMockedTxServiceModel({ isExecuted: true }) // when diff --git a/yarn.lock b/yarn.lock index c6eea477..19dc1109 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1553,9 +1553,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#2e7574f": version "0.4.0" - resolved "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6" + resolved "https://github.com/gnosis/safe-react-components.git#2e7574fa0ea4ec798aac35249f532be86a1c5cb8" dependencies: classnames "^2.2.6" polished "^3.6.7" @@ -1815,11 +1815,26 @@ "@ledgerhq/logs" "^5.38.0" rxjs "^6.6.3" +"@ledgerhq/devices@^5.41.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.41.0.tgz#e69d6aa4379a30f60cc8109f9855d902eb0d2f27" + integrity sha512-3rZzjJ8JEX2dBU+Nq8+FygxFoMZJ/XucjwNBXRaZQfqIFfYGywUH2RueQTY8d0JpzeSS5XYALAXzwLCRZmYrAg== + dependencies: + "@ledgerhq/errors" "^5.41.0" + "@ledgerhq/logs" "^5.41.0" + rxjs "^6.6.3" + semver "^7.3.4" + "@ledgerhq/errors@^5.34.0", "@ledgerhq/errors@^5.38.0": version "5.38.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.38.0.tgz#1642b87de47cbabc7b75ca93005a690895920a72" integrity sha512-d4gQzbOLNBoGIwDtEGFNSb0w0aYN10T5Y749e+vqiJoS3dWrB+5BCSQ/U/ALet0wi/UMIyFY/xmgd1gPaPB3Hw== +"@ledgerhq/errors@^5.36.0", "@ledgerhq/errors@^5.41.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.41.0.tgz#246668c6a6ee2e0e9fbb87f00b354fdd386308b7" + integrity sha512-Di6Uq9l9c/W9V1jZAQjsVPbvSOSRipF+zXd/Z6SVKB1QxIHwXZu7KYSUnDLV6jqKZ9fxXoLjTYWq2Gl/EW87Kw== + "@ledgerhq/hw-app-eth@^5.21.0": version "5.37.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-5.37.0.tgz#bd01465c78add275f6769e025a2ce4d142f1a10c" @@ -1875,11 +1890,25 @@ "@ledgerhq/errors" "^5.38.0" events "^3.2.0" +"@ledgerhq/hw-transport@^5.36.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.41.0.tgz#b4d222fdef304582efaadf098d1916f5edb9caa3" + integrity sha512-4zmTW1XwxvO5Ei+LoV11qXPd97UI8XxwuCeb794g/r6dkqlAL8bh35aVCHpbiHRXnoCt51a74ZciQ1yYteR/8Q== + dependencies: + "@ledgerhq/devices" "^5.41.0" + "@ledgerhq/errors" "^5.41.0" + events "^3.2.0" + "@ledgerhq/logs@^5.30.0", "@ledgerhq/logs@^5.38.0": version "5.38.0" resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.38.0.tgz#3c5dbd1f62c0bf5580477b218fb57c67b575643a" integrity sha512-i87Yn89Cq2D9Y0KmrEzCm62XHzI2edeOTBENKH6vAyzESGzyF+SBoqtZNwrjJcKup3/9dNn/zHjpicY7ev94Vw== +"@ledgerhq/logs@^5.41.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.41.0.tgz#377b77aa29c33c63c6b0b7e1916fa300bd52ce17" + integrity sha512-pY3nIxOWISAreVTiKRDdKgjQvKWkzz3lov9LsJLyaTn0Q1PISHPjcuMpchueLI50BMA6v1M8ENzXgpqqw+KQDw== + "@material-ui/core@^4.11.0": version "4.11.2" resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.2.tgz#f8276dfa40d88304e6ceb98962af73803d27d42d" @@ -17854,7 +17883,7 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2: +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== From 2bd73c3443560f2e0619d3183279b0e7b9ae76df Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Mon, 1 Feb 2021 12:32:08 +0100 Subject: [PATCH 17/20] Set v2.19.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb592303..3e6b3123 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "2.18.1", + "version": "2.19.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { From 4bf1a74b330597beb56cb124e41885a58b6ec62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Mon, 1 Feb 2021 16:02:14 -0300 Subject: [PATCH 18/20] fix 'Advanced options' presentation in modal --- .../components/Settings/ManageOwners/AddOwnerModal/index.tsx | 1 - .../components/Settings/ManageOwners/RemoveOwnerModal/index.tsx | 1 - .../components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index c3121921..37a3427c 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -21,7 +21,6 @@ import ThresholdForm from './screens/ThresholdForm' const styles = createStyles({ biggerModalWindow: { width: '775px', - minHeight: '500px', height: 'auto', }, }) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 575046d3..6a5e8f86 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -18,7 +18,6 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara const styles = createStyles({ biggerModalWindow: { width: '775px', - minHeight: '500px', height: 'auto', }, }) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 6e0e9600..a1d6c7db 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -21,7 +21,6 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara const styles = createStyles({ biggerModalWindow: { width: '775px', - minHeight: '500px', height: 'auto', }, }) From 42821c11d253401080535082d6df021be00a82b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Mon, 1 Feb 2021 19:20:15 -0300 Subject: [PATCH 19/20] Avoids changing the transaction fee message while loading the estimation --- src/components/TransactionsFees/index.tsx | 3 +++ .../RemoveOwnerModal/screens/Review/index.tsx | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/TransactionsFees/index.tsx b/src/components/TransactionsFees/index.tsx index c5e54f19..09187210 100644 --- a/src/components/TransactionsFees/index.tsx +++ b/src/components/TransactionsFees/index.tsx @@ -21,6 +21,9 @@ export const TransactionFees = ({ txEstimationExecutionStatus, }: TransactionFailTextProps): React.ReactElement | null => { let transactionAction + if (txEstimationExecutionStatus === EstimationStatus.LOADING) { + return null + } if (isCreation) { transactionAction = 'create' } else if (isExecution) { diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index ece059c9..36ec7e79 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -216,16 +216,18 @@ export const ReviewRemoveOwnerModal = ({ isTransactionExecution={isExecution} /> - - - - + {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( + + + + + )}