From f0223958d4afa88070452962735ad8cbe21f1cb1 Mon Sep 17 00:00:00 2001 From: lukasschor Date: Tue, 2 Feb 2021 17:42:23 +0300 Subject: [PATCH 01/34] Update copy during onboarding (#1830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update copy during onboarding Quite a few users have pointed out that it was not clear that owners can be changed after the Safe is created. E.g. "- Once I’ve created a wallet, it seems it’s possible to revoke and change a signer, but it wasn’t immediately clear if I can change the threshold." Co-authored-by: Daniel Sanchez --- .../open/components/SafeOwnersConfirmationsForm/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx index a1469842..8dd4c27d 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -157,8 +157,8 @@ const SafeOwnersForm = (props): React.ReactElement => {

Add additional owners (e.g. wallets of your teammates) and specify how many of them have to confirm a - transaction before it gets executed. In general, the more confirmations required, the more secure is your - Safe. + transaction before it gets executed. You can also add/remove owners and change the signature threshold after + your Safe is created. From 306ea4a14ba8da47e3bda399a30f1683f026661e Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Tue, 2 Feb 2021 12:39:54 -0300 Subject: [PATCH 02/34] Check that the estimation of the node is bigger than 0 before returning --- src/logic/safe/transactions/gas.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index bcee4537..c7e54e0b 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -182,15 +182,17 @@ const calculateMinimumGasForTransaction = async ( const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + additionalGas console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`) try { - await getGasEstimationTxResponse({ + const estimation = await getGasEstimationTxResponse({ to: safeAddress, from: safeAddress, data: estimateData, gasPrice: 0, gas: amountOfGasToTryTx, }) - console.info(`Gas estimation successfully finished with gas amount: ${amountOfGasToTryTx}`) - return amountOfGasToTryTx + if (estimation > 0) { + console.info(`Gas estimation successfully finished with gas amount: ${amountOfGasToTryTx}`) + return amountOfGasToTryTx + } } catch (error) { console.log(`Error trying to estimate gas with amount: ${amountOfGasToTryTx}`) } From 7921a8d9f31a36cb1546bc18fb283348b948b944 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Tue, 2 Feb 2021 22:16:38 +0100 Subject: [PATCH 03/34] Set v2.19.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e6b3123..af48d2d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "2.19.0", + "version": "2.19.1", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { From 038b51e3983d44cdcf27a201cf0949dffec8ab10 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 3 Feb 2021 14:15:44 +0100 Subject: [PATCH 04/34] Bump dependencies (#1850) * Upgrade dependencies without breaking changes --- package.json | 26 +- yarn.lock | 1216 +++++++++++++++++++++++++------------------------- 2 files changed, 632 insertions(+), 610 deletions(-) diff --git a/package.json b/package.json index af48d2d0..dfc08394 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ } ] }, - "files": [ + "files": [ "build", "patches", "public", @@ -158,20 +158,20 @@ ] }, "dependencies": { - "@gnosis.pm/safe-apps-sdk": "1.0.2", + "@gnosis.pm/safe-apps-sdk": "1.0.3", "@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#2e7574f", "@gnosis.pm/util-contracts": "2.0.6", - "@ledgerhq/hw-transport-node-hid-singleton": "5.38.0", + "@ledgerhq/hw-transport-node-hid-singleton": "5.41.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.11.0", - "@material-ui/lab": "4.0.0-alpha.56", + "@material-ui/lab": "4.0.0-alpha.57", "@openzeppelin/contracts": "3.1.0", - "@sentry/react": "^5.28.0", - "@sentry/tracing": "^5.28.0", - "@unstoppabledomains/resolution": "^1.11.1", + "@sentry/react": "^5.30.0", + "@sentry/tracing": "^5.30.0", "@truffle/contract": "^4.3.0", + "@unstoppabledomains/resolution": "^1.17.0", "async-sema": "^3.1.0", "axios": "0.21.1", "bignumber.js": "9.0.1", @@ -229,7 +229,7 @@ }, "devDependencies": { "@rescripts/cli": "^0.0.15", - "@sentry/cli": "^1.59.0", + "@sentry/cli": "^1.62.0", "@storybook/addon-actions": "^5.3.19", "@storybook/addon-links": "^5.3.19", "@storybook/addons": "^5.3.19", @@ -247,16 +247,16 @@ "@types/react-redux": "^7.1.11", "@types/react-router-dom": "^5.1.6", "@types/styled-components": "^5.1.4", - "@typescript-eslint/eslint-plugin": "^4.6.0", - "@typescript-eslint/parser": "^4.6.0", + "@typescript-eslint/eslint-plugin": "^4.14.0", + "@typescript-eslint/parser": "^4.14.0", "cross-env": "^7.0.3", "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", "electron": "^9.4.0", "electron-builder": "22.9.1", "electron-notarize": "1.0.0", - "eslint": "^7.11.0", - "eslint-config-prettier": "^7.0.0", + "eslint": "^7.17.0", + "eslint-config-prettier": "^7.2.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prettier": "^3.1.4", @@ -267,7 +267,7 @@ "patch-package": "^6.2.2", "postinstall-postinstall": "^2.1.0", "prettier": "^2.2.0", - "sass": "^1.29.0", + "sass": "^1.32.0", "typechain": "^4.0.0", "typescript": "4.1.3", "wait-on": "5.2.1" diff --git a/yarn.lock b/yarn.lock index 19dc1109..2b42543a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,11 +29,11 @@ "@babel/highlight" "^7.0.0" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== dependencies: - "@babel/highlight" "^7.10.4" + "@babel/highlight" "^7.12.13" "@babel/compat-data@^7.12.1", "@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": version "7.12.7" @@ -280,12 +280,12 @@ "@babel/traverse" "^7.12.5" "@babel/types" "^7.12.5" -"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== +"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" + integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" @@ -1091,9 +1091,9 @@ "@babel/plugin-transform-typescript" "^7.12.1" "@babel/runtime-corejs3@^7.10.2": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4" - integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ== + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.13.tgz#53d09813b7c20d616caf258e9325550ff701c039" + integrity sha512-8fSpqYRETHATtNitsCXq8QQbKJP31/KnDl2Wz2Vtui9nKzjss2ysuZtyVsWjBtvkeEFo346gkwjYPab1hvrXkQ== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" @@ -1105,10 +1105,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.1.5", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" + integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== dependencies: regenerator-runtime "^0.13.4" @@ -1182,16 +1182,17 @@ ajv-keywords "^3.4.1" "@electron/get@^1.0.1": - version "1.12.2" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.2.tgz#6442066afb99be08cefb9a281e4b4692b33764f3" - integrity sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg== + version "1.12.3" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.3.tgz#fa2723385c4b565a34c4c82f46087aa2a5fbf6d0" + integrity sha512-NFwSnVZQK7dhOYF1NQCt+HGqgL1aNdj0LUSx75uCqnZJqyiWCVdAMFV4b4/kC8HjUJAnsvdSEmjEt4G2qNQ9+Q== dependencies: debug "^4.1.1" env-paths "^2.2.0" + filenamify "^4.1.0" fs-extra "^8.1.0" got "^9.6.0" progress "^2.0.3" - sanitize-filename "^1.6.2" + semver "^6.2.0" sumchecker "^3.0.1" optionalDependencies: global-agent "^2.0.2" @@ -1308,10 +1309,10 @@ 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" - integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ== +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -1320,7 +1321,7 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -1370,9 +1371,9 @@ "@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" - integrity sha512-fqJXkewcGdi8LogKMgRyzc/Ls2js07yor7+g9KfPs09uPOcQLg7cc34JN+lk34HH9gg2HU0DIA5797ZR8znkfw== + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.9.tgz#a55410b73e3994842884eb82b1f43e3a9f653eea" + integrity sha512-X9fMkqpeu9ayC3JyBkeeZhn35P4xQkpGX/l+FrxDtEW9tybf/UWXSMi8bGThpPtfJ6q6U2LDetXSpSwK4TfYQQ== dependencies: "@ethersproject/bignumber" "^5.0.13" "@ethersproject/bytes" "^5.0.9" @@ -1383,9 +1384,9 @@ "@ethersproject/web" "^5.0.12" "@ethersproject/abstract-signer@^5.0.10": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.0.10.tgz#0b35359d470f2996823769ec183442352deb4c6c" - integrity sha512-irx7kH7FDAeW7QChDPW19WsxqeB1d3XLyOLSXm0bfPqL1SS07LXWltBJUBUxqC03ORpAOcM3JQj57DU8JnVY2g== + version "5.0.12" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.0.12.tgz#04ab597eb87a08faaab19dd5a739339e1e3beb58" + integrity sha512-qt4jAEzQGPZ31My1gFGPzzJHJveYhVycW7RHkuX0W8fvMdg7wr0uvP7mQEptMVrb+jYwsVktCf6gBGwWDpFiTA== dependencies: "@ethersproject/abstract-provider" "^5.0.8" "@ethersproject/bignumber" "^5.0.13" @@ -1394,9 +1395,9 @@ "@ethersproject/properties" "^5.0.7" "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.0.9": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.9.tgz#347ef30dc8243c682574a3f23ff63f73c8f8cbf1" - integrity sha512-gKkmbZDMyGbVjr8nA5P0md1GgESqSGH7ILIrDidPdNXBl4adqbuA3OAuZx/O2oGpL6PtJ9BDa0kHheZ1ToHU3w== + version "5.0.10" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.10.tgz#2bc69fdff4408e0570471cd19dee577ab06a10d0" + integrity sha512-70vqESmW5Srua1kMDIN6uVfdneZMaMyRYH4qPvkAXGkbicrCOsA9m01vIloA4wYiiF+HLEfL1ENKdn5jb9xiAw== dependencies: "@ethersproject/bignumber" "^5.0.13" "@ethersproject/bytes" "^5.0.9" @@ -1405,39 +1406,39 @@ "@ethersproject/rlp" "^5.0.7" "@ethersproject/base64@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.0.7.tgz#d5da73699b4a33dc92bd8e5056ad1880b262057d" - integrity sha512-S5oh5DVfCo06xwJXT8fQC68mvJfgScTl2AXvbYMsHNfIBTDb084Wx4iA9MNlEReOv6HulkS+gyrUM/j3514rSw== + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.0.8.tgz#1bc4b4b8c59c1debf972c7164b96c0b8964a20a1" + integrity sha512-PNbpHOMgZpZ1skvQl119pV2YkCPXmZTxw+T92qX0z7zaMFPypXWTZBzim+hUceb//zx4DFjeGT4aSjZRTOYThg== dependencies: "@ethersproject/bytes" "^5.0.9" "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.0.13", "@ethersproject/bignumber@^5.0.7": - version "5.0.13" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.13.tgz#a5466412b3b80104097b9c694f6ae827df4353fe" - integrity sha512-b89bX5li6aK492yuPP5mPgRVgIxxBP7ksaBtKX5QQBsrZTpNOjf/MR4CjcUrAw8g+RQuD6kap9lPjFgY4U1/5A== + version "5.0.14" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.14.tgz#605bc61dcbd4a8c6df8b5a7a77c0210273f3de8a" + integrity sha512-Q4TjMq9Gg3Xzj0aeJWqJgI3tdEiPiET7Y5OtNtjTAODZ2kp4y9jMNg97zVcvPedFvGROdpGDyCI77JDFodUzOw== dependencies: "@ethersproject/bytes" "^5.0.9" "@ethersproject/logger" "^5.0.8" bn.js "^4.4.0" "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.0.9": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.9.tgz#2748247402ad20df69f3a3e935dc7b58c0d75c08" - integrity sha512-k+17ZViDtAugC0s7HM6rdsTWEdIYII4RPCDkPEuxKc6i40Bs+m6tjRAtCECX06wKZnrEoR9pjOJRXHJ/VLoOcA== + version "5.0.10" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.10.tgz#aa49afe7491ba24ff76fa33d98677351263f9ba4" + integrity sha512-vpu0v1LZ1j1s9kERQIMnVU69MyHEzUff7nqK9XuCU4vx+AM8n9lU2gj7jtJIvGSt9HzatK/6I6bWusI5nyuaTA== dependencies: "@ethersproject/logger" "^5.0.8" "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4", "@ethersproject/constants@^5.0.8": - version "5.0.8" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.8.tgz#50f2e23f48c0d1d0de3759ea79b68ec3e06435a1" - integrity sha512-sCc73pFBsl59eDfoQR5OCEZCRv5b0iywadunti6MQIr5lt3XpwxK1Iuzd8XSFO02N9jUifvuZRrt0cY0+NBgTg== + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.9.tgz#81ac44c3bf612de63eb1c490b314ea1b932cda9f" + integrity sha512-2uAKH89UcaJP/Sc+54u92BtJtZ4cPgcS1p0YbB1L3tlkavwNvth+kNCUplIB1Becqs7BOZr0B/3dMNjhJDy4Dg== dependencies: "@ethersproject/bignumber" "^5.0.13" "@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.0.4": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.0.10.tgz#41bf37428e8ddbc229ffd81c47af667174cb491a" - integrity sha512-Tf0bvs6YFhw28LuHnhlDWyr0xfcDxSXdwM4TcskeBbmXVSKLv3bJQEEEBFUcRX0fJuslR3gCVySEaSh7vuMx5w== + version "5.0.11" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.0.11.tgz#da89517438bbbf8a39df56fff09f0a71669ae7a7" + integrity sha512-H3KJ9fk33XWJ2djAW03IL7fg3DsDMYjO1XijiUb1hJ85vYfhvxu0OmsU7d3tg2Uv1H1kFSo8ghr3WFQ8c+NL3g== dependencies: "@ethersproject/abstract-signer" "^5.0.10" "@ethersproject/address" "^5.0.9" @@ -1449,44 +1450,44 @@ "@ethersproject/strings" "^5.0.8" "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.3", "@ethersproject/keccak256@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.0.7.tgz#2eedb5e4c160fcdf0079660f8ae362d7855ea943" - integrity sha512-zpUBmofWvx9PGfc7IICobgFQSgNmTOGTGLUxSYqZzY/T+b4y/2o5eqf/GGmD7qnTGzKQ42YlLNo+LeDP2qe55g== + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.0.8.tgz#13aaf69e1c8bd15fc59a2ebd055c0878f2a059c8" + integrity sha512-zoGbwXcWWs9MX4NOAZ7N0hhgIRl4Q/IO/u9c/RHRY4WqDy3Ywm0OLamEV53QDwhjwn3YiiVwU1Ve5j7yJ0a/KQ== dependencies: "@ethersproject/bytes" "^5.0.9" js-sha3 "0.5.7" "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5", "@ethersproject/logger@^5.0.8": - version "5.0.8" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.0.8.tgz#135c1903d35c878265f3cbf2b287042c4c20d5d4" - integrity sha512-SkJCTaVTnaZ3/ieLF5pVftxGEFX56pTH+f2Slrpv7cU0TNpUZNib84QQdukd++sWUp/S7j5t5NW+WegbXd4U/A== + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.0.9.tgz#0e6a0b3ecc938713016954daf4ac7967467aa763" + integrity sha512-kV3Uamv3XOH99Xf3kpIG3ZkS7mBNYcLDM00JSDtNgNB4BihuyxpQzIZPRIDmRi+95Z/R1Bb0X2kUNHa/kJoVrw== "@ethersproject/networks@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.0.7.tgz#8d06e41197b27c2404d89a29ca21f741a01acbfc" - integrity sha512-dI14QATndIcUgcCBL1c5vUr/YsI5cCHLN81rF7PU+yS7Xgp2/Rzbr9+YqpC6NBXHFUASjh6GpKqsVMpufAL0BQ== + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.0.8.tgz#37e6f8c058f2d540373ea5939056cd3de069132e" + integrity sha512-PYpptlO2Tu5f/JEBI5hdlMds5k1DY1QwVbh3LKPb3un9dQA2bC51vd2/gRWAgSBpF3kkmZOj4FhD7ATLX4H+DA== dependencies: "@ethersproject/logger" "^5.0.8" "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.0.3", "@ethersproject/properties@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.0.7.tgz#951d11ba592ff90bbe8ec34c5a03a5157e3b3360" - integrity sha512-812H1Rus2vjw0zbasfDI1GLNPDsoyX1pYqiCgaR1BuyKxUTbwcH1B+214l6VGe1v+F6iEVb7WjIwMjKhb4EUsg== + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.0.8.tgz#e45d28d25402c73394873dbf058f856c966cae01" + integrity sha512-zEnLMze2Eu2VDPj/05QwCwMKHh506gpT9PP9KPVd4dDB+5d6AcROUYVLoIIQgBYK7X/Gw0UJmG3oVtnxOQafAw== dependencies: "@ethersproject/logger" "^5.0.8" "@ethersproject/rlp@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.0.7.tgz#cfa4fa6415960a435b7814e1a29bdfea657e2b6e" - integrity sha512-ulUTVEuV7PT4jJTPpfhRHK57tkLEDEY9XSYJtrSNHOqdwMvH0z7BM2AKIMq4LVDlnu4YZASdKrkFGEIO712V9w== + version "5.0.8" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.0.8.tgz#ff54e206d0ae28640dd054f2bcc7070f06f9dfbe" + integrity sha512-E4wdFs8xRNJfzNHmnkC8w5fPeT4Wd1U2cust3YeT16/46iSkLT8nn8ilidC6KhR7hfuSZE4UqSPzyk76p7cdZg== dependencies: "@ethersproject/bytes" "^5.0.9" "@ethersproject/logger" "^5.0.8" "@ethersproject/signing-key@^5.0.8": - version "5.0.8" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.0.8.tgz#156522e542916b9aa9135527b40c5b6f9235af02" - integrity sha512-YKxQM45eDa6WAD+s3QZPdm1uW1MutzVuyoepdRRVmMJ8qkk7iOiIhUkZwqKLNxKzEJijt/82ycuOREc9WBNAKg== + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.0.9.tgz#37e3038e26b53979d41dd90a2077fb0efd020fcc" + integrity sha512-AobnsEiLv+Z4a/NbbelwB/Lsnc+qxeNejXDlEwbo/nwjijvxLpwiNN+rjx/lQGel1QnQ/d+lEv7xezyUaXdKFQ== dependencies: "@ethersproject/bytes" "^5.0.9" "@ethersproject/logger" "^5.0.8" @@ -1494,18 +1495,18 @@ elliptic "6.5.3" "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.4", "@ethersproject/strings@^5.0.8": - version "5.0.8" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.8.tgz#11a1b0ed1e8417408693789839f0b5f4e323c0c9" - integrity sha512-5IsdXf8tMY8QuHl8vTLnk9ehXDDm6x9FB9S9Og5IA1GYhLe5ZewydXSjlJlsqU2t9HRbfv97OJZV/pX8DVA/Hw== + version "5.0.9" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.9.tgz#8e2eb2918b140231e1d1b883d77e43213a8ac280" + integrity sha512-ogxBpcUpdO524CYs841MoJHgHxEPUy0bJFDS4Ezg8My+WYVMfVAOlZSLss0Rurbeeam8CpUVDzM4zUn09SU66Q== dependencies: "@ethersproject/bytes" "^5.0.9" "@ethersproject/constants" "^5.0.8" "@ethersproject/logger" "^5.0.8" "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.0.9": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.0.9.tgz#ccfcc1d395b5e3ce7342545fa28bfe5541182fd6" - integrity sha512-0Fu1yhdFBkrbMjenEr+39tmDxuHmaw0pe9Jb18XuKoItj7Z3p7+UzdHLr2S/okvHDHYPbZE5gtANDdQ3ZL1nBA== + version "5.0.10" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.0.10.tgz#d50cafd80d27206336f80114bc0f18bc18687331" + integrity sha512-Tqpp+vKYQyQdJQQk4M73tDzO7ODf2D42/sJOcKlDAAbdSni13v6a+31hUdo02qYXhVYwIs+ZjHnO4zKv5BNk8w== dependencies: "@ethersproject/address" "^5.0.9" "@ethersproject/bignumber" "^5.0.13" @@ -1518,9 +1519,9 @@ "@ethersproject/signing-key" "^5.0.8" "@ethersproject/web@^5.0.12": - version "5.0.12" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.0.12.tgz#f123397c107f863c31fce5f31a97c66ec155e755" - integrity sha512-gVxS5iW0bgidZ76kr7LsTxj4uzN5XpCLzvZrLp8TP+4YgxHfCeetFyQkRPgBEAJdNrexdSBayvyJvzGvOq0O8g== + version "5.0.13" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.0.13.tgz#5a92ac6d835d2ebce95b6b645a86668736e2f532" + integrity sha512-G3x/Ns7pQm21ALnWLbdBI5XkW/jrsbXXffI9hKNPHqf59mTxHYtlNiSwxdoTSwCef3Hn7uvGZpaSgTyxs7IufQ== dependencies: "@ethersproject/base64" "^5.0.7" "@ethersproject/bytes" "^5.0.9" @@ -1533,10 +1534,10 @@ resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-0.4.2.tgz#ae87b2164931c006cb0efdede3d82ff210df1648" integrity sha512-BwA2dyCebPMdi4JhhTkp6EjkhEM6vAIviKdhqHiHnSmL+sDfxtP1jdOuE8ME2/4+5TiLSS8k8qscYjLSlf1LLw== -"@gnosis.pm/safe-apps-sdk@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-1.0.2.tgz#c1a3e8799a0d36888556e15438a1bbe3b3c586ff" - integrity sha512-bXbD4ri92Ckc8otQBcESCXT5mdynrz7284xWDKNlGAQSnJU2JKimex5W/mpf80QUD3UNnww7YoEvz1h1nNz7+A== +"@gnosis.pm/safe-apps-sdk@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-1.0.3.tgz#006d709700419302af490f6beaac67b1db4b36fb" + integrity sha512-77c6lg4SfCbQtMjgB2vVPKQglUCOjSpwweNeoD4m0c5hnd5lFg0Dlcc45LwtcUL8j5XhDf+aBxT8LDD+XGDSNw== dependencies: semver "^7.3.2" web3-core "^1.3.0" @@ -1806,15 +1807,6 @@ dependencies: invariant "2" -"@ledgerhq/devices@^5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.38.0.tgz#0b805020c4f3ac40d35f1b1af6d64d04256b1e0f" - integrity sha512-1RZ+Dh+oVUDMeaPSCeQ56qzgiMHHy481dbkRCDUMRJEzkGqOLI3k34x7XdkVKy1NQdt8G8sYyobP/yixDry5ow== - dependencies: - "@ledgerhq/errors" "^5.38.0" - "@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" @@ -1825,12 +1817,7 @@ 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": +"@ledgerhq/errors@^5.34.0", "@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== @@ -1846,27 +1833,27 @@ bignumber.js "^9.0.1" rlp "^2.2.6" -"@ledgerhq/hw-transport-node-hid-noevents@^5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-5.38.0.tgz#ffbfb0b997e585fc08b6b220997caaa59e094554" - integrity sha512-zuxN3gWfCuN+pbK3BKc8z3VCulKI7zee1N3xhCWua9TiwL3leBTBs25Bvm22fmiLJYhzszccbF673/bpnnIQAA== +"@ledgerhq/hw-transport-node-hid-noevents@^5.41.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-5.41.0.tgz#5049a64344305540698f947c1f60bb238546cc85" + integrity sha512-XMFM10KAPLQMkNYqawcxy9uXGw6B/SRxNILx/gKXhsBvl20yYCMz6zIwgdlOTPTcgckS7IkAaghwoEN8U0Fk8A== dependencies: - "@ledgerhq/devices" "^5.38.0" - "@ledgerhq/errors" "^5.38.0" - "@ledgerhq/hw-transport" "^5.38.0" - "@ledgerhq/logs" "^5.38.0" + "@ledgerhq/devices" "^5.41.0" + "@ledgerhq/errors" "^5.41.0" + "@ledgerhq/hw-transport" "^5.41.0" + "@ledgerhq/logs" "^5.41.0" node-hid "2.1.1" -"@ledgerhq/hw-transport-node-hid-singleton@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-singleton/-/hw-transport-node-hid-singleton-5.38.0.tgz#ea133ac3a7015aaafd5d629388da4d4a2086c761" - integrity sha512-FPpr6R2Cs6YKCamprC9vzJa+P7RieyMLQC3cG6EjYg8tmvHPtIxpmWFclNi45jZ05Ve1YvNjutoHVTVn0cOnpg== +"@ledgerhq/hw-transport-node-hid-singleton@5.41.0": + version "5.41.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-singleton/-/hw-transport-node-hid-singleton-5.41.0.tgz#d5b0fa4d45dfa6c904e726ab35e30fec025edb99" + integrity sha512-1wF+FQ5m9NTv/aInisiQ7VLgN3JY3xTIhDxn/5S0a4t1JojK0JPJAiOSp9IgEkCKRsKNBEwB4ghOmtB/qD/5EQ== dependencies: - "@ledgerhq/devices" "^5.38.0" - "@ledgerhq/errors" "^5.38.0" - "@ledgerhq/hw-transport" "^5.38.0" - "@ledgerhq/hw-transport-node-hid-noevents" "^5.38.0" - "@ledgerhq/logs" "^5.38.0" + "@ledgerhq/devices" "^5.41.0" + "@ledgerhq/errors" "^5.41.0" + "@ledgerhq/hw-transport" "^5.41.0" + "@ledgerhq/hw-transport-node-hid-noevents" "^5.41.0" + "@ledgerhq/logs" "^5.41.0" lodash "^4.17.20" node-hid "2.1.1" usb-detection "^4.10.0" @@ -1881,16 +1868,7 @@ "@ledgerhq/logs" "^5.30.0" u2f-api "0.2.7" -"@ledgerhq/hw-transport@^5.34.0", "@ledgerhq/hw-transport@^5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.38.0.tgz#b02bea73d77e729d13c367a967df1666f9ef940d" - integrity sha512-CAxvHukCcp+RqaEsSltmBw4Lb1yW42fiF/LTYN7JvCkZyLIQhvkndLDVCgp4hpMdtLK4bkf7RJRElqbN0vRoAQ== - dependencies: - "@ledgerhq/devices" "^5.38.0" - "@ledgerhq/errors" "^5.38.0" - events "^3.2.0" - -"@ledgerhq/hw-transport@^5.36.0": +"@ledgerhq/hw-transport@^5.34.0", "@ledgerhq/hw-transport@^5.36.0", "@ledgerhq/hw-transport@^5.41.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== @@ -1899,24 +1877,19 @@ "@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": +"@ledgerhq/logs@^5.30.0", "@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" - integrity sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ== + version "4.11.3" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.3.tgz#f22e41775b0bd075e36a7a093d43951bf7f63850" + integrity sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw== dependencies: "@babel/runtime" "^7.4.4" - "@material-ui/styles" "^4.11.2" - "@material-ui/system" "^4.11.2" + "@material-ui/styles" "^4.11.3" + "@material-ui/system" "^4.11.3" "@material-ui/types" "^5.1.0" "@material-ui/utils" "^4.11.2" "@types/react-transition-group" "^4.2.0" @@ -1934,21 +1907,21 @@ dependencies: "@babel/runtime" "^7.4.4" -"@material-ui/lab@4.0.0-alpha.56": - version "4.0.0-alpha.56" - resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz#ff63080949b55b40625e056bbda05e130d216d34" - integrity sha512-xPlkK+z/6y/24ka4gVJgwPfoCF4RCh8dXb1BNE7MtF9bXEBLN/lBxNTK8VAa0qm3V2oinA6xtUIdcRh0aeRtVw== +"@material-ui/lab@4.0.0-alpha.57": + version "4.0.0-alpha.57" + resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a" + integrity sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw== dependencies: "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.10.2" + "@material-ui/utils" "^4.11.2" clsx "^1.0.4" prop-types "^15.7.2" - react-is "^16.8.0" + react-is "^16.8.0 || ^17.0.0" -"@material-ui/styles@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.2.tgz#e70558be3f41719e8c0d63c7a3c9ae163fdc84cb" - integrity sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw== +"@material-ui/styles@^4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2" + integrity sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg== dependencies: "@babel/runtime" "^7.4.4" "@emotion/hash" "^0.8.0" @@ -1957,20 +1930,20 @@ clsx "^1.0.4" csstype "^2.5.2" hoist-non-react-statics "^3.3.2" - jss "^10.0.3" - jss-plugin-camel-case "^10.0.3" - jss-plugin-default-unit "^10.0.3" - jss-plugin-global "^10.0.3" - jss-plugin-nested "^10.0.3" - jss-plugin-props-sort "^10.0.3" - jss-plugin-rule-value-function "^10.0.3" - jss-plugin-vendor-prefixer "^10.0.3" + jss "^10.5.1" + jss-plugin-camel-case "^10.5.1" + jss-plugin-default-unit "^10.5.1" + jss-plugin-global "^10.5.1" + jss-plugin-nested "^10.5.1" + jss-plugin-props-sort "^10.5.1" + jss-plugin-rule-value-function "^10.5.1" + jss-plugin-vendor-prefixer "^10.5.1" prop-types "^15.7.2" -"@material-ui/system@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.11.2.tgz#7f0a754bba3673ed5fdbfa02fe438096c104b1f6" - integrity sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A== +"@material-ui/system@^4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.11.3.tgz#466bc14c9986798fd325665927c963eb47cc4143" + integrity sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.11.2" @@ -1982,7 +1955,7 @@ resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== -"@material-ui/utils@^4.10.2", "@material-ui/utils@^4.11.2": +"@material-ui/utils@^4.11.2": version "4.11.2" resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a" integrity sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA== @@ -2161,20 +2134,20 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@sentry/browser@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.29.2.tgz#51adb4005511b1a4a70e4571880fc6653d651f91" - integrity sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ== +"@sentry/browser@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.30.0.tgz#c28f49d551db3172080caef9f18791a7fd39e3b3" + integrity sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw== dependencies: - "@sentry/core" "5.29.2" - "@sentry/types" "5.29.2" - "@sentry/utils" "5.29.2" + "@sentry/core" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" tslib "^1.9.3" -"@sentry/cli@^1.59.0": - version "1.61.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.61.0.tgz#98be4cce2d61df91a8cb42afb740b46eba029925" - integrity sha512-pHEhqP1bB4sdO7N5ow/IkRBrPbKT9HZRinq4PhTVIvmG+NW4VVuVZ6k4tlbp+JXmzMcUc/iXynVkTL7zJIlTQw== +"@sentry/cli@^1.62.0": + version "1.62.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.62.0.tgz#2e2d8cacb5ec494f86c188ef95b41745605be028" + integrity sha512-MCkx+zjetdIWhAVaFuEuoD4MOAFlb3/GLR5B5uFZ1AcegcsggasLo/3rb2gq6jYIic/pubtRjH4ltmOL/s3cag== dependencies: https-proxy-agent "^5.0.0" mkdirp "^0.5.5" @@ -2182,69 +2155,69 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/core@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.29.2.tgz#9e05fe197234161d57aabaf52fab336a7c520d81" - integrity sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw== +"@sentry/core@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" + integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== dependencies: - "@sentry/hub" "5.29.2" - "@sentry/minimal" "5.29.2" - "@sentry/types" "5.29.2" - "@sentry/utils" "5.29.2" + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" tslib "^1.9.3" -"@sentry/hub@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.29.2.tgz#208f10fe6674695575ad74182a1151f71d6df00a" - integrity sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ== +"@sentry/hub@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" + integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== dependencies: - "@sentry/types" "5.29.2" - "@sentry/utils" "5.29.2" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" tslib "^1.9.3" -"@sentry/minimal@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.29.2.tgz#420bebac8d03d30980fdb05c72d7b253d8aa541b" - integrity sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw== +"@sentry/minimal@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" + integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== dependencies: - "@sentry/hub" "5.29.2" - "@sentry/types" "5.29.2" + "@sentry/hub" "5.30.0" + "@sentry/types" "5.30.0" tslib "^1.9.3" -"@sentry/react@^5.28.0": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.29.2.tgz#e0e2055db6f9c7a3957630e726b23d6a0f2d12f2" - integrity sha512-Fvh4l6/wrnO3FWqte7lPUsuWE5o6t3FHwZqVINgCIabpwPMorvOVzm/gwG7uzhuCoyNTP28svR670sl4BnRuTg== +"@sentry/react@^5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.30.0.tgz#320e05f766b6a26faefa8d76d1101fd50c69f541" + integrity sha512-dvn4mqCgbeEuUXEGp5P9PaW5j4GWTFUSdx/yG8f9IxNZv5zM+7otjog9ukrubFZvlxVxD/PrIxK0MhadfFY/Dw== dependencies: - "@sentry/browser" "5.29.2" - "@sentry/minimal" "5.29.2" - "@sentry/types" "5.29.2" - "@sentry/utils" "5.29.2" + "@sentry/browser" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" hoist-non-react-statics "^3.3.2" tslib "^1.9.3" -"@sentry/tracing@^5.28.0": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.29.2.tgz#6012788547d2ab7893799d82c4941bda145dcd47" - integrity sha512-iumYbVRpvoU3BUuIooxibydeaOOjl5ysc+mzsqhRs2NGW/C3uKAsFXdvyNfqt3bxtRQwJEhwJByLP2u3pLThpw== +"@sentry/tracing@^5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" + integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== dependencies: - "@sentry/hub" "5.29.2" - "@sentry/minimal" "5.29.2" - "@sentry/types" "5.29.2" - "@sentry/utils" "5.29.2" + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" tslib "^1.9.3" -"@sentry/types@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.29.2.tgz#ac87383df1222c2d9b9f8f9ed7a6b86ea41a098a" - integrity sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA== +"@sentry/types@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" + integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== -"@sentry/utils@5.29.2": - version "5.29.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.29.2.tgz#99a5cdda2ea19d34a41932f138d470adcb3ee673" - integrity sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ== +"@sentry/utils@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" + integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== dependencies: - "@sentry/types" "5.29.2" + "@sentry/types" "5.30.0" tslib "^1.9.3" "@sideway/address@^4.1.0": @@ -2872,9 +2845,9 @@ defer-to-connect "^1.0.1" "@testing-library/dom@^7.28.1": - version "7.28.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.28.1.tgz#dea78be6e1e6db32ddcb29a449e94d9700c79eb9" - integrity sha512-acv3l6kDwZkQif/YqJjstT3ks5aaI33uxGNVIQmdKzbZ2eMKgg3EV2tB84GDdc72k3Kjhl6mO8yUt6StVIdRDg== + version "7.29.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.4.tgz#1647c2b478789621ead7a50614ad81ab5ae5b86c" + integrity sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" @@ -2886,9 +2859,9 @@ pretty-format "^26.6.2" "@testing-library/jest-dom@^5.11.6": - version "5.11.8" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.8.tgz#433a84d6f9a089485101b9e112ef03e5c30bcbfc" - integrity sha512-ScyKrWQM5xNcr79PkSewnA79CLaoxVskE+f7knTOhDD9ftZSA1Jw8mj+pneqhEu3x37ncNfW84NUr7lqK+mXjA== + version "5.11.9" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz#e6b3cd687021f89f261bd53cbe367041fbd3e975" + integrity sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" @@ -2900,9 +2873,9 @@ redent "^3.0.0" "@testing-library/react@^11.2.2": - version "11.2.2" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.2.tgz#099c6c195140ff069211143cb31c0f8337bdb7b7" - integrity sha512-jaxm0hwUjv+hzC+UFEywic7buDC9JQ1q3cDsrWVSDAPmLotfA6E6kUHlYm/zOeGCac6g48DR36tFHxl7Zb+N5A== + version "11.2.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9" + integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" @@ -2978,10 +2951,10 @@ dependencies: source-map-support "^0.5.19" -"@truffle/codec@^0.9.4": - version "0.9.4" - resolved "https://registry.yarnpkg.com/@truffle/codec/-/codec-0.9.4.tgz#738187e6afc74a0a67dc201762b6ccb8121712d5" - integrity sha512-EAx/6Dg8Dj/O6/zqXpp4Q9OtxKmPKItmHWD91FM7bOXvX71wx66XZqIxD7ORjLEzQqYfJj81ttSnOZgpGxj3RA== +"@truffle/codec@^0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@truffle/codec/-/codec-0.9.5.tgz#f12896e115524e85173ccd617cd3c8c562575acc" + integrity sha512-h5oT5y2FPCKLGo2bfRFyBJe2YWuA3CGnCuo4eD2omZy4y6uj2yO6IePJpDt2nEIxnvX6cUf26iLVy/Upq+W+Ag== dependencies: big.js "^5.2.2" bn.js "^4.11.8" @@ -3006,13 +2979,13 @@ debug "^4.1.0" "@truffle/contract@^4.3.0": - version "4.3.5" - resolved "https://registry.yarnpkg.com/@truffle/contract/-/contract-4.3.5.tgz#360434b09bbf9a8d34ed2f7075f40d36509b180c" - integrity sha512-08gLSId+spnXqdM/25pOiurNcSBeCBa6KrplCgojy1RvqlG8JbEa9jXHWt1DCACZTtSnCsC2ywSNxAKRb4HqIA== + version "4.3.6" + resolved "https://registry.yarnpkg.com/@truffle/contract/-/contract-4.3.6.tgz#e6a77ea8dd5c4832c41529313b0dfa092de65208" + integrity sha512-XcIZ4eG1BrkoW30ZYTEqQGskmB2a2CoFJOLDjo2heP8xx+mKAPI0ceg2Ym4PbBUtqPBL6CiWE2WRh2zpFWPrOw== dependencies: "@truffle/blockchain-utils" "^0.0.25" "@truffle/contract-schema" "^3.3.3" - "@truffle/debug-utils" "^5.0.8" + "@truffle/debug-utils" "^5.0.9" "@truffle/error" "^0.0.11" "@truffle/interface-adapter" "^0.4.18" bignumber.js "^7.2.1" @@ -3025,18 +2998,18 @@ web3-eth-abi "1.2.9" web3-utils "1.2.9" -"@truffle/debug-utils@^5.0.8": - version "5.0.8" - resolved "https://registry.yarnpkg.com/@truffle/debug-utils/-/debug-utils-5.0.8.tgz#681c2bb595750c768f9a17f5c47facef19181044" - integrity sha512-2YzZzhhbZnUr+zQSiqkjF9cwwUPBFyMuH2/iGvjR+9l3+gpOm9+psN658dCziLV0qwiME4Tor4pIW7FnOEDegQ== +"@truffle/debug-utils@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@truffle/debug-utils/-/debug-utils-5.0.9.tgz#1daf5d99f8ada3f5167f3b899413c8f78cd68e9d" + integrity sha512-2wnof3sA1826KZYdHUm1PGQI/De/kNRwKECHI1+9iVLtutOLho/5rdSZVGM0hDPc2RPGBvivtgwpHsYe5Nxwjg== dependencies: - "@truffle/codec" "^0.9.4" + "@truffle/codec" "^0.9.5" "@trufflesuite/chromafi" "^2.2.2" bn.js "^5.1.3" chalk "^2.4.2" debug "^4.1.0" highlight.js "^10.4.0" - highlightjs-solidity "^1.0.20" + highlightjs-solidity "^1.0.21" "@truffle/error@^0.0.11": version "0.0.11" @@ -3178,9 +3151,9 @@ integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== "@types/aria-query@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" - integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== + version "4.2.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" + integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" @@ -3315,17 +3288,17 @@ "@types/istanbul-lib-report" "*" "@types/jest@*", "@types/jest@^26.0.16": - version "26.0.19" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790" - integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ== + version "26.0.20" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307" + integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== "@types/json5@^0.0.29": version "0.0.29" @@ -3357,19 +3330,19 @@ "@types/node" "*" "@types/node@*", "@types/node@^14.14.10": - version "14.14.20" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" - integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== "@types/node@^10.12.18": - version "10.17.50" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.50.tgz#7a20902af591282aa9176baefc37d4372131c32d" - integrity sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA== + version "10.17.51" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.51.tgz#639538575befbcf3d3861f95c41de8e47124d674" + integrity sha512-KANw+MkL626tq90l++hGelbl67irOJzGhUJk6a1Bt8QHOeh9tztJx+L0AqttraWKinmZn7Qi5lJZJzx45Gq0dg== "@types/node@^12.0.12", "@types/node@^12.12.6", "@types/node@^12.6.1": - version "12.19.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.12.tgz#04793c2afa4ce833a9972e4c476432e30f9df47b" - integrity sha512-UwfL2uIU9arX/+/PRcIkT08/iBadGN2z6ExOROA2Dh5mAuWTBj6iJbQX4nekiV5H8cTrEG569LeX+HRco9Cbxw== + version "12.19.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182" + integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -3431,9 +3404,9 @@ "@types/react" "*" "@types/react-redux@^7.1.11": - version "7.1.15" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.15.tgz#19075884db94101be762accef924d266a603fb1b" - integrity sha512-+piY42tUflPfI7y9Vy3UkG6MEMuJlrxfdtgeUcWmd5Z0qB57NXAPG6smkqu1DNXluo/KDyXPeRYhcFzMwt1BEA== + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -3479,17 +3452,17 @@ "@types/react" "*" "@types/react@*": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" - integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw== + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.1.tgz#eb1f1407dea8da3bc741879c1192aa703ab9975b" + integrity sha512-w8t9f53B2ei4jeOqf/gxtc2Sswnc3LBK5s0DyJcg5xd10tMHXts2N31cKjWfH9IC/JvEPa/YF1U4YeP1t4R6HQ== dependencies: "@types/prop-types" "*" csstype "^3.0.2" "@types/react@^16", "@types/react@^16.9.55": - version "16.14.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" - integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== + version "16.14.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.3.tgz#f5210f5deecf35d8794845549c93c2c3ad63aa9c" + integrity sha512-zPrXn03hmPYqh9DznqSFQsoRtrQ4aHgnZDO+hMGvsE/PORvDTdJCHQ6XvJV31ic+0LzF73huPFXUb++W6Kri0Q== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -3586,24 +3559,24 @@ source-map "^0.6.0" "@types/yargs-parser@*": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" - integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + version "20.2.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" + integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== "@types/yargs@^15.0.0", "@types/yargs@^15.0.5": - version "15.0.12" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.12.tgz#6234ce3e3e3fa32c5db301a170f96a599c960d74" - integrity sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw== + version "15.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc" + integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^4.5.0", "@typescript-eslint/eslint-plugin@^4.6.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz#5f580ea520fa46442deb82c038460c3dd3524bb6" - integrity sha512-ygqDUm+BUPvrr0jrXqoteMqmIaZ/bixYOc3A4BRwzEPTZPi6E+n44rzNZWaB0YvtukgP+aoj0i/fyx7FkM2p1w== +"@typescript-eslint/eslint-plugin@^4.14.0", "@typescript-eslint/eslint-plugin@^4.5.0": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.2.tgz#47a15803cfab89580b96933d348c2721f3d2f6fe" + integrity sha512-uMGfG7GFYK/nYutK/iqYJv6K/Xuog/vrRRZX9aEP4Zv1jsYXuvFUMDFLhUnc8WFv3D2R5QhNQL3VYKmvLS5zsQ== dependencies: - "@typescript-eslint/experimental-utils" "4.13.0" - "@typescript-eslint/scope-manager" "4.13.0" + "@typescript-eslint/experimental-utils" "4.14.2" + "@typescript-eslint/scope-manager" "4.14.2" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -3611,15 +3584,15 @@ semver "^7.3.2" tsutils "^3.17.1" -"@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" - integrity sha512-/ZsuWmqagOzNkx30VWYV3MNB/Re/CGv/7EzlqZo5RegBN8tMuPaBgNK6vPBCQA8tcYrbsrTdbx3ixMRRKEEGVw== +"@typescript-eslint/experimental-utils@4.14.2", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.2.tgz#9df35049d1d36b6cbaba534d703648b9e1f05cbb" + integrity sha512-mV9pmET4C2y2WlyHmD+Iun8SAEqkLahHGBkGqDVslHkmoj3VnxnGP4ANlwuxxfq1BsKdl/MPieDbohCEQgKrwA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.13.0" - "@typescript-eslint/types" "4.13.0" - "@typescript-eslint/typescript-estree" "4.13.0" + "@typescript-eslint/scope-manager" "4.14.2" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/typescript-estree" "4.14.2" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -3634,33 +3607,33 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.5.0", "@typescript-eslint/parser@^4.6.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.13.0.tgz#c413d640ea66120cfcc37f891e8cb3fd1c9d247d" - integrity sha512-KO0J5SRF08pMXzq9+abyHnaGQgUJZ3Z3ax+pmqz9vl81JxmTTOUfQmq7/4awVfq09b6C4owNlOgOwp61pYRBSg== +"@typescript-eslint/parser@^4.14.0", "@typescript-eslint/parser@^4.5.0": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.2.tgz#31e216e4baab678a56e539f9db9862e2542c98d0" + integrity sha512-ipqSP6EuUsMu3E10EZIApOJgWSpcNXeKZaFeNKQyzqxnQl8eQCbV+TSNsl+s2GViX2d18m1rq3CWgnpOxDPgHg== dependencies: - "@typescript-eslint/scope-manager" "4.13.0" - "@typescript-eslint/types" "4.13.0" - "@typescript-eslint/typescript-estree" "4.13.0" + "@typescript-eslint/scope-manager" "4.14.2" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/typescript-estree" "4.14.2" debug "^4.1.1" -"@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" - integrity sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ== +"@typescript-eslint/scope-manager@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.2.tgz#64cbc9ca64b60069aae0c060b2bf81163243b266" + integrity sha512-cuV9wMrzKm6yIuV48aTPfIeqErt5xceTheAgk70N1V4/2Ecj+fhl34iro/vIssJlb7XtzcaD07hWk7Jk0nKghg== dependencies: - "@typescript-eslint/types" "4.13.0" - "@typescript-eslint/visitor-keys" "4.13.0" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/visitor-keys" "4.14.2" "@typescript-eslint/types@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== -"@typescript-eslint/types@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.13.0.tgz#6a7c6015a59a08fbd70daa8c83dfff86250502f8" - integrity sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ== +"@typescript-eslint/types@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.2.tgz#d96da62be22dc9dc6a06647f3633815350fb3174" + integrity sha512-LltxawRW6wXy4Gck6ZKlBD05tCHQUj4KLn4iR69IyRiDHX3d3NCAhO+ix5OR2Q+q9bjCrHE/HKt+riZkd1At8Q== "@typescript-eslint/typescript-estree@3.10.1": version "3.10.1" @@ -3676,13 +3649,13 @@ 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" - integrity sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg== +"@typescript-eslint/typescript-estree@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.2.tgz#9c5ebd8cae4d7b014f890acd81e8e17f309c9df9" + integrity sha512-ESiFl8afXxt1dNj8ENEZT12p+jl9PqRur+Y19m0Z/SPikGL6rqq4e7Me60SU9a2M28uz48/8yct97VQYaGl0Vg== dependencies: - "@typescript-eslint/types" "4.13.0" - "@typescript-eslint/visitor-keys" "4.13.0" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/visitor-keys" "4.14.2" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -3697,12 +3670,12 @@ dependencies: eslint-visitor-keys "^1.1.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" - integrity sha512-6RoxWK05PAibukE7jElqAtNMq+RWZyqJ6Q/GdIxaiUj2Ept8jh8+FUVlbq9WxMYxkmEOPvCE5cRSyupMpwW31g== +"@typescript-eslint/visitor-keys@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.2.tgz#997cbe2cb0690e1f384a833f64794e98727c70c6" + integrity sha512-KBB+xLBxnBdTENs/rUgeUKO0UkPBRs2vD09oMRRIkj5BEN8PX1ToXV532desXfpQnZsYTyLLviS7JrPhdL154w== dependencies: - "@typescript-eslint/types" "4.13.0" + "@typescript-eslint/types" "4.14.2" eslint-visitor-keys "^2.0.0" "@unilogin/provider@^0.6.1": @@ -3713,10 +3686,10 @@ "@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== +"@unstoppabledomains/resolution@^1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@unstoppabledomains/resolution/-/resolution-1.17.0.tgz#3614e00ce241b1237ca6236e85512fcec7920549" + integrity sha512-RU2vTXETUX6T/r913anHQcKxcHmB34LIZCPCiy8dk/tiIuWXo3BL8l8YN0MLCD9lPpzSd8yYqw405/qBiSHezg== dependencies: "@ensdomains/address-encoder" "0.1.8" "@ethersproject/abi" "^5.0.1" @@ -4128,9 +4101,9 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4, ajv uri-js "^4.2.2" ajv@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" - integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + version "7.0.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.4.tgz#827e5f5ae32f5e5c1637db61f253a112229b5e2f" + integrity sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -5916,7 +5889,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= @@ -6286,10 +6259,10 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -call-bind@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.1.tgz#29aca9151f8ddcfd5b9b786898f005f425e88567" - integrity sha512-tvAvUwNcRikl3RVF20X9lsYmmepsovzTWeJiXjO0PkJp15uy/6xKFZOQtuiSULwYW+6ToZBprphCgWXC2dSgcQ== +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== dependencies: function-bind "^1.1.1" get-intrinsic "^1.0.2" @@ -6523,9 +6496,9 @@ chokidar@3.4.2: fsevents "~2.1.2" "chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.0.tgz#458a4816a415e9d3b3caa4faec2b96a6935a9e65" - integrity sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q== + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -7102,9 +7075,9 @@ core-js-compat@^3.6.2, core-js-compat@^3.7.0: semver "7.0.0" core-js-pure@^3.0.0, core-js-pure@^3.0.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119" - integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g== + version "3.8.3" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.3.tgz#10e9e3b2592ecaede4283e8f3ad7020811587c02" + integrity sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA== core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" @@ -7112,9 +7085,9 @@ core-js@^2.4.0, core-js@^2.5.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.0.1, core-js@^3.0.4, core-js@^3.6.5: - version "3.8.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47" - integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg== + version "3.8.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" + integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -7617,9 +7590,9 @@ csstype@^2.5.2, csstype@^2.5.7: integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== csstype@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" - integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== + version "3.0.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" + integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== currency-flags@2.1.2: version "2.1.2" @@ -8318,9 +8291,9 @@ electron-updater@4.3.5: semver "^7.3.2" electron@^9.4.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-9.4.1.tgz#62a2aae4cd93f1b56d794a47541505a71654177a" - integrity sha512-r4CxoVG9Ja7tBtkilWMnBsBGup8G8Z+v7icZmwysHa8/OSr0OrLjrcOF/30BAP7yPE5fz/XTxygnltzW4OTZdw== + version "9.4.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.4.2.tgz#0c76dfc3d317108adac66844b868a9e2e57d48f5" + integrity sha512-WpnJLDFHtj5eIewAi4hMHxGdbwkzjzmxsMu/BtDFCic3wpruchkskL7EV28Sg/IYTAqo6yN5ISfnFaQcLsIdng== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -8346,7 +8319,7 @@ elliptic@6.5.2: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -elliptic@6.5.3, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: +elliptic@6.5.3: version "6.5.3" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== @@ -8359,6 +8332,19 @@ elliptic@6.5.3, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -8455,7 +8441,12 @@ entities@^1.1.1, entities@^1.1.2: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0, entities@~2.1.0: +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== @@ -8504,22 +8495,24 @@ es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: string.prototype.trimstart "^1.0.1" es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + version "1.18.0-next.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" + integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== dependencies: + call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" + get-intrinsic "^1.0.2" has "^1.0.3" has-symbols "^1.0.1" is-callable "^1.2.2" - is-negative-zero "^2.0.0" + is-negative-zero "^2.0.1" is-regex "^1.1.1" - object-inspect "^1.8.0" + object-inspect "^1.9.0" object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.3" + string.prototype.trimstart "^1.0.3" es-array-method-boxes-properly@^1.0.0: version "1.0.0" @@ -8637,10 +8630,10 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz#5402eb559aa94b894effd6bddfa0b1ca051c858f" - integrity sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA== +eslint-config-prettier@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9" + integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg== eslint-config-react-app@^6.0.0: version "6.0.0" @@ -8803,13 +8796,13 @@ eslint-webpack-plugin@^2.1.0: micromatch "^4.0.2" schema-utils "^3.0.0" -eslint@^7.11.0: - version "7.17.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0" - integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ== +eslint@^7.11.0, eslint@^7.17.0: + version "7.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" + integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== dependencies: "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.2.2" + "@eslint/eslintrc" "^0.3.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -8833,7 +8826,7 @@ eslint@^7.11.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -9159,9 +9152,9 @@ ethereum-blockies-base64@^1.0.2: pnglib "0.0.1" ethereum-bloom-filters@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.7.tgz#b7b80735e385dbb7f944ce6b4533e24511306060" - integrity sha512-cDcJJSJ9GMAcURiAWO3DxIEhTL/uWqlQnvgKpuYQzYPrt/izuGU+1ntQmHt0IRq6ADoSYHFnB+aCEFIldjhkMQ== + version "1.0.9" + resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.9.tgz#4a59dead803af0c9e33834170bd7695df67061ec" + integrity sha512-GiK/RQkAkcVaEdxKVkPcG07PQ5vD7v2MFSHgZmBJSfMzNRHimntdBithsHAT89tAXnIpzVDWt8iaCD1DvkaxGg== dependencies: js-sha3 "^0.8.0" @@ -9747,9 +9740,9 @@ fast-glob@^2.0.2: micromatch "^3.1.10" fast-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -9779,9 +9772,9 @@ fast-safe-stringify@^2.0.6: integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== fastq@^1.6.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb" - integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA== + version "1.10.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" + integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== dependencies: reusify "^1.0.4" @@ -9890,6 +9883,20 @@ filelist@^1.0.1: dependencies: minimatch "^3.0.4" +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= + +filenamify@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.2.0.tgz#c99716d676869585b3b5d328b3f06590d032e89f" + integrity sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.1" + trim-repeated "^1.0.0" + filesize@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -10034,9 +10041,9 @@ flat@^4.1.0: is-buffer "~2.0.3" flatted@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" - integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== flatten@^1.0.2: version "1.0.3" @@ -10367,9 +10374,9 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.1, get-intrinsic@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49" - integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.0.tgz#892e62931e6938c8a23ea5aaebcfb67bd97da97e" + integrity sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg== dependencies: function-bind "^1.1.1" has "^1.0.3" @@ -10887,10 +10894,10 @@ highlight.js@~9.13.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== -highlightjs-solidity@^1.0.20: - version "1.0.20" - resolved "https://registry.yarnpkg.com/highlightjs-solidity/-/highlightjs-solidity-1.0.20.tgz#37482fd47deda617994e1d1262df5a319c0a8580" - integrity sha512-Ixb87/4huazRJ7mriimL0DP2GvE5zgSk11VdMPGKMQCNwszDe8qK0PySySsuB88iXyDT/H2gdmvC2bgfrOi3qQ== +highlightjs-solidity@^1.0.21: + version "1.0.21" + resolved "https://registry.yarnpkg.com/highlightjs-solidity/-/highlightjs-solidity-1.0.21.tgz#6d257215b5b635231d4d0c523f2c419bbff6fe42" + integrity sha512-ozOtTD986CBIxuIuauzz2lqCOTpd27TbfYm+msMtNSB69mJ0cdFNvZ6rOO5iFtEHtDkVYVEFQywXffG2sX3XTw== history@4.10.1, history@^4.9.0: version "4.10.1" @@ -10904,7 +10911,7 @@ history@4.10.1, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.0, hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= @@ -11150,9 +11157,9 @@ human-signals@^1.1.1: integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== husky@^4.3.0: - version "4.3.7" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.7.tgz#ca47bbe6213c1aa8b16bbd504530d9600de91e88" - integrity sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw== + version "4.3.8" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" + integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== dependencies: chalk "^4.0.0" ci-info "^2.0.0" @@ -11370,9 +11377,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.6" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.6.tgz#f1c46a2a93a253e7b3905115e74d527cd23061a1" - integrity sha512-IZUoxEjNjubzrmvzZU4lKP7OnYmX72XRl3sqkfJhBKweKi5rnGi5+IUdlj/H1M+Ip5JQ1WzaDMOBRY90Ajc5jg== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@6.2.1: version "6.2.1" @@ -11564,9 +11571,9 @@ is-buffer@^2.0.2, is-buffer@~2.0.3: integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== is-ci@^2.0.0: version "2.0.0" @@ -11788,7 +11795,7 @@ is-my-json-valid@^2.12.4: jsonpointer "^4.0.0" xtend "^4.0.0" -is-negative-zero@^2.0.0: +is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== @@ -11877,10 +11884,11 @@ is-property@^1.0.0, is-property@^1.0.2: 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" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" + integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== dependencies: + call-bind "^1.0.2" has-symbols "^1.0.1" is-regex@~1.0.5: @@ -12832,70 +12840,70 @@ jsqr@^1.2.0: resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.3.1.tgz#515a766e58b00c80142f3a2dc4b8751100ceedcf" integrity sha512-zCTP6Qd/WwjrpuHFkJuXc5opRdKprUr7eI7+JCCtcetThJt45qptu82MWQ+eET+FtDrMo7+BYjo3iD0XIq1L9Q== -jss-plugin-camel-case@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz#4b0a9c85e65e5eb72cbfba59373686c604d88f72" - integrity sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg== +jss-plugin-camel-case@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.1.tgz#427b24a9951b4c2eaa7e3d5267acd2e00b0934f9" + integrity sha512-9+oymA7wPtswm+zxVti1qiowC5q7bRdCJNORtns2JUj/QHp2QPXYwSNRD8+D2Cy3/CEMtdJzlNnt5aXmpS6NAg== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.5.0" + jss "10.5.1" -jss-plugin-default-unit@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz#e9f2e89741b0118ba15d52b4c13bda2b27262373" - integrity sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw== +jss-plugin-default-unit@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.1.tgz#2be385d71d50aee2ee81c2a9ac70e00592ed861b" + integrity sha512-D48hJBc9Tj3PusvlillHW8Fz0y/QqA7MNmTYDQaSB/7mTrCZjt7AVRROExoOHEtd2qIYKOYJW3Jc2agnvsXRlQ== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.0" + jss "10.5.1" -jss-plugin-global@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz#eb357ccd35cb4894277fb2117a78d1e498668ad6" - integrity sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ== +jss-plugin-global@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.5.1.tgz#0e1793dea86c298360a7e2004721351653c7e764" + integrity sha512-jX4XpNgoaB8yPWw/gA1aPXJEoX0LNpvsROPvxlnYe+SE0JOhuvF7mA6dCkgpXBxfTWKJsno7cDSCgzHTocRjCQ== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.0" + jss "10.5.1" -jss-plugin-nested@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz#790c506432a23a63c71ceb5044e2ac85f0958702" - integrity sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg== +jss-plugin-nested@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.5.1.tgz#8753a80ad31190fb6ac6fdd39f57352dcf1295bb" + integrity sha512-xXkWKOCljuwHNjSYcXrCxBnjd8eJp90KVFW1rlhvKKRXnEKVD6vdKXYezk2a89uKAHckSvBvBoDGsfZrldWqqQ== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.0" + jss "10.5.1" tiny-warning "^1.0.2" -jss-plugin-props-sort@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz#5bcc3bd8e68cd3e2dafb47d67db28fd5a4fcf102" - integrity sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg== +jss-plugin-props-sort@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.1.tgz#ab1c167fd2d4506fb6a1c1d66c5f3ef545ff1cd8" + integrity sha512-t+2vcevNmMg4U/jAuxlfjKt46D/jHzCPEjsjLRj/J56CvP7Iy03scsUP58Iw8mVnaV36xAUZH2CmAmAdo8994g== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.0" + jss "10.5.1" -jss-plugin-rule-value-function@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz#60ee8240dfe60418e1ba4729adee893cbe9be7a3" - integrity sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA== +jss-plugin-rule-value-function@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.1.tgz#37f4030523fb3032c8801fab48c36c373004de7e" + integrity sha512-3gjrSxsy4ka/lGQsTDY8oYYtkt2esBvQiceGBB4PykXxHoGRz14tbCK31Zc6DHEnIeqsjMUGbq+wEly5UViStQ== dependencies: "@babel/runtime" "^7.3.1" - jss "10.5.0" + jss "10.5.1" tiny-warning "^1.0.2" -jss-plugin-vendor-prefixer@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz#01f04cfff31f43f153f5d71972f5800b10a2eb84" - integrity sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA== +jss-plugin-vendor-prefixer@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.1.tgz#45a183a3a0eb097bdfab0986b858d99920c0bbd8" + integrity sha512-cLkH6RaPZWHa1TqSfd2vszNNgxT1W0omlSjAd6hCFHp3KIocSrW21gaHjlMU26JpTHwkc+tJTCQOmE/O1A4FKQ== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.8" - jss "10.5.0" + jss "10.5.1" -jss@10.5.0, jss@^10.0.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.5.0.tgz#0c2de8a29874b2dc8162ab7f34ef6573a87d9dd3" - integrity sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw== +jss@10.5.1, jss@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.5.1.tgz#93e6b2428c840408372d8b548c3f3c60fa601c40" + integrity sha512-hbbO3+FOTqVdd7ZUoTiwpHzKXIo5vGpMNbuXH1a0wubRSWLWSBvwvaq4CiHH/U42CmjOnp6lVNNs/l+Z7ZdDmg== dependencies: "@babel/runtime" "^7.3.1" csstype "^3.0.2" @@ -14914,9 +14922,9 @@ parse-json@^4.0.0: json-parse-better-errors "^1.0.1" parse-json@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" - integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -16596,11 +16604,11 @@ react-fast-compare@^3.2.0: integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== react-final-form-listeners@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/react-final-form-listeners/-/react-final-form-listeners-1.0.2.tgz#b52da984300281cf1f69a6412e86df6249e2bf1c" - integrity sha512-AaUUHcXRhD3esC80yUfYPI8FJ3TUiMu0f4hk18QL8NMCWjokg6NWS32WkRJsH3bLWDoiy7+uNVOAAyO/XoupyA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-final-form-listeners/-/react-final-form-listeners-1.0.3.tgz#88e0bd6af06edc9a74b81ddad2ce0ded53f3331e" + integrity sha512-OrdCNxSS4JQS/EXD+R530kZKFqaPfa+WcXPgVro/h4BpaBDF/Ja+BtHyCzDezCIb5rWaGGdOJIj+tN2YdtvrXg== dependencies: - "@babel/runtime" "^7.1.5" + "@babel/runtime" "^7.12.5" react-final-form@^6.5.2: version "6.5.2" @@ -16667,7 +16675,7 @@ react-inspector@^4.0.0: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.9.0: +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -17702,7 +17710,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-filename@^1.6.2, sanitize-filename@^1.6.3: +sanitize-filename@^1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== @@ -17725,10 +17733,10 @@ sass-loader@8.0.2, sass-loader@^9.0.0: schema-utils "^2.7.0" semver "^7.3.2" -sass@^1.29.0: - version "1.32.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.2.tgz#66dc0250bc86c15d19ddee7135e93d0cf3d3257b" - integrity sha512-u1pUuzqwz3SAgvHSWp1k0mRhX82b2DdlVnP6UIetQPZtYbuJUDaPQhZE12jyjB7vYeOScfz9WPsZJB6Rpk7heA== +sass@^1.32.0: + version "1.32.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.6.tgz#e3646c8325cd97ff75a8a15226007f3ccd221393" + integrity sha512-1bcDHDcSqeFtMr0JXI3xc/CXX6c4p0wHHivJdru8W7waM7a1WjKMm4m/Z5sY7CbVw4Whi2Chpcw6DFfSWwGLzQ== dependencies: chokidar ">=2.0.0 <4.0.0" @@ -18709,7 +18717,7 @@ string.prototype.trim@~1.2.1: define-properties "^1.1.3" es-abstract "^1.18.0-next.1" -string.prototype.trimend@^1.0.1: +string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== @@ -18717,7 +18725,7 @@ string.prototype.trimend@^1.0.1: call-bind "^1.0.0" define-properties "^1.1.3" -string.prototype.trimstart@^1.0.1: +string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== @@ -18855,6 +18863,13 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-outer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + sturdy-websocket@^0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/sturdy-websocket/-/sturdy-websocket-0.1.12.tgz#84bb779f948b585a695f76961dc7d1c4a5e87629" @@ -19451,6 +19466,13 @@ trim-newlines@^2.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= + dependencies: + escape-string-regexp "^1.0.2" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -19533,9 +19555,9 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== tsutils@^3.17.1: - version "3.19.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.19.1.tgz#d8566e0c51c82f32f9c25a4d367cd62409a547a9" - integrity sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw== + version "3.20.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" + integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== dependencies: tslib "^1.8.1" @@ -20203,10 +20225,10 @@ web3-bzz@1.2.9: swarm-js "^0.1.40" underscore "1.9.1" -web3-bzz@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.3.1.tgz#c7e13e5fbbbe4634b0d883e5440069fc58e58044" - integrity sha512-MN726zFpFpwhs3NMC35diJGkwTVUj+8LM/VWqooGX/MOjgYzNrJ7Wr8EzxoaTCy87edYNBprtxBkd0HzzLmung== +web3-bzz@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.3.3.tgz#e3d9c8ff5db4d61fe3a4b7b7b93d61000c0e4322" + integrity sha512-lFERlqnr/upJhADT6US7BGUkM5cy6idw86/GvWKo9h/uyrbV14gk+bUqcQdBBSopa1Mvvy5ZaO6rKtRe8PTsQw== dependencies: "@types/node" "^12.12.6" got "9.6.0" @@ -20231,14 +20253,14 @@ web3-core-helpers@1.2.9: web3-eth-iban "1.2.9" web3-utils "1.2.9" -web3-core-helpers@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.3.1.tgz#ffd6f47c1b54a8523f00760a8d713f44d0f97e97" - integrity sha512-tMVU0ScyQUJd/HFWfZrvGf+QmPCodPyKQw1gQ+n9We/H3vPPbUxDjNeYnd4BbYy5O9ox+0XG6i3+JlwiSkgDkA== +web3-core-helpers@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.3.3.tgz#58a7a34278e4b338dde9520012679d5952701a69" + integrity sha512-rUTC9sgn1Wvw2KGBtc9/bsQKUd+yjzIm14mlaqqiO0vpFueTmmagwiGRE2CWzEfYg+r2jnYIIgh9qnsCykgVkQ== dependencies: underscore "1.9.1" - web3-eth-iban "1.3.1" - web3-utils "1.3.1" + web3-eth-iban "1.3.3" + web3-utils "1.3.3" web3-core-method@1.2.11: version "1.2.11" @@ -20264,17 +20286,17 @@ web3-core-method@1.2.9: web3-core-subscriptions "1.2.9" web3-utils "1.2.9" -web3-core-method@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.3.1.tgz#c1d8bf1e2104a8d625c99caf94218ad2dc948c92" - integrity sha512-dA38tNVZWTxBFMlLFunLD5Az1AWRi5HqM+AtQrTIhxWCzg7rJSHuaYOZ6A5MHKGPWpdykLhzlna0SsNv5AVs8w== +web3-core-method@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.3.3.tgz#f992744180e003d0ca55b1b17b4a9678cb9f7247" + integrity sha512-d3AA1lyw0dvLs53X17pHpD5QpxJdkfolbN31UQymRF5Y+swFweqRiCuJoNTplE95ZX2uUtsLhEIbaszj7dQgFg== dependencies: "@ethersproject/transactions" "^5.0.0-beta.135" underscore "1.9.1" - web3-core-helpers "1.3.1" - web3-core-promievent "1.3.1" - web3-core-subscriptions "1.3.1" - web3-utils "1.3.1" + web3-core-helpers "1.3.3" + web3-core-promievent "1.3.3" + web3-core-subscriptions "1.3.3" + web3-utils "1.3.3" web3-core-promievent@1.2.11: version "1.2.11" @@ -20290,10 +20312,10 @@ web3-core-promievent@1.2.9: dependencies: eventemitter3 "3.1.2" -web3-core-promievent@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.3.1.tgz#b4da4b34cd9681e22fcda25994d7629280a1e046" - integrity sha512-jGu7TkwUqIHlvWd72AlIRpsJqdHBQnHMeMktrows2148gg5PBPgpJ10cPFmCCzKT6lDOVh9B7pZMf9eckMDmiA== +web3-core-promievent@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.3.3.tgz#b6dcede41965a35ad53041f4f16c6ef7865bf13c" + integrity sha512-ARgO+BWUCxK8U/977SdJ8oyJo51mDYUzlZFoa2NFjUH+QYrFoKA7l9Hhw/vxhy13jE2LaVUM31JBLzVb+GM9dQ== dependencies: eventemitter3 "4.0.4" @@ -20319,17 +20341,17 @@ web3-core-requestmanager@1.2.9: web3-providers-ipc "1.2.9" web3-providers-ws "1.2.9" -web3-core-requestmanager@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.3.1.tgz#6dd2b5161ba778dfffe68994a4accff2decc54fe" - integrity sha512-9WTaN2SoyJX1amRyTzX2FtbVXsyWBI2Wef2Q3gPiWaEo/VRVm3e4Bq8MwxNTUMIJMO8RLGHjtdgsoDKPwfL73Q== +web3-core-requestmanager@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.3.3.tgz#deaf18307ddf0e292ac783b5486d2f6e2480a9ac" + integrity sha512-4/J23wK5IXRw/1kqda7FXtvySKjX7Phcevqjx0EkcBtrxAfLedcqf8k2PlDh5LtCXfPW66u4V3fDgHdLZMrVgQ== dependencies: underscore "1.9.1" util "^0.12.0" - web3-core-helpers "1.3.1" - web3-providers-http "1.3.1" - web3-providers-ipc "1.3.1" - web3-providers-ws "1.3.1" + web3-core-helpers "1.3.3" + web3-providers-http "1.3.3" + web3-providers-ipc "1.3.3" + web3-providers-ws "1.3.3" web3-core-subscriptions@1.2.11: version "1.2.11" @@ -20349,14 +20371,14 @@ web3-core-subscriptions@1.2.9: underscore "1.9.1" web3-core-helpers "1.2.9" -web3-core-subscriptions@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.3.1.tgz#be1103259f91b7fc7f4c6a867aa34dea70a636f7" - integrity sha512-eX3N5diKmrxshc6ZBZ8EJxxAhCxdYPbYXuF2EfgdIyHmxwmYqIVvKepzO8388Bx8JD3D0Id/pKE0dC/FnDIHTQ== +web3-core-subscriptions@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.3.3.tgz#296752e7cfad5a559162eea4dbc9086e4e24e62d" + integrity sha512-VvcPuNYcGLb6HfgMrNN6Q/1CwSk2uIqUjhrVTQ67JIxIddsEdV1f6SsQH9MX1cmwi39ffGsYtssOT1pht4Zc8g== dependencies: eventemitter3 "4.0.4" underscore "1.9.1" - web3-core-helpers "1.3.1" + web3-core-helpers "1.3.3" web3-core@1.2.11: version "1.2.11" @@ -20384,18 +20406,18 @@ web3-core@1.2.9: web3-core-requestmanager "1.2.9" web3-utils "1.2.9" -web3-core@1.3.1, web3-core@^1.2.11, web3-core@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.3.1.tgz#fb0fc5d952a7f3d580a7e6155d2f28be064e64cb" - integrity sha512-QlBwSyjl2pqYUBE7lH9PfLxa8j6AzzAtvLUqkgoaaFJYLP/+XavW1n6dhVCTq+U3L3eNc+bMp9GLjGDJNXMnGg== +web3-core@1.3.3, web3-core@^1.2.11, web3-core@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.3.3.tgz#c012792d82a26743c068658a9fed56894501e396" + integrity sha512-hCDWj/3PBHhSJSSBi+nV7MiW9Djf/pRuUXcVO2jWroAXqAbTSXLHpju0AWTzXnlsqs1QHK0Yk8nF9jojGUQVYg== dependencies: "@types/bn.js" "^4.11.5" "@types/node" "^12.12.6" bignumber.js "^9.0.0" - web3-core-helpers "1.3.1" - web3-core-method "1.3.1" - web3-core-requestmanager "1.3.1" - web3-utils "1.3.1" + web3-core-helpers "1.3.3" + web3-core-method "1.3.3" + web3-core-requestmanager "1.3.3" + web3-utils "1.3.3" web3-eth-abi@1.2.11: version "1.2.11" @@ -20415,14 +20437,14 @@ web3-eth-abi@1.2.9: underscore "1.9.1" web3-utils "1.2.9" -web3-eth-abi@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.3.1.tgz#d60fe5f15c7a3a426c553fdaa4199d07f1ad899c" - integrity sha512-ds4aTeKDUEqTXgncAtxvcfMpPiei9ey7+s2ZZ+OazK2CK5jWhFiJuuj9Q68kOT+hID7E1oSDVsNmJWFD/7lbMw== +web3-eth-abi@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.3.3.tgz#21dbebc7c50d66cb61195f13ece9262601a2b5b7" + integrity sha512-9GQ7YTALt1uxGwdMBpBHlagCj4yn0fPUT2wDDAGoyJFVJMsUt3arF855zsVpJL3zfhHmUgRNoVrAkobRR2YYLw== dependencies: "@ethersproject/abi" "5.0.7" underscore "1.9.1" - web3-utils "1.3.1" + web3-utils "1.3.3" web3-eth-accounts@1.2.11: version "1.2.11" @@ -20458,10 +20480,10 @@ web3-eth-accounts@1.2.9: web3-core-method "1.2.9" web3-utils "1.2.9" -web3-eth-accounts@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.3.1.tgz#63b247461f1ae0ae46f9a5d5aa896ea80237143e" - integrity sha512-wsV3/0Pbn5+pI8PiCD1CYw7I1dkQujcP//aJ+ZH8PoaHQoG6HnJ7nTp7foqa0r/X5lizImz/g5S8D76t3Z9tHA== +web3-eth-accounts@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.3.3.tgz#1fda75f55bdb780765d627315fe9f499a27fda28" + integrity sha512-Jn9nguNsCLnY7Po6lv7Mg5JDaYuKdvL0Ezv1V2LTLy+EhcVt5i19h+/3M92Xynpe5Tx+WY/ELfeA2jLTeP5jRg== dependencies: crypto-browserify "3.12.0" eth-lib "0.2.8" @@ -20470,10 +20492,10 @@ web3-eth-accounts@1.3.1: scrypt-js "^3.0.1" underscore "1.9.1" uuid "3.3.2" - web3-core "1.3.1" - web3-core-helpers "1.3.1" - web3-core-method "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-helpers "1.3.3" + web3-core-method "1.3.3" + web3-utils "1.3.3" web3-eth-contract@1.2.11: version "1.2.11" @@ -20505,20 +20527,20 @@ web3-eth-contract@1.2.9: web3-eth-abi "1.2.9" web3-utils "1.2.9" -web3-eth-contract@1.3.1, web3-eth-contract@^1.2.11, web3-eth-contract@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.3.1.tgz#05cb77bd2a671c5480897d20de487f3bae82e113" - integrity sha512-cHu9X1iGrK+Zbrj4wYKwHI1BtVGn/9O0JRsZqd9qcFGLwwAmaCJYy0sDn7PKCKDSL3qB+MDILoyI7FaDTWWTHg== +web3-eth-contract@1.3.3, web3-eth-contract@^1.2.11, web3-eth-contract@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.3.3.tgz#a89944b04eb40227b3490cb5f807cb95c0e48eeb" + integrity sha512-TKGs1qvc/v7TriyGKtnTqVrB3J/mWSeqLkWtLY60lGqY8KopZ9k7dZ/g5Cvfiox57VHWkpOk0xDwUQjlIe4Ikg== dependencies: "@types/bn.js" "^4.11.5" underscore "1.9.1" - web3-core "1.3.1" - web3-core-helpers "1.3.1" - web3-core-method "1.3.1" - web3-core-promievent "1.3.1" - web3-core-subscriptions "1.3.1" - web3-eth-abi "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-helpers "1.3.3" + web3-core-method "1.3.3" + web3-core-promievent "1.3.3" + web3-core-subscriptions "1.3.3" + web3-eth-abi "1.3.3" + web3-utils "1.3.3" web3-eth-ens@1.2.11: version "1.2.11" @@ -20550,20 +20572,20 @@ web3-eth-ens@1.2.9: web3-eth-contract "1.2.9" web3-utils "1.2.9" -web3-eth-ens@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.3.1.tgz#ccfd621ddc1fecb44096bc8e60689499a9eb4421" - integrity sha512-MUQvYgUYQ5gAwbZyHwI7y+NTT6j98qG3MVhGCUf58inF5Gxmn9OlLJRw8Tofgf0K87Tk9Kqw1/2QxUE4PEZMMA== +web3-eth-ens@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.3.3.tgz#88c8d061c2f5eeeeea3736ef0712451d76b5091b" + integrity sha512-tresrI1CM6RbxsUCM6kfG1W10LDMqWJnU+lNhfaD5mt5IzJ4GcfDAHO9WzoYl8Esh+Epj/jD+vI30clI4j90Vg== dependencies: content-hash "^2.5.2" eth-ens-namehash "2.0.8" underscore "1.9.1" - web3-core "1.3.1" - web3-core-helpers "1.3.1" - web3-core-promievent "1.3.1" - web3-eth-abi "1.3.1" - web3-eth-contract "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-helpers "1.3.3" + web3-core-promievent "1.3.3" + web3-eth-abi "1.3.3" + web3-eth-contract "1.3.3" + web3-utils "1.3.3" web3-eth-iban@1.2.11: version "1.2.11" @@ -20581,13 +20603,13 @@ web3-eth-iban@1.2.9: bn.js "4.11.8" web3-utils "1.2.9" -web3-eth-iban@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.3.1.tgz#4351e1a658efa5f3218357f0a38d6d8cad82481e" - integrity sha512-RCQLfR9Z+DNfpw7oUauYHg1HcVoEljzhwxKn3vi15gK0ssWnTwRGqUiIyVTeSb836G6oakOd5zh7XYqy7pn+nw== +web3-eth-iban@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.3.3.tgz#ad3e4e30a2a101febfeed0248a107889a13372ec" + integrity sha512-+9a+bZHAKQ4oBcRxiGbC1MC8S2cOgDlXo8qcw0XpMhLJZ3c/brZM7ZbPdiuU8Z7AMYf3PknaGFQyVmedZhrauA== dependencies: bn.js "^4.11.9" - web3-utils "1.3.1" + web3-utils "1.3.3" web3-eth-personal@1.2.11: version "1.2.11" @@ -20613,17 +20635,17 @@ web3-eth-personal@1.2.9: web3-net "1.2.9" web3-utils "1.2.9" -web3-eth-personal@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.3.1.tgz#cfe8af01588870d195dabf0a8d9e34956fb8856d" - integrity sha512-/vZEQpXJfBfYoy9KT911ItfoscEfF0Q2j8tsXzC2xmmasSZ6YvAUuPhflVmAo0IHQSX9rmxq0q1p3sbnE3x2pQ== +web3-eth-personal@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.3.3.tgz#45ecf0a6fcb2bca0a7a4de11d4c2ed405579ee44" + integrity sha512-S/TSGTm7x9oHRXUHXi8f+y187RKpn5aqYJRlSoyTmB3B4EMrv9NcZZQmHaiXwM48wkFdRhTMECW1Ar8E5zZLFw== dependencies: "@types/node" "^12.12.6" - web3-core "1.3.1" - web3-core-helpers "1.3.1" - web3-core-method "1.3.1" - web3-net "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-helpers "1.3.3" + web3-core-method "1.3.3" + web3-net "1.3.3" + web3-utils "1.3.3" web3-eth@1.2.11: version "1.2.11" @@ -20663,24 +20685,24 @@ web3-eth@1.2.9: web3-net "1.2.9" web3-utils "1.2.9" -web3-eth@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.3.1.tgz#60ac4b58e5fd17b8dbbb8378abd63b02e8326727" - integrity sha512-e4iL8ovj0zNxzbv4LTHEv9VS03FxKlAZD+95MolwAqtVoUnKC2H9X6dli0w6eyXP0aKw+mwY0g0CWQHzqZvtXw== +web3-eth@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.3.3.tgz#e03d1a4a7ea97b36c63bb54f000ebe0ef9f140c7" + integrity sha512-NvbkCaN26o7f9EogsRsA/lbwF+8dXimJWsaGpZK3ANa+AZrYkWj3NuaxfPO/S/RLsC9ptJdt7id72qxT40r5QQ== dependencies: underscore "1.9.1" - web3-core "1.3.1" - web3-core-helpers "1.3.1" - web3-core-method "1.3.1" - web3-core-subscriptions "1.3.1" - web3-eth-abi "1.3.1" - web3-eth-accounts "1.3.1" - web3-eth-contract "1.3.1" - web3-eth-ens "1.3.1" - web3-eth-iban "1.3.1" - web3-eth-personal "1.3.1" - web3-net "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-helpers "1.3.3" + web3-core-method "1.3.3" + web3-core-subscriptions "1.3.3" + web3-eth-abi "1.3.3" + web3-eth-accounts "1.3.3" + web3-eth-contract "1.3.3" + web3-eth-ens "1.3.3" + web3-eth-iban "1.3.3" + web3-eth-personal "1.3.3" + web3-net "1.3.3" + web3-utils "1.3.3" web3-net@1.2.11: version "1.2.11" @@ -20700,14 +20722,14 @@ web3-net@1.2.9: web3-core-method "1.2.9" web3-utils "1.2.9" -web3-net@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.3.1.tgz#79374b1df37429b0839b83b0abc4440ac6181568" - integrity sha512-vuMMWMk+NWHlrNfszGp3qRjH/64eFLiNIwUi0kO8JXQ896SP3Ma0su5sBfSPxNCig047E9GQimrL9wvYAJSO5A== +web3-net@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.3.3.tgz#dacfe2456d0584ed1500e98bdd12082353883cd6" + integrity sha512-GcPj2lyAC5CP6FOCwoURCRMFsh0khWBi6sGqiKtUPMa7dKnLw8CLCAFcwX//d3ucnn1E7I78Va6k8liKjj87sA== dependencies: - web3-core "1.3.1" - web3-core-method "1.3.1" - web3-utils "1.3.1" + web3-core "1.3.3" + web3-core-method "1.3.3" + web3-utils "1.3.3" web3-provider-engine@15.0.4: version "15.0.4" @@ -20809,12 +20831,12 @@ web3-providers-http@1.2.9: web3-core-helpers "1.2.9" xhr2-cookies "1.1.0" -web3-providers-http@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.3.1.tgz#becbea61706b2fa52e15aca6fe519ee108a8fab9" - integrity sha512-DOujG6Ts7/hAMj0PW5p9/1vwxAIr+1CJ6ZWHshtfOq1v1KnMphVTGOrjcTTUvPT33/DA/so2pgGoPMrgaEIIvQ== +web3-providers-http@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.3.3.tgz#55c15c79ecd7f5415d2d0d0f748ff4304ba23edd" + integrity sha512-V2x27IFXQqsaZrAbA4GJurKuyrNXapmmpSJ7jxPDOxewOy9dEURlKIg5W1bb4QXGh2YSCksuH9fKquvTfPfc/A== dependencies: - web3-core-helpers "1.3.1" + web3-core-helpers "1.3.3" xhr2-cookies "1.1.0" web3-providers-ipc@1.2.11: @@ -20835,14 +20857,14 @@ web3-providers-ipc@1.2.9: underscore "1.9.1" web3-core-helpers "1.2.9" -web3-providers-ipc@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.3.1.tgz#3cb2572fc5286ab2f3117e0a2dce917816c3dedb" - integrity sha512-BNPscLbvwo+u/tYJrLvPnl/g/SQVSnqP/TjEsB033n4IXqTC4iZ9Of8EDmI0U6ds/9nwNqOBx3KsxbinL46UZA== +web3-providers-ipc@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.3.3.tgz#b3e1f30875e9c71e94c23be264ed4d9c50313f37" + integrity sha512-XMQo/YsH/2lBaRlkYa5d/Q+2EJ2RTzVjio1i2G9TESESfHCj0l2AWLb3zet+f/QRVxfvXGmGlZuf99diof2a1g== dependencies: oboe "2.1.5" underscore "1.9.1" - web3-core-helpers "1.3.1" + web3-core-helpers "1.3.3" web3-providers-ws@1.2.11: version "1.2.11" @@ -20864,14 +20886,14 @@ web3-providers-ws@1.2.9: web3-core-helpers "1.2.9" websocket "^1.0.31" -web3-providers-ws@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.3.1.tgz#a70140811d138a1a5cf3f0c39d11887c8e341c83" - integrity sha512-DAbVbiizv0Hr/bLKjyyKMHc/66ccVkudan3eRsf+R/PXWCqfXb7q6Lwodj4llvC047pEuLKR521ZKr5wbfk1KQ== +web3-providers-ws@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.3.3.tgz#f654ee83444d141c365eee7fba6985590b674ba5" + integrity sha512-yuzqB3jST9JS19oOR1FRaARM7JBeP6cbKffM8HoWp4Y98/OowjW1mbDQVS47YTSHBP2QiLzSrwBxjIEPm8f48Q== dependencies: eventemitter3 "4.0.4" underscore "1.9.1" - web3-core-helpers "1.3.1" + web3-core-helpers "1.3.3" websocket "^1.0.32" web3-shh@1.2.11: @@ -20894,15 +20916,15 @@ web3-shh@1.2.9: web3-core-subscriptions "1.2.9" web3-net "1.2.9" -web3-shh@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.3.1.tgz#42294d684358c22aa48616cb9a3eb2e9c1e6362f" - integrity sha512-57FTQvOW1Zm3wqfZpIEqL4apEQIR5JAxjqA4RM4eL0jbdr+Zj5Y4J93xisaEVl6/jMtZNlsqYKTVswx8mHu1xw== +web3-shh@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.3.3.tgz#55e4e6826b5af51b60708969da268c76a960f2bd" + integrity sha512-byp2+sHnc8UAj6sNcVFacF3pmRzIaMATsI4ARfU+0S8EpaQ3trojww2QBYPnZ4r0QOMH+I6+bVl8qTu0Zz4eoA== dependencies: - web3-core "1.3.1" - web3-core-method "1.3.1" - web3-core-subscriptions "1.3.1" - web3-net "1.3.1" + web3-core "1.3.3" + web3-core-method "1.3.3" + web3-core-subscriptions "1.3.3" + web3-net "1.3.3" web3-utils@1.2.1: version "1.2.1" @@ -20945,10 +20967,10 @@ web3-utils@1.2.9: underscore "1.9.1" utf8 "3.0.0" -web3-utils@1.3.1, web3-utils@^1.2.11, web3-utils@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.3.1.tgz#9aa880dd8c9463fe5c099107889f86a085370c2e" - integrity sha512-9gPwFm8SXtIJuzdrZ37PRlalu40fufXxo+H2PiCwaO6RpKGAvlUlWU0qQbyToFNXg7W2H8djEgoAVac8NLMCKQ== +web3-utils@1.3.3, web3-utils@^1.2.11, web3-utils@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.3.3.tgz#71b837958f1fcc970223eb14a1d0110233348305" + integrity sha512-ZwpdqEcBBzqRgXUbCj+kyu1jFnsDauURSQ79yVqgnTKSI4C3s0Qjpp4WLThV+LKhCKR5GZtBTkgGHeiq0FT88A== dependencies: bn.js "^4.11.9" eth-lib "0.2.8" @@ -20960,17 +20982,17 @@ web3-utils@1.3.1, web3-utils@^1.2.11, web3-utils@^1.3.0: utf8 "3.0.0" web3@*, web3@^1.0.0-beta.34: - version "1.3.1" - resolved "https://registry.yarnpkg.com/web3/-/web3-1.3.1.tgz#f780138c92ae3c42ea45e1a3c6ae8844e0aa5054" - integrity sha512-lDJwOLSRWHYwhPy4h5TNgBRJ/lED7lWXyVOXHCHcEC8ai3coBNdgEXWBu/GGYbZMsS89EoUOJ14j3Ufi4dUkog== + version "1.3.3" + resolved "https://registry.yarnpkg.com/web3/-/web3-1.3.3.tgz#be2dda6f58b177c00275fc19529f7465b1460687" + integrity sha512-fI/g0yC1FC0m4envv8FsPh7tbBoe/eXbEho+iY/hahs7YGgGt3nYNrAFTkR9pLhQaVMpOilhwgFxXEp+O7My/g== dependencies: - web3-bzz "1.3.1" - web3-core "1.3.1" - web3-eth "1.3.1" - web3-eth-personal "1.3.1" - web3-net "1.3.1" - web3-shh "1.3.1" - web3-utils "1.3.1" + web3-bzz "1.3.3" + web3-core "1.3.3" + web3-eth "1.3.3" + web3-eth-personal "1.3.3" + web3-net "1.3.3" + web3-shh "1.3.3" + web3-utils "1.3.3" web3@1.2.11: version "1.2.11" From 0ea73c4edaab606c1ae75b3bfdb27716dd96e205 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Fri, 5 Feb 2021 05:17:25 -0300 Subject: [PATCH 05/34] (Fix) - #1798 broken safe creation deeplink (#1840) * Add types for SafeProps Adds getSafePropsValuesFromQueryParams implementation Replaces window.location with useLocation hook * Replaces SafeProps with import in Layout.tsx Adds downlevelIteration to tsconfig.json to allow array.entries() * Type createSafe() * SafeDeployment Types * Type Paragraph and refactor to functional component * Fix validateQueryParams and types Co-authored-by: nicolas Co-authored-by: Daniel Sanchez --- src/components/layout/Paragraph/index.tsx | 37 +++++++---- src/routes/open/components/Layout.tsx | 10 +-- src/routes/open/container/Open.tsx | 78 ++++++++++++++++------- src/routes/opening/index.tsx | 40 +++++++----- src/test/builder/safe.redux.builder.ts | 1 + tsconfig.json | 3 +- 6 files changed, 108 insertions(+), 61 deletions(-) diff --git a/src/components/layout/Paragraph/index.tsx b/src/components/layout/Paragraph/index.tsx index 2666974c..da0d5705 100644 --- a/src/components/layout/Paragraph/index.tsx +++ b/src/components/layout/Paragraph/index.tsx @@ -1,23 +1,34 @@ import classNames from 'classnames/bind' -import * as React from 'react' +import React, { MouseEventHandler, CSSProperties, ReactElement, ReactNode } from 'react' import styles from './index.module.scss' const cx = classNames.bind(styles) -class Paragraph extends React.PureComponent { - render() { - const { align, children, className, color, dot, noMargin, size, transform, weight, ...props } = this.props +interface Props { + align?: string + children: ReactNode + className?: string + color?: string + dot?: string + noMargin?: boolean + size?: string + transform?: string + weight?: string + onClick?: MouseEventHandler + style?: CSSProperties +} - return ( -

- {children} -

- ) - } +const Paragraph = (props: Props): ReactElement => { + const { align, children, className, color, dot, noMargin, size, transform, weight, ...restProps } = props + return ( +

+ {children} +

+ ) } export default Paragraph diff --git a/src/routes/open/components/Layout.tsx b/src/routes/open/components/Layout.tsx index 2bce3846..9620c05b 100644 --- a/src/routes/open/components/Layout.tsx +++ b/src/routes/open/components/Layout.tsx @@ -24,19 +24,13 @@ import { networkSelector, providerNameSelector, userAccountSelector } from 'src/ import { useSelector } from 'react-redux' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getNameFromAddressBook } from 'src/logic/addressBook/utils' +import { SafeProps } from 'src/routes/open/container/Open' const { useEffect } = React const getSteps = () => ['Name', 'Owners and confirmations', 'Review'] -type SafeProps = { - name: string - ownerAddresses: any - ownerNames: string - threshold: string -} - -type InitialValuesForm = { +export type InitialValuesForm = { owner0Address?: string owner0Name?: string confirmations: string diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 62aa7fc2..8b5df977 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -3,8 +3,8 @@ import queryString from 'query-string' import React, { useEffect, useState } from 'react' import ReactGA from 'react-ga' import { useDispatch, useSelector } from 'react-redux' -import Opening from 'src/routes/opening' -import { Layout } from 'src/routes/open/components/Layout' +import { SafeDeployment } from 'src/routes/opening' +import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout' import Page from 'src/components/layout/Page' import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts' import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions' @@ -24,21 +24,51 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { PromiEvent, TransactionReceipt } from 'web3-core' +import { useLocation } from 'react-router-dom' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' -const validateQueryParams = (ownerAddresses, ownerNames, threshold, safeName) => { +interface SafeCreationQueryParams { + ownerAddresses: string | string[] | null + ownerNames: string | string[] | null + threshold: number | null + safeName: string | null +} + +export interface SafeProps { + name: string + ownerAddresses: string[] + ownerNames: string[] + threshold: string +} + +const validateQueryParams = (queryParams: SafeCreationQueryParams): boolean => { + const { ownerAddresses, ownerNames, threshold, safeName } = queryParams + if (!ownerAddresses || !ownerNames || !threshold || !safeName) { return false } - if (!ownerAddresses.length || ownerNames.length === 0) { + + if (Number.isNaN(threshold)) { return false } - if (Number.isNaN(Number(threshold))) { - return false + return threshold > 0 && threshold <= ownerAddresses.length +} + +const getSafePropsValuesFromQueryParams = (queryParams: SafeCreationQueryParams): SafeProps | undefined => { + if (!validateQueryParams(queryParams)) { + return + } + + const { threshold, safeName, ownerAddresses, ownerNames } = queryParams + + return { + name: safeName as string, + threshold: (threshold as number).toString(), + ownerAddresses: Array.isArray(ownerAddresses) ? ownerAddresses : [ownerAddresses as string], + ownerNames: Array.isArray(ownerNames) ? ownerNames : [ownerNames as string], } - return threshold <= ownerAddresses.length } export const getSafeProps = async ( @@ -54,7 +84,7 @@ export const getSafeProps = async ( return safeProps } -export const createSafe = (values, userAccount) => { +export const createSafe = (values: InitialValuesForm, userAccount: string): PromiEvent => { const confirmations = getThresholdFrom(values) const name = getSafeNameFrom(values) const ownersNames = getNamesFrom(values) @@ -86,24 +116,26 @@ const Open = (): React.ReactElement => { const [loading, setLoading] = useState(false) const [showProgress, setShowProgress] = useState(false) const [creationTxPromise, setCreationTxPromise] = useState>() - const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState() - const [safePropsFromUrl, setSafePropsFromUrl] = useState() + const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<{ txHash?: string } | undefined>() + const [safePropsFromUrl, setSafePropsFromUrl] = useState() const userAccount = useSelector(userAccountSelector) const dispatch = useDispatch() + const location = useLocation() useEffect(() => { // #122: Allow to migrate an old Multisig by passing the parameters to the URL. - const query = queryString.parse(window.location.search, { arrayFormat: 'comma' }) + const query = queryString.parse(location.search, { arrayFormat: 'comma' }) const { name, owneraddresses, ownernames, threshold } = query - if (validateQueryParams(owneraddresses, ownernames, threshold, name)) { - setSafePropsFromUrl({ - name, - ownerAddresses: owneraddresses, - ownerNames: ownernames, - threshold, - } as any) - } - }, []) + + const safeProps = getSafePropsValuesFromQueryParams({ + ownerAddresses: owneraddresses, + ownerNames: ownernames, + threshold: Number(threshold), + safeName: name as string | null, + }) + + setSafePropsFromUrl(safeProps) + }, [location]) // check if there is a safe being created useEffect(() => { @@ -121,7 +153,7 @@ const Open = (): React.ReactElement => { load() }, []) - const createSafeProxy = async (formValues?: any) => { + const createSafeProxy = async (formValues?: InitialValuesForm) => { let values = formValues // save form values, used when the user rejects the TX and wants to retry @@ -132,7 +164,7 @@ const Open = (): React.ReactElement => { values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) } - const promiEvent = createSafe(values, userAccount) + const promiEvent = createSafe(values as InitialValuesForm, userAccount) setCreationTxPromise(promiEvent) setShowProgress(true) } @@ -186,7 +218,7 @@ const Open = (): React.ReactElement => { return ( {showProgress ? ( - (p.inverseColors ? connected : background)}; - color: ${(p) => (p.inverseColors ? background : connected)}; + +interface FullParagraphProps { + inversecolors: string +} + +const FullParagraph = styled(Paragraph)` + background-color: ${(p) => (p.inversecolors ? connected : background)}; + color: ${(p) => (p.inversecolors ? background : connected)}; padding: 24px; font-size: 16px; margin-bottom: 16px; - transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; ` @@ -95,16 +100,21 @@ const BackButton = styled(Button)` margin: 20px auto 0; ` -// type Props = { -// provider: string -// creationTxHash: Promise -// submittedPromise: Promise -// onRetry: () => void -// onSuccess: () => void -// onCancel: () => void -// } +type Props = { + creationTxHash?: string + submittedPromise?: PromiEvent + onRetry: () => void + onSuccess: (createdSafeAddress: string) => void + onCancel: () => void +} -const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submittedPromise }): React.ReactElement => { +export const SafeDeployment = ({ + creationTxHash, + onCancel, + onRetry, + onSuccess, + submittedPromise, +}: Props): React.ReactElement => { const [loading, setLoading] = useState(true) const [stepIndex, setStepIndex] = useState(0) const [safeCreationTxHash, setSafeCreationTxHash] = useState('') @@ -326,7 +336,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte {!error && stepIndex <= 4 && Loader dots} - + {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} @@ -350,5 +360,3 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte ) } - -export default SafeDeployment diff --git a/src/test/builder/safe.redux.builder.ts b/src/test/builder/safe.redux.builder.ts index 8a44555a..a9478064 100644 --- a/src/test/builder/safe.redux.builder.ts +++ b/src/test/builder/safe.redux.builder.ts @@ -82,6 +82,7 @@ export const aMinedSafe = async ( [FIELD_NAME]: name, [FIELD_CONFIRMATIONS]: `${threshold}`, [FIELD_OWNERS]: `${owners}`, + safeCreationSalt: 0 } for (let i = 0; i < owners; i += 1) { diff --git a/tsconfig.json b/tsconfig.json index 853eca4a..e3d810ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "isolatedModules": true, "noEmit": true, "jsx": "react", - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "downlevelIteration": true }, "paths": { "src/*": [ From 63cd88d845087995505d6dae7602035281092277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Fri, 5 Feb 2021 14:58:08 -0300 Subject: [PATCH 06/34] Gas calculation info UI fixes (#1861) * upgrade SRC * add styles to GasInfo container * reject modal Co-authored-by: Daniel Sanchez --- package.json | 2 +- src/components/TransactionsFees/index.tsx | 16 ++- .../components/ConfirmTransactionModal.tsx | 115 ++++++++++-------- .../ContractInteraction/Review/index.tsx | 21 ++-- .../ReviewCustomTx/index.tsx | 10 +- .../ReviewCustomTx/style.ts | 6 +- .../screens/ContractInteraction/style.ts | 6 +- .../screens/ReviewCollectible/index.tsx | 20 ++- .../screens/ReviewCollectible/style.ts | 6 +- .../screens/ReviewSendFundsTx/index.tsx | 10 +- .../screens/ReviewSendFundsTx/style.ts | 6 +- .../Settings/Advanced/RemoveModuleModal.tsx | 24 ++-- .../components/Settings/Advanced/style.ts | 10 +- .../AddOwnerModal/screens/Review/style.ts | 1 + .../RemoveOwnerModal/screens/Review/index.tsx | 1 - .../RemoveOwnerModal/screens/Review/style.ts | 1 + .../screens/Review/index.tsx | 1 - .../ReplaceOwnerModal/screens/Review/style.ts | 1 + .../SpendingLimit/NewLimitModal/Review.tsx | 26 ++-- .../SpendingLimit/RemoveLimitModal.tsx | 18 +-- .../Settings/SpendingLimit/style.ts | 4 + .../ChangeThreshold/index.tsx | 11 +- .../ChangeThreshold/style.ts | 6 +- .../Settings/UpdateSafeModal/index.tsx | 9 +- .../Settings/UpdateSafeModal/style.ts | 6 +- .../ExpandedTx/ApproveTxModal/index.tsx | 20 +-- .../ExpandedTx/ApproveTxModal/style.ts | 6 +- .../ExpandedTx/RejectTxModal/index.tsx | 9 +- .../ExpandedTx/RejectTxModal/style.ts | 6 +- .../helpers/TxParametersDetail/index.tsx | 2 +- yarn.lock | 4 +- 31 files changed, 221 insertions(+), 163 deletions(-) diff --git a/package.json b/package.json index 74667b3f..4e533a49 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@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#2e7574f", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#420f595", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.41.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/TransactionsFees/index.tsx b/src/components/TransactionsFees/index.tsx index 09187210..b9f2afe7 100644 --- a/src/components/TransactionsFees/index.tsx +++ b/src/components/TransactionsFees/index.tsx @@ -3,6 +3,7 @@ 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 { Text } from '@gnosis.pm/safe-react-components' type TransactionFailTextProps = { txEstimationExecutionStatus: EstimationStatus @@ -34,11 +35,18 @@ export const TransactionFees = ({ return ( <> - + You're about to {transactionAction} a transaction and will have to confirm it with your currently connected - wallet. - {!isOffChainSignature && - ` Make sure you have ${gasCostFormatted} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} + wallet.{' '} + {!isOffChainSignature && ( + <> + Make sure you have{' '} + + {gasCostFormatted} + {' '} + (fee price) {nativeCoin.name} in this wallet to fund this confirmation. + + )} diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 057cf611..e20bf62d 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -32,7 +32,7 @@ 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' +import { md, lg, sm } from 'src/theme/variables' const isTxValid = (t: Transaction): boolean => { if (!['string', 'number'].includes(typeof t.value)) { @@ -84,6 +84,10 @@ const ModalFooter = styled(Row)` padding: ${md} ${lg}; justify-content: center; ` +const TransactionFeesWrapper = styled.div` + background-color: ${({ theme }) => theme.colors.background}; + padding: ${sm} ${lg}; +` type OwnProps = { isOpen: boolean @@ -198,61 +202,65 @@ export const ConfirmTransactionModal = ({ ) : (txParameters, toggleEditMode) => { return ( - - - - {txs.map((tx, index) => ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - - {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} - + <> + + + + {txs.map((tx, index) => ( + + } title={`Transaction ${index + 1}`}> + +
+ Value +
+ Ether + + {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} + +
-
-
- Data (hex encoded)* - {tx.data} -
- - - - ))} - - {params?.safeTxGas && ( -
- SafeTxGas - {params?.safeTxGas} - -
- )} +
+ Data (hex encoded)* + {tx.data} +
+ + + + ))} + + {params?.safeTxGas && ( +
+ SafeTxGas + {params?.safeTxGas} + +
+ )} - {/* Tx Parameters */} - - - - - + + {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( + + + + )} + ) } @@ -272,7 +280,6 @@ export const ConfirmTransactionModal = ({ {body(txParameters, toggleEditMode)} - - - - - - +
+ +
+ + if (showAnalytics && !isDesktop) { + loadGoogleAnalytics() + } + + if (showIntercom) { + loadIntercom() + } + + const CookiesBannerForm = (props: CookiesBannerFormProps) => { + const { alertMessage } = props + return ( +
+
+ {alertMessage && ( +
+ + You attempted to open the customer support chat. Please accept the customer support cookie. +
+ )} +

+ We use cookies to provide you with the best experience and to help improve our website and application. + Please read our{' '} + + Cookie Policy + {' '} + for more information. By clicking "Accept all", you agree to the storing of cookies on your device + to enhance site navigation, analyze site usage and provide customer support. +

+
+
+ } + disabled + label="Necessary" + name="Necessary" + onChange={() => setLocalNecessary((prev) => !prev)} + value={localNecessary} + /> +
+
+ } + label="Customer support" + name="Customer support" + onChange={() => setLocalIntercom((prev) => !prev)} + value={localIntercom} + /> +
+
+ } + label="Analytics" + name="Analytics" + onChange={() => setLocalAnalytics((prev) => !prev)} + value={localAnalytics} + /> +
+
+ +
+
+ +
-
- ) - - if (showAnalytics) { - loadIntercom() - loadGoogleAnalytics() + ) } - if (isDesktop) loadIntercom() - return showBanner && !isDesktop ? cookieBannerContent : null + return ( + <> + {!isDesktop && !showIntercom && ( + dispatch(openCookieBanner(true, true))} + /> + )} + {!isDesktop && showBanner?.cookieBannerOpen && ( + + )} + + ) } export default CookiesBanner diff --git a/src/logic/cookies/store/actions/openCookieBanner.ts b/src/logic/cookies/store/actions/openCookieBanner.ts index 64eaa774..2f0a6231 100644 --- a/src/logic/cookies/store/actions/openCookieBanner.ts +++ b/src/logic/cookies/store/actions/openCookieBanner.ts @@ -2,6 +2,10 @@ import { createAction } from 'redux-actions' export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER' -export const openCookieBanner = createAction(OPEN_COOKIE_BANNER, (cookieBannerOpen) => ({ - cookieBannerOpen, -})) +export const openCookieBanner = createAction( + OPEN_COOKIE_BANNER, + (cookieBannerOpen, intercomAlertDisplayed = false) => ({ + cookieBannerOpen, + intercomAlertDisplayed, + }), +) diff --git a/src/logic/cookies/store/reducer/cookies.ts b/src/logic/cookies/store/reducer/cookies.ts index 11da0cc9..43b1bca3 100644 --- a/src/logic/cookies/store/reducer/cookies.ts +++ b/src/logic/cookies/store/reducer/cookies.ts @@ -1,17 +1,12 @@ import { Map } from 'immutable' import { handleActions } from 'redux-actions' - import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner' export const COOKIES_REDUCER_ID = 'cookies' export default handleActions( { - [OPEN_COOKIE_BANNER]: (state, action) => { - const { cookieBannerOpen } = action.payload - - return state.set('cookieBannerOpen', cookieBannerOpen) - }, + [OPEN_COOKIE_BANNER]: (state, action) => state.set('cookieBannerOpen', action.payload), }, Map(), ) diff --git a/src/logic/wallets/store/actions/fetchProvider.ts b/src/logic/wallets/store/actions/fetchProvider.ts index 79ce4479..def1045f 100644 --- a/src/logic/wallets/store/actions/fetchProvider.ts +++ b/src/logic/wallets/store/actions/fetchProvider.ts @@ -1,4 +1,5 @@ import ReactGA from 'react-ga' +import { Dispatch } from 'redux' import addProvider from './addProvider' @@ -8,7 +9,6 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3' import { makeProvider } from 'src/logic/wallets/store/model/provider' import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' -import { Dispatch } from 'redux' export const processProviderResponse = (dispatch, provider) => { const walletRecord = makeProvider(provider) diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 8b5df977..9744f4f3 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -1,8 +1,10 @@ import { Loader } from '@gnosis.pm/safe-react-components' import queryString from 'query-string' import React, { useEffect, useState } from 'react' -import ReactGA from 'react-ga' import { useDispatch, useSelector } from 'react-redux' +import { useLocation } from 'react-router-dom' +import { PromiEvent, TransactionReceipt } from 'web3-core' + import { SafeDeployment } from 'src/routes/opening' import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout' import Page from 'src/components/layout/Page' @@ -23,8 +25,7 @@ import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/sto import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' -import { PromiEvent, TransactionReceipt } from 'web3-core' -import { useLocation } from 'react-router-dom' +import { useAnalytics } from 'src/utils/googleAnalytics' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' @@ -121,6 +122,7 @@ const Open = (): React.ReactElement => { const userAccount = useSelector(userAccountSelector) const dispatch = useDispatch() const location = useLocation() + const { trackEvent } = useAnalytics() useEffect(() => { // #122: Allow to migrate an old Multisig by passing the parameters to the URL. @@ -179,7 +181,7 @@ const Open = (): React.ReactElement => { await dispatch(addOrUpdateSafe(safeProps)) - ReactGA.event({ + trackEvent({ category: 'User', action: 'Created a safe', }) diff --git a/src/utils/intercom.ts b/src/utils/intercom.ts index 68035e7d..605ac63d 100644 --- a/src/utils/intercom.ts +++ b/src/utils/intercom.ts @@ -1,11 +1,15 @@ import { INTERCOM_ID } from 'src/utils/constants' +let intercomLoaded = false + +export const isIntercomLoaded = () => intercomLoaded + // eslint-disable-next-line consistent-return -export const loadIntercom = () => { +export const loadIntercom = (): void => { const APP_ID = INTERCOM_ID if (!APP_ID) { console.error('[Intercom] - In order to use Intercom you need to add an appID') - return null + return } const d = document const s = d.createElement('script') @@ -20,5 +24,12 @@ export const loadIntercom = () => { app_id: APP_ID, consent: true, }) + intercomLoaded = true } } + +export const closeIntercom = (): void => { + if (!isIntercomLoaded()) return + intercomLoaded = false + ;(window as any).Intercom('shutdown') +} From 8c50cda0adb2dc73566227517b3671acd5b16b82 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 10 Feb 2021 15:56:32 +0100 Subject: [PATCH 10/34] Update balancer pool safe app (#1869) --- 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 b6bd98aa..cf133f2c 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}/QmQsmxUVtcEWmKcXxKwYsZFKJ2kDdqqjqdExujiGY1g3tV`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVaxypk2FTyfcTS9oZKxmpQziPUTu2VRhhW7sso1mGysf`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 47d20aa645ed4a79bbffa996a415802ad781b0aa Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 10 Feb 2021 13:43:23 -0300 Subject: [PATCH 11/34] Transaction List v2 (#1781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add types for redux actions (#1737) * solve errors after rebase - added `isStoredTransaction` to differentiate tx provided to `isCancelTransaction` Co-authored-by: Mati Dastugue * Add types + loadGateway transactions cosumer * add client-gateway endpoints to networks configs * add client-gateway getters * WIP: consume gateway-client endpoint - added the history transactions to the store - updated types to `/queued` and `/history` endpoints Co-authored-by: Mati Dastugue * add queued transactions to the store Co-authored-by: Mati Dastugue * add queued transactions selectors Co-authored-by: Mati Dastugue * WIP: display history transactions Co-authored-by: Mati Dastugue * WIP: arrange lists queue/history * prevent loading data from txs-service * cherry-pick TokenTransferAmount component * extract queue transactions logic into a hook `useQueueTransactions` * Add TxType and TokenTransferAmount components Co-authored-by: fernandomg * wip: history transactions * wip: use grid to display list content * wip: use Accordion * wip: tx history Co-authored-by: Mati Dastugue * wip: tx details Co-authored-by: Mati Dastugue * wip: TxInfo Co-authored-by: Mati Dastugue * wip: TxSummary Co-authored-by: Mati Dastugue * wip: TxSettingsInfo Co-authored-by: Mati Dastugue * Wip: style owners list Co-authored-by: fernandomg * wip: Owners List Co-authored-by: Mati Dastugue * wip: TxInfoCreation Co-authored-by: Mati Dastugue * wip: stop using backOff for client-gateway requests Co-authored-by: Mati Dastugue * refactor reorganize files and components Co-authored-by: Mati Dastugue * refactor - Accordion implementation - extract summaryContent to a reusable component Co-authored-by: Mati Dastugue * Fix prettier issue in src/config/index * add methods names and descriptions to collapsed row Co-authored-by: Mati Dastugue * wip: split components to render tx-data depending on the tx type Co-authored-by: Mati Dastugue * add multiSend tx details Co-authored-by: Mati Dastugue * refactor TxData - separate into specified components `HexEncodedData`, `MethodDetails` & `MultiSendDetails` * remove unused imported type * wip: infinite-scroll Co-authored-by: Mati Dastugue * refactor `ADD_HISTORY_TRANSACTIONS` reducer Co-authored-by: Mati Dastugue * implement infinite scroll pagination Co-authored-by: Mati Dastugue * avoid defining `page_url` param * refactor InfiniteScroll implementation - created a wrapper component to simplify interface - rearranged code * add `missingSigners` key to `ExecutionInfo` type * add `lodash.get` * update `useTransactionStatus` hook to support queued transactions * use `lodash.get` to access queued objects * add votes info to TxCollapsed * add TxQueueCollapsed - also update the usage of `useTransactionStatus` hook * split `TxRow` into `TxHistoryRow` and `TxQueueRow` * use `txLocation` instead of `title` for `QueueTxList` component * make `TxDetails` generic * fix queue list elements arrangement * export `useTransactionDetails` return type [skip ci] * wip: group txs by nonce [skip ci] * request tx details on demand [skip ci] * display cancelling message in queued transactions only [skip ci] * wip: implement tree view for grouped transactions * styled components - reorganized - added comments where necessary - refactored * refactor QueueTxList [skip ci] * update styled-component [skip ci] * refactor - Accordion implementation - extract summaryContent to a reusable component Co-authored-by: Mati Dastugue * update safe-react-components Co-authored-by: Mati Dastugue * update styled-component [skip ci] * fix most-recent list of history transactions update * make queued transactions list scrollable * make scrollableTarget a const * styles fixes - queued grouped transactions styles - add styles to scrollbar in scrollable areas * add safe apps info to tx lists * wip: add action buttons to tx details * fix column distribution for transactions rows * display action count for multiSend transactions * TxExpandedActions * add action buttons - also did a slight refactor around grouped vs. not-grouped transactions * fix txDetails selector * adapt button to current SRC specs * wip: action buttons "action" - TODO: handle the store update -> screen refresh * fix execution/confirmation conditions - fixed modals conditions for execution when last confirmation is able to execute * fix tree view (no

as descendant of

) * wip: handle transactions actions through a context provider * provide txLocation through context * fix `react-hooks/exhaustive-deps` warnings * Sort history list * Add objects utils * add `lodash.merge` as a dependency Co-authored-by: Mati Dastugue * update `ADD_QUEUED_TRANSACTIONS` reducer Co-authored-by: Mati Dastugue * implement pagination for `queued` transactions Co-authored-by: Mati Dastugue * prevent rendering action modal if `txDetails` is not available Co-authored-by: Mati Dastugue * pending status for tx execution * small style-based behavior fixes * allow to identify txs to be replaced * redirect to `gatewayTransaction` * adjust behavior for grouped vs individual transactions * add help links * display execute action only when threshold is reached * make `setActiveHover` required * prevent `

` as child of `

` * fix cards background colors * revert staging config * fix linting errors * prevent using `no-owner` class in history list * add `PENDING` status to confirmation transactions This will mark as _pending_ a transaction by its id, the rest of the txs that share same nonce will remain untouched * unify action buttons status - created `useActionButtonsHandler` hook - extracted `CollapsedActions` into `TxCollapsedActions` component Co-authored-by: Mati Dastugue * fix wording * fix pending status Co-authored-by: Mati Dastugue * fix close Action modal Co-authored-by: Mati Dastugue * extract `addressInList` as a util function Co-authored-by: Mati Dastugue * fix action buttons' "disabled" status condition Co-authored-by: Mati Dastugue * provide proper `to` and `value` for `processTransaction` based on Transfer Type (ERC20, ERC721 or ETHER) Co-authored-by: Mati Dastugue * update queued transactions pointers if we reached the last page Co-authored-by: Mati Dastugue * use `as string` for `next` pointer - also fixed typo Co-authored-by: Mati Dastugue * add JSDocs * explicitly discard unused client-gateway headers Co-authored-by: Mati Dastugue Co-authored-by: nicosampler * add loading status to the queue transactions list * fix tx actions after rebase of v2.19.1 * fix issue with safe data update * fix types issues * skip `isCancelTransaction` tests * fix loading status for queue transactions * Update notifications for tx-list v2 (#1839) Co-authored-by: Mati Dastugue * use `sameString` to verify `method` value Co-authored-by: Mati Dastugue * TxDetails refactor cancelTxDetails condition Co-authored-by: Mati Dastugue * remove unused TxType component Co-authored-by: Mati Dastugue * remove unused `isReadyToExecute` function Co-authored-by: Mati Dastugue * Fix eslint * Update txs details after `PENDING` status update * remove log * Fix send transaction because of removed notification message * Cleanup pending unwanted notifications * wip: ellipsis actions * wip: ellipsis actions - fix tokenAmount * Refactor to txInfoDetails * refactor `TxInfoDetails` * remove old `utils.tsx` file * support SpendingLimit transactions * fix `isSpendingLimitMethod` * Fix styles for tx list v2 (#1859) Co-authored-by: fernandomg Co-authored-by: Agustín Longoni * wip: performance enhancement Co-authored-by: Mati Dastugue Co-authored-by: nicosampler * wip: extract data calculation to a hook * refactor huge ternaries * fix columns styles for small screens Co-authored-by: Mati Dastugue * add extra information for `Cancel` transaction identification * undo custom selector Co-authored-by: Mati Dastugue * undo custom selector * Pass `action` by prop to TxDetails Co-authored-by: Mati Dastugue Co-authored-by: nicosampler * Unify `processTransaction` / `createTransaction` actions Co-authored-by: Mati Dastugue Co-authored-by: nicosampler Co-authored-by: Daniel Sanchez * Disable send again when the user is offline * set pending status for the executed tx only (not the group by nonce) Co-authored-by: Mati Dastugue Co-authored-by: nicosampler Co-authored-by: Daniel Sanchez * Use gatewayTransactions as default transaction list * fix styles for TxDetails row Co-authored-by: Mati Dastugue Co-authored-by: nicosampler Co-authored-by: Daniel Sanchez * Remove old transactions list legacy code Move gatewayTransactions within transactions folder * Remove allTransactions legacy code * Types * Fix redirect after createTransaction * fix performance issue for `ApproveTxModal` Co-authored-by: Mati Dastugue Co-authored-by: nicosampler Co-authored-by: Agustin Pane * fix asset icon size * fix status wording * add time tooltip * add _breadcrumb_ * properly identify non existing nonce * fix open cookie banner types after merge * add isCancellation flag support * fix expanded tx styles Co-authored-by: Mati Dastugue Co-authored-by: Mati Dastugue Co-authored-by: Daniel Sanchez Co-authored-by: nicosampler Co-authored-by: Mati Dastugue Co-authored-by: Agustin Pane Co-authored-by: nicolas Co-authored-by: Agustín Longoni --- package.json | 9 +- src/components/AppLayout/Footer/index.tsx | 2 +- src/components/CookiesBanner/index.tsx | 8 +- src/components/CustomIconText/index.tsx | 8 +- src/components/InfiniteScroll/index.tsx | 39 ++ src/config/index.ts | 7 + src/config/networks/energy_web_chain.ts | 1 + src/config/networks/local.ts | 1 + src/config/networks/mainnet.ts | 2 + src/config/networks/network.d.ts | 3 +- src/config/networks/rinkeby.ts | 2 + src/config/networks/volta.ts | 1 + src/config/networks/xdai.ts | 1 + .../store/actions/addAddressBookEntry.ts | 2 +- .../addressBook/store/reducer/addressBook.ts | 23 +- .../store/reducer/collectibles.ts | 10 +- src/logic/collectibles/utils/index.ts | 8 +- src/logic/contracts/methodIds.ts | 14 +- .../cookies/store/actions/openCookieBanner.ts | 10 +- src/logic/cookies/store/reducer/cookies.ts | 10 +- .../store/actions/fetchCurrencyRate.ts | 11 +- .../store/actions/fetchSelectedCurrency.ts | 10 +- .../store/actions/setSelectedCurrency.ts | 7 +- .../store/reducer/currencyValues.ts | 21 +- .../store/reducer/currentSession.ts | 11 +- src/logic/hooks/useEstimateTransactionGas.tsx | 2 +- .../notifications/notificationBuilder.tsx | 17 +- src/logic/notifications/notificationTypes.ts | 98 +--- .../store/models/notification.ts | 8 - .../store/reducer/notifications.ts | 39 +- .../__tests__/transactionHelpers.test.ts | 2 +- .../safe/store/actions/addOrUpdateSafe.ts | 3 +- .../safe/store/actions/createTransaction.ts | 67 +-- .../store/actions/fetchTransactionDetails.ts | 41 ++ .../store/actions/loadSafesFromStorage.ts | 2 +- .../safe/store/actions/processTransaction.ts | 93 ++-- .../transactions/fetchTransactions/index.ts | 69 +-- .../loadGatewayTransactions.ts | 103 ++++ .../loadOutgoingTransactions.ts | 25 +- .../transactions/gatewayTransactions.ts | 9 + .../transactions/utils/transactionHelpers.ts | 85 +-- .../store/actions/updateTransactionStatus.ts | 6 + .../middleware/notificationsMiddleware.ts | 74 +-- .../safe/store/models/types/gateway.d.ts | 384 ++++++++++++++ .../safe/store/models/types/transaction.ts | 7 +- .../store/reducer/cancellationTransactions.ts | 15 +- .../safe/store/reducer/gatewayTransactions.ts | 368 +++++++++++++ .../store/reducer/incomingTransactions.ts | 3 +- src/logic/safe/store/reducer/safe.ts | 48 +- src/logic/safe/store/reducer/transactions.ts | 18 +- .../store/selectors/gatewayTransactions.ts | 98 ++++ src/logic/safe/store/selectors/index.ts | 4 +- .../safe/store/selectors/transactions.ts | 28 +- src/logic/safe/store/selectors/utils.ts | 13 + .../safe/transactions/awaitingTransactions.ts | 23 +- .../safe/utils/shouldSafeStoreBeUpdated.ts | 6 +- .../tokens/store/actions/fetchSafeTokens.ts | 38 +- src/logic/tokens/store/reducer/tokens.ts | 15 +- src/logic/tokens/utils/tokenHelpers.ts | 9 +- .../wallets/store/actions/fetchProvider.ts | 1 - .../wallets/store/actions/removeProvider.ts | 7 - .../EllipsisTransactionDetails/index.tsx | 4 +- .../safe/components/AllTransactions/index.tsx | 58 --- .../components/ConfirmTransactionModal.tsx | 2 +- .../Balances/SendModal/SafeInfo/index.tsx | 2 +- .../components/Balances/SendModal/index.tsx | 3 +- .../ContractInteraction/Review/index.tsx | 2 +- .../ReviewCustomTx/index.tsx | 2 +- .../screens/ReviewCollectible/index.tsx | 2 +- .../screens/ReviewSendFundsTx/index.tsx | 2 +- .../screens/SendCollectible/index.tsx | 10 +- .../Settings/Advanced/RemoveModuleModal.tsx | 2 +- .../ManageOwners/AddOwnerModal/index.tsx | 2 +- .../ManageOwners/RemoveOwnerModal/index.tsx | 2 +- .../ManageOwners/ReplaceOwnerModal/index.tsx | 2 +- .../SpendingLimit/InfoDisplay/DataDisplay.tsx | 2 +- .../SpendingLimit/NewLimitModal/Review.tsx | 2 +- .../SpendingLimit/RemoveLimitModal.tsx | 2 +- .../Settings/ThresholdSettings/index.tsx | 2 +- .../Settings/UpdateSafeModal/index.tsx | 2 +- .../GatewayTransactions/ActionModal.tsx | 59 +++ .../GatewayTransactions/AddressInfo.tsx | 24 + .../GatewayTransactions/HexEncodedData.tsx | 13 + .../GatewayTransactions/HistoryTxList.tsx | 46 ++ .../GatewayTransactions/InfoDetails.tsx | 16 + .../GatewayTransactions/MethodDetails.tsx | 64 +++ .../GatewayTransactions/MultiSendDetails.tsx | 82 +++ .../GatewayTransactions/OwnerRow.tsx | 22 + .../GatewayTransactions/QueueTransactions.tsx | 61 +++ .../GatewayTransactions/QueueTxList.tsx | 93 ++++ .../SpendingLimitDetails.tsx | 75 +++ .../TokenTransferAmount.tsx | 39 ++ .../GatewayTransactions/TxActionProvider.tsx | 55 ++ .../GatewayTransactions/TxCollapsed.tsx | 216 ++++++++ .../TxCollapsedActions.tsx | 58 +++ .../GatewayTransactions/TxData.tsx | 37 ++ .../GatewayTransactions/TxDetails.tsx | 128 +++++ .../GatewayTransactions/TxExpandedActions.tsx | 49 ++ .../TxHistoryCollapsed.tsx | 16 + .../GatewayTransactions/TxHistoryRow.tsx | 29 ++ .../GatewayTransactions/TxHoverProvider.tsx | 15 + .../GatewayTransactions/TxInfo.tsx | 25 + .../GatewayTransactions/TxInfoCreation.tsx | 88 ++++ .../GatewayTransactions/TxInfoDetails.tsx | 81 +++ .../GatewayTransactions/TxInfoSettings.tsx | 75 +++ .../GatewayTransactions/TxInfoTransfer.tsx | 24 + .../TxLocationProvider.tsx | 18 + .../GatewayTransactions/TxOwners.tsx | 81 +++ .../GatewayTransactions/TxQueueCollapsed.tsx | 53 ++ .../GatewayTransactions/TxQueueRow.tsx | 46 ++ .../GatewayTransactions/TxSummary.tsx | 66 +++ .../assets/check-circle-green.svg | 11 + .../GatewayTransactions/assets/custom.svg | 3 + .../GatewayTransactions/assets/incoming.svg | 3 + .../assets/no-transactions.svg | 15 + .../GatewayTransactions/assets/outgoing.svg | 3 + .../assets/plus-circle-green.svg | 11 + .../GatewayTransactions/assets/settings.svg | 3 + .../assets/transactions-list-active.svg | 10 + .../assets/transactions-list-inactive.svg | 10 + .../hooks/useActionButtonsHandlers.ts | 84 +++ .../GatewayTransactions/hooks/useAssetInfo.ts | 99 ++++ .../hooks/useHistoryTransactions.ts | 22 + .../hooks/usePagedHistoryTransactions.ts | 46 ++ .../hooks/usePagedQueuedTransactions.ts | 52 ++ .../hooks/useQueueTransactions.ts | 45 ++ .../hooks/useTransactionActions.ts | 90 ++++ .../hooks/useTransactionDetails.ts | 36 ++ .../hooks/useTransactionStatus.ts | 57 +++ .../hooks/useTransactionType.ts | 72 +++ .../GatewayTransactions/index.tsx | 36 ++ .../modals/ApproveTxModal.tsx | 417 +++++++++++++++ .../modals/RejectTxModal.tsx | 157 ++++++ .../GatewayTransactions/modals/style.ts | 33 ++ .../GatewayTransactions/styled.tsx | 484 ++++++++++++++++++ .../Transactions/GatewayTransactions/utils.ts | 142 +++++ .../ExpandedTx/ApproveTxModal/index.tsx | 2 +- .../ExpandedTx/RejectTxModal/index.tsx | 2 +- .../TxDescription/CustomDescription.tsx | 2 +- .../ExpandedTx/TxDescription/Value.tsx | 8 +- .../TxsTable/Status/assets/awaiting.svg | 3 - .../TxsTable/Status/assets/error.svg | 3 - .../TxsTable/Status/assets/ok.svg | 3 - .../Transactions/TxsTable/Status/index.tsx | 52 -- .../Transactions/TxsTable/Status/style.ts | 52 -- .../Transactions/TxsTable/index.tsx | 139 ----- .../components/Transactions/TxsTable/style.ts | 35 -- src/routes/safe/container/index.tsx | 2 +- src/routes/safe/container/selector.ts | 88 ++-- src/store/index.ts | 14 +- src/test/safe.dom.funds.thresholdGt1.ts | 11 +- .../transactions/transactionList.helper.ts | 3 +- src/types/helpers.d.ts | 1 + src/utils/constants.ts | 3 + src/utils/date.ts | 6 + src/utils/objects.ts | 11 + yarn.lock | 35 +- 157 files changed, 5383 insertions(+), 941 deletions(-) create mode 100644 src/components/InfiniteScroll/index.tsx delete mode 100644 src/logic/notifications/store/models/notification.ts create mode 100644 src/logic/safe/store/actions/fetchTransactionDetails.ts create mode 100644 src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts create mode 100644 src/logic/safe/store/actions/transactions/gatewayTransactions.ts create mode 100644 src/logic/safe/store/actions/updateTransactionStatus.ts create mode 100644 src/logic/safe/store/models/types/gateway.d.ts create mode 100644 src/logic/safe/store/reducer/gatewayTransactions.ts create mode 100644 src/logic/safe/store/selectors/gatewayTransactions.ts create mode 100644 src/logic/safe/store/selectors/utils.ts delete mode 100644 src/routes/safe/components/AllTransactions/index.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/ActionModal.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/AddressInfo.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/HexEncodedData.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/InfoDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/MultiSendDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TokenTransferAmount.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxActionProvider.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxExpandedActions.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryRow.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxHoverProvider.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxInfo.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxInfoDetails.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxInfoSettings.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxInfoTransfer.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxOwners.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxQueueCollapsed.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxQueueRow.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/check-circle-green.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/custom.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/incoming.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/no-transactions.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/outgoing.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/plus-circle-green.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/settings.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-active.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-inactive.svg create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useAssetInfo.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useHistoryTransactions.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useQueueTransactions.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionDetails.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionStatus.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionType.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/index.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/modals/ApproveTxModal.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/modals/style.ts create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx create mode 100644 src/routes/safe/components/Transactions/GatewayTransactions/utils.ts delete mode 100644 src/routes/safe/components/Transactions/TxsTable/Status/assets/awaiting.svg delete mode 100644 src/routes/safe/components/Transactions/TxsTable/Status/assets/error.svg delete mode 100644 src/routes/safe/components/Transactions/TxsTable/Status/assets/ok.svg delete mode 100644 src/routes/safe/components/Transactions/TxsTable/Status/index.tsx delete mode 100644 src/routes/safe/components/Transactions/TxsTable/Status/style.ts delete mode 100644 src/routes/safe/components/Transactions/TxsTable/index.tsx delete mode 100644 src/routes/safe/components/Transactions/TxsTable/style.ts create mode 100644 src/types/helpers.d.ts create mode 100644 src/utils/objects.ts diff --git a/package.json b/package.json index 4e533a49..cfc2c775 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@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#420f595", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8dea3a6", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.41.0", "@material-ui/core": "^4.11.0", @@ -199,9 +199,13 @@ "immutable": "^4.0.0-rc.12", "js-cookie": "^2.2.1", "lodash.debounce": "^4.0.8", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", "lodash.memoize": "^4.1.2", + "lodash.merge": "^4.6.2", "material-ui-search-bar": "^1.0.0", "notistack": "https://github.com/gnosis/notistack.git#v0.9.4", + "object-hash": "^2.1.1", "qrcode.react": "1.0.1", "query-string": "6.13.8", "react": "16.13.1", @@ -211,6 +215,7 @@ "react-final-form-listeners": "^1.0.2", "react-ga": "3.3.0", "react-hot-loader": "4.13.0", + "react-infinite-scroll-component": "^5.1.0", "react-qr-reader": "^2.2.1", "react-redux": "7.2.2", "react-router-dom": "5.2.0", @@ -240,12 +245,14 @@ "@typechain/web3-v1": "^2.0.0", "@types/history": "4.6.2", "@types/jest": "^26.0.16", + "@types/lodash.get": "^4.4.6", "@types/lodash.memoize": "^4.1.6", "@types/node": "^14.14.10", "@types/react": "^16.9.55", "@types/react-dom": "^16.9.9", "@types/react-redux": "^7.1.11", "@types/react-router-dom": "^5.1.6", + "@types/redux-actions": "^2.6.1", "@types/styled-components": "^5.1.4", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", diff --git a/src/components/AppLayout/Footer/index.tsx b/src/components/AppLayout/Footer/index.tsx index d5269b61..598041bf 100644 --- a/src/components/AppLayout/Footer/index.tsx +++ b/src/components/AppLayout/Footer/index.tsx @@ -50,7 +50,7 @@ const Footer = (): React.ReactElement => { const dispatch = useDispatch() const openCookiesHandler = () => { - dispatch(openCookieBanner(true)) + dispatch(openCookieBanner({ cookieBannerOpen: true })) } return ( diff --git a/src/components/CookiesBanner/index.tsx b/src/components/CookiesBanner/index.tsx index bd5e2551..5386a556 100644 --- a/src/components/CookiesBanner/index.tsx +++ b/src/components/CookiesBanner/index.tsx @@ -110,7 +110,7 @@ const CookiesBanner = (): ReactElement => { async function fetchCookiesFromStorage() { const cookiesState = await loadFromCookie(COOKIES_KEY) if (!cookiesState) { - dispatch(openCookieBanner(true)) + dispatch(openCookieBanner({ cookieBannerOpen: true })) } else { const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState if (acceptedIntercom === undefined) { @@ -143,7 +143,7 @@ const CookiesBanner = (): ReactElement => { await saveCookie(COOKIES_KEY, newState, 365) setShowAnalytics(!isDesktop) setShowIntercom(true) - dispatch(openCookieBanner(false)) + dispatch(openCookieBanner({ cookieBannerOpen: false })) } const closeCookiesBannerHandler = async () => { @@ -159,7 +159,7 @@ const CookiesBanner = (): ReactElement => { if (!localIntercom && isIntercomLoaded()) { closeIntercom() } - dispatch(openCookieBanner(false)) + dispatch(openCookieBanner({ cookieBannerOpen: false })) } if (showAnalytics && !isDesktop) { @@ -254,7 +254,7 @@ const CookiesBanner = (): ReactElement => { dispatch(openCookieBanner(true, true))} + onClick={() => dispatch(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))} /> )} {!isDesktop && showBanner?.cookieBannerOpen && ( diff --git a/src/components/CustomIconText/index.tsx b/src/components/CustomIconText/index.tsx index 0a1b5c91..31fc13ba 100644 --- a/src/components/CustomIconText/index.tsx +++ b/src/components/CustomIconText/index.tsx @@ -1,3 +1,4 @@ +import { Text } from '@gnosis.pm/safe-react-components' import React from 'react' import styled from 'styled-components' @@ -8,16 +9,13 @@ const Wrapper = styled.div` const Icon = styled.img` max-width: 15px; max-height: 15px; -` -const Text = styled.span` - margin-left: 5px; - height: 17px; + margin-right: 9px; ` const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => ( - {text && {text}} + {text && {text}} ) diff --git a/src/components/InfiniteScroll/index.tsx b/src/components/InfiniteScroll/index.tsx new file mode 100644 index 00000000..b8bd7a1b --- /dev/null +++ b/src/components/InfiniteScroll/index.tsx @@ -0,0 +1,39 @@ +import { Loader } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component' +import styled from 'styled-components' + +import { Overwrite } from 'src/types/helpers' + +export const Centered = styled.div<{ padding?: number }>` + width: 100%; + height: 100%; + display: flex; + padding: ${({ padding }) => `${padding}px`}; + justify-content: center; + align-items: center; +` + +export const SCROLLABLE_TARGET_ID = 'scrollableDiv' + +type InfiniteScrollProps = Overwrite + +export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => { + return ( + + + + } + scrollThreshold="120px" + scrollableTarget={SCROLLABLE_TARGET_ID} + > + {props.children} + + ) +} diff --git a/src/config/index.ts b/src/config/index.ts index e1affc98..6d7145b1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -68,6 +68,8 @@ const configuration = (): NetworkSpecificConfiguration => { const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration) +export const getClientGatewayUrl = (): string => getConfig().clientGatewayUrl + export const getTxServiceUrl = (): string => getConfig().txServiceUrl export const getRelayUrl = (): string | undefined => getConfig().relayApiUrl @@ -81,6 +83,11 @@ export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?. export const getRpcServiceUrl = (): string => usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl +export const getSafeClientGatewayBaseUrl = (safeAddress: string) => `${getClientGatewayUrl()}/safes/${safeAddress}` + +export const getTxDetailsUrl = (clientGatewayTxId: string) => + `${getClientGatewayUrl()}/transactions/${clientGatewayTxId}` + export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}` export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens` diff --git a/src/config/networks/energy_web_chain.ts b/src/config/networks/energy_web_chain.ts index df13c013..2db43be1 100644 --- a/src/config/networks/energy_web_chain.ts +++ b/src/config/networks/energy_web_chain.ts @@ -4,6 +4,7 @@ import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 's // @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0 // once the oracle is fixed we need to remove the fixed value const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.ewc.gnosis.io/v1', txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1', safeAppsUrl: 'https://safe-apps-ewc.staging.gnosisdev.com', gasPriceOracle: { diff --git a/src/config/networks/local.ts b/src/config/networks/local.ts index bbb7d15b..a18d87bf 100644 --- a/src/config/networks/local.ts +++ b/src/config/networks/local.ts @@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg' import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'http://localhost:8001/v1', txServiceUrl: 'http://localhost:8000/api/v1', relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1', safeAppsUrl: 'http://localhost:3002', diff --git a/src/config/networks/mainnet.ts b/src/config/networks/mainnet.ts index 5aea9a61..cd330a8c 100644 --- a/src/config/networks/mainnet.ts +++ b/src/config/networks/mainnet.ts @@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg' import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.mainnet.staging.gnosisdev.com/v1', txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1', safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com', gasPriceOracle: { @@ -25,6 +26,7 @@ const mainnet: NetworkConfig = { }, production: { ...baseConfig, + clientGatewayUrl: 'https://safe-client.mainnet.gnosis.io/v1', txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1', safeAppsUrl: 'https://apps.gnosis-safe.io', }, diff --git a/src/config/networks/network.d.ts b/src/config/networks/network.d.ts index c88a076e..3a5b973d 100644 --- a/src/config/networks/network.d.ts +++ b/src/config/networks/network.d.ts @@ -85,8 +85,9 @@ type GasPrice = } export type EnvironmentSettings = GasPrice & { + clientGatewayUrl: string txServiceUrl: string - // Shall we keep a reference to the relay? + // TODO: Shall we keep a reference to the relay? relayApiUrl?: string safeAppsUrl: string rpcServiceUrl: string diff --git a/src/config/networks/rinkeby.ts b/src/config/networks/rinkeby.ts index 722e8751..0e035ecb 100644 --- a/src/config/networks/rinkeby.ts +++ b/src/config/networks/rinkeby.ts @@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg' import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.rinkeby.staging.gnosisdev.com/v1', txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1', safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com', gasPriceOracle: { @@ -25,6 +26,7 @@ const rinkeby: NetworkConfig = { }, production: { ...baseConfig, + clientGatewayUrl: 'https://safe-client.rinkeby.gnosis.io/v1', txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1', safeAppsUrl: 'https://apps.gnosis-safe.io', }, diff --git a/src/config/networks/volta.ts b/src/config/networks/volta.ts index 40ef4ebd..f36a09af 100644 --- a/src/config/networks/volta.ts +++ b/src/config/networks/volta.ts @@ -2,6 +2,7 @@ import EwcLogo from 'src/config/assets/token_ewc.svg' import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.volta.gnosis.io/v1', txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1', safeAppsUrl: 'https://safe-apps-volta.staging.gnosisdev.com', gasPriceOracle: { diff --git a/src/config/networks/xdai.ts b/src/config/networks/xdai.ts index 7f218e5c..3806260b 100644 --- a/src/config/networks/xdai.ts +++ b/src/config/networks/xdai.ts @@ -2,6 +2,7 @@ import xDaiLogo from 'src/config/assets/token_xdai.svg' import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.xdai.gnosis.io/v1', txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1', safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com', gasPrice: 1e9, diff --git a/src/logic/addressBook/store/actions/addAddressBookEntry.ts b/src/logic/addressBook/store/actions/addAddressBookEntry.ts index b3d80b50..b2717a26 100644 --- a/src/logic/addressBook/store/actions/addAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/addAddressBookEntry.ts @@ -9,7 +9,7 @@ type addAddressBookEntryOptions = { export const addAddressBookEntry = createAction( ADD_ENTRY, - (entry: AddressBookEntry, options: addAddressBookEntryOptions) => { + (entry: AddressBookEntry, options?: addAddressBookEntryOptions) => { let notifyEntryUpdate = true if (options) { notifyEntryUpdate = options.notifyEntryUpdate diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts index a59e2ab0..7edd4e7c 100644 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ b/src/logic/addressBook/store/reducer/addressBook.ts @@ -1,11 +1,12 @@ -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' -import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook' import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' +import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' import { getValidAddressBookName } from 'src/logic/addressBook/utils' @@ -18,13 +19,19 @@ export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBo }) } -export default handleActions( +type AddressBookPayload = { addressBook: AddressBookState } +type EntryPayload = { entry: AddressBookEntry } +type RemoveEntryPayload = { entryAddress: string } + +type Payloads = AddressBookPayload | EntryPayload | RemoveEntryPayload + +export default handleActions( { - [LOAD_ADDRESS_BOOK]: (state, action) => { + [LOAD_ADDRESS_BOOK]: (state, action: Action) => { const { addressBook } = action.payload return addressBook }, - [ADD_ENTRY]: (state, action) => { + [ADD_ENTRY]: (state, action: Action) => { const { entry } = action.payload const entryFound = state.find((oldEntry) => oldEntry.address === entry.address) @@ -34,7 +41,7 @@ export default handleActions( } return state }, - [UPDATE_ENTRY]: (state, action) => { + [UPDATE_ENTRY]: (state, action: Action) => { const { entry } = action.payload const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) if (entryIndex >= 0) { @@ -42,13 +49,13 @@ export default handleActions( } return state }, - [REMOVE_ENTRY]: (state, action) => { + [REMOVE_ENTRY]: (state, action: Action) => { const { entryAddress } = action.payload const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress) state.splice(entryIndex, 1) return state }, - [ADD_OR_UPDATE_ENTRY]: (state, action) => { + [ADD_OR_UPDATE_ENTRY]: (state, action: Action) => { const { entry } = action.payload // Only updates entries with valid names diff --git a/src/logic/collectibles/store/reducer/collectibles.ts b/src/logic/collectibles/store/reducer/collectibles.ts index 39ae649d..3c58d9ec 100644 --- a/src/logic/collectibles/store/reducer/collectibles.ts +++ b/src/logic/collectibles/store/reducer/collectibles.ts @@ -1,11 +1,15 @@ import { handleActions } from 'redux-actions' import { ADD_NFT_ASSETS, ADD_NFT_TOKENS } from 'src/logic/collectibles/store/actions/addCollectibles' +import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles' +import { AppReduxState } from 'src/store' export const NFT_ASSETS_REDUCER_ID = 'nftAssets' export const NFT_TOKENS_REDUCER_ID = 'nftTokens' -export const nftAssetReducer = handleActions( +type NFTAssetsPayload = { nftAssets: NFTAssets } + +export const nftAssetReducer = handleActions( { [ADD_NFT_ASSETS]: (state, action) => { const { nftAssets } = action.payload @@ -16,7 +20,9 @@ export const nftAssetReducer = handleActions( {}, ) -export const nftTokensReducer = handleActions( +type NFTTokensPayload = { nftTokens: NFTTokens } + +export const nftTokensReducer = handleActions( { [ADD_NFT_TOKENS]: (state, action) => { const { nftTokens } = action.payload diff --git a/src/logic/collectibles/utils/index.ts b/src/logic/collectibles/utils/index.ts index 8f7b346b..5eae89c2 100644 --- a/src/logic/collectibles/utils/index.ts +++ b/src/logic/collectibles/utils/index.ts @@ -1,7 +1,7 @@ import { getNetworkId, getNetworkInfo } from 'src/config' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors' -import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { BuildTx, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d' import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -31,10 +31,10 @@ export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' /** * Verifies that a tx received by the transaction service is an ERC721 token-related transaction - * @param {TxServiceModel} tx + * @param {BuildTx['tx']} tx * @returns boolean */ -export const isSendERC721Transaction = (tx: TxServiceModel): boolean => { +export const isSendERC721Transaction = (tx: BuildTx['tx']): boolean => { let hasERC721Transfer = false if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) { @@ -44,7 +44,7 @@ export const isSendERC721Transaction = (tx: TxServiceModel): boolean => { // Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this const state = store.getState() const knownAssets = nftAssetsListAddressesSelector(state) - return knownAssets.includes(tx.to) || hasERC721Transfer + return knownAssets.includes((tx as ServiceTx).to) || hasERC721Transfer } /** diff --git a/src/logic/contracts/methodIds.ts b/src/logic/contracts/methodIds.ts index 36d72d08..086c9151 100644 --- a/src/logic/contracts/methodIds.ts +++ b/src/logic/contracts/methodIds.ts @@ -25,7 +25,7 @@ const decodeInfo = ({ paramsHash, params }: DecodeInfoProps): DataDecoded['param })) } -export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => { +export const decodeParamsFromSafeMethod = (data: string): DataDecoded | undefined => { const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] const method = SAFE_METHODS_NAMES[methodId] @@ -99,7 +99,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => } default: - return null + return } } @@ -113,7 +113,7 @@ export const isDeleteAllowanceMethod = (data: string): boolean => { return sameString(SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE) } -export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null => { +export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | undefined => { const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] const method = SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] @@ -171,7 +171,7 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null } default: - return null + return } } @@ -183,9 +183,9 @@ const isSpendingLimitMethod = (methodId: string): boolean => { return !!SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] } -export const decodeMethods = (data: string | null): DataDecoded | null => { +export const decodeMethods = (data: string | null): DataDecoded | undefined => { if (!data?.length) { - return null + return } const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)] @@ -226,6 +226,6 @@ export const decodeMethods = (data: string | null): DataDecoded | null => { } default: - return null + return } } diff --git a/src/logic/cookies/store/actions/openCookieBanner.ts b/src/logic/cookies/store/actions/openCookieBanner.ts index 2f0a6231..e3db9cb7 100644 --- a/src/logic/cookies/store/actions/openCookieBanner.ts +++ b/src/logic/cookies/store/actions/openCookieBanner.ts @@ -1,11 +1,7 @@ import { createAction } from 'redux-actions' +import { OpenCookieBannerPayload } from 'src/logic/cookies/store/reducer/cookies' + export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER' -export const openCookieBanner = createAction( - OPEN_COOKIE_BANNER, - (cookieBannerOpen, intercomAlertDisplayed = false) => ({ - cookieBannerOpen, - intercomAlertDisplayed, - }), -) +export const openCookieBanner = createAction(OPEN_COOKIE_BANNER) diff --git a/src/logic/cookies/store/reducer/cookies.ts b/src/logic/cookies/store/reducer/cookies.ts index 43b1bca3..e8411153 100644 --- a/src/logic/cookies/store/reducer/cookies.ts +++ b/src/logic/cookies/store/reducer/cookies.ts @@ -1,12 +1,18 @@ import { Map } from 'immutable' import { handleActions } from 'redux-actions' import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner' +import { AppReduxState } from 'src/store' export const COOKIES_REDUCER_ID = 'cookies' -export default handleActions( +export type OpenCookieBannerPayload = { cookieBannerOpen: boolean; intercomAlertDisplayed?: boolean } + +export default handleActions( { - [OPEN_COOKIE_BANNER]: (state, action) => state.set('cookieBannerOpen', action.payload), + [OPEN_COOKIE_BANNER]: (state, action) => { + const { intercomAlertDisplayed = false, cookieBannerOpen } = action.payload + return state.set('cookieBannerOpen', { intercomAlertDisplayed, cookieBannerOpen }) + }, }, Map(), ) diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts index 75fe3088..580b90e4 100644 --- a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts +++ b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts @@ -1,13 +1,18 @@ +import { Action } from 'redux-actions' +import { ThunkDispatch } from 'redux-thunk' + import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates' import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' -import { Dispatch } from 'redux' +import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues' +import { AppReduxState } from 'src/store' const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async ( - dispatch: Dispatch, + dispatch: ThunkDispatch>, ): Promise => { if (AVAILABLE_CURRENCIES.USD === selectedCurrency) { - return dispatch(setCurrencyRate(safeAddress, 1)) + dispatch(setCurrencyRate(safeAddress, 1)) + return } const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates( diff --git a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts index 42898646..2297b676 100644 --- a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts @@ -1,12 +1,14 @@ -import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' -import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' +import { Action } from 'redux-actions' +import { ThunkDispatch } from 'redux-thunk' + import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' +import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' -import { Dispatch } from 'redux' +import { AppReduxState } from 'src/store' export const fetchSelectedCurrency = (safeAddress: string) => async ( - dispatch: Dispatch, + dispatch: ThunkDispatch>, ): Promise => { try { const storedSelectedCurrency = await loadSelectedCurrency() diff --git a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts index 390b9cb7..8a87126d 100644 --- a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts @@ -1,6 +1,7 @@ -import { createAction } from 'redux-actions' +import { Action, createAction } from 'redux-actions' import { ThunkDispatch } from 'redux-thunk' -import { AnyAction } from 'redux' + +import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues' import { AppReduxState } from 'src/store' import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' @@ -12,7 +13,7 @@ const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: stri })) export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => ( - dispatch: ThunkDispatch, + dispatch: ThunkDispatch>, ): void => { dispatch(setCurrentCurrency(safeAddress, selectedCurrency)) dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) diff --git a/src/logic/currencyValues/store/reducer/currencyValues.ts b/src/logic/currencyValues/store/reducer/currencyValues.ts index f42d0b73..ee626001 100644 --- a/src/logic/currencyValues/store/reducer/currencyValues.ts +++ b/src/logic/currencyValues/store/reducer/currencyValues.ts @@ -1,10 +1,10 @@ import { Map } from 'immutable' -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate' import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' +import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' export const CURRENCY_VALUES_KEY = 'currencyValues' @@ -15,19 +15,26 @@ export interface CurrencyReducerMap extends Map { export type CurrencyValuesState = Map -export default handleActions( +type CurrencyBasePayload = { safeAddress: string } +export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number } +export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList } +export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string } + +export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload + +export default handleActions( { - [SET_CURRENCY_RATE]: (state: CurrencyReducerMap, action) => { + [SET_CURRENCY_RATE]: (state, action: Action) => { const { currencyRate, safeAddress } = action.payload return state.setIn([safeAddress, 'currencyRate'], currencyRate) }, - [SET_CURRENCY_BALANCES]: (state: CurrencyReducerMap, action) => { - const { currencyBalances, safeAddress } = action.payload + [SET_CURRENCY_BALANCES]: (state, action: Action) => { + const { safeAddress, currencyBalances } = action.payload return state.setIn([safeAddress, 'currencyBalances'], currencyBalances) }, - [SET_CURRENT_CURRENCY]: (state: CurrencyReducerMap, action) => { + [SET_CURRENT_CURRENCY]: (state, action: Action) => { const { safeAddress, selectedCurrency } = action.payload return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency) diff --git a/src/logic/currentSession/store/reducer/currentSession.ts b/src/logic/currentSession/store/reducer/currentSession.ts index 236da1b9..3864575c 100644 --- a/src/logic/currentSession/store/reducer/currentSession.ts +++ b/src/logic/currentSession/store/reducer/currentSession.ts @@ -1,8 +1,9 @@ -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' import { LOAD_CURRENT_SESSION } from 'src/logic/currentSession/store/actions/loadCurrentSession' import { UPDATE_VIEWED_SAFES } from 'src/logic/currentSession/store/actions/updateViewedSafes' import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils' +import { AppReduxState } from 'src/store' export const CURRENT_SESSION_REDUCER_ID = 'currentSession' @@ -14,13 +15,15 @@ export const initialState = { viewedSafes: [], } -export default handleActions( +type CurrentSessionPayloads = CurrentSessionState | string + +export default handleActions( { - [LOAD_CURRENT_SESSION]: (state = initialState, action) => ({ + [LOAD_CURRENT_SESSION]: (state = initialState, action: Action) => ({ ...state, ...action.payload, }), - [UPDATE_VIEWED_SAFES]: (state, action) => { + [UPDATE_VIEWED_SAFES]: (state, action: Action) => { const safeAddress = action.payload const viewedSafes = state.viewedSafes const newState = { diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 51e68960..7ef5f6fb 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -155,7 +155,7 @@ type UseEstimateTransactionGasProps = { manualGasPrice?: string } -type TransactionGasEstimationResult = { +export type TransactionGasEstimationResult = { txEstimationExecutionStatus: EstimationStatus gasEstimation: number // Amount of gas needed for execute or approve the transaction gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice) diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index 912bbabd..fae0a821 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -22,18 +22,15 @@ const getStandardTxNotificationsQueue = ( origin: string, ): Record | Notification> => ({ beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), - pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin), afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin), afterExecution: { noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin), - moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin), }, afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin), }) const waitingTransactionNotificationsQueue = { beforeExecution: null, - pendingExecution: null, afterRejection: null, waitingConfirmation: NOTIFICATIONS.TX_WAITING_MSG, afterExecution: null, @@ -43,7 +40,6 @@ const waitingTransactionNotificationsQueue = { const getConfirmationTxNotificationsQueue = (origin: string) => { return { beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), - pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin), afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin), afterExecution: { noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin), @@ -56,7 +52,6 @@ const getConfirmationTxNotificationsQueue = (origin: string) => { const getCancellationTxNotificationsQueue = (origin: string) => { return { beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), - pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin), afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin), afterExecution: { noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin), @@ -68,7 +63,6 @@ const getCancellationTxNotificationsQueue = (origin: string) => { const safeNameChangeNotificationsQueue = { beforeExecution: null, - pendingExecution: null, afterRejection: null, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.SAFE_NAME_CHANGED_MSG, @@ -79,7 +73,6 @@ const safeNameChangeNotificationsQueue = { const ownerNameChangeNotificationsQueue = { beforeExecution: null, - pendingExecution: null, afterRejection: null, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG, @@ -90,7 +83,6 @@ const ownerNameChangeNotificationsQueue = { const settingsChangeTxNotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_SETTINGS_CHANGE_MSG, - pendingExecution: NOTIFICATIONS.SETTINGS_CHANGE_PENDING_MSG, afterRejection: NOTIFICATIONS.SETTINGS_CHANGE_REJECTED_MSG, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.SETTINGS_CHANGE_EXECUTED_MSG, @@ -101,7 +93,6 @@ const settingsChangeTxNotificationsQueue = { const newSpendingLimitTxNotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_NEW_SPENDING_LIMIT_MSG, - pendingExecution: NOTIFICATIONS.NEW_SPENDING_LIMIT_PENDING_MSG, afterRejection: NOTIFICATIONS.NEW_SPENDING_LIMIT_REJECTED_MSG, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.NEW_SPENDING_LIMIT_EXECUTED_MSG, @@ -112,7 +103,6 @@ const newSpendingLimitTxNotificationsQueue = { const removeSpendingLimitTxNotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_REMOVE_SPENDING_LIMIT_MSG, - pendingExecution: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_PENDING_MSG, afterRejection: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_REJECTED_MSG, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_EXECUTED_MSG, @@ -123,18 +113,15 @@ const removeSpendingLimitTxNotificationsQueue = { const defaultNotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_TX_MSG, - pendingExecution: NOTIFICATIONS.TX_PENDING_MSG, afterRejection: NOTIFICATIONS.TX_REJECTED_MSG, afterExecution: { noMoreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MSG, - moreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, }, afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG, } const addressBookNewEntry = { beforeExecution: null, - pendingExecution: null, afterRejection: null, waitingConfirmation: null, afterExecution: { @@ -146,7 +133,6 @@ const addressBookNewEntry = { const addressBookEditEntry = { beforeExecution: null, - pendingExecution: null, afterRejection: null, waitingConfirmation: null, afterExecution: { @@ -158,7 +144,6 @@ const addressBookEditEntry = { const addressBookDeleteEntry = { beforeExecution: null, - pendingExecution: null, afterRejection: null, waitingConfirmation: null, afterExecution: { @@ -231,7 +216,7 @@ export const getNotificationsFromTxType: any = (txType, origin) => { export const enhanceSnackbarForAction = ( notification: Notification, - key?: number | string, + key?: string, onClick?: () => void, ): Notification => ({ ...notification, diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index e70636b7..0a6dcef4 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -15,45 +15,35 @@ export type NotificationId = keyof typeof NOTIFICATION_IDS export type Notification = { message: string options: OptionsObject - key?: number | string + key?: string + dismissed?: boolean } const NOTIFICATION_IDS = { - CONNECT_WALLET_MSG: 'CONNECT_WALLET_MSG', - CONNECT_WALLET_READ_MODE_MSG: 'CONNECT_WALLET_READ_MODE_MSG', - WALLET_CONNECTED_MSG: 'WALLET_CONNECTED_MSG', - WALLET_DISCONNECTED_MSG: 'WALLET_DISCONNECTED_MSG', UNLOCK_WALLET_MSG: 'UNLOCK_WALLET_MSG', CONNECT_WALLET_ERROR_MSG: 'CONNECT_WALLET_ERROR_MSG', SIGN_TX_MSG: 'SIGN_TX_MSG', - TX_PENDING_MSG: 'TX_PENDING_MSG', TX_REJECTED_MSG: 'TX_REJECTED_MSG', TX_EXECUTED_MSG: 'TX_EXECUTED_MSG', TX_CANCELLATION_EXECUTED_MSG: 'TX_CANCELLATION_EXECUTED_MSG', TX_FAILED_MSG: 'TX_FAILED_MSG', - TX_EXECUTED_MORE_CONFIRMATIONS_MSG: 'TX_EXECUTED_MORE_CONFIRMATIONS_MSG', TX_WAITING_MSG: 'TX_WAITING_MSG', - TX_INCOMING_MSG: 'TX_INCOMING_MSG', - TX_CONFIRMATION_PENDING_MSG: 'TX_CONFIRMATION_PENDING_MSG', TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG', TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG', SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG', OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG', SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG', - SETTINGS_CHANGE_PENDING_MSG: 'SETTINGS_CHANGE_PENDING_MSG', SETTINGS_CHANGE_REJECTED_MSG: 'SETTINGS_CHANGE_REJECTED_MSG', SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG', SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG', SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG', TESTNET_VERSION_MSG: 'TESTNET_VERSION_MSG', SIGN_NEW_SPENDING_LIMIT_MSG: 'SIGN_NEW_SPENDING_LIMIT_MSG', - NEW_SPENDING_LIMIT_PENDING_MSG: 'NEW_SPENDING_LIMIT_PENDING_MSG', NEW_SPENDING_LIMIT_REJECTED_MSG: 'NEW_SPENDING_LIMIT_REJECTED_MSG', NEW_SPENDING_LIMIT_EXECUTED_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MSG', NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG', NEW_SPENDING_LIMIT_FAILED_MSG: 'NEW_SPENDING_LIMIT_FAILED_MSG', SIGN_REMOVE_SPENDING_LIMIT_MSG: 'SIGN_REMOVE_SPENDING_LIMIT_MSG', - REMOVE_SPENDING_LIMIT_PENDING_MSG: 'REMOVE_SPENDING_LIMIT_PENDING_MSG', REMOVE_SPENDING_LIMIT_REJECTED_MSG: 'REMOVE_SPENDING_LIMIT_REJECTED_MSG', REMOVE_SPENDING_LIMIT_EXECUTED_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MSG', REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG', @@ -67,32 +57,6 @@ const NOTIFICATION_IDS = { export const NOTIFICATIONS: Record = { // Wallet Connection - CONNECT_WALLET_MSG: { - message: 'Please connect wallet to continue', - options: { variant: WARNING, persist: true, preventDuplicate: true }, - }, - CONNECT_WALLET_READ_MODE_MSG: { - message: 'You are in read-only mode: Please connect wallet', - options: { variant: WARNING, persist: true, preventDuplicate: true }, - }, - WALLET_CONNECTED_MSG: { - message: 'Wallet connected', - options: { - variant: SUCCESS, - persist: false, - autoHideDuration: shortDuration, - }, - }, - WALLET_DISCONNECTED_MSG: { - message: 'Wallet disconnected', - key: 'WALLET_DISCONNECTED_MSG', - options: { - variant: SUCCESS, - persist: false, - autoHideDuration: shortDuration, - preventDuplicate: true, - }, - }, UNLOCK_WALLET_MSG: { message: 'Unlock your wallet to connect', options: { variant: WARNING, persist: true, preventDuplicate: true }, @@ -107,62 +71,40 @@ export const NOTIFICATIONS: Record = { message: 'Please sign the transaction', options: { variant: INFO, persist: true }, }, - TX_PENDING_MSG: { - message: 'Transaction pending', - options: { variant: INFO, persist: true }, - }, TX_REJECTED_MSG: { message: 'Transaction rejected', - options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, }, TX_EXECUTED_MSG: { message: 'Transaction successfully executed', - options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration }, }, TX_CANCELLATION_EXECUTED_MSG: { message: 'Rejection successfully submitted', - options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, - }, - TX_EXECUTED_MORE_CONFIRMATIONS_MSG: { - message: 'Transaction successfully created. More confirmations needed to execute', - options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration }, }, TX_FAILED_MSG: { message: 'Transaction failed', - options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, }, TX_WAITING_MSG: { - message: 'A pending transaction requires your confirmation!', + message: 'A transaction requires your confirmation', key: 'TX_WAITING_MSG', options: { variant: WARNING, - persist: true, - preventDuplicate: true, - }, - }, - TX_INCOMING_MSG: { - message: 'Incoming transfer: ', - key: 'TX_INCOMING_MSG', - options: { - variant: SUCCESS, persist: false, - autoHideDuration: longDuration, + autoHideDuration: shortDuration, preventDuplicate: true, }, }, - // Approval Transactions - TX_CONFIRMATION_PENDING_MSG: { - message: 'Confirmation transaction pending', - options: { variant: INFO, persist: true }, - }, TX_CONFIRMATION_EXECUTED_MSG: { message: 'Confirmation transaction was successful', - options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration }, }, TX_CONFIRMATION_FAILED_MSG: { message: 'Confirmation transaction failed', - options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, }, // Safe Name @@ -182,17 +124,13 @@ export const NOTIFICATIONS: Record = { message: 'Please sign the settings change', options: { variant: INFO, persist: true }, }, - SETTINGS_CHANGE_PENDING_MSG: { - message: 'Settings change pending', - options: { variant: INFO, persist: true }, - }, SETTINGS_CHANGE_REJECTED_MSG: { message: 'Settings change rejected', - options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, }, SETTINGS_CHANGE_EXECUTED_MSG: { message: 'Settings change successfully executed', - options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration }, }, SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: { message: 'Settings change successfully created. More confirmations needed to execute', @@ -200,7 +138,7 @@ export const NOTIFICATIONS: Record = { }, SETTINGS_CHANGE_FAILED_MSG: { message: 'Settings change failed', - options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, }, // Spending Limit @@ -208,10 +146,6 @@ export const NOTIFICATIONS: Record = { message: 'Please sign the new Spending Limit', options: { variant: INFO, persist: true }, }, - NEW_SPENDING_LIMIT_PENDING_MSG: { - message: 'New Spending Limit pending', - options: { variant: INFO, persist: true }, - }, NEW_SPENDING_LIMIT_REJECTED_MSG: { message: 'New Spending Limit rejected', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, @@ -232,10 +166,6 @@ export const NOTIFICATIONS: Record = { message: 'Please sign the remove Spending Limit', options: { variant: INFO, persist: true }, }, - REMOVE_SPENDING_LIMIT_PENDING_MSG: { - message: 'Remove Spending Limit pending', - options: { variant: INFO, persist: true }, - }, REMOVE_SPENDING_LIMIT_REJECTED_MSG: { message: 'Remove Spending Limit rejected', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, @@ -256,7 +186,7 @@ export const NOTIFICATIONS: Record = { // Network TESTNET_VERSION_MSG: { message: "Testnet Version: Don't send production assets to this Safe", - options: { variant: WARNING, persist: true, preventDuplicate: true }, + options: { variant: WARNING, persist: false, preventDuplicate: true, autoHideDuration: longDuration }, }, WRONG_NETWORK_MSG: { message: `Wrong network: Please use ${getNetworkName()}`, diff --git a/src/logic/notifications/store/models/notification.ts b/src/logic/notifications/store/models/notification.ts deleted file mode 100644 index e08ed2bf..00000000 --- a/src/logic/notifications/store/models/notification.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Record } from 'immutable' - -export const makeNotification = Record({ - key: 0, - message: '', - options: {}, - dismissed: false, -}) diff --git a/src/logic/notifications/store/reducer/notifications.ts b/src/logic/notifications/store/reducer/notifications.ts index bb05f747..3fb42435 100644 --- a/src/logic/notifications/store/reducer/notifications.ts +++ b/src/logic/notifications/store/reducer/notifications.ts @@ -1,38 +1,55 @@ import { Map } from 'immutable' -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' +import { Notification } from 'src/logic/notifications/notificationTypes' +import { AppReduxState } from 'src/store' import { CLOSE_SNACKBAR } from '../actions/closeSnackbar' import { ENQUEUE_SNACKBAR } from '../actions/enqueueSnackbar' import { REMOVE_SNACKBAR } from '../actions/removeSnackbar' -import { makeNotification } from 'src/logic/notifications/store/models/notification' - export const NOTIFICATIONS_REDUCER_ID = 'notifications' -export default handleActions( +type CloseSnackBarPayload = { key: string; dismissAll: boolean } +type Payloads = Notification | CloseSnackBarPayload | string + +export default handleActions( { - [ENQUEUE_SNACKBAR]: (state, action) => { + [ENQUEUE_SNACKBAR]: (state, action: Action) => { const notification = action.payload - return state.set(notification.key, makeNotification(notification)) + if (!notification.key) { + return state + } + + return state.set(notification.key, notification) }, - [CLOSE_SNACKBAR]: (state, action) => { + [CLOSE_SNACKBAR]: (state, action: Action) => { const { dismissAll, key } = action.payload - if (key) { - return state.update(key, (prev) => prev?.set('dismissed', true)) + if (key && state.get(key)) { + return state.update(key, (notification) => { + if (notification) { + return { + ...notification, + dismissed: true, + } + } + + return notification + }) } + if (dismissAll) { return state.withMutations((map) => { map.forEach((notification, notificationKey) => { - map.set(notificationKey, notification.set('dismissed', true)) + map.set(notificationKey, { ...notification, dismissed: true }) }) }) } return state }, - [REMOVE_SNACKBAR]: (state, action) => { + [REMOVE_SNACKBAR]: (state, action: Action) => { const key = action.payload return state.delete(key) diff --git a/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts index a639ead5..027f6cd8 100644 --- a/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts +++ b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts @@ -90,7 +90,7 @@ describe('isInnerTransaction', () => { }) }) -describe('isCancelTransaction', () => { +describe.skip('isCancelTransaction', () => { const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' const mockedETHAccount = '0xd76e0B566e218a80F4c96458FE09a322EBAa9aF2' it('It should return false if given a inner transaction with empty data', () => { diff --git a/src/logic/safe/store/actions/addOrUpdateSafe.ts b/src/logic/safe/store/actions/addOrUpdateSafe.ts index 92dd0faf..ae559ea7 100644 --- a/src/logic/safe/store/actions/addOrUpdateSafe.ts +++ b/src/logic/safe/store/actions/addOrUpdateSafe.ts @@ -12,7 +12,6 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List ({ +export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({ safe, - loadedFromStorage, })) diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 0efccdae..54d43740 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -2,7 +2,6 @@ import { push } from 'connected-react-router' import { ThunkAction } from 'redux-thunk' import { onboardUser } from 'src/components/ConnectButton' -import { decodeMethods } from 'src/logic/contracts/methodIds' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getNotificationsFromTxType } from 'src/logic/notifications' import { @@ -20,16 +19,7 @@ import { providerSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS } from 'src/routes/routes' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' -import { - removeTxFromStore, - storeSignedTx, - storeExecutedTx, -} from 'src/logic/safe/store/actions/transactions/pendingTransactions' -import { - generateSafeTxHash, - mockTransaction, - TxToMock, -} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' +import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' import { getErrorMessage } from 'src/test/utils/ethereumErrors' import fetchTransactions from './transactions/fetchTransactions' @@ -60,7 +50,7 @@ type ConfirmEventHandler = (safeTxHash: string) => void type ErrorEventHandler = () => void export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001 -const createTransaction = ( +export const createTransaction = ( { safeAddress, to, @@ -105,8 +95,6 @@ const createTransaction = ( const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin) const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution)) - let pendingExecutionKey - let txHash const txArgs: TxArgs = { safeInstance, @@ -124,13 +112,13 @@ const createTransaction = ( sigs, } const safeTxHash = generateSafeTxHash(safeAddress, txArgs) + try { if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) { const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) if (signature) { dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded)) dispatch(fetchTransactions(safeAddress)) await saveTxToHistory({ ...txArgs, signature, origin }) @@ -148,57 +136,28 @@ const createTransaction = ( nonce: ethParameters?.ethNonce, } - const txToMock: TxToMock = { - ...txArgs, - confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper - value: txArgs.valueInWei, - safeTxHash, - dataDecoded: decodeMethods(txArgs.data), - submissionDate: new Date().toISOString(), - } - const mockedTx = await mockTransaction(txToMock, safeAddress, state) - await tx .send(sendParams) .once('transactionHash', async (hash) => { onUserConfirm?.(safeTxHash) - try { - txHash = hash - dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution)) + txHash = hash + dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - await Promise.all([ - saveTxToHistory({ ...txArgs, txHash, origin }), - storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }), - ]) - dispatch(fetchTransactions(safeAddress)) - } catch (e) { - removeTxFromStore(mockedTx, safeAddress, dispatch, state) - } + await saveTxToHistory({ ...txArgs, txHash, origin }) + + dispatch(fetchTransactions(safeAddress)) }) .on('error', (error) => { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) - removeTxFromStore(mockedTx, safeAddress, dispatch, state) console.error('Tx error: ', error) onError?.() }) .then(async (receipt) => { - if (pendingExecutionKey) { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) + if (isExecution) { + dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded)) } - dispatch( - enqueueSnackbar( - isExecution - ? notificationsQueue.afterExecution.noMoreConfirmationsNeeded - : notificationsQueue.afterExecution.moreConfirmationsNeeded, - ), - ) - - await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state }) - dispatch(fetchTransactions(safeAddress)) return receipt.transactionHash @@ -210,10 +169,6 @@ const createTransaction = ( dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - if (pendingExecutionKey) { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) - } - dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } })) if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) { @@ -227,5 +182,3 @@ const createTransaction = ( return txHash } - -export default createTransaction diff --git a/src/logic/safe/store/actions/fetchTransactionDetails.ts b/src/logic/safe/store/actions/fetchTransactionDetails.ts new file mode 100644 index 00000000..b7aad88e --- /dev/null +++ b/src/logic/safe/store/actions/fetchTransactionDetails.ts @@ -0,0 +1,41 @@ +import axios, { AxiosResponse } from 'axios' +import { createAction } from 'redux-actions' + +import { getTxDetailsUrl } from 'src/config' +import { Dispatch } from 'src/logic/safe/store/actions/types' +import { ExpandedTxDetails, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d' +import { TransactionDetailsPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { getTransactionDetails } from 'src/logic/safe/store/selectors/gatewayTransactions' +import { AppReduxState } from 'src/store' + +export const UPDATE_TRANSACTION_DETAILS = 'UPDATE_TRANSACTION_DETAILS' +const updateTransactionDetails = createAction(UPDATE_TRANSACTION_DETAILS) + +export const fetchTransactionDetails = ({ + transactionId, + txLocation, +}: { + transactionId: Transaction['id'] + txLocation: TxLocation +}) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { + const txDetails = getTransactionDetails(getState())({ + attributeValue: transactionId, + attributeName: 'id', + txLocation, + }) + const safeAddress = safeParamAddressFromStateSelector(getState()) + + if (txDetails) { + return + } + + try { + const url = getTxDetailsUrl(transactionId) + const { data: transactionDetails } = await axios.get>(url) + + dispatch(updateTransactionDetails({ transactionId, txLocation, safeAddress, value: transactionDetails })) + } catch (error) { + console.error(`Failed to retrieve transaction ${transactionId} details`, error.message) + } +} diff --git a/src/logic/safe/store/actions/loadSafesFromStorage.ts b/src/logic/safe/store/actions/loadSafesFromStorage.ts index 00e3d99f..8aa5f560 100644 --- a/src/logic/safe/store/actions/loadSafesFromStorage.ts +++ b/src/logic/safe/store/actions/loadSafesFromStorage.ts @@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise => if (safes) { Object.values(safes).forEach((safeProps) => { - dispatch(addOrUpdateSafe(buildSafe(safeProps), true)) + dispatch(addOrUpdateSafe(buildSafe(safeProps))) }) } } catch (err) { diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index 5062e487..f54d2c12 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -1,3 +1,4 @@ +import { List } from 'immutable' import { AnyAction } from 'redux' import { ThunkAction } from 'redux-thunk' @@ -17,22 +18,38 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' import fetchSafe from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' -import { mockTransaction, TxToMock } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' 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' +import { updateTransactionStatus } from 'src/logic/safe/store/actions/updateTransactionStatus' +import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' +import { Operation } from 'src/logic/safe/store/models/types/gateway.d' + interface ProcessTransactionArgs { approveAndExecute: boolean notifiedTransaction: string safeAddress: string - tx: Transaction + tx: { + id: string + confirmations: List + origin: string // json.stringified url, name + to: string + value: string + data: string + operation: Operation + nonce: number + safeTxGas: number + safeTxHash: string + baseGas: number + gasPrice: string + gasToken: string + refundReceiver: string + } userAddress: string ethParameters?: Pick thresholdReached: boolean @@ -71,14 +88,13 @@ export const processTransaction = ({ const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin) const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution)) - let pendingExecutionKey let txHash let transaction const txArgs = { - ...tx.toJS(), // merge the previous tx with new data + ...tx, // merge the previous tx with new data safeInstance, - to: tx.recipient, + to: tx.to, valueInWei: tx.value, data: tx.data ?? EMPTY_DATA, operation: tx.operation, @@ -99,10 +115,10 @@ export const processTransaction = ({ if (signature) { dispatch(closeSnackbarAction({ key: beforeExecutionKey })) + dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id })) await saveTxToHistory({ ...txArgs, signature }) // TODO: while we wait for the tx to be stored in the service and later update the tx info // we should update the tx status in the store to disable owners' action buttons - dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded)) dispatch(fetchTransactions(safeAddress)) return @@ -119,52 +135,47 @@ export const processTransaction = ({ nonce: ethParameters?.ethNonce, } - const txToMock: TxToMock = { - ...txArgs, - value: txArgs.valueInWei, - } - const mockedTx = await mockTransaction(txToMock, safeAddress, state) - await transaction .send(sendParams) .once('transactionHash', async (hash: string) => { txHash = hash dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution)) + dispatch( + updateTransactionStatus({ + txStatus: 'PENDING', + safeAddress, + nonce: tx.nonce, + // if we provide the tx ID that sole tx will have the _pending_ status. + // if not, all the txs that share the same nonce will have the _pending_ status. + id: tx.id, + }), + ) try { - await Promise.all([ - saveTxToHistory({ ...txArgs, txHash }), - storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }), - ]) + await saveTxToHistory({ ...txArgs, txHash }) dispatch(fetchTransactions(safeAddress)) } catch (e) { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) - await storeTx({ transaction: tx, safeAddress, dispatch, state }) console.error(e) } }) .on('error', (error) => { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) - storeTx({ transaction: tx, safeAddress, dispatch, state }) + dispatch( + updateTransactionStatus({ + txStatus: 'PENDING_FAILED', + safeAddress, + nonce: tx.nonce, + id: tx.id, + }), + ) + console.error('Processing transaction error: ', error) }) .then(async (receipt) => { - if (pendingExecutionKey) { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) + if (isExecution) { + dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded)) } - dispatch( - enqueueSnackbar( - isExecution - ? notificationsQueue.afterExecution.noMoreConfirmationsNeeded - : notificationsQueue.afterExecution.moreConfirmationsNeeded, - ), - ) - - await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state }) - dispatch(fetchTransactions(safeAddress)) if (isExecution) { @@ -180,10 +191,14 @@ export const processTransaction = ({ dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - if (pendingExecutionKey) { - dispatch(closeSnackbarAction({ key: pendingExecutionKey })) - } - + dispatch( + updateTransactionStatus({ + txStatus: 'PENDING_FAILED', + safeAddress, + nonce: tx.nonce, + id: tx.id, + }), + ) dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } })) if (txHash) { diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts index 271ee746..cb903502 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts @@ -1,58 +1,35 @@ -import { batch } from 'react-redux' -import { ThunkAction, ThunkDispatch } from 'redux-thunk' +import { ThunkDispatch } from 'redux-thunk' import { AnyAction } from 'redux' -import { backOff } from 'exponential-backoff' -import { addIncomingTransactions } from 'src/logic/safe/store/actions/addIncomingTransactions' -import { addModuleTransactions } from 'src/logic/safe/store/actions/addModuleTransactions' - -import { loadIncomingTransactions } from './loadIncomingTransactions' -import { loadModuleTransactions } from './loadModuleTransactions' -import { loadOutgoingTransactions } from './loadOutgoingTransactions' - -import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions' -import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions' +import { + addHistoryTransactions, + addQueuedTransactions, +} from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import { loadHistoryTransactions, loadQueuedTransactions } from './loadGatewayTransactions' import { AppReduxState } from 'src/store' -const noFunc = () => {} - -export default (safeAddress: string): ThunkAction, AppReduxState, undefined, AnyAction> => async ( +export default (safeAddress: string) => async ( dispatch: ThunkDispatch, ): Promise => { - try { - const transactions = await backOff(() => loadOutgoingTransactions(safeAddress)) + const [history, queued] = await Promise.allSettled([ + loadHistoryTransactions(safeAddress), + loadQueuedTransactions(safeAddress), + ]) - if (transactions) { - const { cancel, outgoing } = transactions - const updateCancellationTxs = cancel.size - ? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel }) - : noFunc - const updateOutgoingTxs = outgoing.size - ? addOrUpdateTransactions({ - safeAddress, - transactions: outgoing, - }) - : noFunc + if (history.status === 'fulfilled') { + const values = history.value - batch(() => { - dispatch(updateCancellationTxs) - dispatch(updateOutgoingTxs) - }) + if (values.length) { + dispatch(addHistoryTransactions({ safeAddress, values })) } + } else { + console.error('Failed to load history transactions', history.reason) + } - const incomingTransactions = await loadIncomingTransactions(safeAddress) - const safeIncomingTxs = incomingTransactions.get(safeAddress) - - if (safeIncomingTxs?.size) { - dispatch(addIncomingTransactions(incomingTransactions)) - } - - const moduleTransactions = await loadModuleTransactions(safeAddress) - - if (moduleTransactions.length) { - dispatch(addModuleTransactions({ modules: moduleTransactions, safeAddress })) - } - } catch (error) { - console.log('Error fetching transactions:', error) + if (queued.status === 'fulfilled') { + const values = queued.value + dispatch(addQueuedTransactions({ safeAddress, values })) + } else { + console.error('Failed to load queued transactions', queued.reason) } } diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts new file mode 100644 index 00000000..15c002c5 --- /dev/null +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions.ts @@ -0,0 +1,103 @@ +import axios, { AxiosResponse } from 'axios' + +import { getSafeClientGatewayBaseUrl } from 'src/config' +import { HistoryGatewayResponse, QueuedGatewayResponse } from 'src/logic/safe/store/models/types/gateway' +import { checksumAddress } from 'src/utils/checksumAddress' + +/*************/ +/* HISTORY */ +/*************/ +const getHistoryTransactionsUrl = (safeAddress: string): string => { + const address = checksumAddress(safeAddress) + return `${getSafeClientGatewayBaseUrl(address)}/transactions/history/` +} + +const historyPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {} + +/** + * Fetch next page if there is a next pointer for the safeAddress. + * If the fetch was success, updates the pointers. + * @param {string} safeAddress + */ +export const loadPagedHistoryTransactions = async ( + safeAddress: string, +): Promise<{ values: HistoryGatewayResponse['results']; next: string | null } | undefined> => { + // if `historyPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called + // if `historyPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client + if (!historyPointers[safeAddress]?.next) { + return + } + + const { + data: { results, ...pointers }, + } = await axios.get>( + historyPointers[safeAddress].next as string, + ) + + historyPointers[safeAddress] = pointers + + return { values: results, next: historyPointers[safeAddress].next } +} + +export const loadHistoryTransactions = async (safeAddress: string): Promise => { + const historyTransactionsUrl = getHistoryTransactionsUrl(safeAddress) + + const { + data: { results, ...pointers }, + } = await axios.get>(historyTransactionsUrl) + + if (!historyPointers[safeAddress]) { + historyPointers[safeAddress] = pointers + } + + return results +} + +/************/ +/* QUEUED */ +/************/ +const getQueuedTransactionsUrl = (safeAddress: string): string => { + const address = checksumAddress(safeAddress) + return `${getSafeClientGatewayBaseUrl(address)}/transactions/queued/` +} + +const queuedPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {} + +/** + * Fetch next page if there is a next pointer for the safeAddress. + * If the fetch was success, updates the pointers. + * @param {string} safeAddress + */ +export const loadPagedQueuedTransactions = async ( + safeAddress: string, +): Promise<{ values: QueuedGatewayResponse['results']; next: string | null } | undefined> => { + // if `queuedPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called + // if `queuedPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client + if (!queuedPointers[safeAddress]?.next) { + return + } + + const { + data: { results, ...pointers }, + } = await axios.get>( + queuedPointers[safeAddress].next as string, + ) + + queuedPointers[safeAddress] = pointers + + return { values: results, next: queuedPointers[safeAddress].next } +} + +export const loadQueuedTransactions = async (safeAddress: string): Promise => { + const queuedTransactionsUrl = getQueuedTransactionsUrl(safeAddress) + + const { + data: { results, ...pointers }, + } = await axios.get>(queuedTransactionsUrl) + + if (!queuedPointers[safeAddress] || queuedPointers[safeAddress].next === null) { + queuedPointers[safeAddress] = pointers + } + + return results +} diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts index 60e5e6ad..299d6205 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts @@ -1,6 +1,7 @@ import { fromJS, List, Map } from 'immutable' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' +import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider' import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' @@ -57,8 +58,8 @@ export type SafeTransactionsType = { } export type OutgoingTxs = { - cancellationTxs: Record - outgoingTxs: TxServiceModel[] + cancellationTxs: Record | CancellationTransactions + outgoingTxs: TxServiceModel[] | List } export type BatchProcessTxsProps = OutgoingTxs & { @@ -94,21 +95,25 @@ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxService ) } -type BatchRequestReturnValues = [TxServiceModel, string | undefined] +type BatchRequestReturnValues = [TxServiceModel | Transaction, string | undefined] /** * Requests Contract's code for all the Contracts the Safe has interacted with * @param transactions * @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>} */ -const batchRequestContractCode = (transactions: TxServiceModel[]): Promise => { +const batchRequestContractCode = ( + transactions: (TxServiceModel | Transaction)[], +): Promise => { if (!transactions || !Array.isArray(transactions)) { throw new Error('`transactions` must be provided in order to lookup information') } const batch = new web3ReadOnly.BatchRequest() - const whenTxsValues = transactions.map((tx) => { + // this will no longer be used when txs-list-v2 feature is finished + // that's why I'm doing this to move forward + const whenTxsValues = (transactions as any[]).map((tx) => { return generateBatchRequests({ abi: [], address: tx.to, @@ -142,8 +147,8 @@ const batchProcessOutgoingTransactions = async ({ outgoing: Transaction[] }> => { // cancellation transactions - const cancelTxsValues = Object.values(cancellationTxs) - const cancellationTxsWithData = cancelTxsValues.length ? await batchRequestContractCode(cancelTxsValues) : [] + const cancelTxsValues = List(Object.values(cancellationTxs)) + const cancellationTxsWithData = cancelTxsValues.size ? await batchRequestContractCode(cancelTxsValues.toArray()) : [] const cancel = {} for (const [tx] of cancellationTxsWithData) { @@ -157,7 +162,11 @@ const batchProcessOutgoingTransactions = async ({ } // outgoing transactions - const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : [] + const outgoingTxsList: List = + (outgoingTxs as TxServiceModel[]).length !== undefined + ? List(outgoingTxs as TxServiceModel[]) + : (outgoingTxs as List) + const outgoingTxsWithData = outgoingTxsList.size ? await batchRequestContractCode(outgoingTxsList.toArray()) : [] const outgoing: Transaction[] = [] for (const [tx] of outgoingTxsWithData) { diff --git a/src/logic/safe/store/actions/transactions/gatewayTransactions.ts b/src/logic/safe/store/actions/transactions/gatewayTransactions.ts new file mode 100644 index 00000000..fd0109e5 --- /dev/null +++ b/src/logic/safe/store/actions/transactions/gatewayTransactions.ts @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions' + +import { HistoryPayload, QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' + +export const ADD_HISTORY_TRANSACTIONS = 'ADD_HISTORY_TRANSACTIONS' +export const addHistoryTransactions = createAction(ADD_HISTORY_TRANSACTIONS) + +export const ADD_QUEUED_TRANSACTIONS = 'ADD_QUEUED_TRANSACTIONS' +export const addQueuedTransactions = createAction(ADD_QUEUED_TRANSACTIONS) diff --git a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts index 26bc3656..e14a0f3b 100644 --- a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts @@ -15,6 +15,7 @@ import { TransactionTypeValues, TxArgs, RefundParams, + isStoredTransaction, } from 'src/logic/safe/store/models/types/transaction' import { AppReduxState, store } from 'src/store' import { @@ -30,14 +31,14 @@ import { import { TypedDataUtils } from 'eth-sig-util' import { ProviderRecord } from 'src/logic/wallets/store/model/provider' import { SafeRecord } from 'src/logic/safe/store/models/safe' -import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' +import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' import { CALL } from 'src/logic/safe/transactions' export const isEmptyData = (data?: string | null): boolean => { return !data || data === EMPTY_DATA } -export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress: string): boolean => { +export const isInnerTransaction = (tx: BuildTx['tx'] | Transaction, safeAddress: string): boolean => { let isSameAddress = false if ((tx as TxServiceModel).to !== undefined) { @@ -49,9 +50,15 @@ export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress return isSameAddress && Number(tx.value) === 0 } -export const isCancelTransaction = (tx: TxServiceModel, safeAddress: string): boolean => { - if (!sameAddress(tx.to, safeAddress)) { - return false +export const isCancelTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => { + if (isStoredTransaction(tx)) { + if (!sameAddress(tx.recipient, safeAddress)) { + return false + } + } else { + if (!sameAddress(tx.to, safeAddress)) { + return false + } } if (Number(tx.value)) { @@ -89,15 +96,15 @@ export const isPendingTransaction = (tx: Transaction, cancelTx: Transaction): bo return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending' } -export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: string): boolean => { +export const isModifySettingsTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => { return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data) } -export const isMultiSendTransaction = (tx: TxServiceModel): boolean => { +export const isMultiSendTransaction = (tx: BuildTx['tx']): boolean => { return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0 } -export const isUpgradeTransaction = (tx: TxServiceModel): boolean => { +export const isUpgradeTransaction = (tx: BuildTx['tx']): boolean => { return ( !isEmptyData(tx.data) && isMultiSendTransaction(tx) && @@ -106,11 +113,11 @@ export const isUpgradeTransaction = (tx: TxServiceModel): boolean => { ) } -export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => { - return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data) +export const isOutgoingTransaction = (tx: BuildTx['tx'], safeAddress?: string): boolean => { + return !sameAddress((tx as ServiceTx).to, safeAddress) && !isEmptyData(tx.data) } -export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: string): Promise => { +export const isCustomTransaction = async (tx: BuildTx['tx'], safeAddress?: string): Promise => { const isOutgoing = isOutgoingTransaction(tx, safeAddress) const isErc20 = await isSendERC20Transaction(tx) const isUpgrade = isUpgradeTransaction(tx) @@ -120,7 +127,7 @@ export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: strin } export const getRefundParams = async ( - tx: TxServiceModel, + tx: BuildTx['tx'], tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>, ): Promise => { const { nativeCoin } = getNetworkInfo() @@ -155,7 +162,7 @@ export const getRefundParams = async ( return refundParams } -export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => { +export const getDecodedParams = (tx: BuildTx['tx']): DecodedParams | null => { if (tx.dataDecoded) { return { [tx.dataDecoded.method]: tx.dataDecoded.parameters.reduce( @@ -170,22 +177,22 @@ export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => { return null } -export const getConfirmations = (tx: TxServiceModel): List => { +export const getConfirmations = (tx: BuildTx['tx']): List => { return List( - tx.confirmations.map((conf) => + (tx.confirmations as ServiceTx['confirmations'])?.map((conf) => makeConfirmation({ owner: conf.owner, hash: conf.transactionHash, signature: conf.signature, }), - ), + ) ?? [], ) } export const isTransactionCancelled = ( - tx: TxServiceModel, - outgoingTxs: Array, - cancellationTxs: Record, + tx: BuildTx['tx'], + outgoingTxs: BuildTx['outgoingTxs'], + cancellationTxs: BuildTx['cancellationTxs'], ): boolean => { return ( // not executed @@ -252,8 +259,10 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues return txType } +export type ServiceTx = TxServiceModel | TxToMock + export type BuildTx = BatchProcessTxsProps & { - tx: TxServiceModel + tx: ServiceTx | Transaction } export const buildTx = async ({ @@ -281,19 +290,19 @@ export const buildTx = async ({ let tokenSymbol = nativeCoin.symbol try { if (isSendERC20Tx) { - const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to) + const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to) tokenDecimals = decimals tokenSymbol = symbol } else if (isSendERC721Tx) { - tokenSymbol = await getERC721Symbol(tx.to) + tokenSymbol = await getERC721Symbol((tx as ServiceTx).to) } } catch (err) { - console.log(`Failed to retrieve token data from ${tx.to}`) + console.log(`Failed to retrieve token data from ${(tx as ServiceTx).to}`) } const txToStore = makeTransaction({ baseGas: tx.baseGas, - blockNumber: tx.blockNumber, + blockNumber: (tx as ServiceTx).blockNumber, cancelled: isTxCancelled, confirmations, customTx: isCustomTx, @@ -301,23 +310,23 @@ export const buildTx = async ({ dataDecoded: tx.dataDecoded, decimals: tokenDecimals, decodedParams, - executionDate: tx.executionDate, - executionTxHash: tx.transactionHash, - executor: tx.executor, - fee: tx.fee, + executionDate: (tx as ServiceTx).executionDate, + executionTxHash: (tx as ServiceTx).transactionHash, + executor: (tx as ServiceTx).executor, + fee: (tx as ServiceTx).fee, gasPrice: tx.gasPrice, gasToken: tx.gasToken || ZERO_ADDRESS, isCancellationTx, isCollectibleTransfer: isSendERC721Tx, isExecuted: tx.isExecuted, - isSuccessful: tx.isSuccessful, + isSuccessful: (tx as ServiceTx).isSuccessful, isTokenTransfer: isSendERC20Tx, modifySettingsTx: isModifySettingsTx, multiSendTx: isMultiSendTx, nonce: tx.nonce, operation: tx.operation, - origin: tx.origin, - recipient: tx.to, + origin: (tx as ServiceTx).origin, + recipient: (tx as ServiceTx).to, refundParams, refundReceiver: tx.refundReceiver || ZERO_ADDRESS, safeTxGas: tx.safeTxGas, @@ -325,7 +334,7 @@ export const buildTx = async ({ submissionDate: tx.submissionDate, symbol: tokenSymbol, upgradeTx: isUpgradeTx, - value: tx.value.toString(), + value: tx.value?.toString(), }) return txToStore @@ -333,13 +342,7 @@ export const buildTx = async ({ .set('type', calculateTransactionType(txToStore)) } -export type TxToMock = TxArgs & { - confirmations: [] - safeTxHash: string - value: string - submissionDate: string - dataDecoded: DataDecoded | null -} +export type TxToMock = TxArgs & Partial export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise => { const safe = safeSelector(state) @@ -355,7 +358,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed currentUser: undefined, outgoingTxs, safe, - tx: (tx as unknown) as TxServiceModel, + tx, }) } @@ -369,7 +372,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe dispatch( addOrUpdateTransactions({ safeAddress, - transactions: transactions.withMutations((list: any[]) => + transactions: transactions.withMutations((list) => list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))), ), }), diff --git a/src/logic/safe/store/actions/updateTransactionStatus.ts b/src/logic/safe/store/actions/updateTransactionStatus.ts new file mode 100644 index 00000000..a6a155a2 --- /dev/null +++ b/src/logic/safe/store/actions/updateTransactionStatus.ts @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions' + +import { TransactionStatusPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' + +export const UPDATE_TRANSACTION_STATUS = 'UPDATE_TRANSACTION_STATUS' +export const updateTransactionStatus = createAction(UPDATE_TRANSACTION_STATUS) diff --git a/src/logic/safe/store/middleware/notificationsMiddleware.ts b/src/logic/safe/store/middleware/notificationsMiddleware.ts index c07ea64e..a3edabf5 100644 --- a/src/logic/safe/store/middleware/notificationsMiddleware.ts +++ b/src/logic/safe/store/middleware/notificationsMiddleware.ts @@ -3,14 +3,17 @@ import { push } from 'connected-react-router' import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import { getAwaitingTransactions } from 'src/logic/safe/transactions/awaitingTransactions' +import { + getAwaitingTransactions, + getAwaitingGatewayTransactions, +} from 'src/logic/safe/transactions/awaitingTransactions' import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion' import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' -import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' import { grantedSelector } from 'src/routes/safe/container/selector' import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions' import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions' +import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions' import updateSafe from 'src/logic/safe/store/actions/updateSafe' import { safeParamAddressFromStateSelector, @@ -18,10 +21,16 @@ import { safeCancellationTransactionsSelector, } from 'src/logic/safe/store/selectors' +import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d' import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe' -const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_OR_UPDATE_SAFE] +const watchedActions = [ + ADD_OR_UPDATE_TRANSACTIONS, + ADD_INCOMING_TRANSACTIONS, + ADD_OR_UPDATE_SAFE, + ADD_QUEUED_TRANSACTIONS, +] const sendAwaitingTransactionNotification = async ( dispatch, @@ -34,7 +43,7 @@ const sendAwaitingTransactionNotification = async ( if (!dispatch || !safeAddress || !awaitingTxsSubmissionDateList || !notificationKey) { return } - if (awaitingTxsSubmissionDateList.size === 0) { + if (awaitingTxsSubmissionDateList.length === 0) { return } @@ -48,7 +57,7 @@ const sendAwaitingTransactionNotification = async ( return lastTimeUserLoggedIn ? new Date(submissionDate) > new Date(lastTimeUserLoggedIn) : true }) - if (filteredDuplicatedAwaitingTxList.size === 0) { + if (filteredDuplicatedAwaitingTxList.length === 0) { return } dispatch( @@ -101,40 +110,39 @@ const notificationsMiddleware = (store) => (next) => async (action) => { break } + case ADD_QUEUED_TRANSACTIONS: { + const { safeAddress, values } = action.payload + const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction) + const userAddress: string = userAccountSelector(state) + const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress) + + const awaitingTxsSubmissionDateList = awaitingTransactions.map((tx) => tx.timestamp) + + const safes = safesMapSelector(state) + const currentSafe = safes.get(safeAddress) + + if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.length === 0) { + break + } + + const notificationKey = `${safeAddress}-awaiting` + + await sendAwaitingTransactionNotification( + dispatch, + safeAddress, + awaitingTxsSubmissionDateList, + notificationKey, + onNotificationClicked(dispatch, notificationKey, safeAddress), + ) + + break + } case ADD_INCOMING_TRANSACTIONS: { action.payload.forEach((incomingTransactions, safeAddress) => { const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {}) - const viewedSafes = state.currentSession['viewedSafes'] - const recurringUser = viewedSafes?.includes(safeAddress) const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock) - const { message, ...TX_INCOMING_MSG } = NOTIFICATIONS.TX_INCOMING_MSG - - if (recurringUser) { - if (newIncomingTransactions.size > 3) { - dispatch( - enqueueSnackbar( - enhanceSnackbarForAction({ - ...TX_INCOMING_MSG, - message: 'Multiple incoming transfers', - }), - ), - ) - } else { - newIncomingTransactions.forEach((tx) => { - dispatch( - enqueueSnackbar( - enhanceSnackbarForAction({ - ...TX_INCOMING_MSG, - message: `${message}${getIncomingTxAmount(tx)}`, - }), - ), - ) - }) - } - } - dispatch( updateSafe({ address: safeAddress, diff --git a/src/logic/safe/store/models/types/gateway.d.ts b/src/logic/safe/store/models/types/gateway.d.ts new file mode 100644 index 00000000..c462971a --- /dev/null +++ b/src/logic/safe/store/models/types/gateway.d.ts @@ -0,0 +1,384 @@ +type TransferDirection = 'INCOMING' | 'OUTGOING' + +type Erc20Transfer = { + type: 'ERC20' + tokenAddress: string + tokenName: string | null + tokenSymbol: string | null + logoUri: string | null + decimals: number | null + value: string +} + +type Erc721Transfer = { + type: 'ERC721' + tokenAddress: string + tokenId: string + tokenName: string | null + tokenSymbol: string | null + logoUri: string | null + decimals: number | null + value: string +} + +type NativeTransfer = { + type: 'ETHER' + value: string + tokenSymbol: string | null + decimals: number | null +} + +type TransferInfo = Erc20Transfer | Erc721Transfer | NativeTransfer + +type Transfer = { + type: 'Transfer' + sender: string + recipient: string + direction?: TransferDirection + transferInfo: TransferInfo // Polymorphic: Erc20, Erc721, Ether +} + +export enum Operation { + CALL, + DELEGATE, +} + +type InternalTransaction = { + operation: Operation + to: string + value: number | null + data: string | null + dataDecoded: DataDecoded | null +} + +type Parameter = { + name: string + type: string + value: string + valueDecoded: InternalTransaction[] | null +} + +type DataDecoded = { + method: string + parameters: Parameter[] | null +} + +type SetFallbackHandler = { + type: 'SET_FALLBACK_HANDLER' + handler: string +} + +type AddOwner = { + type: 'ADD_OWNER' + owner: string + threshold: number +} + +type RemoveOwner = { + type: 'REMOVE_OWNER' + owner: string + threshold: number +} + +type SwapOwner = { + type: 'SWAP_OWNER' + oldOwner: string + newOwner: string +} + +type ChangeThreshold = { + type: 'CHANGE_THRESHOLD' + threshold: number +} + +type ChangeImplementation = { + type: 'CHANGE_IMPLEMENTATION' + implementation: string +} + +type EnableModule = { + type: 'ENABLE_MODULE' + module: string +} + +type DisableModule = { + type: 'DISABLE_MODULE' + module: string +} + +type SettingsInfo = + | SetFallbackHandler + | AddOwner + | RemoveOwner + | SwapOwner + | ChangeThreshold + | ChangeImplementation + | EnableModule + | DisableModule + +type SettingsChange = { + type: 'SettingsChange' + dataDecoded: DataDecoded + settingsInfo: SettingsInfo | null +} + +type AddressInfo = { + name: string + logoUri: string | null +} + +type BaseCustom = { + type: 'Custom' + to: string + dataSize: string + value: string + isCancellation: boolean + toInfo: AddressInfo +} + +type Custom = BaseCustom & { + methodName: string | null +} + +type MultiSend = BaseCustom & { + methodName: 'multiSend' + actionCount: number +} + +type Creation = { + type: 'Creation' + creator: string + transactionHash: string + implementation: string | null + factory: string | null +} + +type TransactionStatus = + | 'AWAITING_CONFIRMATIONS' + | 'AWAITING_EXECUTION' + | 'CANCELLED' + | 'FAILED' + | 'SUCCESS' + | 'PENDING' + | 'PENDING_FAILED' + | 'WILL_BE_REPLACED' + +type TransactionInfo = Transfer | SettingsChange | Custom | MultiSend | Creation + +type ExecutionInfo = { + nonce: number + confirmationsRequired: number + confirmationsSubmitted: number + missingSigners?: string[] +} + +type SafeAppInfo = { + name: string + url: string + logoUrl: string +} + +type TransactionSummary = { + id: string + timestamp: number + txStatus: TransactionStatus + txInfo: TransactionInfo // Polymorphic: Transfer, SettingsChange, Custom, Creation + executionInfo: ExecutionInfo | null + safeAppInfo: SafeAppInfo | null +} + +type TransactionData = { + hexData: string | null + dataDecoded: DataDecoded | null + to: string + value: string | null + operation: Operation +} + +type ModuleExecutionDetails = { + type: 'MODULE' + address: string +} + +type MultiSigConfirmations = { + signer: string + signature: string | null +} + +type TokenType = 'ERC721' | 'ERC20' | 'ETHER' + +type TokenInfo = { + tokenType: TokenType + address: string + decimals: number | null + symbol: string + name: string + logoUri: string | null +} + +type MultiSigExecutionDetails = { + type: 'MULTISIG' + submittedAt: number + nonce: number + safeTxGas: number + baseGas: number + gasPrice: string + gasToken: string + refundReceiver: string + safeTxHash: string + executor: string | null + signers: string[] + confirmationsRequired: number + confirmations: MultiSigConfirmations[] + gasTokenInfo: TokenInfo | null +} + +type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails + +type ExpandedTxDetails = { + executedAt: number + txStatus: TransactionStatus + txInfo: TransactionInfo + txData: TransactionData | null + detailedExecutionInfo: DetailedExecutionInfo | null + txHash: string | null +} + +type Transaction = TransactionSummary & { + txDetails?: ExpandedTxDetails +} + +type StoreStructure = { + queued: { + next: { [nonce: number]: Transaction[] } // 1 Transaction element + queued: { [nonce: number]: Transaction[] } // n Transaction elements + } + history: { [timestamp: number]: Transaction[] } // n Transaction elements +} + +type TxQueuedLocation = 'queued.next' | 'queued.queued' + +type TxHistoryLocation = 'history' + +type TxLocation = TxHistoryLocation | TxQueuedLocation + +type Label = { + type: 'LABEL' + label: 'Next' | 'Queued' +} + +type DateLabel = { + type: 'DATE_LABEL' + timestamp: number +} + +type ConflictHeader = { + type: 'CONFLICT_HEADER' + nonce: number +} + +type TransactionGatewayResult = { + type: 'TRANSACTION' + transaction: TransactionSummary + conflictType: 'HasNext' | 'End' | 'None' +} + +type GatewayResponse = { + next: string | null + previous: string | null +} + +type HistoryGatewayResult = DateLabel | TransactionGatewayResult + +type HistoryGatewayResponse = GatewayResponse & { + results: HistoryGatewayResult[] +} + +type QueuedGatewayResult = Label | ConflictHeader | TransactionGatewayResult + +type QueuedGatewayResponse = GatewayResponse & { + results: QueuedGatewayResult[] +} + +export type TransactionDetails = { + count: number + transactions: Array<[nonce: string, transactions: Transaction[]]> +} + +/** + * Helper functions + */ + +export const isDateLabel = (value: HistoryGatewayResult): value is DateLabel => { + return value.type === 'DATE_LABEL' +} + +export const isLabel = (value: QueuedGatewayResult): value is Label => { + return value.type === 'LABEL' +} + +export const isConflictHeader = (value: QueuedGatewayResult): value is ConflictHeader => { + return value.type === 'CONFLICT_HEADER' +} + +export const isTransactionSummary = ( + value: HistoryGatewayResult | QueuedGatewayResult, +): value is TransactionGatewayResult => { + return value.type === 'TRANSACTION' +} + +export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => { + return value.type === 'Transfer' +} + +export const isSettingsChangeTxInfo = (value: TransactionInfo): value is SettingsChange => { + return value.type === 'SettingsChange' +} + +export const isCustomTxInfo = (value: TransactionInfo): value is Custom => { + return value.type === 'Custom' +} + +export const isMultiSendTxInfo = (value: TransactionInfo): value is MultiSend => { + return isCustomTxInfo(value) && value.methodName === 'multiSend' +} + +export const isCreationTxInfo = (value: TransactionInfo): value is Creation => { + return value.type === 'Creation' +} + +export const isStatusSuccess = (value: Transaction['txStatus']): value is 'SUCCESS' => { + return value === 'SUCCESS' +} + +export const isStatusFailed = (value: Transaction['txStatus']): value is 'FAILED' => { + return value === 'FAILED' +} + +export const isStatusCancelled = (value: Transaction['txStatus']): value is 'CANCELLED' => { + return value === 'CANCELLED' +} + +export const isStatusPending = (value: Transaction['txStatus']): value is 'PENDING' => { + return value === 'PENDING' +} + +export const isStatusAwaitingConfirmation = (value: Transaction['txStatus']): value is 'AWAITING_CONFIRMATIONS' => { + return value === 'AWAITING_CONFIRMATIONS' +} + +export const isStatusWillBeReplaced = (value: Transaction['txStatus']): value is 'WILL_BE_REPLACED' => { + return value === 'WILL_BE_REPLACED' +} + +export const isMultiSigExecutionDetails = ( + value: ExpandedTxDetails['detailedExecutionInfo'], +): value is MultiSigExecutionDetails => { + return value?.type === 'MULTISIG' +} + +export const isModuleExecutionDetails = ( + value: ExpandedTxDetails['detailedExecutionInfo'], +): value is ModuleExecutionDetails => { + return value?.type === 'MODULE' +} diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index d190a1e0..c69b6cf1 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -6,6 +6,7 @@ import { Confirmation } from './confirmation' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { DataDecoded, Transfer } from './transactions' import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' +import { BuildTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' export enum TransactionTypes { INCOMING = 'incoming', @@ -89,7 +90,11 @@ export type TransactionProps = { value: string } -export type Transaction = RecordOf +export type Transaction = RecordOf & Readonly + +export const isStoredTransaction = (tx: BuildTx['tx']): tx is Transaction => { + return typeof (tx as Transaction).recipient !== 'undefined' +} export type TxArgs = { baseGas: number diff --git a/src/logic/safe/store/reducer/cancellationTransactions.ts b/src/logic/safe/store/reducer/cancellationTransactions.ts index 1659376e..52258daa 100644 --- a/src/logic/safe/store/reducer/cancellationTransactions.ts +++ b/src/logic/safe/store/reducer/cancellationTransactions.ts @@ -1,18 +1,25 @@ import { Map } from 'immutable' -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions' import { REMOVE_CANCELLATION_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction' +import { AppReduxState } from 'src/store' export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions' export type CancellationTransactions = Map export type CancellationTxState = Map -export default handleActions( +type CancellationTransactionsPayload = { safeAddress: string; transactions: CancellationTransactions } +type CancellationTransactionPayload = { safeAddress: string; transaction: Transaction } + +export default handleActions< + AppReduxState['cancellationTransactions'], + CancellationTransactionsPayload | CancellationTransactionPayload +>( { - [ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => { + [ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action: Action) => { const { safeAddress, transactions } = action.payload if (!safeAddress || !transactions || !transactions.size) { @@ -41,7 +48,7 @@ export default handleActions( } }) }, - [REMOVE_CANCELLATION_TRANSACTION]: (state, action) => { + [REMOVE_CANCELLATION_TRANSACTION]: (state, action: Action) => { const { safeAddress, transaction } = action.payload if (!safeAddress || !transaction) { diff --git a/src/logic/safe/store/reducer/gatewayTransactions.ts b/src/logic/safe/store/reducer/gatewayTransactions.ts new file mode 100644 index 00000000..8037fa90 --- /dev/null +++ b/src/logic/safe/store/reducer/gatewayTransactions.ts @@ -0,0 +1,368 @@ +import get from 'lodash.get' +import merge from 'lodash.merge' +import { Action, handleActions } from 'redux-actions' + +import { + ADD_HISTORY_TRANSACTIONS, + ADD_QUEUED_TRANSACTIONS, +} from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import { UPDATE_TRANSACTION_STATUS } from 'src/logic/safe/store/actions/updateTransactionStatus' +import { + HistoryGatewayResponse, + isConflictHeader, + isDateLabel, + isLabel, + isTransactionSummary, + QueuedGatewayResponse, + StoreStructure, + Transaction, + TransactionStatus, + TxLocation, +} from 'src/logic/safe/store/models/types/gateway.d' +import { UPDATE_TRANSACTION_DETAILS } from 'src/logic/safe/store/actions/fetchTransactionDetails' + +import { AppReduxState } from 'src/store' +import { getUTCStartOfDate } from 'src/utils/date' +import { sameString } from 'src/utils/strings' +import { sortObject } from 'src/utils/objects' + +export const GATEWAY_TRANSACTIONS_ID = 'gatewayTransactions' + +type BasePayload = { safeAddress: string; isTail?: boolean } +export type HistoryPayload = BasePayload & { values: HistoryGatewayResponse['results'] } +export type QueuedPayload = BasePayload & { values: QueuedGatewayResponse['results'] } +export type TransactionDetailsPayload = { + safeAddress: string + txLocation: TxLocation + transactionId: string + value: Transaction['txDetails'] +} +export type TransactionStatusPayload = { + safeAddress: string + nonce: number + id?: string + txStatus: TransactionStatus +} + +type Payload = HistoryPayload | QueuedPayload | TransactionDetailsPayload | TransactionStatusPayload + +const findTransactionLocation = ( + transactionsGroup: { [p: number]: Transaction[] }, + transactionId: string, +): { key: string; index: number } => { + let key + let index + let transactions + + for ([key, transactions] of Object.entries(transactionsGroup)) { + index = transactions.findIndex(({ id }) => sameString(id, transactionId)) + + if (index !== -1) { + break + } + } + + return { key, index } +} + +export const gatewayTransactions = handleActions( + { + [ADD_HISTORY_TRANSACTIONS]: (state, action: Action) => { + const { safeAddress, values, isTail = false } = action.payload + const history: StoreStructure['history'] = Object.assign({}, state[safeAddress]?.history) + + values.forEach((value) => { + if (isDateLabel(value)) { + // DATE_LABEL is discarded as it's not needed for the current implementation + return + } + + if (isTransactionSummary(value)) { + const startOfDate = getUTCStartOfDate(value.transaction.timestamp) + + if (typeof history[startOfDate] === 'undefined') { + history[startOfDate] = [] + } + + const txExist = history[startOfDate].some(({ id }) => sameString(id, value.transaction.id)) + + if (!txExist) { + history[startOfDate].push(value.transaction) + // pushing a newer transaction to the existing list messes the transactions order + // this happens when most recent transactions are added to the existing txs in the store + history[startOfDate] = history[startOfDate].sort((a, b) => b.timestamp - a.timestamp) + } + return + } + }) + + return { + // all the safes with their respective states + ...state, + // current safe + [safeAddress]: { + // keep queued list + ...state[safeAddress], + // extend history list + history: isTail ? history : sortObject(history, 'desc'), + }, + } + }, + [ADD_QUEUED_TRANSACTIONS]: (state, action: Action) => { + // we're assuming that `next` and `queued` labels will be provided in the first page + // as for usage experience there were no more than 5 transactions competing for the same nonce. + // Thus, given the client-gateway page size of 20, we have plenty of "room" to be provided with + // `next` and `queued` transactions in the first page. + const { safeAddress, values } = action.payload + let next = Object.assign({}, state[safeAddress]?.queued?.next) + const queued = Object.assign({}, state[safeAddress]?.queued?.queued) + + let label: 'next' | 'queued' | undefined + values.forEach((value) => { + if (isLabel(value)) { + // we're assuming that the first page will always provide `next` and `queued` labels + label = value.label.toLowerCase() as 'next' | 'queued' + return + } + + if (isConflictHeader(value)) { + // conflict header is discarded as it's not needed for the current implementation + return + } + + if (isTransactionSummary(value)) { + const txNonce = value.transaction.executionInfo?.nonce + + if (typeof txNonce === 'undefined') { + console.warn('A transaction without nonce was provided by client-gateway:', JSON.stringify(value)) + return + } + + if (typeof label === 'undefined') { + label = next[txNonce] ? 'next' : 'queued' + } + + switch (label) { + case 'next': { + if (next[txNonce]) { + const txIndex = next[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id)) + + if (txIndex !== -1) { + const storedTransaction = next[txNonce][txIndex] + const updateFromService = + storedTransaction.executionInfo?.confirmationsSubmitted !== + value.transaction.executionInfo?.confirmationsSubmitted + + if (storedTransaction.txStatus === 'PENDING' && !updateFromService) { + // we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status + value.transaction.txStatus = 'PENDING' + } + + next[txNonce][txIndex] = updateFromService + ? // by replacing the current transaction with the one returned by the service + // we remove the `txDetails`, so this will force a re-request of the data + value.transaction + : // we merge, to keep the current unchanged information + merge(storedTransaction, value.transaction) + break + } + + // we add the transaction returned by the service to the list of transactions + next[txNonce] = [...next[txNonce], value.transaction] + break + } + + // a new tx has arrived to the `next` queue + // we re-create the `next` object with the new transaction + next = { [txNonce]: [value.transaction] } + + // we remove the new `next` transaction from the `queue` list, if it exist + queued[txNonce] && delete queued[txNonce] + + break + } + case 'queued': { + if (queued[txNonce]) { + const txIndex = queued[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id)) + + if (txIndex !== -1) { + const storedTransaction = queued[txNonce][txIndex] + const updateFromService = + storedTransaction.executionInfo?.confirmationsSubmitted !== + value.transaction.executionInfo?.confirmationsSubmitted + + if (storedTransaction.txStatus === 'PENDING' && !updateFromService) { + // we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status + value.transaction.txStatus = 'PENDING' + } + + queued[txNonce][txIndex] = updateFromService + ? // by replacing the current transaction with the one returned by the service + // we remove the `txDetails`, so this will force a re-request of the data + value.transaction + : // we merge, to keep the current unchanged information + merge(storedTransaction, value.transaction) + break + } + + // we add the transaction returned by the service to the list of transactions + queued[txNonce] = [...queued[txNonce], value.transaction] + break + } + + queued[txNonce] = [value.transaction] + break + } + } + return + } + }) + + // no new transactions + if (!values.length) { + // queued list already empty + if (!Object.keys(queued).length) { + // there was an existing next transaction + if (Object.keys(next).length === 1) { + // we cleanup the next queue + next = {} + } + } + } + + return { + // all the safes with their respective states + ...state, + // current safe + [safeAddress]: { + // keep history list + ...state[safeAddress], + // overwrites queued lists + queued: { + next, + queued, + }, + }, + } + }, + [UPDATE_TRANSACTION_DETAILS]: (state, action: Action) => { + const { safeAddress, transactionId, txLocation, value } = action.payload + const storedTransactions = Object.assign({}, state[safeAddress]) + const { queued } = storedTransactions + let { history } = storedTransactions + + // get the tx group (it will be `queued.next`, `queued.queued` or `history`) + const txGroup: StoreStructure['queued']['next' | 'queued'] | StoreStructure['history'] = get( + storedTransactions, + txLocation, + ) + + // find the transaction location + const { key, index } = findTransactionLocation(txGroup, transactionId) + // add details to tx object + txGroup[key][index]['txDetails'] = value + + // replace the updated group in its corresponding location + switch (txLocation) { + case 'history': + history = txGroup + break + case 'queued.next': + queued['next'] = txGroup + break + case 'queued.queued': + queued['queued'] = txGroup + break + } + + // update state + return { + // all the safes with their respective states + ...state, + // current safe + [safeAddress]: { + history, + queued, + }, + } + }, + [UPDATE_TRANSACTION_STATUS]: (state, action: Action) => { + // if we provide the tx ID that sole tx will have the _pending_ status. + // if not, all the txs that share the same nonce will have the _pending_ status. + const { nonce, id, safeAddress, txStatus } = action.payload + const storedTransactions = Object.assign({}, state[safeAddress]) + const { queued } = storedTransactions + const { history } = storedTransactions + + let txLocation: TxLocation | undefined + let historyLocation: string | undefined + + if (queued.next[nonce]) { + txLocation = 'queued.next' + } else if (queued.queued[nonce]) { + txLocation = 'queued.queued' + } else { + Object.entries(history).forEach(([timestamp, transactions]) => { + const txIndex = transactions.findIndex((transaction) => Number(transaction.executionInfo?.nonce) === nonce) + + if (txIndex !== -1) { + txLocation = 'history' + historyLocation = `${timestamp}[${txIndex}]` + } + }) + } + + if (!txLocation) { + return state + } + + switch (txLocation) { + case 'history': { + if (historyLocation) { + const txToUpdate = get(history, historyLocation) + txToUpdate.txStatus = txStatus + } + break + } + case 'queued.next': { + queued.next[nonce] = queued.next[nonce].map((txToUpdate) => { + if (typeof id !== 'undefined') { + if (sameString(txToUpdate.id, id)) { + txToUpdate.txStatus = txStatus + } + } else { + txToUpdate.txStatus = txStatus + } + return txToUpdate + }) + break + } + case 'queued.queued': { + queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => { + if (typeof id !== 'undefined') { + if (sameString(txToUpdate.id, id)) { + txToUpdate.txStatus = txStatus + } + } else { + txToUpdate.txStatus = txStatus + } + return txToUpdate + }) + break + } + } + + // update state + return { + // all the safes with their respective states + ...state, + // current safe + [safeAddress]: { + history, + queued, + }, + } + }, + }, + {}, +) diff --git a/src/logic/safe/store/reducer/incomingTransactions.ts b/src/logic/safe/store/reducer/incomingTransactions.ts index bac3f139..a5273385 100644 --- a/src/logic/safe/store/reducer/incomingTransactions.ts +++ b/src/logic/safe/store/reducer/incomingTransactions.ts @@ -2,10 +2,11 @@ import { Map } from 'immutable' import { handleActions } from 'redux-actions' import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions' +import { AppReduxState } from 'src/store' export const INCOMING_TRANSACTIONS_REDUCER_ID = 'incomingTransactions' -export default handleActions( +export default handleActions( { [ADD_INCOMING_TRANSACTIONS]: (state, action) => action.payload, }, diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index b5997f68..d327e560 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -1,5 +1,5 @@ import { Map, Set, List } from 'immutable' -import { handleActions } from 'redux-actions' +import { Action, handleActions } from 'redux-actions' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' @@ -13,9 +13,9 @@ import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' import { makeOwner } from 'src/logic/safe/store/models/owner' -import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe' +import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' -import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated' @@ -73,9 +73,22 @@ const updateSafeProps = (prevSafe, safe) => { }) } -export default handleActions( +export type SafePayload = { safe: SafeRecord } +type SafePayloads = SafeRecord | SafePayload | string + +type BaseOwnerPayload = { safeAddress: string; ownerAddress: string } +type FullOwnerPayload = BaseOwnerPayload & { ownerName: string } +type ReplaceOwnerPayload = FullOwnerPayload & { oldOwnerAddress: string } + +type OwnerPayloads = BaseOwnerPayload | FullOwnerPayload | ReplaceOwnerPayload + +type SafeWithAddressPayload = SafeRecord & { safeAddress: string } + +type Payloads = SafePayloads | OwnerPayloads | SafeWithAddressPayload + +export default handleActions( { - [UPDATE_SAFE]: (state: SafeReducerMap, action) => { + [UPDATE_SAFE]: (state, action: Action) => { const safe = action.payload const safeAddress = safe.address @@ -89,7 +102,7 @@ export default handleActions( ) : state }, - [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => { + [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action) => { const tokenAddress = action.payload return state.withMutations((map) => { @@ -104,8 +117,7 @@ export default handleActions( }) }) }, - - [ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => { + [ADD_OR_UPDATE_SAFE]: (state, action: Action) => { const { safe } = action.payload const safeAddress = safe.address @@ -123,7 +135,7 @@ export default handleActions( ) : state }, - [REMOVE_SAFE]: (state: SafeReducerMap, action) => { + [REMOVE_SAFE]: (state, action: Action) => { const safeAddress = action.payload const currentDefaultSafe = state.get('defaultSafe') @@ -135,7 +147,7 @@ export default handleActions( return newState }, - [ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => { + [ADD_SAFE_OWNER]: (state, action: Action) => { const { ownerAddress, ownerName, safeAddress } = action.payload const addressFound = state @@ -152,7 +164,7 @@ export default handleActions( }), ) }, - [REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => { + [REMOVE_SAFE_OWNER]: (state, action: Action) => { const { ownerAddress, safeAddress } = action.payload return state.updateIn(['safes', safeAddress], (prevSafe) => @@ -161,7 +173,7 @@ export default handleActions( }), ) }, - [REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => { + [REPLACE_SAFE_OWNER]: (state, action: Action) => { const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload return state.updateIn(['safes', safeAddress], (prevSafe) => @@ -172,7 +184,7 @@ export default handleActions( }), ) }, - [EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => { + [EDIT_SAFE_OWNER]: (state, action: Action) => { const { ownerAddress, ownerName, safeAddress } = action.payload return state.updateIn(['safes', safeAddress], (prevSafe) => { @@ -183,7 +195,7 @@ export default handleActions( return prevSafe.merge({ owners: updatedOwners }) }) }, - [UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => { + [UPDATE_TOKENS_LIST]: (state, action: Action) => { // Only activeTokens or blackListedTokens is required const { safeAddress, activeTokens, blacklistedTokens } = action.payload @@ -192,7 +204,7 @@ export default handleActions( return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) }, - [UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => { + [UPDATE_ASSETS_LIST]: (state, action: Action) => { // Only activeAssets or blackListedAssets is required const { safeAddress, activeAssets, blacklistedAssets } = action.payload @@ -201,13 +213,13 @@ export default handleActions( return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) }, - [SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload), - [SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) => + [SET_DEFAULT_SAFE]: (state, action: Action) => state.set('defaultSafe', action.payload), + [SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action) => state.set('latestMasterContractVersion', action.payload), }, Map({ defaultSafe: DEFAULT_SAFE_INITIAL_STATE, safes: Map(), latestMasterContractVersion: '', - }), + }) as AppReduxState['safes'], ) diff --git a/src/logic/safe/store/reducer/transactions.ts b/src/logic/safe/store/reducer/transactions.ts index 3ce4df74..fe19bb18 100644 --- a/src/logic/safe/store/reducer/transactions.ts +++ b/src/logic/safe/store/reducer/transactions.ts @@ -1,14 +1,22 @@ -import { Map } from 'immutable' -import { handleActions } from 'redux-actions' +import { List, Map } from 'immutable' +import { Action, handleActions } from 'redux-actions' import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions' import { REMOVE_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeTransaction' +import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { AppReduxState } from 'src/store' export const TRANSACTIONS_REDUCER_ID = 'transactions' -export default handleActions( +type TransactionBasePayload = { safeAddress: string } +type TransactionsPayload = TransactionBasePayload & { transactions: List } +type TransactionPayload = TransactionBasePayload & { transaction: Transaction } + +type Payload = TransactionsPayload | TransactionPayload + +export default handleActions( { - [ADD_OR_UPDATE_TRANSACTIONS]: (state, action) => { + [ADD_OR_UPDATE_TRANSACTIONS]: (state, action: Action) => { const { safeAddress, transactions } = action.payload if (!safeAddress || !transactions || !transactions.size) { @@ -46,7 +54,7 @@ export default handleActions( } }) }, - [REMOVE_TRANSACTION]: (state, action) => { + [REMOVE_TRANSACTION]: (state, action: Action) => { const { safeAddress, transaction } = action.payload if (!safeAddress || !transaction) { diff --git a/src/logic/safe/store/selectors/gatewayTransactions.ts b/src/logic/safe/store/selectors/gatewayTransactions.ts new file mode 100644 index 00000000..d994f7ff --- /dev/null +++ b/src/logic/safe/store/selectors/gatewayTransactions.ts @@ -0,0 +1,98 @@ +import get from 'lodash.get' +import { createSelector } from 'reselect' + +import { StoreStructure, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d' +import { GATEWAY_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/gatewayTransactions' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { createHashBasedSelector } from 'src/logic/safe/store/selectors/utils' +import { AppReduxState } from 'src/store' + +export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewayTransactions'] => { + return state[GATEWAY_TRANSACTIONS_ID] +} + +export const historyTransactions = createSelector( + gatewayTransactions, + safeParamAddressFromStateSelector, + (gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => { + return gatewayTransactions[safeAddress]?.history + }, +) + +export const pendingTransactions = createSelector( + gatewayTransactions, + safeParamAddressFromStateSelector, + (gatewayTransactions, safeAddress): StoreStructure['queued'] | undefined => { + return gatewayTransactions[safeAddress]?.queued + }, +) + +export const nextTransactions = createSelector(pendingTransactions, (pendingTransactions): + | StoreStructure['queued']['next'] + | undefined => { + return pendingTransactions?.next +}) + +export const queuedTransactions = createSelector(pendingTransactions, (pendingTransactions): + | StoreStructure['queued']['queued'] + | undefined => { + return pendingTransactions?.queued +}) + +type TxByLocationAttr = { attributeName: string; attributeValue: string | number; txLocation: TxLocation } + +type TxByLocation = { + attributeName: string + attributeValue: string | number + transactions: StoreStructure['history'] | StoreStructure['queued']['queued' | 'next'] +} + +const getTransactionsByLocation = createHashBasedSelector( + gatewayTransactions, + safeParamAddressFromStateSelector, + (gatewayTransactions, safeAddress) => (rest: TxByLocationAttr): TxByLocation => ({ + attributeName: rest.attributeName, + attributeValue: rest.attributeValue, + transactions: get(gatewayTransactions[safeAddress], rest.txLocation), + }), +) + +export const getTransactionByAttribute = createSelector( + getTransactionsByLocation, + (fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction | undefined => { + const { attributeName, attributeValue, transactions } = fn(rest) + + if (transactions && attributeValue) { + for (const [, txs] of Object.entries(transactions)) { + const foundTx = txs.find((transaction) => transaction[attributeName] === attributeValue) + + if (foundTx) { + return foundTx + } + } + } + }, +) + +export const getTransactionDetails = createSelector( + getTransactionByAttribute, + (fn: (rest: TxByLocationAttr) => Transaction | undefined) => ( + rest: TxByLocationAttr, + ): Transaction['txDetails'] | undefined => { + const transaction = fn(rest) + return transaction?.txDetails + }, +) + +export const getQueuedTransactionsByNonce = createSelector( + getTransactionsByLocation, + (fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction[] => { + const { attributeValue, attributeName, transactions } = fn(rest) + + if (attributeName === 'nonce') { + return transactions?.[attributeValue] ?? [] + } + + return [] + }, +) diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index a526925d..95c2ec12 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -93,7 +93,7 @@ export const safeCancellationTransactionsSelector = createSelector( return Map() } - return cancellationTransactions.get(address, Map({})) + return cancellationTransactions.get(address, Map()) }, ) @@ -109,7 +109,7 @@ export const safeIncomingTransactionsSelector = createSelector( return List([]) } - return incomingTransactions.get(address, List([])) + return incomingTransactions.get(address, List()) }, ) diff --git a/src/logic/safe/store/selectors/transactions.ts b/src/logic/safe/store/selectors/transactions.ts index 323d7be8..7fc6e3eb 100644 --- a/src/logic/safe/store/selectors/transactions.ts +++ b/src/logic/safe/store/selectors/transactions.ts @@ -1,14 +1,18 @@ -import { List } from 'immutable' -import { createSelector } from 'reselect' +// import { List } from 'immutable' +// import { createSelector } from 'reselect' +// +// import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors' +// import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' +// import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector' -import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors' -import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' -import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector' +// export const extendedTransactionsSelector = createSelector( +// safeTransactionsSelector, +// safeIncomingTransactionsSelector, +// safeModuleTransactionsSelector, +// (transactions, incomingTransactions, moduleTransactions): List => +// List().withMutations((list) => { +// list.concat(transactions).concat(incomingTransactions).concat(moduleTransactions) +// }), +// ) -export const extendedTransactionsSelector = createSelector( - safeTransactionsSelector, - safeIncomingTransactionsSelector, - safeModuleTransactionsSelector, - (transactions, incomingTransactions, moduleTransactions): List => - List([...transactions, ...incomingTransactions, ...moduleTransactions]), -) +export {} diff --git a/src/logic/safe/store/selectors/utils.ts b/src/logic/safe/store/selectors/utils.ts new file mode 100644 index 00000000..b636cb94 --- /dev/null +++ b/src/logic/safe/store/selectors/utils.ts @@ -0,0 +1,13 @@ +import hash from 'object-hash' +import isEqual from 'lodash.isequal' +import memoize from 'lodash.memoize' +import { createSelectorCreator, defaultMemoize } from 'reselect' + +import { AppReduxState } from 'src/store' + +export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual) + +const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string => + hash(gatewayTransactions[safeAddress]) + +export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn) diff --git a/src/logic/safe/transactions/awaitingTransactions.ts b/src/logic/safe/transactions/awaitingTransactions.ts index 85ea6568..7b1345de 100644 --- a/src/logic/safe/transactions/awaitingTransactions.ts +++ b/src/logic/safe/transactions/awaitingTransactions.ts @@ -1,18 +1,22 @@ import { List } from 'immutable' import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' +import { isStatusAwaitingConfirmation } from 'src/logic/safe/store/models/types/gateway.d' import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { Transaction as GatewayTransaction } from 'src/logic/safe/store/models/types/gateway' +import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions' export const getAwaitingTransactions = ( allTransactions: List, - cancellationTxs, + cancellationTxs: CancellationTransactions, userAccount: string, ): List => { return allTransactions.filter((tx) => { const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null // The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations - if (!tx.executionTxHash && !tx.cancelled && !isPendingTransaction(tx, cancelTx)) { + if (!tx.executionTxHash && !tx.cancelled && cancelTx && !isPendingTransaction(tx, cancelTx)) { // Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction const transactionWaitingUser = tx.confirmations.filter(({ owner }) => owner !== userAccount) return transactionWaitingUser.size > 0 @@ -21,3 +25,18 @@ export const getAwaitingTransactions = ( return false }) } + +export const getAwaitingGatewayTransactions = ( + allTransactions: GatewayTransaction[], + userAccount: string, +): GatewayTransaction[] => { + return allTransactions.filter((tx) => { + // The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations + if (isStatusAwaitingConfirmation(tx.txStatus)) { + // Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction + return addressInList(tx.executionInfo?.missingSigners)(userAccount) + } + + return false + }) +} diff --git a/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts b/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts index 9247fd48..d8adaa4c 100644 --- a/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts +++ b/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts @@ -1,4 +1,4 @@ -import { Map } from 'immutable' +import isEqual from 'lodash.isequal' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' @@ -6,9 +6,9 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe' const isStateSubset = (superObj, subObj) => { return Object.keys(subObj).every((key) => { if (subObj[key] && typeof subObj[key] == 'object') { - if (Map.isMap(subObj[key]) || subObj[key].size >= 0) { + if (typeof subObj[key] === 'object' || subObj[key].length >= 0) { // If type is Immutable Map, List or Object we use Immutable equals - return superObj[key].equals(subObj[key]) + return isEqual(superObj[key], subObj[key]) } return isStateSubset(superObj[key], subObj[key]) } diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 6664e5db..826cc265 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -15,21 +15,8 @@ import { TokenState } from 'src/logic/tokens/store/reducer/tokens' import updateSafe from 'src/logic/safe/store/actions/updateSafe' import { AppReduxState } from 'src/store' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' -import { SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { - safeActiveTokensSelector, - safeBalancesSelector, - safeBlacklistedTokensSelector, - safeEthBalanceSelector, - safeSelector, -} from 'src/logic/safe/store/selectors' +import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors' -import { currencyValuesSelector } from 'src/logic/currencyValues/store/selectors' - -const noFunc = (): void => {} - -const updateSafeValue = (address: string) => (valueToUpdate: Partial) => - updateSafe({ address, ...valueToUpdate }) interface ExtractedData { balances: Map @@ -78,11 +65,8 @@ const fetchSafeTokens = (safeAddress: string) => async ( } const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress)) - const currentEthBalance = safeEthBalanceSelector(state) - const safeBalances = safeBalancesSelector(state) const alreadyActiveTokens = safeActiveTokensSelector(state) const blacklistedTokens = safeBlacklistedTokensSelector(state) - const currencyValues = currencyValuesSelector(state) const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.reduce( extractDataFromResult(currentTokens), @@ -100,24 +84,10 @@ const fetchSafeTokens = (safeAddress: string) => async ( balances.keySeq().toSet().subtract(blacklistedTokens), ) - const update = updateSafeValue(safeAddress) - const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens }) - const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances }) - const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance }) - const storedCurrencyBalances = currencyValues?.get(safeAddress)?.get('currencyBalances') - - const updateCurrencies = currencyList.equals(storedCurrencyBalances) - ? noFunc - : setCurrencyBalances(safeAddress, currencyList) - - const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens) - batch(() => { - dispatch(updateActiveTokens) - dispatch(updateBalances) - dispatch(updateEthBalance) - dispatch(updateCurrencies) - dispatch(updateTokens) + dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance })) + dispatch(setCurrencyBalances(safeAddress, currencyList)) + dispatch(addTokens(tokens)) }) } catch (err) { console.error('Error fetching active token list', err) diff --git a/src/logic/tokens/store/reducer/tokens.ts b/src/logic/tokens/store/reducer/tokens.ts index 39a8c4f1..c567e630 100644 --- a/src/logic/tokens/store/reducer/tokens.ts +++ b/src/logic/tokens/store/reducer/tokens.ts @@ -1,17 +1,22 @@ -import { Map } from 'immutable' -import { handleActions } from 'redux-actions' +import { List, Map } from 'immutable' +import { Action, handleActions } from 'redux-actions' import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken' import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens' import { makeToken, Token } from 'src/logic/tokens/store/model/token' +import { AppReduxState } from 'src/store' export const TOKEN_REDUCER_ID = 'tokens' export type TokenState = Map -export default handleActions( +type TokensPayload = { tokens: List } +type TokenPayload = { token: Token } +type Payloads = TokensPayload | TokenPayload + +export default handleActions( { - [ADD_TOKENS]: (state: TokenState, action) => { + [ADD_TOKENS]: (state: TokenState, action: Action) => { const { tokens } = action.payload return state.withMutations((map) => { @@ -20,7 +25,7 @@ export default handleActions( }) }) }, - [ADD_TOKEN]: (state: TokenState, action) => { + [ADD_TOKEN]: (state: TokenState, action: Action) => { const { token } = action.payload const { address: tokenAddress } = token diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index b1361467..7fbdcd0a 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -8,8 +8,7 @@ import { isSendERC721Transaction } from 'src/logic/collectibles/utils' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' -import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' -import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { BuildTx, isEmptyData, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' import { CALL } from 'src/logic/safe/transactions' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -35,7 +34,7 @@ export const isAddressAToken = async (tokenAddress: string): Promise => return call !== '0x' } -export const isTokenTransfer = (tx: TxServiceModel): boolean => { +export const isTokenTransfer = (tx: BuildTx['tx']): boolean => { return ( !isEmptyData(tx.data) && // Check if contains 'transfer' method code @@ -70,11 +69,11 @@ export const getERC20DecimalsAndSymbol = async ( return tokenInfo } -export const isSendERC20Transaction = async (tx: TxServiceModel): Promise => { +export const isSendERC20Transaction = async (tx: BuildTx['tx']): Promise => { let isSendTokenTx = !isSendERC721Transaction(tx) && isTokenTransfer(tx) if (isSendTokenTx) { - const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to) + const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to) // some contracts may implement the same methods as in ERC20 standard // we may falsely treat them as tokens, so in case we get any errors when getting token info diff --git a/src/logic/wallets/store/actions/fetchProvider.ts b/src/logic/wallets/store/actions/fetchProvider.ts index def1045f..e6e40153 100644 --- a/src/logic/wallets/store/actions/fetchProvider.ts +++ b/src/logic/wallets/store/actions/fetchProvider.ts @@ -44,7 +44,6 @@ const handleProviderNotification = (provider, dispatch) => { action: 'Connect a wallet', label: provider.name, }) - dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WALLET_CONNECTED_MSG))) } else { dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG))) } diff --git a/src/logic/wallets/store/actions/removeProvider.ts b/src/logic/wallets/store/actions/removeProvider.ts index be7396b7..6a95c43a 100644 --- a/src/logic/wallets/store/actions/removeProvider.ts +++ b/src/logic/wallets/store/actions/removeProvider.ts @@ -2,8 +2,6 @@ import { Dispatch } from 'src/logic/safe/store/actions/types.d' import { createAction } from 'redux-actions' import { onboard } from 'src/components/ConnectButton' -import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' -import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { resetWeb3 } from 'src/logic/wallets/getWeb3' export const REMOVE_PROVIDER = 'REMOVE_PROVIDER' @@ -15,9 +13,4 @@ export default () => (dispatch: Dispatch): void => { resetWeb3() dispatch(removeProvider()) - dispatch( - enqueueSnackbar( - enhanceSnackbarForAction(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, NOTIFICATIONS.WALLET_DISCONNECTED_MSG.key), - ), - ) } diff --git a/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx b/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx index 0e3889e1..c9a3a386 100644 --- a/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx +++ b/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { xs } from 'src/theme/variables' +import { grantedSelector } from 'src/routes/safe/container/selector' const useStyles = makeStyles( createStyles({ @@ -48,6 +49,7 @@ export const EllipsisTransactionDetails = ({ const dispatch = useDispatch() const currentSafeAddress = useSelector(safeParamAddressFromStateSelector) + const isOwnerConnected = useSelector(grantedSelector) const handleClick = (event) => setAnchorEl(event.currentTarget) @@ -65,7 +67,7 @@ export const EllipsisTransactionDetails = ({

{sendModalOpenHandler ? [ - + Send Again , , diff --git a/src/routes/safe/components/AllTransactions/index.tsx b/src/routes/safe/components/AllTransactions/index.tsx deleted file mode 100644 index ba208e21..00000000 --- a/src/routes/safe/components/AllTransactions/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { useTransactions } from 'src/routes/safe/container/hooks/useTransactions' -import { ButtonLink, Loader } from '@gnosis.pm/safe-react-components' -import { Transaction } from 'src/logic/safe/store/models/types/transactions.d' - -const Transactions = (): React.ReactElement => { - const [currentPage, setCurrentPage] = useState(0) - const [limit] = useState(50) - const [offset, setOffset] = useState(0) - const [maxPages, setMaxPages] = useState(0) - const { transactions, totalTransactionsCount } = useTransactions({ offset, limit }) - const [transactionsByPage, setTransactionsByPage] = useState(transactions) - - useEffect(() => { - const currentPage = Math.floor(offset / limit) + 1 - const maxPages = Math.ceil(totalTransactionsCount / limit) - setCurrentPage(currentPage) - setMaxPages(maxPages) - const newTransactionsByPage = transactions ? transactions.slice(offset, offset * 2 || limit) : [] - setTransactionsByPage(newTransactionsByPage) - }, [offset, limit, totalTransactionsCount, transactions]) - - // TODO: Remove this once we implement infinite scroll - const nextPageButtonHandler = () => { - setOffset(offset + limit) - } - - const previousPageButtonHandler = () => { - setOffset(offset > 0 ? offset - limit : offset) - } - - if (!transactionsByPage) return
No txs available for safe
- - if (!transactionsByPage.length) return - - return ( - <> - {transactionsByPage.map((tx: Transaction, index) => { - let txHash = '' - if ('transactionHash' in tx) { - txHash = tx.transactionHash as string - } - if ('txHash' in tx) { - txHash = tx.txHash - } - return
Tx hash: {txHash}
- })} - - Previous Page - - = maxPages}> - Next Page - - - ) -} - -export default Transactions diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 5a8bfe9a..5d28bfd0 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -16,7 +16,7 @@ import Img from 'src/components/layout/Img' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' 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 { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions' import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index 0f544e04..36691422 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -14,7 +14,7 @@ const { nativeCoin } = getNetworkInfo() const StyledBlock = styled(Block)` font-size: 12px; line-height: 1.08; - letter-spacing: -0.5; + letter-spacing: -0.5px; background-color: ${border}; width: fit-content; padding: 5px 10px; diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 84876785..68a4e5ea 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -4,6 +4,7 @@ import cn from 'classnames' import React, { Suspense, useEffect, useState } from 'react' import Modal from 'src/components/Modal' +import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway' import { CollectibleTx } from './screens/ReviewCollectible' import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx' import { ContractInteractionTx } from './screens/ContractInteraction' @@ -62,7 +63,7 @@ type Props = { isOpen: boolean onClose: () => void recipientAddress?: string - selectedToken?: string | NFTToken + selectedToken?: string | NFTToken | Erc721Transfer tokenAmount?: string } 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 68bb3bab..2fe1da17 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 @@ -18,7 +18,7 @@ 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 { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' 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 177d577a..ed3f3d2f 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 @@ -16,7 +16,7 @@ import Hairline from 'src/components/layout/Hairline' import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' 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 9db0f811..b11f3e87 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -14,7 +14,7 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { nftTokensSelector } from 'src/logic/collectibles/store/selectors' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index 860b1d81..e6462391 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -16,7 +16,7 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens' diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx index fd1586c6..f8f61d5f 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -19,6 +19,7 @@ import WhenFieldChanges from 'src/components/WhenFieldChanges' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors' +import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d' @@ -52,7 +53,7 @@ type SendCollectibleProps = { onClose: () => void onNext: (txInfo: SendCollectibleTxInfo) => void recipientAddress?: string - selectedToken?: NFTToken + selectedToken?: NFTToken | Erc721Transfer } export type SendCollectibleTxInfo = { @@ -243,7 +244,12 @@ const SendCollectible = ({ - + diff --git a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx index 1dd44b44..5421b50a 100644 --- a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx +++ b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx @@ -18,7 +18,7 @@ import Row from 'src/components/layout/Row' import Modal from 'src/components/Modal' import { getExplorerInfo } from 'src/config' import { getDisableModuleTxData } from 'src/logic/safe/utils/modules' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { ModulePair } from 'src/logic/safe/store/models/safe' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 37a3427c..f88db617 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -7,7 +7,7 @@ import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions 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 { 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' diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 6a5e8f86..a86ab0b4 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -9,7 +9,7 @@ import ThresholdForm from './screens/ThresholdForm' import Modal from 'src/components/Modal' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +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' diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index a1d6c7db..7c812f90 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -6,7 +6,7 @@ import Modal from 'src/components/Modal' import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import replaceSafeOwner from 'src/logic/safe/store/actions/replaceSafeOwner' import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx index 71c342d4..62c87c37 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx @@ -9,7 +9,7 @@ interface GenericInfoProps { const DataDisplay = ({ title, children }: GenericInfoProps): ReactElement => ( <> {title && ( - + {title} )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index fbe83334..0eced1a2 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -6,7 +6,7 @@ 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, { CreateTransactionArgs } 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, diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 09e570cf..0c5a5090 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { getDeleteAllowanceTxData } from 'src/logic/safe/utils/spendingLimits' diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index 1d498058..39cd0a36 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -12,7 +12,7 @@ import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { grantedSelector } from 'src/routes/safe/container/selector' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeOwnersSelector, safeParamAddressFromStateSelector, diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx index eb315b3d..aea5f0ce 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx @@ -12,7 +12,7 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getUpgradeSafeTransactionHash } from 'src/logic/safe/utils/upgradeSafe' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { makeStyles } from '@material-ui/core' import { TransactionFees } from 'src/components/TransactionsFees' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/ActionModal.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/ActionModal.tsx new file mode 100644 index 00000000..4838f76c --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/ActionModal.tsx @@ -0,0 +1,59 @@ +import React, { ReactElement, useContext } from 'react' +import { useSelector } from 'react-redux' + +import { ExpandedTxDetails, Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { getTransactionByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions' +import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import { AppReduxState } from 'src/store' +import { ApproveTxModal } from './modals/ApproveTxModal' +import { RejectTxModal } from './modals/RejectTxModal' +import { TransactionActionStateContext } from './TxActionProvider' +import { Overwrite } from 'src/types/helpers' + +export const ActionModal = (): ReactElement | null => { + const { selectedAction, selectAction } = useContext(TransactionActionStateContext) + const txParameters = useTransactionParameters() + + const transaction = useSelector((state: AppReduxState) => + getTransactionByAttribute(state)({ + attributeValue: selectedAction.transactionId, + attributeName: 'id', + txLocation: selectedAction.txLocation, + }), + ) + + const onClose = () => selectAction({ actionSelected: 'none', transactionId: '', txLocation: 'history' }) + + if (!transaction?.txDetails) { + return null + } + + switch (selectedAction.actionSelected) { + case 'cancel': + return + + case 'confirm': + return ( + } + txParameters={txParameters} + /> + ) + + case 'execute': + return ( + } + txParameters={txParameters} + /> + ) + + case 'none': + return null + } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/AddressInfo.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/AddressInfo.tsx new file mode 100644 index 00000000..46199746 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/AddressInfo.tsx @@ -0,0 +1,24 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import { useSelector } from 'react-redux' +import { getExplorerInfo } from 'src/config' + +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' + +export const AddressInfo = ({ address }: { address: string }): ReactElement | null => { + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address)) + + if (address === '') { + return null + } + + return ( + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/HexEncodedData.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/HexEncodedData.tsx new file mode 100644 index 00000000..8f5d4a7e --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/HexEncodedData.tsx @@ -0,0 +1,13 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { TxData as LegacyTxData } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription' + +export const HexEncodedData = ({ hexData }: { hexData: string }): ReactElement => ( +
+ + Data (hex encoded): + + +
+) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx new file mode 100644 index 00000000..6151f96c --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx @@ -0,0 +1,46 @@ +import { Loader } from '@gnosis.pm/safe-react-components' +import { format } from 'date-fns' +import React, { ReactElement } from 'react' + +import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll' +import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions' +import { + SubTitle, + ScrollableTransactionsContainer, + StyledTransactions, + StyledTransactionsGroup, + Centered, +} from './styled' +import { TxHistoryRow } from './TxHistoryRow' +import { TxLocationContext } from './TxLocationProvider' + +export const HistoryTxList = (): ReactElement => { + const { count, hasMore, next, transactions } = usePagedHistoryTransactions() + + if (count === 0) { + return ( + + + + ) + } + + return ( + + + + {transactions?.map(([timestamp, txs]) => ( + + {format(Number(timestamp), 'MMM d, yyyy')} + + {txs.map((transaction) => ( + + ))} + + + ))} + + + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/InfoDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/InfoDetails.tsx new file mode 100644 index 00000000..752cb981 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/InfoDetails.tsx @@ -0,0 +1,16 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement, ReactNode } from 'react' + +type InfoDetailsProps = { + children: ReactNode + title: string +} + +export const InfoDetails = ({ children, title }: InfoDetailsProps): ReactElement => ( + <> + + {title} + + {children} + +) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx new file mode 100644 index 00000000..1552857c --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx @@ -0,0 +1,64 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' +import styled from 'styled-components' + +import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d' +import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' +import { + DeleteSpendingLimitDetails, + isDeleteAllowance, + isSetAllowance, + ModifySpendingLimitDetails, +} from 'src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails' +import Value from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value' + +const TxDetailsMethodName = styled(Text)` + text-indent: 4px; +` + +const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>` + padding-left: 24px; + display: ${({ isArrayParameter }) => (isArrayParameter ? 'block' : 'flex')}; + align-items: center; + + p:first-of-type { + margin-right: ${({ isArrayParameter }) => (isArrayParameter ? '0' : '4px')}; + } +` + +const TxInfo = styled.div` + padding: 8px 8px 8px 16px; +` + +const StyledMethodName = styled(Text)` + white-space: nowrap; +` + +export const MethodDetails = ({ data }: { data: DataDecoded }): React.ReactElement => { + // FixMe: this way won't scale well + if (isSetAllowance(data.method)) { + return + } + + // FixMe: this way won't scale well + if (isDeleteAllowance(data.method)) { + return + } + + return ( + + + {data.method} + + + {data.parameters?.map((param, index) => ( + + + {param.name}({param.type}): + + + + ))} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/MultiSendDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/MultiSendDetails.tsx new file mode 100644 index 00000000..d1a4ff34 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/MultiSendDetails.tsx @@ -0,0 +1,82 @@ +import { AccordionSummary, IconText } from '@gnosis.pm/safe-react-components' +import React, { ReactElement, ReactNode } from 'react' + +import { getNetworkInfo } from 'src/config' +import { DataDecoded, TransactionData } from 'src/logic/safe/store/models/types/gateway.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { HexEncodedData } from './HexEncodedData' +import { MethodDetails } from './MethodDetails' +import { isSpendingLimitMethod } from './SpendingLimitDetails' +import { ColumnDisplayAccordionDetails, ActionAccordion } from './styled' +import { TxInfoDetails } from './TxInfoDetails' + +type MultiSendTxGroupProps = { + actionTitle: string + children: ReactNode + txDetails: { + title: string + address: string + dataDecoded: DataDecoded | null + } +} + +const MultiSendTxGroup = ({ actionTitle, children, txDetails }: MultiSendTxGroupProps): ReactElement => { + return ( + + + + + + {!isSpendingLimitMethod(txDetails.dataDecoded?.method) && ( + + )} + {children} + + + ) +} + +const { nativeCoin } = getNetworkInfo() + +export const MultiSendDetails = ({ txData }: { txData: TransactionData }): ReactElement | null => { + // no parameters for the `multiSend` + if (!txData.dataDecoded?.parameters) { + // we render the hex encoded data + if (txData.hexData) { + return + } + + return null + } + + // multiSend has one parameter `transactions` therefore `txData.dataDecoded.parameters[0]` is safe to be used here + return ( + <> + {txData.dataDecoded.parameters[0].valueDecoded?.map(({ dataDecoded }, index, valuesDecoded) => { + let details + const { data, value, to } = valuesDecoded[index] + const actionTitle = `Action ${index + 1} ${dataDecoded ? `(${dataDecoded.method})` : ''}` + const amount = value ? fromTokenUnit(value, nativeCoin.decimals) : 0 + const title = `Send ${amount} ${nativeCoin.name} to:` + + if (dataDecoded) { + details = + } else { + details = data && + } + + return ( + details && ( + + {details} + + ) + ) + })} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx new file mode 100644 index 00000000..471a81c3 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx @@ -0,0 +1,22 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import { useSelector } from 'react-redux' + +import { getExplorerInfo } from 'src/config' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' + +export const OwnerRow = ({ ownerAddress }: { ownerAddress: string }): ReactElement => { + const ownerName = useSelector((state) => getNameFromAddressBookSelector(state, ownerAddress)) + + return ( + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx new file mode 100644 index 00000000..87e0804a --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx @@ -0,0 +1,61 @@ +import { Loader, Title } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import style from 'styled-components' + +import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll' +import Img from 'src/components/layout/Img' +import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions' +import { ActionModal } from './ActionModal' +import { TxActionProvider } from './TxActionProvider' +import { TxLocationContext } from './TxLocationProvider' +import { QueueTxList } from './QueueTxList' +import { ScrollableTransactionsContainer, Centered } from './styled' +import NoTransactionsImage from './assets/no-transactions.svg' + +const NoTransactions = style.div` + display: flex; + flex-direction: column; + margin-top: 60px; +` + +export const QueueTransactions = (): ReactElement => { + const { count, loading, hasMore, next, transactions } = usePagedQueuedTransactions() + + // `loading` is, actually `!transactions` + // added the `transaction` verification to prevent `Object is possibly 'undefined'` error + if (loading || !transactions) { + return ( + + + + ) + } + + if (count === 0) { + return ( + + No Transactions yet + Transactions will appear here + + ) + } + + return ( + + + + {/* Next list */} + + {transactions.next.count !== 0 && } + + + {/* Queue list */} + + {transactions.queue.count !== 0 && } + + + + + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx new file mode 100644 index 00000000..0565bdaa --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx @@ -0,0 +1,93 @@ +import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' +import React, { Fragment, ReactElement, useContext } from 'react' + +import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { + DisclaimerContainer, + GroupedTransactions, + GroupedTransactionsCard, + SubTitle, + StyledTransactions, + StyledTransactionsGroup, + AlignItemsWithMargin, +} from './styled' +import { TxHoverProvider } from './TxHoverProvider' +import { TxLocationContext } from './TxLocationProvider' +import { TxQueueRow } from './TxQueueRow' + +const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => { + return

{firstElement ? : null}

+} + +const Disclaimer = ({ nonce }: { nonce: string }): ReactElement => { + return ( + + + {nonce} + + + + These transactions conflict as they use the same nonce. Executing one will automatically replace the other(s).{' '} + + + + + Learn more + + + + + + + ) +} + +type QueueTransactionProps = { + nonce: string + transactions: Transaction[] +} + +const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => { + return transactions.length > 1 ? ( + + + + + {transactions.map((transaction, index) => ( + + + + + ))} + + + + ) : ( + + ) +} + +type QueueTxListProps = { + transactions: TransactionDetails['transactions'] +} + +export const QueueTxList = ({ transactions }: QueueTxListProps): ReactElement => { + const { txLocation } = useContext(TxLocationContext) + const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE' + + return ( + + {title} + + {transactions.map(([nonce, txs]) => ( + + ))} + + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails.tsx new file mode 100644 index 00000000..df30519d --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails.tsx @@ -0,0 +1,75 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' +import { sameString } from 'src/utils/strings' +import styled from 'styled-components' + +import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo' +import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' +import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' + +const SET_ALLOWANCE = 'setAllowance' +const DELETE_ALLOWANCE = 'deleteAllowance' + +export const isSetAllowance = (method?: string) => sameString(method, SET_ALLOWANCE) +export const isDeleteAllowance = (method?: string) => sameString(method, DELETE_ALLOWANCE) +export const isSpendingLimitMethod = (method?: string) => isSetAllowance(method) || isDeleteAllowance(method) + +const SpendingLimitRow = styled.div` + margin-bottom: 16px; +` + +export const ModifySpendingLimitDetails = ({ data }: { data: DataDecoded }): React.ReactElement => { + const [beneficiary, tokenAddress, amount, resetTimeMin] = React.useMemo( + () => data.parameters?.map(({ value }) => value) ?? [], + [data.parameters], + ) + + const resetTimeLabel = React.useMemo( + () => RESET_TIME_OPTIONS.find(({ value }) => +value === +resetTimeMin / 24 / 60)?.label ?? '', + [resetTimeMin], + ) + + const tokenInfo = useTokenInfo(tokenAddress) + + return ( + <> + + + Modify Spending Limit: + + + + + + + {tokenInfo && } + + + + + + ) +} + +export const DeleteSpendingLimitDetails = ({ data }: { data: DataDecoded }): React.ReactElement => { + const [beneficiary, tokenAddress] = React.useMemo(() => data.parameters?.map(({ value }) => value) ?? [], [ + data.parameters, + ]) + const tokenInfo = useTokenInfo(tokenAddress) + + return ( + <> + + + Delete Spending Limit: + + + + + + {tokenInfo && } + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TokenTransferAmount.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TokenTransferAmount.tsx new file mode 100644 index 00000000..36c2d045 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TokenTransferAmount.tsx @@ -0,0 +1,39 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import Img from 'src/components/layout/Img' +import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png' +import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg' +import { TokenTransferAsset } from './hooks/useAssetInfo' + +const Amount = styled(Text)` + margin-left: 10px; + line-height: 16px; +` + +const AmountWrapper = styled.div` + display: flex; + align-items: center; +` + +export type TokenTransferAmountProps = { + assetInfo: TokenTransferAsset +} + +export const TokenTransferAmount = ({ assetInfo }: TokenTransferAmountProps): ReactElement => { + return ( + + {assetInfo.name} { + error.currentTarget.onerror = null + error.currentTarget.src = assetInfo.tokenType === 'ERC721' ? NFTIcon : TokenPlaceholder + }} + src={assetInfo.logoUri} + /> + {`${assetInfo.directionSign}${assetInfo.amountWithSymbol}`} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxActionProvider.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxActionProvider.tsx new file mode 100644 index 00000000..763f4154 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxActionProvider.tsx @@ -0,0 +1,55 @@ +import React, { createContext, ReactElement, ReactNode, useCallback, useRef, useState } from 'react' +import { useDispatch } from 'react-redux' + +import { fetchTransactionDetails } from 'src/logic/safe/store/actions/fetchTransactionDetails' +import { TxLocation } from 'src/logic/safe/store/models/types/gateway.d' + +export type ActionType = 'cancel' | 'confirm' | 'execute' | 'none' + +export type SelectedAction = { + // FixMe: give proper names to the keys + // for instance: + // `action->{ type; forTransactionId; txLocation; }` + // `setAction` as callback + selectedAction: { + actionSelected: ActionType + transactionId: string + txLocation: TxLocation + } + selectAction: (args: SelectedAction['selectedAction']) => Promise +} + +export const TransactionActionStateContext = createContext({ + selectedAction: { + actionSelected: 'none', + transactionId: '', + txLocation: 'history', + }, + selectAction: () => Promise.resolve(), +}) + +export const TxActionProvider = ({ children }: { children: ReactNode }): ReactElement => { + const dispatch = useRef(useDispatch()) + const [selectedAction, setSelectedAction] = useState({ + actionSelected: 'none', + transactionId: '', + txLocation: 'history', + }) + + const selectAction = useCallback( + async ({ actionSelected, transactionId, txLocation }: SelectedAction['selectedAction']) => { + if (transactionId) { + await dispatch.current(fetchTransactionDetails({ transactionId, txLocation })) + } + + setSelectedAction({ actionSelected, transactionId, txLocation }) + }, + [], + ) + + return ( + + {children} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx new file mode 100644 index 00000000..c9ce1b0c --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx @@ -0,0 +1,216 @@ +import { Dot, IconText as IconTextSrc, Text } from '@gnosis.pm/safe-react-components' +import { ThemeColors } from '@gnosis.pm/safe-react-components/dist/theme' +import { Tooltip } from '@material-ui/core' +import CircularProgress from '@material-ui/core/CircularProgress' +import { createStyles, makeStyles } from '@material-ui/core/styles' +import React, { ReactElement, useRef } from 'react' + +import CustomIconText from 'src/components/CustomIconText' +import { + isCustomTxInfo, + isMultiSendTxInfo, + isSettingsChangeTxInfo, + Transaction, +} from 'src/logic/safe/store/models/types/gateway.d' +import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions' +import { formatDateTime, formatTime } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { KNOWN_MODULES } from 'src/utils/constants' +import styled from 'styled-components' +import { AssetInfo, isTokenTransferAsset } from './hooks/useAssetInfo' +import { TransactionActions } from './hooks/useTransactionActions' +import { TransactionStatusProps } from './hooks/useTransactionStatus' +import { TxTypeProps } from './hooks/useTransactionType' +import { StyledGroupedTransactions, StyledTransaction } from './styled' +import { TokenTransferAmount } from './TokenTransferAmount' +import { CalculatedVotes } from './TxQueueCollapsed' + +const TxInfo = ({ info }: { info: AssetInfo }) => { + if (isTokenTransferAsset(info)) { + return + } + + if (isSettingsChangeTxInfo(info)) { + const UNKNOWN_MODULE = 'Unknown module' + + switch (info.settingsInfo?.type) { + case 'SET_FALLBACK_HANDLER': + case 'ADD_OWNER': + case 'REMOVE_OWNER': + case 'SWAP_OWNER': + case 'CHANGE_THRESHOLD': + case 'CHANGE_IMPLEMENTATION': + break + case 'ENABLE_MODULE': + case 'DISABLE_MODULE': + return ( + + {KNOWN_MODULES[info.settingsInfo.module] ?? UNKNOWN_MODULE} + + ) + } + } + + if (isCustomTxInfo(info)) { + if (isMultiSendTxInfo(info)) { + return ( + + {info.actionCount} {`action${info.actionCount > 1 ? 's' : ''}`} + + ) + } + + return ( + + {info.methodName} + + ) + } + return null +} + +const CircularProgressPainter = styled.div<{ color: ThemeColors }>` + color: ${({ theme, color }) => theme.colors[color]}; +` + +const SmallDot = styled(Dot)` + height: 8px; + width: 8px; + background-color: ${({ theme, color }) => theme.colors[color]} !important; +` + +const IconText = styled(IconTextSrc)` + p { + font-weight: bold; + } +` + +const useTooltipStyles = makeStyles( + createStyles(() => ({ + arrow: { + color: 'white', + }, + tooltip: { + backgroundColor: 'white', + color: 'rgba(0, 0, 0, 0.87)', + boxShadow: '#00000026 0 2px 4px 0', + fontSize: '14px', + lineHeight: '14px', + }, + })), +) + +const TooltipContent = styled.div` + width: max-content; +` + +type TxCollapsedProps = { + transaction?: Transaction + isGrouped?: boolean + nonce?: number + type: TxTypeProps + info?: AssetInfo + time: number + votes?: CalculatedVotes + actions?: TransactionActions + status: TransactionStatusProps +} + +export const TxCollapsed = ({ + transaction, + isGrouped = false, + nonce, + type, + info, + time, + votes, + actions, + status, +}: TxCollapsedProps): ReactElement => { + const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : '' + + const txCollapsedNonce = ( +
+ {nonce} +
+ ) + + const txCollapsedType = ( +
+ +
+ ) + + const txCollapsedInfo =
{info && }
+ + const tooltipStyles = useTooltipStyles() + const timestamp = useRef(null) + + const txCollapsedTime = ( +
+ + + {formatTime(time)} + + +
+ ) + + const txCollapsedVotes = ( +
+ {votes && ( + votes.submitted ? 'secondaryLight' : 'primary'} + iconType="owners" + iconSize="sm" + text={`${votes.votes}`} + textSize="md" + /> + )} +
+ ) + + const txCollapsedActions = ( +
+ {actions?.isUserAnOwner && transaction && } +
+ ) + + const txCollapsedStatus = ( +
+ {transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? ( + + + + ) : ( + (transaction?.txStatus === 'AWAITING_EXECUTION' || transaction?.txStatus === 'AWAITING_CONFIRMATIONS') && ( + + ) + )} + + {status.text} + +
+ ) + + return isGrouped ? ( + + {/* no nonce */} + {txCollapsedType} + {txCollapsedInfo} + {txCollapsedTime} + {txCollapsedVotes} + {txCollapsedActions} + {txCollapsedStatus} + + ) : ( + + {txCollapsedNonce} + {txCollapsedType} + {txCollapsedInfo} + {txCollapsedTime} + {txCollapsedVotes} + {txCollapsedActions} + {txCollapsedStatus} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx new file mode 100644 index 00000000..270878bf --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx @@ -0,0 +1,58 @@ +import { Icon } from '@gnosis.pm/safe-react-components' +import { default as MuiIconButton } from '@material-ui/core/IconButton' +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { useActionButtonsHandlers } from './hooks/useActionButtonsHandlers' + +const IconButton = styled(MuiIconButton)` + padding: 8px !important; + + &.Mui-disabled { + opacity: 0.4; + } +` + +type TxCollapsedActionsProps = { + transaction: Transaction +} + +export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): ReactElement => { + const { + canCancel, + handleConfirmButtonClick, + handleCancelButtonClick, + handleOnMouseEnter, + handleOnMouseLeave, + isPending, + disabledActions, + } = useActionButtonsHandlers(transaction) + + return ( + <> + { + + + + } + {canCancel && ( + + + + )} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx new file mode 100644 index 00000000..bf777dcf --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx @@ -0,0 +1,37 @@ +import React, { ReactElement } from 'react' + +import { ExpandedTxDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { sameString } from 'src/utils/strings' +import { HexEncodedData } from './HexEncodedData' +import { MethodDetails } from './MethodDetails' +import { MultiSendDetails } from './MultiSendDetails' + +type TxDataProps = { + txData: ExpandedTxDetails['txData'] +} + +export const TxData = ({ txData }: TxDataProps): ReactElement | null => { + // nothing to render + if (!txData) { + return null + } + + // unknown tx information + if (!txData.dataDecoded) { + // no hex data, nothing to render + if (!txData.hexData) { + return null + } + + // we render the hex encoded data + return + } + + // known data and particularly `multiSend` data type + if (sameString(txData.dataDecoded.method, 'multiSend')) { + return + } + + // we render the decoded data + return +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxDetails.tsx new file mode 100644 index 00000000..789afd82 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxDetails.tsx @@ -0,0 +1,128 @@ +import { Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components' +import cn from 'classnames' +import React, { ReactElement, useContext } from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { + ExpandedTxDetails, + isMultiSendTxInfo, + isSettingsChangeTxInfo, + isTransferTxInfo, + MultiSigExecutionDetails, + Transaction, +} from 'src/logic/safe/store/models/types/gateway.d' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { TransactionActions } from './hooks/useTransactionActions' +import { useTransactionDetails } from './hooks/useTransactionDetails' +import { TxDetailsContainer, Centered, AlignItemsWithMargin } from './styled' +import { TxData } from './TxData' +import { TxExpandedActions } from './TxExpandedActions' +import { TxInfo } from './TxInfo' +import { TxLocationContext } from './TxLocationProvider' +import { TxOwners } from './TxOwners' +import { TxSummary } from './TxSummary' +import { isCancelTxDetails, NOT_AVAILABLE } from './utils' + +const NormalBreakingText = styled(Text)` + line-break: normal; + word-break: normal; +` + +const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => { + const safeAddress = useSelector(safeParamAddressFromStateSelector) + + if (isTransferTxInfo(txDetails.txInfo) || isSettingsChangeTxInfo(txDetails.txInfo)) { + return + } + + if (isCancelTxDetails({ executedAt: txDetails.executedAt, txInfo: txDetails.txInfo, safeAddress })) { + return ( + <> + + {`This is an empty cancelling transaction that doesn't send any funds. + Executing this transaction will replace all currently awaiting transactions with nonce ${ + (txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE + }.`} + + + + + Why do I need to pay for cancelling a transaction? + + + + + + ) + } + + if (!txDetails.txData) { + return null + } + + return +} + +type TxDetailsProps = { + transaction: Transaction + actions?: TransactionActions +} + +export const TxDetails = ({ transaction, actions }: TxDetailsProps): ReactElement => { + const { txLocation } = useContext(TxLocationContext) + const { data, loading } = useTransactionDetails(transaction.id) + + if (loading) { + return ( + + + + ) + } + + if (!data) { + return ( + + + No data available + + + ) + } + + return ( + +
+ +
+
+ +
+
+ +
+ {!data.executedAt && txLocation !== 'history' && actions?.isUserAnOwner && ( +
+ +
+ )} +
+ ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxExpandedActions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxExpandedActions.tsx new file mode 100644 index 00000000..37175329 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxExpandedActions.tsx @@ -0,0 +1,49 @@ +import { Button } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { useActionButtonsHandlers } from 'src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers' + +type TxExpandedActionsProps = { + transaction: Transaction +} + +export const TxExpandedActions = ({ transaction }: TxExpandedActionsProps): ReactElement | null => { + const { + canCancel, + handleConfirmButtonClick, + handleCancelButtonClick, + handleOnMouseEnter, + handleOnMouseLeave, + isPending, + disabledActions, + } = useActionButtonsHandlers(transaction) + + const onExecuteOrConfirm = (event) => { + handleOnMouseLeave() + handleConfirmButtonClick(event) + } + + // There is a problem in chrome that produces onMouseLeave event not being triggered properly. + // https://github.com/facebook/react/issues/4492 + return ( + <> + + {canCancel && ( + + )} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx new file mode 100644 index 00000000..e7e35167 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx @@ -0,0 +1,16 @@ +import React, { ReactElement } from 'react' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { useAssetInfo } from './hooks/useAssetInfo' +import { useTransactionStatus } from './hooks/useTransactionStatus' +import { useTransactionType } from './hooks/useTransactionType' +import { TxCollapsed } from './TxCollapsed' + +export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction }): ReactElement => { + const nonce = transaction.executionInfo?.nonce + const type = useTransactionType(transaction) + const info = useAssetInfo(transaction.txInfo) + const status = useTransactionStatus(transaction) + + return +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryRow.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryRow.tsx new file mode 100644 index 00000000..7245c7fe --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryRow.tsx @@ -0,0 +1,29 @@ +import { AccordionDetails } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { isCreationTxInfo, Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { NoPaddingAccordion, StyledAccordionSummary } from './styled' +import { TxHistoryCollapsed } from './TxHistoryCollapsed' +import { TxDetails } from './TxDetails' +import { TxInfoCreation } from './TxInfoCreation' + +export const TxHistoryRow = ({ transaction }: { transaction: Transaction }): ReactElement => ( + + + + + + {isCreationTxInfo(transaction.txInfo) ? ( + + ) : ( + + )} + + +) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxHoverProvider.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxHoverProvider.tsx new file mode 100644 index 00000000..e3733689 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxHoverProvider.tsx @@ -0,0 +1,15 @@ +import React, { createContext, ReactElement, ReactNode, useState } from 'react' + +export const TxHoverContext = createContext<{ + activeHover?: string + setActiveHover: (activeHover?: string) => void +}>({ + activeHover: undefined, + setActiveHover: () => {}, +}) + +export const TxHoverProvider = ({ children }: { children: ReactNode }): ReactElement => { + const [activeHover, setActiveHover] = useState() + + return {children} +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfo.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfo.tsx new file mode 100644 index 00000000..9a485d9a --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfo.tsx @@ -0,0 +1,25 @@ +import React, { ReactElement } from 'react' + +import { + ExpandedTxDetails, + isSettingsChangeTxInfo, + isTransferTxInfo, +} from 'src/logic/safe/store/models/types/gateway.d' +import { TxInfoSettings } from './TxInfoSettings' +import { TxInfoTransfer } from './TxInfoTransfer' + +type TxInfoProps = { + txInfo: ExpandedTxDetails['txInfo'] +} + +export const TxInfo = ({ txInfo }: TxInfoProps): ReactElement | null => { + if (isSettingsChangeTxInfo(txInfo)) { + return + } + + if (isTransferTxInfo(txInfo)) { + return + } + + return null +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx new file mode 100644 index 00000000..68f4b0fd --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx @@ -0,0 +1,88 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { getExplorerInfo } from 'src/config' +import { Creation, Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { formatDateTime, NOT_AVAILABLE } from './utils' +import { InlineEthHashInfo, TxDetailsContainer } from './styled' + +export const TxInfoCreation = ({ transaction }: { transaction: Transaction }): ReactElement | null => { + const txInfo = transaction.txInfo as Creation + const timestamp = transaction.timestamp + + return ( + +
+
+ + Hash:{' '} + + +
+
+ + Created:{' '} + + + {formatDateTime(timestamp)} + +
+
+ + Creator:{' '} + + +
+
+ + Factory:{' '} + + {txInfo.factory ? ( + + ) : ( + + {NOT_AVAILABLE} + + )} +
+
+ + Mastercopy:{' '} + + {txInfo.implementation ? ( + + ) : ( + + {NOT_AVAILABLE} + + )} +
+
+
+ + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoDetails.tsx new file mode 100644 index 00000000..4557f2ea --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoDetails.tsx @@ -0,0 +1,81 @@ +import React, { ReactElement, useContext, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { Erc721Transfer, Transfer } from 'src/logic/safe/store/models/types/gateway.d' +import { EllipsisTransactionDetails } from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails' +import SendModal from 'src/routes/safe/components/Balances/SendModal' +import { AddressInfo } from './AddressInfo' +import { InfoDetails } from './InfoDetails' +import { TxLocationContext, TxLocationProps } from './TxLocationProvider' +import { getTxTokenData } from './utils' + +const SingleRow = styled.div` + display: flex; + align-items: flex-end; +` + +type TxInfoDetailsProps = { + title: string + address: string + isTransferType?: boolean + txInfo?: Transfer +} + +export const TxInfoDetails = ({ title, address, isTransferType, txInfo }: TxInfoDetailsProps): ReactElement => { + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address)) + const knownAddress = recipientName !== 'UNKNOWN' + + const { txLocation } = useContext(TxLocationContext) + const canRepeatTransaction = isTransferType && txLocation === 'history' && txInfo?.direction === 'OUTGOING' + + const [sendModalOpen, setSendModalOpen] = useState(false) + const sendModalOpenHandler = () => { + setSendModalOpen(true) + } + const onClose = () => { + setSendModalOpen(false) + } + + const [sendModalParams, setSendModalParams] = useState<{ + activeScreenType: 'sendCollectible' | 'sendFunds' + recipientAddress: string + selectedToken: string | Erc721Transfer + tokenAmount: string + }>({ + activeScreenType: 'sendFunds', + recipientAddress: address, + selectedToken: ZERO_ADDRESS, + tokenAmount: '0', + }) + useEffect(() => { + if (txInfo) { + const isCollectible = txInfo.transferInfo.type === 'ERC721' + const { address, value, decimals } = getTxTokenData(txInfo) + + setSendModalParams((prev) => ({ + ...prev, + activeScreenType: isCollectible ? 'sendCollectible' : 'sendFunds', + selectedToken: isCollectible ? (txInfo.transferInfo as Erc721Transfer) : address, + tokenAmount: isCollectible ? '1' : fromTokenUnit(value, Number(decimals)), + })) + } + }, [txInfo]) + + return ( + + + + + + {canRepeatTransaction && } + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoSettings.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoSettings.tsx new file mode 100644 index 00000000..e653fbb8 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoSettings.tsx @@ -0,0 +1,75 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { SettingsChange } from 'src/logic/safe/store/models/types/gateway.d' +import { AddressInfo } from './AddressInfo' +import { InfoDetails } from './InfoDetails' +import { TxInfoDetails } from './TxInfoDetails' + +type TxInfoSettingsProps = { + settingsInfo: SettingsChange['settingsInfo'] +} + +export const TxInfoSettings = ({ settingsInfo }: TxInfoSettingsProps): ReactElement | null => { + if (!settingsInfo) { + return null + } + + switch (settingsInfo.type) { + case 'SET_FALLBACK_HANDLER': { + return {settingsInfo.handler} + } + case 'ADD_OWNER': { + return ( + + + {settingsInfo.threshold} + + ) + } + case 'REMOVE_OWNER': { + return ( + + + {settingsInfo.threshold} + + ) + } + case 'SWAP_OWNER': { + return ( + + + + + ) + } + case 'CHANGE_THRESHOLD': { + return {settingsInfo.threshold} + } + case 'CHANGE_IMPLEMENTATION': { + return ( + + + {settingsInfo.implementation} + + + ) + } + case 'ENABLE_MODULE': { + return ( + + + + ) + } + case 'DISABLE_MODULE': { + return ( + + + + ) + } + default: + return null + } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoTransfer.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoTransfer.tsx new file mode 100644 index 00000000..bcd10236 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoTransfer.tsx @@ -0,0 +1,24 @@ +import React, { ReactElement, useEffect, useState } from 'react' + +import { Transfer } from 'src/logic/safe/store/models/types/gateway.d' +import { useAssetInfo } from './hooks/useAssetInfo' +import { TxInfoDetails } from './TxInfoDetails' + +export const TxInfoTransfer = ({ txInfo }: { txInfo: Transfer }): ReactElement | null => { + const assetInfo = useAssetInfo(txInfo) + const [details, setDetails] = useState<{ title: string; address: string } | undefined>() + + useEffect(() => { + if (assetInfo && assetInfo.type === 'Transfer') { + if (txInfo.direction === 'INCOMING') { + setDetails({ title: `Received ${assetInfo.amountWithSymbol} from:`, address: txInfo.sender }) + } else { + setDetails({ title: `Send ${assetInfo.amountWithSymbol} to:`, address: txInfo.recipient }) + } + } + }, [assetInfo, txInfo.direction, txInfo.recipient, txInfo.sender]) + + return details ? ( + + ) : null +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider.tsx new file mode 100644 index 00000000..c8114dd0 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider.tsx @@ -0,0 +1,18 @@ +import React, { createContext, ReactElement, ReactNode, useState } from 'react' +import { TxLocation } from 'src/logic/safe/store/models/types/gateway.d' + +export type TxLocationProps = { + txLocation: TxLocation + setTxLocation?: (txLocation: TxLocation) => void +} + +export const TxLocationContext = createContext({ + txLocation: 'history', + setTxLocation: () => {}, +}) + +export const TxLocationProvider = ({ children }: { children: ReactNode }): ReactElement => { + const [txLocation, setTxLocation] = useState('history') + + return {children} +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxOwners.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxOwners.tsx new file mode 100644 index 00000000..441b78a9 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxOwners.tsx @@ -0,0 +1,81 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import Img from 'src/components/layout/Img' +import { ExpandedTxDetails, isModuleExecutionDetails } from 'src/logic/safe/store/models/types/gateway.d' +import TransactionListActive from './assets/transactions-list-active.svg' +import TransactionListInactive from './assets/transactions-list-inactive.svg' +import CheckCircleGreen from './assets/check-circle-green.svg' +import PlusCircleGreen from './assets/plus-circle-green.svg' +import { OwnerRow } from './OwnerRow' +import { OwnerList, OwnerListItem } from './styled' + +type TxOwnersProps = { + detailedExecutionInfo: ExpandedTxDetails['detailedExecutionInfo'] +} + +const StyledImg = styled(Img)` + background-color: transparent; + border-radius: 50%; +` + +export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement | null => { + if (!detailedExecutionInfo || isModuleExecutionDetails(detailedExecutionInfo)) { + return null + } + + const confirmationsNeeded = detailedExecutionInfo.confirmationsRequired - detailedExecutionInfo.confirmations.length + + return ( + + + + + +
+ + Created + +
+
+ {detailedExecutionInfo.confirmations.map(({ signer }) => ( + + + + +
+ + Confirmed + + +
+
+ ))} + {confirmationsNeeded === 0 ? ( + + + + +
+ + {detailedExecutionInfo.executor ? 'Executed' : 'Execute'} + + {detailedExecutionInfo.executor && } +
+
+ ) : ( + + + + +
+ + Execute ({confirmationsNeeded} more {confirmationsNeeded === 1 ? 'confirmation' : 'confirmations'} needed) + +
+
+ )} +
+ ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueCollapsed.tsx new file mode 100644 index 00000000..08ba13f6 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueCollapsed.tsx @@ -0,0 +1,53 @@ +import React, { ReactElement } from 'react' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { useAssetInfo } from './hooks/useAssetInfo' +import { TransactionActions } from './hooks/useTransactionActions' +import { useTransactionStatus } from './hooks/useTransactionStatus' +import { useTransactionType } from './hooks/useTransactionType' +import { TxCollapsed } from './TxCollapsed' + +export type CalculatedVotes = { votes: string; submitted: number; required: number } + +const calculateVotes = (executionInfo: Transaction['executionInfo']): CalculatedVotes | undefined => { + if (!executionInfo) { + return + } + + const submitted = executionInfo.confirmationsSubmitted + const required = executionInfo.confirmationsRequired + + return { + votes: `${submitted} out of ${required}`, + submitted, + required, + } +} + +type TxQueuedCollapsedProps = { + isGrouped?: boolean + transaction: Transaction + actions?: TransactionActions +} + +export const TxQueueCollapsed = ({ isGrouped = false, transaction, actions }: TxQueuedCollapsedProps): ReactElement => { + const nonce = transaction.executionInfo?.nonce + const type = useTransactionType(transaction) + const info = useAssetInfo(transaction.txInfo) + const votes = calculateVotes(transaction.executionInfo) + const status = useTransactionStatus(transaction) + + return ( + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueRow.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueRow.tsx new file mode 100644 index 00000000..5405ddaf --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxQueueRow.tsx @@ -0,0 +1,46 @@ +import { AccordionDetails } from '@gnosis.pm/safe-react-components' +import React, { ReactElement, useContext, useEffect, useState } from 'react' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { useTransactionActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions' +import { NoPaddingAccordion, StyledAccordionSummary } from './styled' +import { TxDetails } from './TxDetails' +import { TxHoverContext } from './TxHoverProvider' +import { TxQueueCollapsed } from './TxQueueCollapsed' + +type TxQueueRowProps = { + isGrouped?: boolean + transaction: Transaction +} + +export const TxQueueRow = ({ isGrouped = false, transaction }: TxQueueRowProps): ReactElement => { + const { activeHover } = useContext(TxHoverContext) + const actions = useTransactionActions(transaction) + const [tx, setTx] = useState(transaction) + + useEffect(() => { + if (activeHover && activeHover !== transaction.id) { + setTx((currTx) => ({ ...currTx, txStatus: 'WILL_BE_REPLACED' })) + return + } + + setTx(transaction) + }, [activeHover, transaction]) + + return ( + + + + + + + + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx new file mode 100644 index 00000000..b0d81c82 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx @@ -0,0 +1,66 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React, { ReactElement } from 'react' + +import { getExplorerInfo } from 'src/config' +import { ExpandedTxDetails, isMultiSigExecutionDetails, Operation } from 'src/logic/safe/store/models/types/gateway.d' +import { InlineEthHashInfo } from './styled' +import { formatDateTime, NOT_AVAILABLE } from './utils' + +export const TxSummary = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement => { + const { txHash, detailedExecutionInfo, executedAt, txData } = txDetails + const explorerUrl = txHash ? getExplorerInfo(txHash) : null + const nonce = isMultiSigExecutionDetails(detailedExecutionInfo) ? detailedExecutionInfo.nonce : undefined + const created = isMultiSigExecutionDetails(detailedExecutionInfo) ? detailedExecutionInfo.submittedAt : undefined + + return ( + <> +
+ + Hash:{' '} + + {txHash ? ( + + ) : ( + + {NOT_AVAILABLE} + + )} +
+ {nonce && ( +
+ + Nonce:{' '} + + + {nonce} + +
+ )} + {created && ( +
+ + Created:{' '} + + + {formatDateTime(created)} + +
+ )} +
+ + Executed:{' '} + + + {executedAt ? formatDateTime(executedAt) : NOT_AVAILABLE} + +
+ {txData?.operation === Operation.DELEGATE && ( +
+ + Delegate Call + +
+ )} + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/check-circle-green.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/check-circle-green.svg new file mode 100644 index 00000000..a91102cb --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/check-circle-green.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/custom.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/custom.svg new file mode 100644 index 00000000..ff8de3f4 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/custom.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/incoming.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/incoming.svg new file mode 100644 index 00000000..c08ef2a1 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/incoming.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/no-transactions.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/no-transactions.svg new file mode 100644 index 00000000..d209a893 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/no-transactions.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/outgoing.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/outgoing.svg new file mode 100644 index 00000000..89b07111 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/outgoing.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/plus-circle-green.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/plus-circle-green.svg new file mode 100644 index 00000000..d59922ce --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/plus-circle-green.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/settings.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/settings.svg new file mode 100644 index 00000000..eb64eda0 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-active.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-active.svg new file mode 100644 index 00000000..e2e3c6f4 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-active.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-inactive.svg b/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-inactive.svg new file mode 100644 index 00000000..3e429f55 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/assets/transactions-list-inactive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts new file mode 100644 index 00000000..db5399cf --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts @@ -0,0 +1,84 @@ +import { MouseEvent as ReactMouseEvent, useCallback, useContext, useMemo, useRef } from 'react' +import { useSelector } from 'react-redux' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { useTransactionActions } from './useTransactionActions' +import { TransactionActionStateContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxActionProvider' +import { TxHoverContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxHoverProvider' +import { TxLocationContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider' + +type ActionButtonsHandlers = { + canCancel: boolean + handleConfirmButtonClick: (event: ReactMouseEvent) => void + handleCancelButtonClick: (event: ReactMouseEvent) => void + handleOnMouseEnter: () => void + handleOnMouseLeave: () => void + isPending: boolean + disabledActions: boolean +} + +export const useActionButtonsHandlers = (transaction: Transaction): ActionButtonsHandlers => { + const currentUser = useSelector(userAccountSelector) + const actionContext = useRef(useContext(TransactionActionStateContext)) + const hoverContext = useRef(useContext(TxHoverContext)) + const locationContext = useRef(useContext(TxLocationContext)) + const { canCancel, canConfirmThenExecute, canExecute } = useTransactionActions(transaction) + + const handleConfirmButtonClick = useCallback( + (event: ReactMouseEvent) => { + event.stopPropagation() + actionContext.current.selectAction({ + actionSelected: canExecute || canConfirmThenExecute ? 'execute' : 'confirm', + transactionId: transaction.id, + txLocation: locationContext.current.txLocation, + }) + }, + [canConfirmThenExecute, canExecute, transaction.id], + ) + + const handleCancelButtonClick = useCallback( + (event: ReactMouseEvent) => { + event.stopPropagation() + actionContext.current.selectAction({ + actionSelected: 'cancel', + transactionId: transaction.id, + txLocation: locationContext.current.txLocation, + }) + }, + [transaction.id], + ) + + const handleOnMouseEnter = useCallback(() => { + if (canExecute) { + hoverContext.current.setActiveHover(transaction.id) + } + }, [canExecute, transaction.id]) + + const handleOnMouseLeave = useCallback(() => { + hoverContext.current.setActiveHover() + }, []) + + const isPending = useMemo(() => !!transaction.txStatus.match(/^PENDING.*/), [transaction.txStatus]) + + const signaturePending = useCallback(addressInList(transaction.executionInfo?.missingSigners), []) + + const disabledActions = useMemo( + () => + isPending || + (transaction.txStatus === 'AWAITING_EXECUTION' && locationContext.current.txLocation === 'queued.queued') || + (transaction.txStatus === 'AWAITING_CONFIRMATIONS' && !signaturePending(currentUser)), + [currentUser, isPending, signaturePending, transaction.txStatus], + ) + + return { + canCancel, + handleConfirmButtonClick, + handleCancelButtonClick, + handleOnMouseEnter, + handleOnMouseLeave, + isPending, + disabledActions, + } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useAssetInfo.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useAssetInfo.ts new file mode 100644 index 00000000..dc6b3467 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useAssetInfo.ts @@ -0,0 +1,99 @@ +import { useEffect, useState } from 'react' + +import { getNetworkInfo } from 'src/config' +import { + Custom, + isCustomTxInfo, + isSettingsChangeTxInfo, + isTransferTxInfo, + SettingsChange, + TransactionInfo, +} from 'src/logic/safe/store/models/types/gateway.d' +import { getTxAmount } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { NOT_AVAILABLE } from 'src/routes/safe/components/Transactions/TxsTable/columns' + +export type TokenTransferAsset = { + type: 'Transfer' + name: string + logoUri: string + directionSign: '+' | '-' | '' + amountWithSymbol: string + tokenType: string +} + +export type AssetInfo = TokenTransferAsset | SettingsChange | Custom + +export const isTokenTransferAsset = (value: AssetInfo): value is TokenTransferAsset => { + return value.type === 'Transfer' +} + +const defaultTokenTransferAsset: TokenTransferAsset = { + type: 'Transfer', + name: NOT_AVAILABLE, + logoUri: NOT_AVAILABLE, + directionSign: '', + amountWithSymbol: NOT_AVAILABLE, + tokenType: 'UNKNOWN', +} + +export const useAssetInfo = (txInfo: TransactionInfo): AssetInfo | undefined => { + const [asset, setAsset] = useState() + const amountWithSymbol = getTxAmount(txInfo) + + useEffect(() => { + if (isTransferTxInfo(txInfo)) { + const { direction, transferInfo } = txInfo + const directionSign = direction === 'INCOMING' ? '+' : '-' + + switch (transferInfo.type) { + case 'ERC20': { + setAsset({ + type: 'Transfer', + name: transferInfo.tokenName ?? defaultTokenTransferAsset.name, + logoUri: transferInfo.logoUri ?? defaultTokenTransferAsset.logoUri, + directionSign, + amountWithSymbol, + tokenType: transferInfo.type, + }) + break + } + case 'ERC721': { + setAsset({ + type: 'Transfer', + name: transferInfo.tokenName ?? defaultTokenTransferAsset.name, + logoUri: transferInfo.logoUri ?? defaultTokenTransferAsset.logoUri, + directionSign: directionSign, + amountWithSymbol, + tokenType: transferInfo.type, + }) + break + } + case 'ETHER': { + const { nativeCoin } = getNetworkInfo() + + setAsset({ + type: 'Transfer', + name: nativeCoin.name ?? defaultTokenTransferAsset.name, + logoUri: nativeCoin.logoUri ?? defaultTokenTransferAsset.logoUri, + directionSign: directionSign, + amountWithSymbol, + tokenType: transferInfo.type, + }) + break + } + } + return + } + + if (isSettingsChangeTxInfo(txInfo)) { + setAsset(txInfo) + return + } + + if (isCustomTxInfo(txInfo)) { + setAsset(txInfo) + } + }, [txInfo, amountWithSymbol]) + + return asset +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useHistoryTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useHistoryTransactions.ts new file mode 100644 index 00000000..71f3fbd9 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useHistoryTransactions.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { historyTransactions } from 'src/logic/safe/store/selectors/gatewayTransactions' + +export const useHistoryTransactions = (): TransactionDetails => { + const historyTxs = useSelector(historyTransactions) + const [count, setCount] = useState(0) + + useEffect(() => { + const history = historyTxs + ? Object.entries(historyTxs).reduce((acc, [, transactions]) => (acc += transactions.length), 0) + : 0 + setCount(history) + }, [historyTxs]) + + return { + count, + transactions: historyTxs ? Object.entries(historyTxs) : [], + } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts new file mode 100644 index 00000000..fcad65d9 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts @@ -0,0 +1,46 @@ +import { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions' +import { addHistoryTransactions } from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { useHistoryTransactions } from 'src/routes/safe/components/Transactions/GatewayTransactions/hooks/useHistoryTransactions' + +type PagedTransactions = { + count: number + transactions: TransactionDetails['transactions'] + hasMore: boolean + next: () => Promise +} + +export const usePagedHistoryTransactions = (): PagedTransactions => { + const { count, transactions } = useHistoryTransactions() + + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [hasMore, setHasMore] = useState(true) + + const next = async () => { + const results = await loadPagedHistoryTransactions(safeAddress) + + if (!results) { + setHasMore(false) + return + } + + const { values, next } = results + + if (next === null) { + setHasMore(false) + } + + if (values) { + dispatch(addHistoryTransactions({ safeAddress, values, isTail: true })) + } else { + setHasMore(false) + } + } + + return { count, transactions, hasMore, next } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts new file mode 100644 index 00000000..0dbe18c4 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { loadPagedQueuedTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions' +import { addQueuedTransactions } from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { QueueTransactionsInfo, useQueueTransactions } from './useQueueTransactions' + +type PagedQueuedTransactions = { + count: number + loading: boolean + transactions?: QueueTransactionsInfo + hasMore: boolean + next: () => Promise +} + +export const usePagedQueuedTransactions = (): PagedQueuedTransactions => { + const transactions = useQueueTransactions() + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [hasMore, setHasMore] = useState(true) + + const nextPage = async () => { + const results = await loadPagedQueuedTransactions(safeAddress) + + if (!results) { + setHasMore(false) + return + } + + const { values, next } = results + + if (next === null) { + setHasMore(false) + } + + if (values) { + dispatch(addQueuedTransactions({ safeAddress, values })) + } else { + setHasMore(false) + } + } + + let count + if (transactions) { + count = transactions.next.count + transactions.queue.count + } + + const loading = typeof transactions === 'undefined' || typeof count === 'undefined' + + return { count, loading, transactions, hasMore, next: nextPage } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useQueueTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useQueueTransactions.ts new file mode 100644 index 00000000..b00bd975 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useQueueTransactions.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { nextTransactions, queuedTransactions } from 'src/logic/safe/store/selectors/gatewayTransactions' + +export type QueueTransactionsInfo = { + next: TransactionDetails + queue: TransactionDetails +} + +/** + * Get transactions (next and queue) from nextTransactions and queuedTransactions selectors + */ +export const useQueueTransactions = (): QueueTransactionsInfo | undefined => { + const nextTxs = useSelector(nextTransactions) + const queuedTxs = useSelector(queuedTransactions) + const [txsCount, setTxsCount] = useState<{ next: number; queued: number } | undefined>() + + useEffect(() => { + const next = nextTxs + ? Object.entries(nextTxs).reduce((acc, [, transactions]) => (acc += transactions.length), 0) + : 0 + const queued = queuedTxs + ? Object.entries(queuedTxs).reduce((acc, [, transactions]) => (acc += transactions.length), 0) + : 0 + setTxsCount({ next, queued }) + }, [nextTxs, queuedTxs]) + + // no data loaded to the store yet + if ((!nextTxs && !queuedTxs) || typeof txsCount === 'undefined') { + return + } + + return { + next: { + count: txsCount.next, + transactions: nextTxs ? Object.entries(nextTxs) : [], + }, + queue: { + count: txsCount.queued, + transactions: queuedTxs ? Object.entries(queuedTxs) : [], + }, + } +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions.ts new file mode 100644 index 00000000..6c371293 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions.ts @@ -0,0 +1,90 @@ +import { useContext, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { ExecutionInfo, isCustomTxInfo, Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { getQueuedTransactionsByNonce } from 'src/logic/safe/store/selectors/gatewayTransactions' +import { sameAddress } from 'src/logic/wallets/ethAddresses' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { TxLocationContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider' +import { isCancelTransaction } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { grantedSelector } from 'src/routes/safe/container/selector' +import { AppReduxState } from 'src/store' + +export const isThresholdReached = (executionInfo: ExecutionInfo): boolean => { + const { confirmationsSubmitted, confirmationsRequired } = executionInfo + return confirmationsSubmitted === confirmationsRequired +} + +export type TransactionActions = { + canConfirm: boolean + canConfirmThenExecute: boolean + canExecute: boolean + canCancel: boolean + isUserAnOwner: boolean + oneToGo: boolean +} + +export const useTransactionActions = (transaction: Transaction): TransactionActions => { + const currentUser = useSelector(userAccountSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const isUserAnOwner = useSelector(grantedSelector) + const { txLocation } = useContext(TxLocationContext) + const { confirmationsSubmitted = 0, confirmationsRequired = 0, missingSigners } = transaction.executionInfo ?? {} + const transactionsByNonce = useSelector((state: AppReduxState) => + getQueuedTransactionsByNonce(state)({ + attributeName: 'nonce', + attributeValue: transaction.executionInfo?.nonce ?? -1, + txLocation, + }), + ) + + const [state, setState] = useState({ + canConfirm: false, + canConfirmThenExecute: false, + canExecute: false, + canCancel: false, + isUserAnOwner, + oneToGo: false, + }) + + useEffect(() => { + if (isUserAnOwner && txLocation !== 'history' && transaction.executionInfo) { + const currentUserSigned = !missingSigners?.some((missingSigner) => sameAddress(missingSigner, currentUser)) + + const oneToGo = confirmationsSubmitted === confirmationsRequired - 1 + const canConfirm = ['queued.next', 'queued.queued'].includes(txLocation) && !currentUserSigned + const thresholdReached = confirmationsSubmitted >= confirmationsRequired + + setState({ + canConfirm, + canConfirmThenExecute: txLocation === 'queued.next' && canConfirm && oneToGo, + canExecute: txLocation === 'queued.next' && thresholdReached, + canCancel: !transactionsByNonce.some( + ({ txInfo }) => + isCustomTxInfo(txInfo) && + isCancelTransaction({ + txInfo, + safeAddress, + }), + ), + isUserAnOwner, + oneToGo, + }) + } else { + setState((prev) => ({ ...prev, isUserAnOwner })) + } + }, [ + confirmationsRequired, + confirmationsSubmitted, + currentUser, + isUserAnOwner, + missingSigners, + safeAddress, + transaction, + transactionsByNonce, + txLocation, + ]) + + return state +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionDetails.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionDetails.ts new file mode 100644 index 00000000..060d4f2a --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionDetails.ts @@ -0,0 +1,36 @@ +import { useContext, useEffect, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { ExpandedTxDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { fetchTransactionDetails } from 'src/logic/safe/store/actions/fetchTransactionDetails' +import { TxLocationContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxLocationProvider' +import { getTransactionDetails } from 'src/logic/safe/store/selectors/gatewayTransactions' +import { AppReduxState } from 'src/store' + +export type LoadTransactionDetails = { + data?: ExpandedTxDetails + loading: boolean +} + +export const useTransactionDetails = (transactionId: string): LoadTransactionDetails => { + const { txLocation } = useContext(TxLocationContext) + const dispatch = useRef(useDispatch()) + const [txDetails, setTxDetails] = useState({ + loading: true, + data: undefined, + }) + const data = useSelector((state: AppReduxState) => + getTransactionDetails(state)({ attributeValue: transactionId, attributeName: 'id', txLocation }), + ) + + useEffect(() => { + if (data) { + setTxDetails({ loading: false, data }) + } else { + // lookup tx details + dispatch.current(fetchTransactionDetails({ transactionId, txLocation })) + } + }, [data, transactionId, txLocation]) + + return txDetails +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionStatus.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionStatus.ts new file mode 100644 index 00000000..299bb0b2 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionStatus.ts @@ -0,0 +1,57 @@ +import { ThemeColors } from '@gnosis.pm/safe-react-components/dist/theme' +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { + isStatusCancelled, + isStatusFailed, + isStatusSuccess, + isStatusWillBeReplaced, + Transaction, +} from 'src/logic/safe/store/models/types/gateway.d' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' + +export type TransactionStatusProps = { + color: ThemeColors + text: string +} + +export const useTransactionStatus = (transaction: Transaction): TransactionStatusProps => { + const currentUser = useSelector(userAccountSelector) + const [status, setStatus] = useState({ color: 'primary', text: '' }) + + useEffect(() => { + if (isStatusSuccess(transaction.txStatus)) { + setStatus({ color: 'primary', text: 'Success' }) + } else if (isStatusFailed(transaction.txStatus)) { + setStatus({ color: 'error', text: 'Failed' }) + } else if (isStatusCancelled(transaction.txStatus)) { + setStatus({ color: 'error', text: 'Cancelled' }) + } else if (isStatusWillBeReplaced(transaction.txStatus)) { + setStatus({ color: 'placeHolder', text: 'Transaction will be replaced' }) + } else { + // AWAITING_EXECUTION, AWAITING_CONFIRMATIONS, PENDING or PENDING_FAILED + let text: string + const signaturePending = addressInList(transaction.executionInfo?.missingSigners) + + switch (transaction.txStatus) { + case 'AWAITING_CONFIRMATIONS': + text = signaturePending(currentUser) ? 'Awaiting your confirmation' : 'Awaiting confirmations' + break + case 'AWAITING_EXECUTION': + text = 'Awaiting execution' + break + case 'PENDING': + case 'PENDING_FAILED': + default: + text = 'Pending' + break + } + + setStatus({ color: 'rinkeby', text }) + } + }, [currentUser, transaction.executionInfo?.missingSigners, transaction.txStatus]) + + return status +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionType.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionType.ts new file mode 100644 index 00000000..61e54268 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionType.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import CustomTxIcon from 'src/routes/safe/components/Transactions/GatewayTransactions/assets/custom.svg' +import IncomingTxIcon from 'src/routes/safe/components/Transactions/GatewayTransactions/assets/incoming.svg' +import OutgoingTxIcon from 'src/routes/safe/components/Transactions/GatewayTransactions/assets/outgoing.svg' +import SettingsTxIcon from 'src/routes/safe/components/Transactions/GatewayTransactions/assets/settings.svg' +import { isCancelTransaction } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' + +export type TxTypeProps = { + icon: string + text: string +} + +export const useTransactionType = (tx: Transaction): TxTypeProps => { + const [type, setType] = useState({ icon: CustomTxIcon, text: 'Custom transaction' }) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + + useEffect(() => { + switch (tx.txInfo.type) { + case 'Creation': { + setType({ icon: SettingsTxIcon, text: 'Safe created' }) + break + } + case 'Transfer': { + const isSendTx = tx.txInfo.direction === 'OUTGOING' + setType({ icon: isSendTx ? OutgoingTxIcon : IncomingTxIcon, text: isSendTx ? 'Send' : 'Receive' }) + break + } + case 'SettingsChange': { + setType({ icon: SettingsTxIcon, text: tx.txInfo.dataDecoded.method }) + break + } + case 'Custom': { + // TODO: is this the only way to identify a 'module' transaction? + if (!tx.executionInfo) { + setType({ icon: SettingsTxIcon, text: 'Module' }) + break + } + + // TODO: isCancel + // there are two 'cancelling' tx identification + // this one is the candidate to remain when the client gateway implements + // https://github.com/gnosis/safe-client-gateway/issues/255 + if (typeof tx.txInfo.isCancellation === 'boolean' && tx.txInfo.isCancellation) { + setType({ icon: CustomTxIcon, text: 'Cancelling transaction' }) + break + } + + // TODO: isCancel + // remove the following condition when issue#255 is implemented + // also remove `isCancelTransaction` function + if (isCancelTransaction({ txInfo: tx.txInfo, safeAddress })) { + setType({ icon: CustomTxIcon, text: 'Cancelling transaction' }) + break + } + + if (tx.safeAppInfo) { + setType({ icon: tx.safeAppInfo.logoUrl, text: tx.safeAppInfo.name }) + break + } + + setType({ icon: CustomTxIcon, text: 'Custom transaction' }) + break + } + } + }, [tx, safeAddress]) + + return type +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx new file mode 100644 index 00000000..910f5aaf --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx @@ -0,0 +1,36 @@ +import { Menu as MenuSrc, Tab } from '@gnosis.pm/safe-react-components' +import { Item } from '@gnosis.pm/safe-react-components/dist/navigation/Tab' +import React, { ReactElement, useState } from 'react' +import styled from 'styled-components' + +import { HistoryTxList } from './HistoryTxList' +import { QueueTransactions } from './QueueTransactions' +import { Breadcrumb, ContentWrapper, Wrapper } from './styled' + +const Menu = styled(MenuSrc)` + justify-content: flex-start; +` + +const items: Item[] = [ + { id: 'queue', label: 'Queue' }, + { id: 'history', label: 'History' }, +] + +const GatewayTransactions = (): ReactElement => { + const [tab, setTab] = useState(items[0].id) + + return ( + + + + + + + {tab === 'queue' && } + {tab === 'history' && } + + + ) +} + +export default GatewayTransactions diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/modals/ApproveTxModal.tsx new file mode 100644 index 00000000..60c18317 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/modals/ApproveTxModal.tsx @@ -0,0 +1,417 @@ +import { List } from 'immutable' +import Checkbox from '@material-ui/core/Checkbox' +import FormControlLabel from '@material-ui/core/FormControlLabel' +import IconButton from '@material-ui/core/IconButton' +import { makeStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import React, { useMemo, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { styles } from './style' + +import Modal from 'src/components/Modal' +import Block from 'src/components/layout/Block' +import Bold from 'src/components/layout/Bold' +import Button from 'src/components/layout/Button' +import Hairline from 'src/components/layout/Hairline' +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { processTransaction } from 'src/logic/safe/store/actions/processTransaction' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +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' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { isThresholdReached } from 'src/routes/safe/components/Transactions/GatewayTransactions/hooks/useTransactionActions' +import { Overwrite } from 'src/types/helpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { makeConfirmation } from 'src/logic/safe/store/models/confirmation' +import { + ExpandedTxDetails, + isMultiSigExecutionDetails, + Operation, + Transaction, +} from 'src/logic/safe/store/models/types/gateway.d' + +const useStyles = makeStyles(styles) + +export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn' +export const REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID = 'reject-tx-modal-submit-btn' + +const getModalTitleAndDescription = (thresholdReached, isCancelTx) => { + const modalInfo = { + title: 'Execute Transaction Rejection', + description: 'This action will execute this transaction.', + } + + if (isCancelTx) { + return modalInfo + } + + if (thresholdReached) { + modalInfo.title = 'Execute Transaction' + modalInfo.description = + 'This action will execute this transaction. A separate Transaction will be performed to submit the execution.' + } else { + modalInfo.title = 'Approve Transaction' + modalInfo.description = + 'This action will approve this transaction. A separate Transaction will be performed to submit the approval.' + } + + return modalInfo +} + +const useTxInfo = (transaction: Props['transaction']) => { + const t = useRef(transaction) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + + const confirmations = useMemo( + () => + t.current.txDetails.detailedExecutionInfo && isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? List( + t.current.txDetails.detailedExecutionInfo.confirmations.map(({ signer, signature }) => + makeConfirmation({ owner: signer, signature }), + ), + ) + : List([]), + [], + ) + + const data = useMemo(() => t.current.txDetails.txData?.hexData ?? EMPTY_DATA, []) + + const baseGas = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.baseGas + : 0, + [], + ) + + const gasPrice = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.gasPrice + : '0', + [], + ) + + const safeTxGas = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.safeTxGas + : 0, + [], + ) + + const gasToken = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.gasToken + : ZERO_ADDRESS, + [], + ) + + const nonce = useMemo(() => t.current.executionInfo?.nonce ?? 0, []) + + const refundReceiver = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.refundReceiver + : ZERO_ADDRESS, + [], + ) + + const safeTxHash = useMemo( + () => + isMultiSigExecutionDetails(t.current.txDetails.detailedExecutionInfo) + ? t.current.txDetails.detailedExecutionInfo.safeTxHash + : EMPTY_DATA, + [], + ) + + const value = useMemo(() => { + switch (t.current.txInfo.type) { + case 'Transfer': + if (t.current.txInfo.transferInfo.type === 'ETHER') { + return t.current.txInfo.transferInfo.value + } else { + return t.current.txDetails.txData?.value ?? '0' + } + break + case 'Custom': + return t.current.txInfo.value + break + case 'Creation': + case 'SettingsChange': + default: + return '0' + break + } + }, []) + + const to = useMemo(() => { + switch (t.current.txInfo.type) { + case 'Transfer': + if (t.current.txInfo.transferInfo.type === 'ETHER') { + return t.current.txInfo.recipient + } else { + return t.current.txInfo.transferInfo.tokenAddress + } + break + case 'Custom': + return t.current.txInfo.to + break + case 'Creation': + case 'SettingsChange': + default: + return safeAddress + break + } + }, [safeAddress]) + + const operation = useMemo(() => t.current.txDetails.txData?.operation ?? Operation.CALL, []) + + const origin = useMemo( + () => + t.current.safeAppInfo ? JSON.stringify({ name: t.current.safeAppInfo.name, url: t.current.safeAppInfo.url }) : '', + [], + ) + + const id = useMemo(() => t.current.id, []) + + return { + confirmations, + data, + baseGas, + gasPrice, + safeTxGas, + gasToken, + nonce, + refundReceiver, + safeTxHash, + value, + to, + operation, + origin, + id, + } +} + +type Props = { + onClose: () => void + canExecute?: boolean + isCancelTx?: boolean + isOpen: boolean + transaction: Overwrite + txParameters: TxParameters +} + +export const ApproveTxModal = ({ + onClose, + canExecute = false, + isCancelTx = false, + isOpen, + transaction, +}: Props): React.ReactElement => { + const dispatch = useDispatch() + const userAddress = useSelector(userAccountSelector) + const classes = useStyles() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [approveAndExecute, setApproveAndExecute] = useState(canExecute) + const thresholdReached = !!(transaction.executionInfo && isThresholdReached(transaction.executionInfo)) + const _threshold = transaction.executionInfo?.confirmationsRequired ?? 0 + const _countingCurrentConfirmation = (transaction.executionInfo?.confirmationsSubmitted ?? 0) + 1 + const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx) + const oneConfirmationLeft = !thresholdReached && _countingCurrentConfirmation === _threshold + const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached + const [manualGasPrice, setManualGasPrice] = useState() + const { + confirmations, + data, + baseGas, + gasPrice, + safeTxGas, + gasToken, + nonce, + refundReceiver, + safeTxHash, + value, + to, + operation, + origin, + id, + } = useTxInfo(transaction) + + const { + gasLimit, + gasPriceFormatted, + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + } = useEstimateTransactionGas({ + txRecipient: to, + txData: data, + txConfirmations: confirmations, + txAmount: value, + preApprovingOwner: approveAndExecute ? userAddress : undefined, + safeTxGas, + operation, + manualGasPrice: manualGasPrice, + }) + + const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) + + const approveTx = (txParameters: TxParameters) => { + dispatch( + processTransaction({ + safeAddress, + tx: { + id, + baseGas, + confirmations, + data, + gasPrice, + gasToken, + nonce, + operation, + origin, + refundReceiver, + safeTxGas, + safeTxHash, + to, + value, + }, + 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 ( + + + {(txParameters, toggleEditMode) => { + return ( + <> + {/* Header */} + + + {title} + + + + + + + + + {/* Tx info */} + + + {description} + + Transaction nonce: +
+ {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/GatewayTransactions/modals/RejectTxModal.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx new file mode 100644 index 00000000..571c0678 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx @@ -0,0 +1,157 @@ +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 { useDispatch, useSelector } from 'react-redux' + +import { styles } from './style' + +import Modal from 'src/components/Modal' +import Block from 'src/components/layout/Block' +import Bold from 'src/components/layout/Bold' +import Button from 'src/components/layout/Button' +import Hairline from 'src/components/layout/Hairline' +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +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/gateway.d' +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) + +type Props = { + isOpen: boolean + onClose: () => void + gwTransaction: Transaction +} + +export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React.ReactElement => { + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const classes = useStyles() + + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + gasLimit, + gasEstimation, + gasPriceFormatted, + } = useEstimateTransactionGas({ + txData: EMPTY_DATA, + txRecipient: safeAddress, + }) + + const origin = gwTransaction.safeAppInfo + ? JSON.stringify({ name: gwTransaction.safeAppInfo.name, url: gwTransaction.safeAppInfo.url }) + : '' + + const nonce = gwTransaction.executionInfo?.nonce ?? 0 + + const sendReplacementTransaction = (txParameters: TxParameters) => { + dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: '0', + txNonce: nonce, + origin, + safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, + ethParameters: txParameters, + notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX, + navigateToTransactionsTab: false, + }), + ) + onClose() + } + + const getParametersStatus = (): ParametersStatus => { + return 'CANCEL_TRANSACTION' + } + + return ( + + + {(txParameters, toggleEditMode) => { + return ( + <> + + + Reject transaction + + + + + + + + + + This action will cancel this transaction. A separate transaction will be performed to submit the + rejection. + + + Transaction nonce: +
+ {nonce} +
+
+ {/* Tx Parameters */} + + + + +
+ + + + + + ) + }} +
+
+ ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/modals/style.ts b/src/routes/safe/components/Transactions/GatewayTransactions/modals/style.ts new file mode 100644 index 00000000..a4440670 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/modals/style.ts @@ -0,0 +1,33 @@ +import { createStyles } from '@material-ui/core' +import { border, lg, md, sm } from 'src/theme/variables' + +export const styles = createStyles({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'space-between', + boxSizing: 'border-box', + height: '74px', + }, + headingText: { + fontSize: lg, + }, + closeIcon: { + height: '35px', + width: '35px', + }, + container: { + padding: `${md} ${lg}`, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + position: 'relative', + bottom: 0, + width: '100%', + borderTop: `1px solid ${border}`, + }, + nonceNumber: { + marginTop: sm, + fontSize: md, + }, +}) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx new file mode 100644 index 00000000..15767db3 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx @@ -0,0 +1,484 @@ +import { + Text, + Accordion, + AccordionDetails, + AccordionSummary, + EthHashInfo, + IconText, +} from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; +` + +export const Breadcrumb = styled(IconText)` + p { + font-weight: bold; + } +` + +export const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + align-items: center; +` + +export const ColumnDisplayAccordionDetails = styled(AccordionDetails)` + flex-flow: column; +` + +export const NoPaddingAccordion = styled(Accordion)` + &.MuiAccordion-root { + background-color: transparent; + + .MuiAccordionDetails-root { + padding: 0; + } + } +` + +export const ActionAccordion = styled(Accordion)` + &.MuiAccordion-root { + &:first-child { + border-top: none; + } + + &.Mui-expanded { + &:last-child { + border-bottom: none; + } + } + + .MuiAccordionDetails-root { + padding: 16px; + } + } +` + +export const StyledTransactionsGroup = styled.div` + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 16px 8px; + width: 98%; +` + +export const H2 = styled.h2` + text-transform: uppercase; + font-size: smaller; +` + +export const SubTitle = styled(Text)` + margin-bottom: 8px; + + font-size: 0.76em; + font-weight: 600; + line-height: 1.5; + letter-spacing: 1px; + color: ${({ theme }) => theme.colors.placeHolder}; + text-transform: uppercase; +` + +export const StyledTransactions = styled.div` + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 8px; + box-shadow: #00000026 0 4px 12px 0; + overflow: hidden; + width: 100%; + + // '^' to prevent applying rules to the 'Actions' accordion components + & > .MuiAccordion-root { + &:first-child { + border-top: none; + } + + &:last-child { + border-bottom: none; + } + } +` + +export const GroupedTransactionsCard = styled(StyledTransactions)` + transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + background-color: transparent; + border-radius: 0; + box-shadow: none; + + &:not(:last-child) { + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + } + + .MuiAccordion-root, + .MuiAccordionSummary-root, + .MuiAccordionDetails-root { + background-color: transparent; + + &:hover, + &.Mui-expanded { + background-color: transparent; + } + } + + &:hover { + background-color: ${({ theme }) => theme.colors.background}; + + .MuiAccordionDetails-root { + div[class^='tx-'] { + background-color: ${({ theme }) => theme.colors.background}; + } + } + + .disclaimer-container { + background-color: ${({ theme }) => theme.colors.inputField}; + } + } +` +const gridColumns = { + nonce: '0.75fr', + type: '3fr', + info: '3fr', + time: '1.5fr', + votes: '1.5fr', + actions: '1fr', + status: '3fr', +} + +export const WillBeReplaced = styled.div` + .will-be-replaced * { + color: gray !important; + text-decoration: line-through !important; + filter: grayscale(1) opacity(0.8) !important; + } +` + +export const StyledTransaction = styled(WillBeReplaced)` + display: grid; + grid-template-columns: ${Object.values(gridColumns).join(' ')}; + width: 100%; + + & > div { + align-self: center; + } + + .tx-actions { + visibility: hidden; + justify-self: end; + } + + .tx-status { + justify-self: end; + margin-right: 8px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: end; + + p { + margin-left: 8px; + } + } + + &:hover { + .tx-actions { + visibility: visible; + + &.will-be-replaced { + visibility: hidden; + } + } + } +` + +export const StyledGroupedTransactions = styled(StyledTransaction)` + // no \`tx-nonce\` column required + grid-template-columns: ${Object.values(gridColumns).slice(1).join(' ')}; +` + +export const GroupedTransactions = styled(StyledTransaction)` + // add a bottom division line for all elements but the last + &:not(:last-of-type) { + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + } + + // builds the tree-view layout + .tree-lines { + height: 100%; + margin-left: 50%; + position: relative; + width: 30%; + + // this is a special case, the first element in the list needs to have a block child component + // add tree lines line to the first item of the list + .first-node { + display: block; + position: absolute; + top: -16px; + width: 100%; + + &::before { + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + border-left: 2px solid ${({ theme }) => theme.colors.separator}; + content: ''; + height: 22px; + position: absolute; + top: 8px; + width: 100%; + } + } + + // add tree lines to all elements of the list (except for the last one) + // :last-of-type won't work with classes selector (HTML elements only) + // as we need block-level elements, we're using paragraphs for .tree-lines and .first-node + // given that divs are already being used for the transaction row, and both (p and div) are siblings + &:not(:last-of-type) { + &::before { + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + border-left: 2px solid ${({ theme }) => theme.colors.separator}; + content: ''; + height: 100%; + margin-top: 14px; + position: absolute; + width: 100%; + } + } + } + + // overrides Accordion styles, as grouped txs behave differently + > .MuiAccordion-root { + transition: none; + border: 0; + grid-column-end: span 6; + grid-column-start: 2; + + &:first-child { + border: 0; + } + + &.Mui-expanded { + justify-self: center; + width: calc(100% - 32px); + + &:not(:last-of-type) { + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + } + + &:not(:first-of-type) { + border-top: 2px solid ${({ theme }) => theme.colors.separator}; + // if two consecutive accordions are expanded, borders will get duplicated + // this rule is to overlap them + margin-top: -2px; + } + + > .MuiAccordionSummary-root { + padding: 0; + } + } + } +} +` + +export const DisclaimerContainer = styled(StyledTransaction)` + background-color: ${({ theme }) => theme.colors.inputField} !important; + border-radius: 4px; + margin: 12px 8px 0 12px; + padding: 8px; + width: calc(100% - 40px); + + .nonce { + grid-column-start: 1; + } + + .disclaimer { + grid-column-start: 2; + grid-column-end: span 6; + } +` + +export const TxDetailsContainer = styled(WillBeReplaced)` + background-color: ${({ theme }) => theme.colors.separator} !important; + column-gap: 2px; + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: minmax(min-content, max-content); + grid-template-rows: [tx-summary] minmax(min-content, max-content) [tx-details] minmax(100px, 1fr); + row-gap: 2px; + width: 100%; + + & > div { + background-color: ${({ theme }) => theme.colors.white}; + line-break: anywhere; + overflow: hidden; + padding: 20px 24px; + word-break: break-all; + } + + .tx-summary { + } + + .tx-details { + &.not-executed { + grid-row-end: span 2; + } + + &.no-padding { + padding: 0; + } + } + + .tx-owners { + padding: 24px; + grid-column-start: 2; + grid-row-end: span 2; + grid-row-start: 1; + + &.no-owner { + grid-row-end: span 3; + } + } + + .tx-details-actions { + align-items: center; + display: flex; + height: 60px; + justify-content: center; + + button { + color: ${({ theme }) => theme.colors.white}; + margin: 0 8px; + + &:hover { + color: ${({ theme }) => theme.colors.white}; + } + + &.error { + background-color: ${({ theme }) => theme.colors.error}; + + &:hover { + background-color: ${({ theme }) => theme.colors.errorHover}; + } + } + + &.primary { + background-color: ${({ theme }) => theme.colors.primary}; + + &:hover { + background-color: ${({ theme }) => theme.colors.secondary}; + } + } + } + } +` + +export const OwnerList = styled.ul` + list-style: none; + margin: 0; + padding-left: 6px; + + .legend { + left: 15px; + padding-bottom: 0.86em; + position: relative; + top: -3px; + + .owner-info { + margin: 5px; + } + + span { + color: #008c73; + font-weight: bold; + } + } + + ul { + margin-top: 0; + } + + .icon { + left: -7px; + position: absolute; + width: 16px; + z-index: 2; + } +` + +export const OwnerListItem = styled.li` + display: flex; + position: relative; + + &::before { + border-left: 2px ${({ theme }) => theme.colors.icon} solid; + border-radius: 1px; + content: ''; + height: calc(100% - 16px); + top: 16px; + left: 0; + position: absolute; + z-index: 1; + } + + &:last-child::before { + border-left: none; + } +` + +export const InlineEthHashInfo = styled(EthHashInfo)` + display: inline-flex; +` + +export const StyledScrollableBar = styled.div` + &::-webkit-scrollbar { + width: 0.7em; + scroll-behavior: smooth; + } + + &::-webkit-scrollbar-track { + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 20px; + } + + &::-webkit-scrollbar-thumb { + background-color: darkgrey; + outline: 1px solid #dadada; + border-radius: 20px; + } + + // firefox experimental + scrollbar-color: darkgrey #dadada; + scrollbar-width: thin; +` + +export const ScrollableTransactionsContainer = styled(StyledScrollableBar)` + height: calc(100vh - 225px); + overflow-x: hidden; + overflow-y: auto; + width: 100%; +` +export const Centered = styled.div<{ padding?: number }>` + width: 100%; + height: 100%; + display: flex; + padding: ${({ padding }) => `${padding}px`}; + justify-content: center; + align-items: center; +` + +export const StyledAccordionSummary = styled(AccordionSummary)` + height: 52px; + .tx-nonce { + margin: 0 16px 0 8px; + } +` +export const AlignItemsWithMargin = styled.div` + display: flex; + align-items: center; + + span:first-child { + margin-right: 6px; + } +` diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts b/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts new file mode 100644 index 00000000..ac3f4b2c --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts @@ -0,0 +1,142 @@ +import { BigNumber } from 'bignumber.js' +import format from 'date-fns/format' + +import { getNetworkInfo } from 'src/config' +import { + Custom, + isCustomTxInfo, + isTransferTxInfo, + Transaction, + TransactionInfo, + Transfer, +} from 'src/logic/safe/store/models/types/gateway.d' +import { SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' + +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { sameAddress } from 'src/logic/wallets/ethAddresses' +import { sameString } from 'src/utils/strings' + +export const TX_TABLE_ID = 'id' +export const TX_TABLE_TYPE_ID = 'type' +export const TX_TABLE_DATE_ID = 'date' +export const TX_TABLE_AMOUNT_ID = 'amount' +export const TX_TABLE_STATUS_ID = 'status' +export const TX_TABLE_RAW_TX_ID = 'tx' +export const TX_TABLE_RAW_CANCEL_TX_ID = 'cancelTx' +export const TX_TABLE_EXPAND_ICON = 'expand' + +export const formatTime = (timestamp: number): string => format(timestamp, 'h:mm a') + +export const formatDateTime = (timestamp: number): string => format(timestamp, 'MMM d, yyyy - h:mm:ss a') + +export const NOT_AVAILABLE = 'n/a' + +interface AmountData { + decimals?: number | string + symbol?: string + value: number | string +} + +const getAmountWithSymbol = ( + { decimals = 0, symbol = NOT_AVAILABLE, value }: AmountData, + formatted = false, +): string => { + const nonFormattedValue = new BigNumber(value).times(`1e-${decimals}`).toFixed() + const finalValue = formatted ? formatAmount(nonFormattedValue).toString() : nonFormattedValue + const txAmount = finalValue === 'NaN' ? NOT_AVAILABLE : finalValue + + return `${txAmount} ${symbol}` +} + +export const getTxAmount = (txInfo?: TransactionInfo, formatted = true): string => { + if (!txInfo || !isTransferTxInfo(txInfo)) { + return NOT_AVAILABLE + } + + switch (txInfo.transferInfo.type) { + case 'ERC20': + return getAmountWithSymbol( + { + decimals: `${txInfo.transferInfo.decimals ?? 0}`, + symbol: `${txInfo.transferInfo.tokenSymbol ?? NOT_AVAILABLE}`, + value: txInfo.transferInfo.value, + }, + formatted, + ) + case 'ERC721': + // simple workaround to avoid displaying unexpected values for incoming NFT transfer + return `1 ${txInfo.transferInfo.tokenSymbol}` + case 'ETHER': { + const { nativeCoin } = getNetworkInfo() + return getAmountWithSymbol( + { + decimals: nativeCoin.decimals, + symbol: nativeCoin.symbol, + value: txInfo.transferInfo.value, + }, + formatted, + ) + } + default: + return NOT_AVAILABLE + } +} + +const { nativeCoin } = getNetworkInfo() + +type txTokenData = { + address: string + value: string + decimals: number | null +} + +export const getTxTokenData = (txInfo: Transfer): txTokenData => { + switch (txInfo.transferInfo.type) { + case 'ERC20': + return { + address: txInfo.transferInfo.tokenAddress, + value: txInfo.transferInfo.value, + decimals: txInfo.transferInfo.decimals, + } + case 'ERC721': + return { address: txInfo.transferInfo.tokenAddress, value: txInfo.transferInfo.value, decimals: 0 } + default: + return { address: nativeCoin.address, value: txInfo.transferInfo.value, decimals: nativeCoin.decimals } + } +} + +export interface TableData { + amount: string + cancelTx?: Transaction + date: string + dateOrder?: number + id: string + status: string + tx: Transaction | SafeModuleTransaction + type: any +} + +// TODO: isCancel +// how can we be sure that it's a cancel tx without asking for tx-details? +// can the client-gateway service provide info about the tx, Like: `isCancelTransaction: boolean`? +// it will be solved as part of: https://github.com/gnosis/safe-client-gateway/issues/255 +export const isCancelTransaction = ({ txInfo, safeAddress }: { txInfo: Custom; safeAddress: string }): boolean => + sameAddress(txInfo.to, safeAddress) && + sameString(txInfo.dataSize, '0') && + sameString(txInfo.value, '0') && + txInfo.methodName === null + +type IsCancelTxDetailsProps = { + executedAt: number | null + txInfo: Transaction['txInfo'] + safeAddress: string +} +export const isCancelTxDetails = ({ executedAt, txInfo, safeAddress }: IsCancelTxDetailsProps): boolean => + !executedAt && + // custom transaction + isCustomTxInfo(txInfo) && + // verify that it's a cancel tx based on it's info + isCancelTransaction({ safeAddress, txInfo }) + +export const addressInList = (list: string[] = []) => (address: string): boolean => + list.some((ownerAddress) => sameAddress(ownerAddress, address)) 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 cf7c17ea..9b9431b5 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx @@ -109,7 +109,7 @@ export const ApproveTxModal = ({ dispatch( processTransaction({ safeAddress, - tx, + tx: tx as any, userAddress, notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX, approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted, 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 6d886f74..8198ee61 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx @@ -15,7 +15,7 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' +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' diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx index 09cc90c1..ed2a11d9 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx @@ -195,7 +195,7 @@ const MultiSendCustomData = ({ txDetails }: { txDetails: MultiSendDetails[] }): ) } -const TxData = ({ data }: { data: string }): React.ReactElement => { +export const TxData = ({ data }: { data: string }): React.ReactElement => { const classes = useStyles() const [showTxData, setShowTxData] = React.useState(false) const showExpandBtn = data.length > 20 diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx index 9bc97b09..2d7e5274 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx @@ -32,9 +32,9 @@ const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactEle {(value as string[]).map((currentValue, index) => { const key = `${parentId}-value-${index}` return ( -
+ {Array.isArray(currentValue) ? getArrayValue(key, currentValue) : getTextValue(currentValue)} -
+ ) })} @@ -52,7 +52,9 @@ const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactEle const Value = ({ type, ...props }: RenderValueProps): React.ReactElement => { const explorerUrl = getExplorerInfo(props.value as string) if (isAddress(type)) { - return + return ( + + ) } return diff --git a/src/routes/safe/components/Transactions/TxsTable/Status/assets/awaiting.svg b/src/routes/safe/components/Transactions/TxsTable/Status/assets/awaiting.svg deleted file mode 100644 index 3d4c6355..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/Status/assets/awaiting.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/routes/safe/components/Transactions/TxsTable/Status/assets/error.svg b/src/routes/safe/components/Transactions/TxsTable/Status/assets/error.svg deleted file mode 100644 index 2c387fe7..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/Status/assets/error.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/routes/safe/components/Transactions/TxsTable/Status/assets/ok.svg b/src/routes/safe/components/Transactions/TxsTable/Status/assets/ok.svg deleted file mode 100644 index 97759387..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/Status/assets/ok.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/routes/safe/components/Transactions/TxsTable/Status/index.tsx b/src/routes/safe/components/Transactions/TxsTable/Status/index.tsx deleted file mode 100644 index ea82c9de..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/Status/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import CircularProgress from '@material-ui/core/CircularProgress' -import React, { ReactElement } from 'react' - -import AwaitingIcon from './assets/awaiting.svg' -import ErrorIcon from './assets/error.svg' -import OkIcon from './assets/ok.svg' -import { useStyles } from './style' - -import Block from 'src/components/layout/Block' -import Img from 'src/components/layout/Img' -import Paragraph from 'src/components/layout/Paragraph/' - -const statusToIcon = { - success: OkIcon, - cancelled: ErrorIcon, - failed: ErrorIcon, - awaiting_your_confirmation: AwaitingIcon, - awaiting_confirmations: AwaitingIcon, - awaiting_execution: AwaitingIcon, - pending: , -} as const - -const statusToLabel = { - success: 'Success', - cancelled: 'Cancelled', - failed: 'Failed', - awaiting_your_confirmation: 'Awaiting your confirmation', - awaiting_confirmations: 'Awaiting confirmations', - awaiting_execution: 'Awaiting execution', - pending: 'Pending', -} as const - -const statusIconStyle = { - height: '14px', - width: '14px', -} - -const Status = ({ status }: { status: keyof typeof statusToLabel }): ReactElement => { - const classes = useStyles() - const Icon: typeof statusToIcon[keyof typeof statusToIcon] = statusToIcon[status] - - return ( - - {typeof Icon === 'object' ? Icon : {statusToLabel[status]}} - - {statusToLabel[status]} - - - ) -} - -export default Status diff --git a/src/routes/safe/components/Transactions/TxsTable/Status/style.ts b/src/routes/safe/components/Transactions/TxsTable/Status/style.ts deleted file mode 100644 index 88dd4e5c..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/Status/style.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core/styles' -import { boldFont, disabled, error, extraSmallFontSize, lg, secondary, sm } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - container: { - display: 'flex', - fontSize: extraSmallFontSize, - fontWeight: boldFont, - padding: sm, - alignItems: 'center', - boxSizing: 'border-box', - height: lg, - marginTop: sm, - marginBottom: sm, - borderRadius: '3px', - }, - success: { - backgroundColor: '#A1D2CA', - color: secondary, - }, - cancelled: { - backgroundColor: 'transparent', - color: error, - border: `1px solid ${error}`, - }, - failed: { - backgroundColor: 'transparent', - color: error, - border: `1px solid ${error}`, - }, - awaiting_your_confirmation: { - backgroundColor: '#d4d5d3', - color: disabled, - }, - awaiting_confirmations: { - backgroundColor: '#d4d5d3', - color: disabled, - }, - awaiting_execution: { - backgroundColor: '#d4d5d3', - color: disabled, - }, - pending: { - backgroundColor: '#fff3e2', - color: '#e8673c', - }, - statusText: { - padding: '0 7px', - }, - }), -) diff --git a/src/routes/safe/components/Transactions/TxsTable/index.tsx b/src/routes/safe/components/Transactions/TxsTable/index.tsx deleted file mode 100644 index 0e5b2687..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/index.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Collapse from '@material-ui/core/Collapse' -import IconButton from '@material-ui/core/IconButton' -import TableCell from '@material-ui/core/TableCell' -import TableContainer from '@material-ui/core/TableContainer' -import TableRow from '@material-ui/core/TableRow' -import { makeStyles } from '@material-ui/core/styles' -import ExpandLess from '@material-ui/icons/ExpandLess' -import ExpandMore from '@material-ui/icons/ExpandMore' -import cn from 'classnames' -import React, { useState, useEffect } from 'react' -import { useSelector } from 'react-redux' - -import { ExpandedTx } from './ExpandedTx' -import Status from './Status' -import { TX_TABLE_ID, generateColumns, getTxTableData } from './columns' -import { styles } from './style' - -import Table from 'src/components/Table' -import { cellWidth } from 'src/components/Table/TableHead' -import Block from 'src/components/layout/Block' -import Row from 'src/components/layout/Row' -import { safeCancellationTransactionsSelector } from 'src/logic/safe/store/selectors' -import { extendedTransactionsSelector } from 'src/logic/safe/store/selectors/transactions' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' - -export const TRANSACTION_ROW_TEST_ID = 'transaction-row' - -const useStyles = makeStyles(styles) - -const TxsTable = (): React.ReactElement => { - const classes = useStyles() - const [expandedTx, setExpandedTx] = useState(null) - const cancellationTransactions = useSelector(safeCancellationTransactionsSelector) - const transactions = useSelector(extendedTransactionsSelector) - const { trackEvent } = useAnalytics() - - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Transactions' }) - }, [trackEvent]) - - const handleTxExpand = (rowId) => { - setExpandedTx((prevRowId) => (prevRowId === rowId ? null : rowId)) - } - - const columns = generateColumns() - const autoColumns = columns.filter((c) => !c.custom) - const filteredData = getTxTableData(transactions, cancellationTransactions) - .sort((tx1, tx2) => { - // First order by nonce - const aNonce = Number(tx1.tx?.nonce) - const bNonce = Number(tx1.tx?.nonce) - if (aNonce && bNonce) { - const difference = aNonce - bNonce - if (difference !== 0) { - return difference - } - } - // If can't be ordered by nonce, order by date - const aDateOrder = tx1.dateOrder - const bDateOrder = tx2.dateOrder - // Second by date - if (!aDateOrder || !bDateOrder) { - return 0 - } - return aDateOrder - bDateOrder - }) - .map((tx, id) => { - return { - ...tx, - id, - } - }) - - return ( - - - - {(sortedData) => - sortedData.map((row) => { - const rowId = `${row.tx.safeTxHash}-${row.tx.type}` - return ( - - handleTxExpand(rowId)} - tabIndex={-1} - > - {autoColumns.map((column) => ( - - {row[column.id]} - - ))} - - - - - - - {expandedTx === rowId ? : } - - - - - - - - - - - ) - }) - } -
-
-
- ) -} - -export default TxsTable diff --git a/src/routes/safe/components/Transactions/TxsTable/style.ts b/src/routes/safe/components/Transactions/TxsTable/style.ts deleted file mode 100644 index 06c73255..00000000 --- a/src/routes/safe/components/Transactions/TxsTable/style.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createStyles } from '@material-ui/core' - -export const styles = createStyles({ - container: { - marginTop: '56px', - }, - row: { - cursor: 'pointer', - '&:hover': { - backgroundColor: '#fff3e2', - }, - }, - expandedRow: { - backgroundColor: '#fff3e2', - }, - cancelledRow: { - opacity: 0.4, - }, - extendedTxContainer: { - padding: 0, - border: 0, - '&:last-child': { - padding: 0, - }, - backgroundColor: '#fffaf4', - }, - actions: { - display: 'flex', - justifyContent: 'flex-end', - }, - expandCellStyle: { - paddingLeft: 0, - paddingRight: 15, - }, -}) diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index b6cb67de..6783ca00 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -20,7 +20,7 @@ export const TRANSACTIONS_TAB_NEW_BTN_TEST_ID = 'transactions-tab-new-btn' const Apps = React.lazy(() => import('../components/Apps')) const Settings = React.lazy(() => import('../components/Settings')) const Balances = React.lazy(() => import('../components/Balances')) -const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/TxsTable')) +const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/GatewayTransactions')) const AddressBookTable = React.lazy(() => import('src/routes/safe/components/AddressBook')) const Container = (): React.ReactElement => { diff --git a/src/routes/safe/container/selector.ts b/src/routes/safe/container/selector.ts index 0c8e0a42..3c64cd3d 100644 --- a/src/routes/safe/container/selector.ts +++ b/src/routes/safe/container/selector.ts @@ -2,9 +2,9 @@ import { List, Map } from 'immutable' import { createSelector } from 'reselect' import { Token } from 'src/logic/tokens/store/model/token' -import { tokenListSelector, tokensSelector } from 'src/logic/tokens/store/selectors' +import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' -import { isUserAnOwner, sameAddress } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { @@ -17,12 +17,12 @@ import { SafeRecord } from 'src/logic/safe/store/models/safe' import { AppReduxState } from 'src/store' import { MODULE_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/moduleTransactions' import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions' -import { - SafeModuleTransaction, - TransactionStatus, - TransactionTypes, -} from 'src/logic/safe/store/models/types/transaction' -import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +// import { +// SafeModuleTransaction, +// TransactionStatus, +// TransactionTypes, +// } from 'src/logic/safe/store/models/types/transaction' +// import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' export const grantedSelector = createSelector( userAccountSelector, @@ -90,39 +90,39 @@ export const modulesTransactionsBySafeSelector = createSelector( }, ) -export const safeModuleTransactionsSelector = createSelector( - tokenListSelector, - modulesTransactionsBySafeSelector, - (tokens, safeModuleTransactions): SafeModuleTransaction[] => { - return safeModuleTransactions.map((moduleTx) => { - // if not spendingLimit module tx, then it's an generic module tx - const type = sameAddress(moduleTx.module, SPENDING_LIMIT_MODULE_ADDRESS) - ? TransactionTypes.SPENDING_LIMIT - : TransactionTypes.MODULE - - // TODO: this is strictly attached to Spending Limit Module. - // This has to be moved nearest the module info rendering. - // add token info to the model, so data can be properly displayed in the UI - let tokenInfo - if (type === TransactionTypes.SPENDING_LIMIT) { - if (moduleTx.data) { - // if `data` is defined, then it's a token transfer - tokenInfo = tokens.find(({ address }) => sameAddress(address, moduleTx.to)) - } else { - // if `data` is not defined, then it's an ETH transfer - // ETH does not exist in the list of tokens, so we recreate the record here - tokenInfo = getEthAsToken(0) - } - } - - return { - ...moduleTx, - safeTxHash: moduleTx.transactionHash, - executionTxHash: moduleTx.transactionHash, - status: TransactionStatus.SUCCESS, - tokenInfo, - type, - } - }) - }, -) +// export const safeModuleTransactionsSelector = createSelector( +// tokenListSelector, +// modulesTransactionsBySafeSelector, +// (tokens, safeModuleTransactions): SafeModuleTransaction[] => { +// return safeModuleTransactions.map((moduleTx) => { +// // if not spendingLimit module tx, then it's an generic module tx +// const type = sameAddress(moduleTx.module, SPENDING_LIMIT_MODULE_ADDRESS) +// ? TransactionTypes.SPENDING_LIMIT +// : TransactionTypes.MODULE +// +// // TODO: this is strictly attached to Spending Limit Module. +// // This has to be moved nearest the module info rendering. +// // add token info to the model, so data can be properly displayed in the UI +// let tokenInfo +// if (type === TransactionTypes.SPENDING_LIMIT) { +// if (moduleTx.data) { +// // if `data` is defined, then it's a token transfer +// tokenInfo = tokens.find(({ address }) => sameAddress(address, moduleTx.to)) +// } else { +// // if `data` is not defined, then it's an ETH transfer +// // ETH does not exist in the list of tokens, so we recreate the record here +// tokenInfo = getEthAsToken(0) +// } +// } +// +// return { +// ...moduleTx, +// safeTxHash: moduleTx.transactionHash, +// executionTxHash: moduleTx.transactionHash, +// status: TransactionStatus.SUCCESS, +// tokenInfo, +// type, +// } +// }) +// }, +// ) diff --git a/src/store/index.ts b/src/store/index.ts index 857b8819..037d71df 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,4 @@ -import { Map } from 'immutable' +import { List, Map } from 'immutable' import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router' import { createHashHistory } from 'history' import { applyMiddleware, combineReducers, compose, createStore, CombinedState, PreloadedState, Store } from 'redux' @@ -22,7 +22,11 @@ import currentSession, { CURRENT_SESSION_REDUCER_ID, CurrentSessionState, } from 'src/logic/currentSession/store/reducer/currentSession' +import { Notification } from 'src/logic/notifications' import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications' +import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { StoreStructure } from 'src/logic/safe/store/models/types/gateway' +import { gatewayTransactions, GATEWAY_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/gatewayTransactions' import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' import provider, { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' @@ -69,6 +73,7 @@ const reducers = combineReducers({ [NFT_ASSETS_REDUCER_ID]: nftAssetReducer, [NFT_TOKENS_REDUCER_ID]: nftTokensReducer, [TOKEN_REDUCER_ID]: tokens, + [GATEWAY_TRANSACTIONS_ID]: gatewayTransactions, [TRANSACTIONS_REDUCER_ID]: transactions, [CANCELLATION_TRANSACTIONS_REDUCER_ID]: cancellationTransactions, [INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions, @@ -87,11 +92,12 @@ export type AppReduxState = CombinedState<{ [NFT_ASSETS_REDUCER_ID]: NFTAssets [NFT_TOKENS_REDUCER_ID]: NFTTokens [TOKEN_REDUCER_ID]: TokenState - [TRANSACTIONS_REDUCER_ID]: Map + [GATEWAY_TRANSACTIONS_ID]: Record + [TRANSACTIONS_REDUCER_ID]: Map> [CANCELLATION_TRANSACTIONS_REDUCER_ID]: CancellationTxState - [INCOMING_TRANSACTIONS_REDUCER_ID]: Map + [INCOMING_TRANSACTIONS_REDUCER_ID]: Map> [MODULE_TRANSACTIONS_REDUCER_ID]: ModuleTransactionsState - [NOTIFICATIONS_REDUCER_ID]: Map + [NOTIFICATIONS_REDUCER_ID]: Map [CURRENCY_VALUES_KEY]: CurrencyValuesState [COOKIES_REDUCER_ID]: Map [ADDRESS_BOOK_REDUCER_ID]: AddressBookState diff --git a/src/test/safe.dom.funds.thresholdGt1.ts b/src/test/safe.dom.funds.thresholdGt1.ts index 7331aba4..af45d955 100644 --- a/src/test/safe.dom.funds.thresholdGt1.ts +++ b/src/test/safe.dom.funds.thresholdGt1.ts @@ -1,4 +1,4 @@ -// +// import { fireEvent, waitForElement } from '@testing-library/react' import { aNewStore } from 'src/store' import { aMinedSafe } from 'src/test/builder/safe.redux.builder' @@ -8,12 +8,11 @@ import { getWeb3, getBalanceInEtherOf } from 'src/logic/wallets/getWeb3' import { sleep } from 'src/utils/timer' import '@testing-library/jest-dom/extend-expect' import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' -import { fillAndSubmitSendFundsForm } from './utils/transactions' +import { fillAndSubmitSendFundsForm, TRANSACTION_ROW_TEST_ID } from './utils/transactions' import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/container' -import { TRANSACTION_ROW_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable' import { useTestAccountAt, resetTestAccount } from './utils/accounts' -//import { CONFIRM_TX_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow' import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal' +import { CONFIRM_TX_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent' afterEach(resetTestAccount) @@ -57,8 +56,8 @@ describe('DOM > Feature > Sending Funds', () => { fireEvent.click(txRows[0]) - //const confirmBtn = await waitForElement(() => SafeDom.getByTestId(CONFIRM_TX_BTN_TEST_ID)) - //fireEvent.click(confirmBtn) + const confirmBtn = await waitForElement(() => SafeDom.getByTestId(CONFIRM_TX_BTN_TEST_ID)) + fireEvent.click(confirmBtn) // Travel confirm modal const approveTxBtn = await waitForElement(() => SafeDom.getByTestId(APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID)) diff --git a/src/test/utils/transactions/transactionList.helper.ts b/src/test/utils/transactions/transactionList.helper.ts index 0483c950..c9e47df4 100644 --- a/src/test/utils/transactions/transactionList.helper.ts +++ b/src/test/utils/transactions/transactionList.helper.ts @@ -2,7 +2,6 @@ import { fireEvent } from '@testing-library/react' import { sleep } from 'src/utils/timer' import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/container' -import { TRANSACTION_ROW_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable' import { TRANSACTIONS_DESC_SEND_TEST_ID, } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription' @@ -11,6 +10,8 @@ import { TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID, } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription' +export const TRANSACTION_ROW_TEST_ID = 'transaction-row' + export const getLastTransaction = async (SafeDom) => { // Travel to transactions const transactionsBtn = SafeDom.getByTestId(TRANSACTIONS_TAB_BTN_TEST_ID) diff --git a/src/types/helpers.d.ts b/src/types/helpers.d.ts new file mode 100644 index 00000000..31b9483d --- /dev/null +++ b/src/types/helpers.d.ts @@ -0,0 +1 @@ +export type Overwrite = Pick> & U diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 57e6b3dd..c9fe05a3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -27,3 +27,6 @@ export const EXCHANGE_RATE_URL_FALLBACK = 'https://api.coinbase.com/v2/exchange- export const IPFS_GATEWAY = process.env.REACT_APP_IPFS_GATEWAY export const SPENDING_LIMIT_MODULE_ADDRESS = process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134' +export const KNOWN_MODULES = { + [SPENDING_LIMIT_MODULE_ADDRESS]: 'Spending limit', +} diff --git a/src/utils/date.ts b/src/utils/date.ts index 931759f6..4e531138 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -11,3 +11,9 @@ export const relativeTime = (baseTimeMin: string, resetTimeMin: string): string return formatRelative(nextResetTimeMilliseconds, Date.now()) } + +export const getUTCStartOfDate = (timestamp: number): number => { + const date = new Date(timestamp) + + return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0) +} diff --git a/src/utils/objects.ts b/src/utils/objects.ts new file mode 100644 index 00000000..9099bce5 --- /dev/null +++ b/src/utils/objects.ts @@ -0,0 +1,11 @@ +export const sortObject = (obj: Record, order = 'asc'): any => { + return Object.keys(obj) + .sort((a: string, b: string) => Number(order === 'desc' ? b : a) - Number(order === 'desc' ? a : b)) + .reduce( + (acc, key) => ({ + ...acc, + [key]: obj[key], + }), + {}, + ) +} diff --git a/yarn.lock b/yarn.lock index 433da77b..09c3a699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1554,9 +1554,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#420f595": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#8dea3a6": version "0.4.0" - resolved "https://github.com/gnosis/safe-react-components.git#420f59570a8d2e8da0acd7d8c192dc9f1bf7bf34" + resolved "https://github.com/gnosis/safe-react-components.git#8dea3a6abedba736b5504927892eac46a0603c6d" dependencies: classnames "^2.2.6" polished "^3.6.7" @@ -1817,11 +1817,16 @@ rxjs "^6.6.3" semver "^7.3.4" -"@ledgerhq/errors@^5.34.0", "@ledgerhq/errors@^5.36.0", "@ledgerhq/errors@^5.41.0": +"@ledgerhq/errors@^5.34.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/errors@^5.36.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.39.0.tgz#1fc392af94c1c1dc12d5e865b44f34bdfbc37503" + integrity sha512-N4/0NO2qgRlF+6QG9a4kGurmi2p6rcYyWeJb4QxU8ypGqBgzMBYjx0LM68JfGFAN5niAuqHZtDZ87EPSrXyu6Q== + "@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" @@ -3305,6 +3310,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash.get@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" + integrity sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ== + dependencies: + "@types/lodash" "*" + "@types/lodash.memoize@^4.1.6": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" @@ -3467,6 +3479,11 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/redux-actions@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.1.tgz#0940e97fa35ad3004316bddb391d8e01d2efa605" + integrity sha512-zKgK+ATp3sswXs6sOYo1tk8xdXTy4CTaeeYrVQlClCjeOpag5vzPo0ASWiiBJ7vsiQRAdb3VkuFLnDoBimF67g== + "@types/resolve@0.0.8", "@types/resolve@^0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -14461,6 +14478,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" + integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== + object-inspect@^1.8.0, object-inspect@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" @@ -16666,6 +16688,13 @@ react-hotkeys@2.0.0: dependencies: prop-types "^15.6.1" +react-infinite-scroll-component@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-5.1.0.tgz#3c0043cd17c6857c25d3ca7aa4171e0c33ce8023" + integrity sha512-ZQ7lYlLByil1ZVdvt57UJeBIj/tSOwKyds+B0LIm/xGmxzJJZwW/hYGKKs74sHO3LoIDwcq2ci6YXqrkWSalLQ== + dependencies: + throttle-debounce "^2.1.0" + react-inspector@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8" From 4b089f349053bd7ed9b964494318c81cb42b2952 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 10 Feb 2021 16:39:18 -0300 Subject: [PATCH 13/34] Add relative time in queue lists (#1878) * Add relative time in queue lists * Add all date-fns functions to src/utils/date Co-authored-by: Daniel Sanchez --- .../GatewayTransactions/HistoryTxList.tsx | 4 ++-- .../Transactions/GatewayTransactions/TxCollapsed.tsx | 9 ++++++--- .../GatewayTransactions/TxInfoCreation.tsx | 3 ++- .../Transactions/GatewayTransactions/TxSummary.tsx | 3 ++- .../Transactions/GatewayTransactions/styled.tsx | 2 +- .../Transactions/GatewayTransactions/utils.ts | 5 ----- src/utils/date.ts | 12 +++++++++++- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx index 6151f96c..4b2ee88c 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx @@ -1,5 +1,4 @@ import { Loader } from '@gnosis.pm/safe-react-components' -import { format } from 'date-fns' import React, { ReactElement } from 'react' import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll' @@ -13,6 +12,7 @@ import { } from './styled' import { TxHistoryRow } from './TxHistoryRow' import { TxLocationContext } from './TxLocationProvider' +import { formatWithSchema } from 'src/utils/date' export const HistoryTxList = (): ReactElement => { const { count, hasMore, next, transactions } = usePagedHistoryTransactions() @@ -31,7 +31,7 @@ export const HistoryTxList = (): ReactElement => { {transactions?.map(([timestamp, txs]) => ( - {format(Number(timestamp), 'MMM d, yyyy')} + {formatWithSchema(Number(timestamp), 'MMM d, yyyy')} {txs.map((transaction) => ( diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx index c9ce1b0c..6f8ea2ca 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx @@ -3,7 +3,7 @@ import { ThemeColors } from '@gnosis.pm/safe-react-components/dist/theme' import { Tooltip } from '@material-ui/core' import CircularProgress from '@material-ui/core/CircularProgress' import { createStyles, makeStyles } from '@material-ui/core/styles' -import React, { ReactElement, useRef } from 'react' +import React, { ReactElement, useContext, useRef } from 'react' import CustomIconText from 'src/components/CustomIconText' import { @@ -13,7 +13,7 @@ import { Transaction, } from 'src/logic/safe/store/models/types/gateway.d' import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions' -import { formatDateTime, formatTime } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils' +import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date' import { KNOWN_MODULES } from 'src/utils/constants' import styled from 'styled-components' import { AssetInfo, isTokenTransferAsset } from './hooks/useAssetInfo' @@ -22,6 +22,7 @@ import { TransactionStatusProps } from './hooks/useTransactionStatus' import { TxTypeProps } from './hooks/useTransactionType' import { StyledGroupedTransactions, StyledTransaction } from './styled' import { TokenTransferAmount } from './TokenTransferAmount' +import { TxLocationContext } from './TxLocationProvider' import { CalculatedVotes } from './TxQueueCollapsed' const TxInfo = ({ info }: { info: AssetInfo }) => { @@ -126,6 +127,8 @@ export const TxCollapsed = ({ actions, status, }: TxCollapsedProps): ReactElement => { + const { txLocation } = useContext(TxLocationContext) + const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : '' const txCollapsedNonce = ( @@ -149,7 +152,7 @@ export const TxCollapsed = ({
- {formatTime(time)} + {txLocation === 'history' ? formatTime(time) : formatTimeInWords(time)}
diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx index 68f4b0fd..85c066a9 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxInfoCreation.tsx @@ -2,8 +2,9 @@ import { Text } from '@gnosis.pm/safe-react-components' import React, { ReactElement } from 'react' import { getExplorerInfo } from 'src/config' +import { formatDateTime } from 'src/utils/date' import { Creation, Transaction } from 'src/logic/safe/store/models/types/gateway.d' -import { formatDateTime, NOT_AVAILABLE } from './utils' +import { NOT_AVAILABLE } from './utils' import { InlineEthHashInfo, TxDetailsContainer } from './styled' export const TxInfoCreation = ({ transaction }: { transaction: Transaction }): ReactElement | null => { diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx index b0d81c82..84fb6cc7 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxSummary.tsx @@ -2,9 +2,10 @@ import { Text } from '@gnosis.pm/safe-react-components' import React, { ReactElement } from 'react' import { getExplorerInfo } from 'src/config' +import { formatDateTime } from 'src/utils/date' import { ExpandedTxDetails, isMultiSigExecutionDetails, Operation } from 'src/logic/safe/store/models/types/gateway.d' import { InlineEthHashInfo } from './styled' -import { formatDateTime, NOT_AVAILABLE } from './utils' +import { NOT_AVAILABLE } from './utils' export const TxSummary = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement => { const { txHash, detailedExecutionInfo, executedAt, txData } = txDetails diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx index 15767db3..a6ca9337 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx @@ -142,7 +142,7 @@ const gridColumns = { nonce: '0.75fr', type: '3fr', info: '3fr', - time: '1.5fr', + time: '2fr', votes: '1.5fr', actions: '1fr', status: '3fr', diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts b/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts index ac3f4b2c..7bd9a5d9 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts +++ b/src/routes/safe/components/Transactions/GatewayTransactions/utils.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'bignumber.js' -import format from 'date-fns/format' import { getNetworkInfo } from 'src/config' import { @@ -25,10 +24,6 @@ export const TX_TABLE_RAW_TX_ID = 'tx' export const TX_TABLE_RAW_CANCEL_TX_ID = 'cancelTx' export const TX_TABLE_EXPAND_ICON = 'expand' -export const formatTime = (timestamp: number): string => format(timestamp, 'h:mm a') - -export const formatDateTime = (timestamp: number): string => format(timestamp, 'MMM d, yyyy - h:mm:ss a') - export const NOT_AVAILABLE = 'n/a' interface AmountData { diff --git a/src/utils/date.ts b/src/utils/date.ts index 4e531138..087b8752 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,4 +1,6 @@ -import { formatRelative } from 'date-fns' +import format from 'date-fns/format' +import formatRelative from 'date-fns/formatRelative' +import formatDistanceToNow from 'date-fns/formatDistanceToNow' export const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { if (resetTimeMin === '0') { @@ -17,3 +19,11 @@ export const getUTCStartOfDate = (timestamp: number): number => { return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0) } + +export const formatWithSchema = (timestamp: number, schema: string): string => format(timestamp, schema) + +export const formatTime = (timestamp: number): string => formatWithSchema(timestamp, 'h:mm a') + +export const formatDateTime = (timestamp: number): string => formatWithSchema(timestamp, 'MMM d, yyyy - h:mm:ss a') + +export const formatTimeInWords = (timestamp: number): string => formatDistanceToNow(timestamp, { addSuffix: true }) From 47762701e1603a230383005ba0cd46a0895d24d2 Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 10 Feb 2021 16:47:02 -0300 Subject: [PATCH 14/34] Fix dark block for tx details with empty 2nd row (#1880) --- .../safe/components/Transactions/GatewayTransactions/styled.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx index a6ca9337..ba824e75 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx @@ -305,7 +305,7 @@ export const TxDetailsContainer = styled(WillBeReplaced)` display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: minmax(min-content, max-content); - grid-template-rows: [tx-summary] minmax(min-content, max-content) [tx-details] minmax(100px, 1fr); + grid-template-rows: [tx-summary] minmax(min-content, max-content) [tx-details] minmax(min-content, 1fr); row-gap: 2px; width: 100%; From 43287dd01835992a21d4f9deb0c2962d7e9e9926 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 11 Feb 2021 05:13:57 -0300 Subject: [PATCH 15/34] [Transaction List v2] - "Send to" action missing (#1879) * show missing `Send to`s sentences * remove "Send To", from 'spending limit' transactions details --- .../GatewayTransactions/MethodDetails.tsx | 26 ++-------- .../GatewayTransactions/TxData.tsx | 52 +++++++++++++++++-- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx index 1552857c..d849aa8a 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/MethodDetails.tsx @@ -4,18 +4,8 @@ import styled from 'styled-components' import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d' import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' -import { - DeleteSpendingLimitDetails, - isDeleteAllowance, - isSetAllowance, - ModifySpendingLimitDetails, -} from 'src/routes/safe/components/Transactions/GatewayTransactions/SpendingLimitDetails' import Value from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value' -const TxDetailsMethodName = styled(Text)` - text-indent: 4px; -` - const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>` padding-left: 24px; display: ${({ isArrayParameter }) => (isArrayParameter ? 'block' : 'flex')}; @@ -27,7 +17,7 @@ const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>` ` const TxInfo = styled.div` - padding: 8px 8px 8px 16px; + padding: 8px 0; ` const StyledMethodName = styled(Text)` @@ -35,21 +25,11 @@ const StyledMethodName = styled(Text)` ` export const MethodDetails = ({ data }: { data: DataDecoded }): React.ReactElement => { - // FixMe: this way won't scale well - if (isSetAllowance(data.method)) { - return - } - - // FixMe: this way won't scale well - if (isDeleteAllowance(data.method)) { - return - } - return ( - + {data.method} - + {data.parameters?.map((param, index) => ( diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx index bf777dcf..060cb6a9 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxData.tsx @@ -1,11 +1,37 @@ -import React, { ReactElement } from 'react' +import React, { ReactElement, ReactNode } from 'react' -import { ExpandedTxDetails } from 'src/logic/safe/store/models/types/gateway.d' +import { getNetworkInfo } from 'src/config' +import { ExpandedTxDetails, TransactionData } from 'src/logic/safe/store/models/types/gateway.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { + DeleteSpendingLimitDetails, + isDeleteAllowance, + isSetAllowance, + ModifySpendingLimitDetails, +} from './SpendingLimitDetails' +import { TxInfoDetails } from './TxInfoDetails' import { sameString } from 'src/utils/strings' import { HexEncodedData } from './HexEncodedData' import { MethodDetails } from './MethodDetails' import { MultiSendDetails } from './MultiSendDetails' +const { nativeCoin } = getNetworkInfo() + +type DetailsWithTxInfoProps = { + children: ReactNode + txData: TransactionData +} + +const DetailsWithTxInfo = ({ children, txData }: DetailsWithTxInfoProps): ReactElement => ( + <> + + {children} + +) + type TxDataProps = { txData: ExpandedTxDetails['txData'] } @@ -24,7 +50,11 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => { } // we render the hex encoded data - return + return ( + + + + ) } // known data and particularly `multiSend` data type @@ -32,6 +62,20 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => { return } + // FixMe: this way won't scale well + if (isSetAllowance(txData.dataDecoded.method)) { + return + } + + // FixMe: this way won't scale well + if (isDeleteAllowance(txData.dataDecoded.method)) { + return + } + // we render the decoded data - return + return ( + + + + ) } From e9bc89488f64386840e52dd56206710af7a15270 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 11 Feb 2021 05:17:56 -0300 Subject: [PATCH 16/34] Group transactions by locale TZ (#1883) --- src/logic/safe/store/reducer/gatewayTransactions.ts | 4 ++-- src/utils/date.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/logic/safe/store/reducer/gatewayTransactions.ts b/src/logic/safe/store/reducer/gatewayTransactions.ts index 8037fa90..df347f4c 100644 --- a/src/logic/safe/store/reducer/gatewayTransactions.ts +++ b/src/logic/safe/store/reducer/gatewayTransactions.ts @@ -22,7 +22,7 @@ import { import { UPDATE_TRANSACTION_DETAILS } from 'src/logic/safe/store/actions/fetchTransactionDetails' import { AppReduxState } from 'src/store' -import { getUTCStartOfDate } from 'src/utils/date' +import { getLocalStartOfDate } from 'src/utils/date' import { sameString } from 'src/utils/strings' import { sortObject } from 'src/utils/objects' @@ -78,7 +78,7 @@ export const gatewayTransactions = handleActions { return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0) } +export const getLocalStartOfDate = (timestamp: number): number => { + const date = new Date(timestamp) + + return date.setHours(0, 0, 0, 0) +} + export const formatWithSchema = (timestamp: number, schema: string): string => format(timestamp, schema) export const formatTime = (timestamp: number): string => formatWithSchema(timestamp, 'h:mm a') From fa2262f8b0ec3efb5631bcd0e392aba3c979325f Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 11 Feb 2021 05:21:37 -0300 Subject: [PATCH 17/34] [Transaction List v2] Action buttons tooltip (#1884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix tooltip for action buttons * add margin botton to the tooltip Co-authored-by: Agustín Longoni --- .../TxCollapsedActions.tsx | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx index 270878bf..19805222 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx @@ -1,11 +1,26 @@ -import { Icon } from '@gnosis.pm/safe-react-components' +import { Icon, theme } from '@gnosis.pm/safe-react-components' +import { Tooltip as TooltipMui } from '@material-ui/core' import { default as MuiIconButton } from '@material-ui/core/IconButton' +import { withStyles } from '@material-ui/core/styles' import React, { ReactElement } from 'react' import styled from 'styled-components' import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' import { useActionButtonsHandlers } from './hooks/useActionButtonsHandlers' +const Tooltip = withStyles(() => ({ + popper: { + zIndex: 2001, + }, + tooltip: { + marginBottom: '4px', + backgroundColor: theme.colors.overlay.color, + border: `1px solid ${theme.colors.icon}`, + boxShadow: `1px 2px 4px ${theme.colors.shadow.color}14`, + color: theme.colors.text, + }, +}))(TooltipMui) + const IconButton = styled(MuiIconButton)` padding: 8px !important; @@ -32,26 +47,25 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re return ( <> { - - - + + + + + } {canCancel && ( - - - + + + + + )} ) From 1994f6282705aaf0e5b5ba36baa5ee45f8377e3b Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 11 Feb 2021 05:22:27 -0300 Subject: [PATCH 18/34] Avoid displaying `UNKNOWN` in owner's list (#1881) --- .../components/Transactions/GatewayTransactions/OwnerRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx index 471a81c3..ebfca691 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/OwnerRow.tsx @@ -11,7 +11,7 @@ export const OwnerRow = ({ ownerAddress }: { ownerAddress: string }): ReactEleme return ( Date: Thu, 11 Feb 2021 05:31:08 -0300 Subject: [PATCH 19/34] Remove curried function from within `useCallback` hook (#1882) --- .../GatewayTransactions/hooks/useActionButtonsHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts index db5399cf..3921d215 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/useActionButtonsHandlers.ts @@ -62,7 +62,7 @@ export const useActionButtonsHandlers = (transaction: Transaction): ActionButton const isPending = useMemo(() => !!transaction.txStatus.match(/^PENDING.*/), [transaction.txStatus]) - const signaturePending = useCallback(addressInList(transaction.executionInfo?.missingSigners), []) + const signaturePending = addressInList(transaction.executionInfo?.missingSigners) const disabledActions = useMemo( () => From 64defa776acfe0424c7560819ed04c12fca57743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Thu, 11 Feb 2021 15:07:49 -0300 Subject: [PATCH 20/34] fix tree align and paddings --- .../Transactions/GatewayTransactions/styled.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx index ba824e75..755d7dd7 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx @@ -139,7 +139,7 @@ export const GroupedTransactionsCard = styled(StyledTransactions)` } ` const gridColumns = { - nonce: '0.75fr', + nonce: '0.5fr', type: '3fr', info: '3fr', time: '2fr', @@ -208,7 +208,7 @@ export const GroupedTransactions = styled(StyledTransaction)` // builds the tree-view layout .tree-lines { height: 100%; - margin-left: 50%; + margin-left: 30px; position: relative; width: 30%; @@ -286,8 +286,8 @@ export const DisclaimerContainer = styled(StyledTransaction)` background-color: ${({ theme }) => theme.colors.inputField} !important; border-radius: 4px; margin: 12px 8px 0 12px; - padding: 8px; - width: calc(100% - 40px); + padding: 8px 12px; + width: calc(100% - 48px); .nonce { grid-column-start: 1; From 7a550e3ed3e13e3e3c8fb883c6e08c9438b3dd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Thu, 11 Feb 2021 15:08:32 -0300 Subject: [PATCH 21/34] fix Advanced option style on reject modal --- .../GatewayTransactions/modals/RejectTxModal.tsx | 10 ++++++---- .../Transactions/GatewayTransactions/modals/style.ts | 6 +++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx index 571c0678..97637461 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/modals/RejectTxModal.tsx @@ -117,12 +117,14 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React. - + + + {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( + - - + + )}