commit
21bd4a2eb7
|
@ -1,10 +1,8 @@
|
|||
name: Build/Release Desktop app
|
||||
|
||||
# this will help you specify where to run
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
on:
|
||||
workflow_dispatch
|
||||
|
||||
env:
|
||||
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
|
||||
|
@ -29,6 +27,19 @@ jobs:
|
|||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Add cache for yarn directory
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Patch node gyp on windows to support Visual Studio 2019
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
shell: powershell
|
||||
|
@ -69,21 +80,21 @@ jobs:
|
|||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
- name: 'Upload Artifacts OSX'
|
||||
if: contains(github.ref, 'master') && startsWith(matrix.os, 'macos')
|
||||
if: contains(github.ref, 'development') && startsWith(matrix.os, 'macos')
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Desktop OSX
|
||||
path: ./dist/Safe[ ]Multisig*.dmg
|
||||
|
||||
- name: 'Upload Artifacts Linux'
|
||||
if: contains(github.ref, 'master') && startsWith(matrix.os, 'ubuntu')
|
||||
if: contains(github.ref, 'development') && startsWith(matrix.os, 'ubuntu')
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Desktop Linux
|
||||
path: ./dist/Safe[ ]Multisig*.AppImage
|
||||
|
||||
- name: 'Upload Artifacts Windows'
|
||||
if: contains(github.ref, 'master') && startsWith(matrix.os, 'windows')
|
||||
if: contains(github.ref, 'development') && startsWith(matrix.os, 'windows')
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Desktop Windows
|
||||
|
|
18
.travis.yml
18
.travis.yml
|
@ -1,18 +1,16 @@
|
|||
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
|
||||
sudo: required
|
||||
dist: xenial
|
||||
services:
|
||||
- docker
|
||||
dist: bionic
|
||||
language: node_js
|
||||
node_js:
|
||||
- '12'
|
||||
os:
|
||||
- linux
|
||||
env:
|
||||
global:
|
||||
- DOCKER_COMPOSE_VERSION=1.22.0
|
||||
matrix:
|
||||
include:
|
||||
- env:
|
||||
- REACT_APP_ENV='production'
|
||||
if: tag IS present
|
||||
- env:
|
||||
- REACT_APP_NETWORK='mainnet'
|
||||
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
|
||||
|
@ -28,10 +26,12 @@ before_install:
|
|||
- sudo apt-get update
|
||||
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev
|
||||
- pip install awscli --upgrade --user
|
||||
# Install truffle
|
||||
- yarn global add truffle
|
||||
script:
|
||||
- bash ./config/travis/build.sh
|
||||
- yarn lint:check
|
||||
- yarn prettier:check
|
||||
- yarn test:coverage
|
||||
- yarn build
|
||||
#- bash ./config/travis/build.sh
|
||||
after_success:
|
||||
# Pull Request - Deploy it to a review environment
|
||||
# Travis doesn't do deploy step with pull requests builds
|
||||
|
|
54
package.json
54
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.11.1",
|
||||
"version": "2.12.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -38,7 +38,7 @@
|
|||
"release": "electron-builder --mac --linux --windows -p always",
|
||||
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
|
||||
"start": "react-app-rewired start",
|
||||
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
|
||||
"test": "react-app-rewired test --env=jsdom",
|
||||
"test:coverage": "yarn test --coverage --watchAll=false",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
|
@ -164,21 +164,21 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#development",
|
||||
"@gnosis.pm/safe-apps-sdk": "0.4.0",
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.19.1",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.22.0",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"axios": "0.20.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.11.1",
|
||||
"bnc-onboard": "1.13.1",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.2.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"currency-flags": "2.1.2",
|
||||
|
@ -190,20 +190,20 @@
|
|||
"eth-sig-util": "^2.5.3",
|
||||
"ethereum-blockies-base64": "^1.0.2",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"exponential-backoff": "^3.0.1",
|
||||
"exponential-backoff": "^3.1.0",
|
||||
"express": "^4.17.1",
|
||||
"final-form": "^4.20.1",
|
||||
"final-form-calculate": "^1.3.1",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.3",
|
||||
"immortal-db": "^1.1.0",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"material-ui-search-bar": "^1.0.0",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"open": "^7.1.0",
|
||||
"polished": "3.6.5",
|
||||
"open": "^7.2.0",
|
||||
"polished": "3.6.7",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.13.1",
|
||||
"react": "16.13.1",
|
||||
|
@ -215,7 +215,7 @@
|
|||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-scripts": "^3.4.3",
|
||||
"react-window": "^1.8.5",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "4.0.5",
|
||||
|
@ -223,7 +223,7 @@
|
|||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"semver": "7.3.2",
|
||||
"styled-components": "^5.1.1",
|
||||
"styled-components": "^5.2.0",
|
||||
"truffle-contract": "4.0.31",
|
||||
"web3": "1.2.9",
|
||||
"web3-core": "^1.2.11",
|
||||
|
@ -236,45 +236,43 @@
|
|||
"@storybook/addons": "^5.3.19",
|
||||
"@storybook/preset-create-react-app": "^3.1.4",
|
||||
"@storybook/react": "^5.3.19",
|
||||
"@testing-library/jest-dom": "5.11.2",
|
||||
"@testing-library/react": "10.4.8",
|
||||
"@testing-library/user-event": "12.1.0",
|
||||
"@testing-library/jest-dom": "5.11.4",
|
||||
"@testing-library/react": "10.4.9",
|
||||
"@typechain/web3-v1": "^1.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.9",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "14.6.0",
|
||||
"@types/react": "^16.9.47",
|
||||
"@types/node": "14.11.2",
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/styled-components": "^5.1.2",
|
||||
"@types/styled-components": "^5.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "3.9.1",
|
||||
"@typescript-eslint/parser": "3.9.1",
|
||||
"autoprefixer": "9.8.6",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "7.2.4",
|
||||
"electron": "9.3.0",
|
||||
"electron-builder": "22.8.0",
|
||||
"electron-notarize": "0.3.0",
|
||||
"electron-notarize": "1.0.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
"eslint-plugin-react": "^7.20.6",
|
||||
"eslint-plugin-sort-destructure-keys": "1.3.5",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "10.2.11",
|
||||
"lint-staged": "10.4.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"prettier": "2.1.2",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-docgen-typescript-loader": "^3.7.2",
|
||||
"truffle": "5.1.36",
|
||||
"typechain": "^2.0.0",
|
||||
"typescript": "3.9.7",
|
||||
"wait-on": "5.1.0"
|
||||
"wait-on": "5.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,6 +142,10 @@ process.on('uncaughtException',function(error){
|
|||
|
||||
app.userAgentFallback = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
|
||||
|
||||
// We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid
|
||||
// This type of modules will be impossible to use after electron 10
|
||||
app.allowRendererProcessReuse = false;
|
||||
|
||||
app.commandLine.appendSwitch('ignore-certificate-errors');
|
||||
app.on("ready", () =>{
|
||||
// Hide the menu
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
|
||||
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
|
||||
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
|
||||
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -1,9 +1,8 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import QRCode from 'qrcode.react'
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||
|
@ -14,72 +13,79 @@ import Col from 'src/components/layout/Col'
|
|||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
|
||||
import { copyToClipboard } from 'src/utils/clipboard'
|
||||
|
||||
const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '75px',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
close: {
|
||||
height: lg,
|
||||
width: lg,
|
||||
fill: secondaryText,
|
||||
},
|
||||
qrContainer: {
|
||||
backgroundColor: '#fff',
|
||||
padding: md,
|
||||
borderRadius: '6px',
|
||||
border: `1px solid ${secondaryText}`,
|
||||
},
|
||||
annotation: {
|
||||
margin: lg,
|
||||
marginBottom: 0,
|
||||
},
|
||||
safeName: {
|
||||
margin: `${md} 0`,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
const useStyles = makeStyles(
|
||||
createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '75px',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
},
|
||||
addressContainer: {
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: `${lg} 0`,
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
flexDirection: 'row',
|
||||
close: {
|
||||
height: lg,
|
||||
width: lg,
|
||||
fill: secondaryText,
|
||||
},
|
||||
},
|
||||
address: {
|
||||
marginLeft: sm,
|
||||
marginRight: sm,
|
||||
maxWidth: '70%',
|
||||
overflowWrap: 'break-word',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
maxWidth: 'none',
|
||||
qrContainer: {
|
||||
backgroundColor: '#fff',
|
||||
padding: md,
|
||||
borderRadius: '6px',
|
||||
border: `1px solid ${secondaryText}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
annotation: {
|
||||
margin: lg,
|
||||
marginBottom: 0,
|
||||
},
|
||||
safeName: {
|
||||
margin: `${md} 0`,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
},
|
||||
},
|
||||
addressContainer: {
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: `${lg} 0`,
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
},
|
||||
address: {
|
||||
marginLeft: sm,
|
||||
marginRight: sm,
|
||||
maxWidth: '70%',
|
||||
overflowWrap: 'break-word',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
maxWidth: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
safeAddress: string
|
||||
safeName: string
|
||||
}
|
||||
|
||||
const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const Receive = ({ classes, onClose }) => {
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin size="xl" weight="bolder">
|
||||
<Paragraph noMargin size="xl" weight="bolder">
|
||||
Receive funds
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
|
@ -122,4 +128,4 @@ const Receive = ({ classes, onClose }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(Receive)
|
||||
export default ReceiveModal
|
|
@ -30,7 +30,7 @@ import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logi
|
|||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
|
||||
import Receive from './ModalReceive'
|
||||
import Receive from './ReceiveModal'
|
||||
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
|
||||
|
||||
const notificationStyles = {
|
||||
|
@ -79,7 +79,8 @@ const App: React.FC = ({ children }) => {
|
|||
|
||||
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string }
|
||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
||||
const balance = !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : null
|
||||
const balance =
|
||||
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (matchSafe?.isExact) {
|
||||
|
@ -133,14 +134,16 @@ const App: React.FC = ({ children }) => {
|
|||
selectedToken={sendFunds.selectedToken}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
description="Receive Tokens Form"
|
||||
handleClose={onReceiveHide}
|
||||
open={safeActionsState.showReceive as boolean}
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<Receive onClose={onReceiveHide} />
|
||||
</Modal>
|
||||
{safeAddress && safeName && (
|
||||
<Modal
|
||||
description="Receive Tokens Form"
|
||||
handleClose={onReceiveHide}
|
||||
open={safeActionsState.showReceive as boolean}
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
</SnackbarProvider>
|
||||
<CookiesBanner />
|
||||
|
|
|
@ -50,7 +50,7 @@ export const Base = (): React.ReactElement => {
|
|||
safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B"
|
||||
safeName="someName"
|
||||
granted={true}
|
||||
balance={null}
|
||||
balance={undefined}
|
||||
onToggleSafeList={() => console.log}
|
||||
onReceiveClick={() => console.log}
|
||||
onNewTransactionClick={() => console.log}
|
||||
|
|
|
@ -50,7 +50,7 @@ const HeaderComponent = (): React.ReactElement => {
|
|||
}
|
||||
|
||||
const getProviderInfoBased = () => {
|
||||
if (!loaded) {
|
||||
if (!loaded || !provider) {
|
||||
return <ProviderDisconnected />
|
||||
}
|
||||
|
||||
|
|
|
@ -79,10 +79,10 @@ const UnStyledButton = styled.button`
|
|||
`
|
||||
|
||||
type Props = {
|
||||
address: string | null
|
||||
safeName: string
|
||||
address: string | undefined
|
||||
safeName: string | undefined
|
||||
granted: boolean
|
||||
balance: string | null
|
||||
balance: string | undefined
|
||||
onToggleSafeList: () => void
|
||||
onReceiveClick: () => void
|
||||
onNewTransactionClick: () => void
|
||||
|
@ -102,9 +102,7 @@ const SafeHeader = ({
|
|||
<Container>
|
||||
<IdenticonContainer>
|
||||
<FlexSpacer />
|
||||
<div>
|
||||
<FixedIcon type="notConnected" />
|
||||
</div>
|
||||
<FixedIcon type="notConnected" />
|
||||
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
|
||||
<Icon size="md" type="circleDropdown" />
|
||||
</UnStyledButton>
|
||||
|
|
|
@ -10,14 +10,14 @@ const StyledDivider = styled(Divider)`
|
|||
`
|
||||
|
||||
const HelpContainer = styled.div`
|
||||
height: 58px;
|
||||
margin-top: auto;
|
||||
`
|
||||
|
||||
const HelpCenterLink = styled.a`
|
||||
height: 30px;
|
||||
width: 166px;
|
||||
padding: 10px 0 0 16px;
|
||||
margin: 10px 0px;
|
||||
padding: 6px 0 0 16px;
|
||||
margin: 14px 0px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
|
||||
|
@ -38,9 +38,9 @@ const HelpCenterLink = styled.a`
|
|||
}
|
||||
`
|
||||
type Props = {
|
||||
safeAddress: string | null
|
||||
safeName: string | null
|
||||
balance: string | null
|
||||
safeAddress?: string
|
||||
safeName?: string
|
||||
balance?: string
|
||||
granted: boolean
|
||||
onToggleSafeList: () => void
|
||||
onReceiveClick: () => void
|
||||
|
@ -57,34 +57,32 @@ const Sidebar = ({
|
|||
onToggleSafeList,
|
||||
onReceiveClick,
|
||||
onNewTransactionClick,
|
||||
}: Props): React.ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<SafeHeader
|
||||
address={safeAddress}
|
||||
safeName={safeName}
|
||||
granted={granted}
|
||||
balance={balance}
|
||||
onToggleSafeList={onToggleSafeList}
|
||||
onReceiveClick={onReceiveClick}
|
||||
onNewTransactionClick={onNewTransactionClick}
|
||||
/>
|
||||
}: Props): React.ReactElement => (
|
||||
<>
|
||||
<SafeHeader
|
||||
address={safeAddress}
|
||||
safeName={safeName}
|
||||
granted={granted}
|
||||
balance={balance}
|
||||
onToggleSafeList={onToggleSafeList}
|
||||
onReceiveClick={onReceiveClick}
|
||||
onNewTransactionClick={onNewTransactionClick}
|
||||
/>
|
||||
|
||||
{items.length ? (
|
||||
<>
|
||||
<StyledDivider />
|
||||
<List items={items} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<HelpContainer>
|
||||
{items.length ? (
|
||||
<>
|
||||
<StyledDivider />
|
||||
<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
|
||||
<IconText text="HELP CENTER" iconSize="md" textSize="md" color="placeHolder" iconType="question" />
|
||||
</HelpCenterLink>
|
||||
</HelpContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<List items={items} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<HelpContainer>
|
||||
<StyledDivider />
|
||||
<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
|
||||
<IconText text="HELP CENTER" iconSize="md" textSize="md" color="placeHolder" iconType="question" />
|
||||
</HelpCenterLink>
|
||||
</HelpContainer>
|
||||
</>
|
||||
)
|
||||
|
||||
export default Sidebar
|
||||
|
|
|
@ -28,18 +28,15 @@ const GridTopbarWrapper = styled.nav`
|
|||
|
||||
const GridSidebarWrapper = styled.aside`
|
||||
width: 200px;
|
||||
padding: 8px;
|
||||
padding: 62px 8px 0 8px;
|
||||
height: 100%;
|
||||
background-color: ${({ theme }) => theme.colors.white};
|
||||
border-right: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
grid-area: sidebar;
|
||||
|
||||
div:last-of-type {
|
||||
margin-top: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const GridBodyWrapper = styled.section`
|
||||
|
@ -60,9 +57,9 @@ export const FooterWrapper = styled.footer`
|
|||
|
||||
type Props = {
|
||||
sidebarItems: ListItemType[]
|
||||
safeAddress: string | null
|
||||
safeName: string | null
|
||||
balance: string | null
|
||||
safeAddress: string | undefined
|
||||
safeName: string | undefined
|
||||
balance: string | undefined
|
||||
granted: boolean
|
||||
onToggleSafeList: () => void
|
||||
onReceiveClick: () => void
|
||||
|
|
|
@ -70,6 +70,21 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||
width: '100%',
|
||||
maxWidth: 200,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
overflowX: 'auto',
|
||||
margin: '8px 0 -4px 0',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '0.5em',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
boxShadow: 'inset 0 0 6px rgba(0, 0, 0, 0.3)',
|
||||
webkitBoxShadow: 'inset 0 0 6px rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: '20px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'darkgrey',
|
||||
outline: '1px solid #dadada',
|
||||
borderRadius: '20px',
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
paddingLeft: theme.spacing(3),
|
||||
|
|
|
@ -82,7 +82,7 @@ const useStyles = makeStyles({
|
|||
})
|
||||
|
||||
type Props = {
|
||||
currentSafe: string | null
|
||||
currentSafe: string | undefined
|
||||
defaultSafe: DefaultSafe
|
||||
safes: SafeRecord[]
|
||||
onSafeClick: () => void
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import Drawer from '@material-ui/core/Drawer'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import SearchBar from 'material-ui-search-bar'
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import SafeList from './SafeList'
|
||||
|
@ -16,12 +16,11 @@ import Link from 'src/components/layout/Link'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
|
||||
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const { useEffect, useMemo, useState } = React
|
||||
|
||||
export const SafeListSidebarContext = React.createContext({
|
||||
isOpen: false,
|
||||
toggleSidebar: () => {},
|
||||
|
@ -39,12 +38,7 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
|
|||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [filter, setFilter] = useState('')
|
||||
const classes = useSidebarStyles()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setFilter('')
|
||||
}, 300)
|
||||
}, [isOpen])
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
const searchClasses = {
|
||||
input: classes.searchInput,
|
||||
|
@ -54,6 +48,9 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
|
|||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (!isOpen) {
|
||||
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Safe List Sidebar' })
|
||||
}
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||
}
|
||||
|
||||
|
@ -73,6 +70,12 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
|
|||
|
||||
const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setFilter('')
|
||||
}, 300)
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<SafeListSidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
||||
<Drawer
|
||||
|
|
|
@ -8,7 +8,7 @@ interface CellWidth {
|
|||
maxWidth: string
|
||||
}
|
||||
|
||||
export const cellWidth = (width: string | number): CellWidth | undefined => {
|
||||
export const cellWidth = (width?: string | number): CellWidth | undefined => {
|
||||
if (!width) {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { Record, RecordOf } from 'immutable'
|
||||
|
||||
export interface AddressBookEntryProps {
|
||||
export type AddressBookEntry = {
|
||||
address: string
|
||||
name: string
|
||||
isOwner: boolean
|
||||
}
|
||||
|
||||
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
|
||||
|
||||
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
|
||||
address: '',
|
||||
name: '',
|
||||
isOwner: false,
|
||||
export const makeAddressBookEntry = ({
|
||||
address = '',
|
||||
name = '',
|
||||
}: {
|
||||
address: string
|
||||
name?: string
|
||||
}): AddressBookEntry => ({
|
||||
address,
|
||||
name,
|
||||
})
|
||||
|
||||
export type AddressBookEntry = RecordOf<AddressBookEntryProps>
|
||||
export type AddressBookState = AddressBookEntry[]
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK'
|
||||
|
||||
export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({
|
||||
addressBook,
|
||||
safeAddress,
|
||||
}))
|
|
@ -1,7 +1,22 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const ADD_ENTRY = 'ADD_ENTRY'
|
||||
|
||||
export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({
|
||||
entry,
|
||||
}))
|
||||
type addAddressBookEntryOptions = {
|
||||
notifyEntryUpdate: boolean
|
||||
}
|
||||
|
||||
export const addAddressBookEntry = createAction(
|
||||
ADD_ENTRY,
|
||||
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
|
||||
let notifyEntryUpdate = true
|
||||
if (options) {
|
||||
notifyEntryUpdate = options.notifyEntryUpdate
|
||||
}
|
||||
return {
|
||||
entry,
|
||||
shouldAvoidUpdatesNotifications: !notifyEntryUpdate,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
|
||||
|
||||
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entryAddress, entry) => ({
|
||||
entryAddress,
|
||||
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
||||
entry,
|
||||
}))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK'
|
||||
|
||||
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook) => ({
|
||||
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({
|
||||
addressBook,
|
||||
}))
|
||||
|
|
|
@ -1,29 +1,17 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
|
||||
import { safesListSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const loadAddressBookFromStorage = () => async (dispatch, getState) => {
|
||||
const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const state = getState()
|
||||
let storedAdBk = await getAddressBookFromStorage()
|
||||
if (!storedAdBk) {
|
||||
storedAdBk = []
|
||||
}
|
||||
|
||||
let addressBook = buildAddressBook(storedAdBk)
|
||||
// Fetch all the current safes, in case that we don't have a safe on the adbk, we add it
|
||||
const safes = safesListSelector(state)
|
||||
const adbkEntries = addressBook.keySeq().toArray()
|
||||
safes.forEach((safe) => {
|
||||
const { address } = safe
|
||||
const found = adbkEntries.includes(address)
|
||||
if (!found) {
|
||||
addressBook = addressBook.set(address, List([]))
|
||||
}
|
||||
})
|
||||
const addressBook = buildAddressBook(storedAdBk)
|
||||
|
||||
dispatch(loadAddressBook(addressBook))
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -2,6 +2,6 @@ import { createAction } from 'redux-actions'
|
|||
|
||||
export const REMOVE_ENTRY = 'REMOVE_ENTRY'
|
||||
|
||||
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({
|
||||
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({
|
||||
entryAddress,
|
||||
}))
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { saveAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook)))
|
||||
await saveAddressBook(addressBook)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while loading active tokens from storage:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export default saveAndUpdateAddressBook
|
|
@ -1,7 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const UPDATE_ENTRY = 'UPDATE_ENTRY'
|
||||
|
||||
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry) => ({
|
||||
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
||||
entry,
|
||||
}))
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt
|
|||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { saveAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
|
@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
|
|||
if (watchedActions.includes(action.type)) {
|
||||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
const addressBook = addressBookMapSelector(state)
|
||||
if (addressBook) {
|
||||
const addressBook = addressBookSelector(state)
|
||||
if (addressBook.length) {
|
||||
await saveAddressBook(addressBook)
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case ADD_ENTRY: {
|
||||
const { isOwner } = action.payload.entry
|
||||
if (!isOwner) {
|
||||
const { shouldAvoidUpdatesNotifications } = action.payload
|
||||
if (!shouldAvoidUpdatesNotifications) {
|
||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY)
|
||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||
}
|
||||
|
|
|
@ -1,130 +1,73 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
|
||||
import { 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 { getAddressesListFromAdbk } from 'src/logic/addressBook/utils'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
|
||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||
|
||||
export type AddressBookCollection = List<AddressBookEntry>
|
||||
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
|
||||
|
||||
export const buildAddressBook = (storedAdbk) => {
|
||||
let addressBookBuilt = Map([])
|
||||
Object.entries(storedAdbk).forEach((adbkProps: any) => {
|
||||
const safeAddress = checksumAddress(adbkProps[0])
|
||||
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
|
||||
const adbkSafeEntries = List(adbkRecords)
|
||||
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
|
||||
export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => {
|
||||
return storedAddressBook.map((addressBookEntry) => {
|
||||
const { address, name } = addressBookEntry
|
||||
return makeAddressBookEntry({ address: checksumAddress(address), name })
|
||||
})
|
||||
return addressBookBuilt
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[LOAD_ADDRESS_BOOK]: (state, action) => {
|
||||
const { addressBook } = action.payload
|
||||
return state.set('addressBook', addressBook)
|
||||
},
|
||||
[ADD_ADDRESS_BOOK]: (state, action) => {
|
||||
const { addressBook, safeAddress } = action.payload
|
||||
// Adds the address book if it does not exists
|
||||
const found = state.getIn(['addressBook', safeAddress])
|
||||
if (!found) {
|
||||
return state.setIn(['addressBook', safeAddress], addressBook)
|
||||
}
|
||||
return state
|
||||
return addressBook
|
||||
},
|
||||
[ADD_ENTRY]: (state, action) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
// Adds the entry to all the safes (if it does not already exists)
|
||||
const newState = state.withMutations((map) => {
|
||||
const adbkMap = map.get('addressBook')
|
||||
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
||||
|
||||
if (adbkMap) {
|
||||
adbkMap.keySeq().forEach((safeAddress) => {
|
||||
const safeAddressBook = state.getIn(['addressBook', safeAddress])
|
||||
|
||||
if (safeAddressBook) {
|
||||
const adbkAddressList = getAddressesListFromAdbk(safeAddressBook)
|
||||
const found = adbkAddressList.includes(entry.address)
|
||||
if (!found) {
|
||||
const updatedSafeAdbkList = safeAddressBook.push(entry)
|
||||
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return newState
|
||||
// Only adds entries with valid names
|
||||
const validName = getValidAddressBookName(entry.name)
|
||||
if (!entryFound && validName) {
|
||||
state.push(entry)
|
||||
}
|
||||
return state
|
||||
},
|
||||
[UPDATE_ENTRY]: (state, action) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
// Updates the entry from all the safes
|
||||
const newState = state.withMutations((map) => {
|
||||
map
|
||||
.get('addressBook')
|
||||
.keySeq()
|
||||
.forEach((safeAddress) => {
|
||||
const entriesList = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address))
|
||||
const updatedEntriesList = entriesList.set(entryIndex, entry)
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
})
|
||||
})
|
||||
|
||||
return newState
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
||||
if (entryIndex >= 0) {
|
||||
state[entryIndex] = entry
|
||||
}
|
||||
return state
|
||||
},
|
||||
[REMOVE_ENTRY]: (state, action) => {
|
||||
const { entryAddress } = action.payload
|
||||
// Removes the entry from all the safes
|
||||
const newState = state.withMutations((map) => {
|
||||
map
|
||||
.get('addressBook')
|
||||
.keySeq()
|
||||
.forEach((safeAddress) => {
|
||||
const entriesList = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress))
|
||||
const updatedEntriesList = entriesList.remove(entryIndex)
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
})
|
||||
})
|
||||
return newState
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
|
||||
state.splice(entryIndex, 1)
|
||||
return state
|
||||
},
|
||||
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
|
||||
const { entry, entryAddress } = action.payload
|
||||
const { entry } = action.payload
|
||||
|
||||
// Adds or Updates the entry to all the safes
|
||||
return state.withMutations((map) => {
|
||||
const addressBook = map.get('addressBook')
|
||||
if (addressBook) {
|
||||
addressBook.keySeq().forEach((safeAddress) => {
|
||||
const safeAddressBook = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress))
|
||||
// Only updates entries with valid names
|
||||
const validName = getValidAddressBookName(entry.name)
|
||||
if (!validName) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (entryIndex !== -1) {
|
||||
const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry))
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
} else {
|
||||
const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry))
|
||||
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
||||
|
||||
if (entryIndex >= 0) {
|
||||
state[entryIndex] = entry
|
||||
} else {
|
||||
state.push(entry)
|
||||
}
|
||||
return state
|
||||
},
|
||||
},
|
||||
Map({
|
||||
addressBook: Map({}),
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook'
|
||||
import { Map, List } from 'immutable'
|
||||
|
||||
export interface AddressBookReducerState {
|
||||
addressBook: AddressBookMap
|
||||
}
|
||||
|
||||
interface AddressBookMapSerialized {
|
||||
[key: string]: AddressBookEntryProps
|
||||
}
|
||||
|
||||
interface AddressBookReducerStateSerialized extends AddressBookReducerState {
|
||||
addressBook: Record<string, AddressBookEntryProps[]>
|
||||
}
|
||||
|
||||
export interface AddressBookMap extends Map<string> {
|
||||
toJS(): AddressBookMapSerialized
|
||||
get(key: string, notSetValue: unknown): List<AddressBookEntryRecord>
|
||||
}
|
||||
|
||||
export interface AddressBookReducerMap extends Map<string, any> {
|
||||
toJS(): AddressBookReducerStateSerialized
|
||||
get<K extends keyof AddressBookReducerState>(key: K): AddressBookReducerState[K]
|
||||
}
|
|
@ -1,35 +1,22 @@
|
|||
import { AppReduxState } from 'src/store'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
|
||||
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')
|
||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const getAddressBook = createSelector(
|
||||
addressBookMapSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(addressBook, safeAddress) => {
|
||||
let result = List([])
|
||||
if (addressBook) {
|
||||
result = addressBook.get(safeAddress, List())
|
||||
}
|
||||
return result
|
||||
},
|
||||
)
|
||||
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
|
||||
|
||||
export const getNameFromAddressBook = createSelector(
|
||||
getAddressBook,
|
||||
export const getNameFromAddressBookSelector = createSelector(
|
||||
addressBookSelector,
|
||||
(_, address) => address,
|
||||
(addressBook, address) => {
|
||||
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
|
||||
|
||||
if (adbkEntry) {
|
||||
return adbkEntry.name
|
||||
}
|
||||
|
||||
return 'UNKNOWN'
|
||||
},
|
||||
)
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
import { List } from 'immutable'
|
||||
import {
|
||||
checkIfEntryWasDeletedFromAddressBook,
|
||||
getAddressBookFromStorage,
|
||||
getAddressesListFromAddressBook,
|
||||
getNameFromAddressBook,
|
||||
getOwnersWithNameFromAddressBook,
|
||||
isValidAddressBookName,
|
||||
migrateOldAddressBook,
|
||||
OldAddressBookEntry,
|
||||
OldAddressBookType,
|
||||
saveAddressBook,
|
||||
} from 'src/logic/addressBook/utils/index'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
|
||||
makeAddressBookEntry({
|
||||
address,
|
||||
name,
|
||||
})
|
||||
|
||||
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
|
||||
return {
|
||||
address,
|
||||
name,
|
||||
isOwner,
|
||||
}
|
||||
}
|
||||
|
||||
describe('getAddressesListFromAdbk', () => {
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
|
||||
it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => {
|
||||
// given
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const expectedResult = [entry1.address, entry2.address, entry3.address]
|
||||
|
||||
// when
|
||||
const result = getAddressesListFromAddressBook(safeAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNameFromSafeAddressBook', () => {
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
it('It should returns the user name given a safeAddressBook and an user account', () => {
|
||||
// given
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const expectedResult = entry2.name
|
||||
|
||||
// when
|
||||
const result = getNameFromAddressBook(safeAddressBook, entry2.address)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOwnersWithNameFromAddressBook', () => {
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
|
||||
// given
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const ownerList = List([
|
||||
{ address: entry1.address, name: '' },
|
||||
{ address: entry2.address, name: '' },
|
||||
])
|
||||
const expectedResult = List([
|
||||
{ address: entry1.address, name: entry1.name },
|
||||
{ address: entry2.address, name: entry2.name },
|
||||
])
|
||||
|
||||
// when
|
||||
const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
jest.mock('src/utils/storage/index')
|
||||
describe('saveAddressBook', () => {
|
||||
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
|
||||
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
|
||||
afterAll(() => {
|
||||
jest.unmock('src/utils/storage/index')
|
||||
})
|
||||
it('It should save a given addressBook to the localStorage', async () => {
|
||||
// given
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
|
||||
// when
|
||||
await saveAddressBook(addressBook)
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook))
|
||||
|
||||
const storedAddressBook = await getAddressBookFromStorage()
|
||||
|
||||
// @ts-ignore
|
||||
let result = buildAddressBook(storedAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(addressBook)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateOldAddressBook', () => {
|
||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
||||
|
||||
it('It should receive an addressBook in old format and return the same addressBook in new format', () => {
|
||||
// given
|
||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
||||
|
||||
const oldAddressBook: OldAddressBookType = {
|
||||
[safeAddress1]: [entry1],
|
||||
[safeAddress2]: [entry2],
|
||||
}
|
||||
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
// when
|
||||
const result = migrateOldAddressBook(oldAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAddressBookFromStorage', () => {
|
||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
||||
beforeAll(() => {
|
||||
jest.mock('src/utils/storage/index')
|
||||
})
|
||||
afterAll(() => {
|
||||
jest.unmock('src/utils/storage/index')
|
||||
})
|
||||
it('It should return null if no addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedResult = null
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null)
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return migrated addressBook if old addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
||||
const oldAddressBook: OldAddressBookType = {
|
||||
[safeAddress1]: [entry1],
|
||||
[safeAddress2]: [entry2],
|
||||
}
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook)
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return addressBook if addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult))
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidAddressBookName', () => {
|
||||
it('It should return false if given a blacklisted name like UNKNOWN', () => {
|
||||
// given
|
||||
const addressNameInput = 'UNKNOWN'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if given a blacklisted name like MY WALLET', () => {
|
||||
// given
|
||||
const addressNameInput = 'MY WALLET'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if given a blacklisted name like OWNER #', () => {
|
||||
// given
|
||||
const addressNameInput = 'OWNER #'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return true if the given address name is valid', () => {
|
||||
// given
|
||||
const addressNameInput = 'User'
|
||||
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkIfEntryWasDeletedFromAddressBook', () => {
|
||||
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
|
||||
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
|
||||
it('It should return true if a given entry was deleted from addressBook', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry2, entry3]
|
||||
const safeAlreadyLoaded = true
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if a given entry was not deleted from addressBook', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
const safeAlreadyLoaded = true
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if the safe was not already loaded', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
const safeAlreadyLoaded = false
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
})
|
|
@ -1,47 +1,140 @@
|
|||
import { List } from 'immutable'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { AddressBookEntryProps } from './../model/addressBook'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
|
||||
|
||||
export const getAddressBookFromStorage = async (): Promise<Array<AddressBookEntryProps> | undefined> => {
|
||||
const data = await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
|
||||
|
||||
return data
|
||||
export type OldAddressBookEntry = {
|
||||
address: string
|
||||
name: string
|
||||
isOwner: boolean
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook) => {
|
||||
export type OldAddressBookType = {
|
||||
[safeAddress: string]: [OldAddressBookEntry]
|
||||
}
|
||||
|
||||
const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
||||
|
||||
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
|
||||
const values: AddressBookState = []
|
||||
const adbkValues = Object.values(oldAddressBook)
|
||||
|
||||
for (const safeIterator of adbkValues) {
|
||||
for (const safeAddressBook of safeIterator) {
|
||||
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
|
||||
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
|
||||
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
|
||||
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof result === 'string') {
|
||||
return JSON.parse(result)
|
||||
}
|
||||
|
||||
return migrateOldAddressBook(result as OldAddressBookType)
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON())
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
|
||||
} catch (err) {
|
||||
console.error('Error storing addressBook in localstorage', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address)
|
||||
export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] =>
|
||||
addressBook.map((entry) => entry.address)
|
||||
|
||||
export const getNameFromAdbk = (addressBook, userAddress) => {
|
||||
type GetNameFromAddressBookOptions = {
|
||||
filterOnlyValidName: boolean
|
||||
}
|
||||
|
||||
export const getNameFromAddressBook = (
|
||||
addressBook: AddressBookState,
|
||||
userAddress: string,
|
||||
options?: GetNameFromAddressBookOptions,
|
||||
): string | null => {
|
||||
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
|
||||
if (entry) {
|
||||
return entry.name
|
||||
return options?.filterOnlyValidName ? getValidAddressBookName(entry.name) : entry.name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const isValidAddressBookName = (addressBookName: string): boolean => {
|
||||
const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) =>
|
||||
addressBookName.toUpperCase().includes(invalidName),
|
||||
)
|
||||
return !hasInvalidName
|
||||
}
|
||||
|
||||
export const getValidAddressBookName = (addressBookName: string): string | null => {
|
||||
return isValidAddressBookName(addressBookName) ? addressBookName : null
|
||||
}
|
||||
|
||||
export const getOwnersWithNameFromAddressBook = (
|
||||
addressBook: AddressBookEntryProps,
|
||||
addressBook: AddressBookState,
|
||||
ownerList: List<SafeOwner>,
|
||||
): List<SafeOwner> | [] => {
|
||||
): List<SafeOwner> => {
|
||||
if (!ownerList) {
|
||||
return []
|
||||
return List([])
|
||||
}
|
||||
const ownersListWithAdbkNames = ownerList.map((owner) => {
|
||||
const ownerName = getNameFromAdbk(addressBook, owner.address)
|
||||
return ownerList.map((owner) => {
|
||||
const ownerName = getNameFromAddressBook(addressBook, owner.address)
|
||||
return {
|
||||
address: owner.address,
|
||||
name: ownerName || owner.name,
|
||||
}
|
||||
})
|
||||
return ownersListWithAdbkNames
|
||||
}
|
||||
|
||||
export const formatAddressListToAddressBookNames = (
|
||||
addressBook: AddressBookState,
|
||||
addresses: string[],
|
||||
): AddressBookEntry[] => {
|
||||
if (!addresses.length) {
|
||||
return []
|
||||
}
|
||||
return addresses.map((address) => {
|
||||
const ownerName = getNameFromAddressBook(addressBook, address)
|
||||
return {
|
||||
address: address,
|
||||
name: ownerName || '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* If the safe is not loaded, the owner wasn't not deleted
|
||||
* If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook
|
||||
* @param name
|
||||
* @param address
|
||||
* @param addressBook
|
||||
* @param safeAlreadyLoaded
|
||||
*/
|
||||
export const checkIfEntryWasDeletedFromAddressBook = (
|
||||
{ name, address }: AddressBookEntry,
|
||||
addressBook: AddressBookState,
|
||||
safeAlreadyLoaded: boolean,
|
||||
): boolean => {
|
||||
if (!safeAlreadyLoaded) {
|
||||
return false
|
||||
}
|
||||
|
||||
const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name)
|
||||
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
|
||||
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@ import { AbiItem } from 'web3-utils'
|
|||
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
export interface AbiItemExtended extends AbiItem {
|
||||
export interface AllowedAbiItem extends AbiItem {
|
||||
name: string
|
||||
type: 'function'
|
||||
}
|
||||
|
||||
export interface AbiItemExtended extends AllowedAbiItem {
|
||||
action: string
|
||||
methodSignature: string
|
||||
signatureHash: string
|
||||
}
|
||||
|
||||
export const getMethodSignature = ({ inputs, name }: AbiItem): string => {
|
||||
const params = inputs.map((x) => x.type).join(',')
|
||||
const params = inputs?.map((x) => x.type).join(',')
|
||||
return `${name}(${params})`
|
||||
}
|
||||
|
||||
|
@ -35,12 +40,17 @@ export const isAllowedMethod = ({ name, type }: AbiItem): boolean => {
|
|||
}
|
||||
|
||||
export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => {
|
||||
if (!stateMutability) {
|
||||
return 'write'
|
||||
}
|
||||
|
||||
return ['view', 'pure'].includes(stateMutability) ? 'read' : 'write'
|
||||
}
|
||||
|
||||
export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
||||
return abi
|
||||
.filter(isAllowedMethod)
|
||||
const allowedAbiItems = abi.filter(isAllowedMethod) as AllowedAbiItem[]
|
||||
|
||||
return allowedAbiItems
|
||||
.map(
|
||||
(method): AbiItemExtended => ({
|
||||
action: getMethodAction(method),
|
||||
|
@ -48,9 +58,11 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
|||
...method,
|
||||
}),
|
||||
)
|
||||
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||
.sort(({ name: a }, { name: b }) => {
|
||||
return a.toLowerCase() > b.toLowerCase() ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
|
||||
return method.payable
|
||||
return !!method?.payable
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
|||
*/
|
||||
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => {
|
||||
const contractInstance: any = new web3.eth.Contract(abi, address)
|
||||
const localBatch = batch ? null : new web3.BatchRequest()
|
||||
const localBatch = new web3.BatchRequest()
|
||||
|
||||
const values = methods.map((methodObject) => {
|
||||
let method, type, args = []
|
||||
|
@ -39,6 +39,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
|
|||
} else {
|
||||
request = contractInstance.methods[method](...args).call.request(resolver)
|
||||
}
|
||||
|
||||
// If batch was provided add to external batch
|
||||
batch ? batch.add(request) : localBatch.add(request)
|
||||
} catch (e) {
|
||||
resolve(null)
|
||||
|
@ -46,7 +48,11 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
|
|||
})
|
||||
})
|
||||
|
||||
localBatch && localBatch.execute()
|
||||
// TODO fix this so all batch.execute() are handled here
|
||||
// If batch was created locally we can already execute it
|
||||
// If batch was provided we should execute once we finish to generate the batch,
|
||||
// in the outside function where the batch object is created.
|
||||
!batch && localBatch.execute()
|
||||
|
||||
const returnValues = context ? [context, ...values] : values
|
||||
|
||||
|
|
|
@ -98,11 +98,9 @@ export const estimateGasForDeployingSafe = async (
|
|||
return gas * parseInt(gasPrice, 10)
|
||||
}
|
||||
|
||||
export const getGnosisSafeInstanceAt = async (safeAddress: string): Promise<GnosisSafe> => {
|
||||
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {
|
||||
const web3 = getWeb3()
|
||||
const gnosisSafe = await new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
|
||||
|
||||
return gnosisSafe
|
||||
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
|
||||
}
|
||||
|
||||
const cleanByteCodeMetadata = (bytecode: string): string => {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { aNewStore } from 'src/store'
|
||||
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||
import axios from 'axios'
|
||||
import { getTxServiceHost } from 'src/config'
|
||||
|
||||
jest.mock('axios')
|
||||
describe('fetchTokenCurrenciesBalances', () => {
|
||||
let store
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const excludeSpamTokens = true
|
||||
beforeEach(() => {
|
||||
store = aNewStore()
|
||||
})
|
||||
afterAll(() => {
|
||||
jest.unmock('axios')
|
||||
})
|
||||
|
||||
it('Given a safe address, calls the API and returns token balances', async () => {
|
||||
// given
|
||||
const expectedResult = [
|
||||
{
|
||||
balance: '849890000000000000',
|
||||
balanceUsd: '337.2449',
|
||||
token: null,
|
||||
tokenAddress: null,
|
||||
usdConversion: '396.81',
|
||||
},
|
||||
{
|
||||
balance: '24698677800000000000',
|
||||
balanceUsd: '29.3432',
|
||||
token: {
|
||||
name: 'Dai',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
||||
},
|
||||
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
|
||||
usdConversion: '1.188',
|
||||
},
|
||||
]
|
||||
const apiUrl = getTxServiceHost()
|
||||
|
||||
// @ts-ignore
|
||||
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
|
||||
|
||||
// when
|
||||
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(axios.get).toHaveBeenCalled()
|
||||
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`, {
|
||||
params: { limit: 3000 },
|
||||
})
|
||||
})
|
||||
})
|
|
@ -6,17 +6,17 @@ import { TokenProps } from 'src/logic/tokens/store/model/token'
|
|||
export type BalanceEndpoint = {
|
||||
balance: string
|
||||
balanceUsd: string
|
||||
tokenAddress?: string
|
||||
tokenAddress: string
|
||||
token?: TokenProps
|
||||
usdConversion: string
|
||||
}
|
||||
|
||||
const fetchTokenCurrenciesBalances = (safeAddress?: string): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
||||
if (!safeAddress) {
|
||||
return null
|
||||
}
|
||||
const fetchTokenCurrenciesBalances = (
|
||||
safeAddress: string,
|
||||
excludeSpamTokens = true,
|
||||
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
||||
const apiUrl = getTxServiceHost()
|
||||
const url = `${apiUrl}safes/${safeAddress}/balances/usd/`
|
||||
const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
|
||||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import { List } from 'immutable'
|
||||
import { batch } from 'react-redux'
|
||||
|
||||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { loadCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const fetchCurrencyValues = (safeAddress: string) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const storedCurrencies: Map<string, CurrencyRateValue> | unknown = await loadCurrencyValues()
|
||||
const storedCurrency = storedCurrencies[safeAddress]
|
||||
if (!storedCurrency) {
|
||||
return batch(() => {
|
||||
dispatch(setCurrencyBalances(safeAddress, List([])))
|
||||
dispatch(setSelectedCurrency(safeAddress, AVAILABLE_CURRENCIES.USD))
|
||||
dispatch(setCurrencyRate(safeAddress, 1))
|
||||
})
|
||||
}
|
||||
// Loads the stored state on redux
|
||||
Object.entries(storedCurrencies).forEach((kv) => {
|
||||
const safeAddr = kv[0]
|
||||
const value = kv[1]
|
||||
|
||||
let { currencyRate, selectedCurrency }: CurrencyRateValue = value
|
||||
|
||||
// Fallback for users that got an undefined saved on localStorage
|
||||
if (!selectedCurrency || selectedCurrency === AVAILABLE_CURRENCIES.USD) {
|
||||
currencyRate = 1
|
||||
selectedCurrency = AVAILABLE_CURRENCIES.USD
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(setSelectedCurrency(safeAddr, selectedCurrency))
|
||||
dispatch(setCurrencyRate(safeAddr, currencyRate))
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error fetching currency values', err)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const fetchSelectedCurrency = (safeAddress: string) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const storedSelectedCurrency = await loadSelectedCurrency()
|
||||
|
||||
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
|
||||
} catch (err) {
|
||||
console.error('Error fetching currency values', err)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const setCurrencyBalances = createAction(SET_CURRENCY_BALANCES, (safeAddress, currencyBalances) => ({
|
||||
safeAddress,
|
||||
currencyBalances,
|
||||
}))
|
||||
export const setCurrencyBalances = createAction(
|
||||
SET_CURRENCY_BALANCES,
|
||||
(safeAddress: string, currencyBalances: BalanceCurrencyList) => ({
|
||||
safeAddress,
|
||||
currencyBalances,
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
import { AnyAction } from 'redux'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { AVAILABLE_CURRENCIES } from '../model/currencyValues'
|
||||
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
|
||||
|
||||
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
|
||||
|
||||
export const setSelectedCurrency = createAction(
|
||||
const setCurrentCurrency = createAction(
|
||||
SET_CURRENT_CURRENCY,
|
||||
(safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => ({
|
||||
safeAddress,
|
||||
selectedCurrency,
|
||||
}),
|
||||
)
|
||||
|
||||
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => (
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
|
||||
): void => {
|
||||
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
|
||||
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
|
||||
}
|
||||
|
|
|
@ -1,39 +1,16 @@
|
|||
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
|
||||
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 { currencyValuesSelector } from 'src/logic/currencyValues/store/selectors'
|
||||
import { saveCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import { AVAILABLE_CURRENCIES, CurrencyRateValue } from '../model/currencyValues'
|
||||
import { Map } from 'immutable'
|
||||
import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
|
||||
const watchedActions = [SET_CURRENT_CURRENCY, SET_CURRENCY_RATE, SET_CURRENCY_BALANCES]
|
||||
const watchedActions = [SET_CURRENT_CURRENCY]
|
||||
|
||||
const currencyValuesStorageMiddleware = (store) => (next) => async (action) => {
|
||||
const currencyValuesStorageMiddleware = () => (next) => async (action) => {
|
||||
const handledAction = next(action)
|
||||
if (watchedActions.includes(action.type)) {
|
||||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
switch (action.type) {
|
||||
case SET_CURRENT_CURRENCY: {
|
||||
const { safeAddress, selectedCurrency } = action.payload
|
||||
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
|
||||
break
|
||||
}
|
||||
case SET_CURRENCY_RATE:
|
||||
case SET_CURRENCY_BALANCES: {
|
||||
const currencyValues = currencyValuesSelector(state)
|
||||
const { selectedCurrency } = action.payload
|
||||
|
||||
const currencyValuesWithoutBalances: Map<string, CurrencyRateValue> = currencyValues.map((currencyValue) => {
|
||||
const currencyRate: number = currencyValue.get('currencyRate')
|
||||
const selectedCurrency: AVAILABLE_CURRENCIES = currencyValue.get('selectedCurrency')
|
||||
return {
|
||||
currencyRate,
|
||||
selectedCurrency,
|
||||
}
|
||||
})
|
||||
|
||||
await saveCurrencyValues(currencyValuesWithoutBalances)
|
||||
saveSelectedCurrency(selectedCurrency)
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export const safeFiatBalancesSelector = createSelector(
|
|||
currencyValuesSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
|
||||
if (!currencyValues) return
|
||||
if (!currencyValues || !safeAddress) return
|
||||
return currencyValues.get(safeAddress)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { CurrencyRateValue } from '../model/currencyValues'
|
||||
import { Map } from 'immutable'
|
||||
import { AVAILABLE_CURRENCIES } from '../model/currencyValues'
|
||||
|
||||
const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY'
|
||||
export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRateValue>): Promise<void> => {
|
||||
const SELECTED_CURRENCY_STORAGE_KEY = 'SELECTED_CURRENCY'
|
||||
export const saveSelectedCurrency = async (selectedCurrency: AVAILABLE_CURRENCIES): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues)
|
||||
await saveToStorage(SELECTED_CURRENCY_STORAGE_KEY, selectedCurrency)
|
||||
} catch (err) {
|
||||
console.error('Error storing currency values info in localstorage', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | unknown> => {
|
||||
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
|
||||
export const loadSelectedCurrency = async (): Promise<AVAILABLE_CURRENCIES | undefined> => {
|
||||
return await loadFromStorage(SELECTED_CURRENCY_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
import updateViewedSafes from 'src/logic/currentSession/store/actions/updateViewedSafes'
|
||||
|
||||
const addViewedSafe = (safeAddress) => (dispatch) => {
|
||||
const addViewedSafe = (safeAddress: string) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateViewedSafes(safeAddress))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Dispatch } from 'redux'
|
||||
|
||||
import loadCurrentSession from 'src/logic/currentSession/store/actions/loadCurrentSession'
|
||||
import { makeCurrentSession } from 'src/logic/currentSession/store/model/currentSession'
|
||||
import { getCurrentSessionFromStorage } from 'src/logic/currentSession/utils'
|
||||
|
||||
const loadCurrentSessionFromStorage = () => async (dispatch) => {
|
||||
const loadCurrentSessionFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||
const currentSession = await getCurrentSessionFromStorage()
|
||||
|
||||
dispatch(loadCurrentSession(makeCurrentSession(currentSession ? currentSession : {})))
|
||||
dispatch(loadCurrentSession(currentSession))
|
||||
}
|
||||
|
||||
export default loadCurrentSessionFromStorage
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { Record } from 'immutable'
|
||||
|
||||
export const makeCurrentSession = Record({
|
||||
viewedSafes: [],
|
||||
})
|
|
@ -1,4 +1,3 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { LOAD_CURRENT_SESSION } from 'src/logic/currentSession/store/actions/loadCurrentSession'
|
||||
|
@ -7,20 +6,32 @@ import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils'
|
|||
|
||||
export const CURRENT_SESSION_REDUCER_ID = 'currentSession'
|
||||
|
||||
export type CurrentSessionState = {
|
||||
viewedSafes: string[]
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
viewedSafes: [],
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[LOAD_CURRENT_SESSION]: (state, action) => state.merge(Map(action.payload)),
|
||||
[LOAD_CURRENT_SESSION]: (state = initialState, action) => ({
|
||||
...state,
|
||||
...action.payload,
|
||||
}),
|
||||
[UPDATE_VIEWED_SAFES]: (state, action) => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
const newState = state.updateIn(['viewedSafes'], (prev) =>
|
||||
prev.includes(safeAddress) ? prev : [...prev, safeAddress],
|
||||
)
|
||||
const viewedSafes = state.viewedSafes
|
||||
const newState = {
|
||||
...state,
|
||||
viewedSafes: viewedSafes.includes(safeAddress) ? viewedSafes : [...viewedSafes, safeAddress],
|
||||
}
|
||||
|
||||
saveCurrentSessionToStorage(newState)
|
||||
|
||||
return newState
|
||||
},
|
||||
},
|
||||
Map(),
|
||||
initialState,
|
||||
)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession'
|
||||
|
||||
const CURRENT_SESSION_STORAGE_KEY = 'CURRENT_SESSION'
|
||||
|
||||
export const getCurrentSessionFromStorage = async () => loadFromStorage(CURRENT_SESSION_STORAGE_KEY)
|
||||
export const getCurrentSessionFromStorage = async (): Promise<CurrentSessionState | undefined> =>
|
||||
loadFromStorage(CURRENT_SESSION_STORAGE_KEY)
|
||||
|
||||
export const saveCurrentSessionToStorage = async (currentSession) => {
|
||||
try {
|
||||
await saveToStorage(CURRENT_SESSION_STORAGE_KEY, currentSession.toJSON())
|
||||
await saveToStorage(CURRENT_SESSION_STORAGE_KEY, currentSession)
|
||||
} catch (err) {
|
||||
console.error('Error storing currentSession in localStorage', err)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ interface DebounceOptions {
|
|||
export const useDebouncedCallback = <T extends (...args: unknown[]) => unknown>(
|
||||
callback: T,
|
||||
delay = 0,
|
||||
options: DebounceOptions,
|
||||
options?: DebounceOptions,
|
||||
): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options])
|
||||
|
||||
export const useDebounce = <T extends unknown>(value: T, delay = 0, options?: DebounceOptions): T => {
|
||||
|
|
|
@ -15,7 +15,7 @@ const setNotificationOrigin = (notification: Notification, origin: string): Noti
|
|||
}
|
||||
|
||||
const appInfo = getAppInfoFromOrigin(origin)
|
||||
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
|
||||
return { ...notification, message: `${appInfo ? appInfo.name : 'Unknown origin'}: ${notification.message}` }
|
||||
}
|
||||
|
||||
const getStandardTxNotificationsQueue = (
|
||||
|
|
|
@ -3,7 +3,7 @@ import { batch, useDispatch } from 'react-redux'
|
|||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import { fetchCurrencyValues } from 'src/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
|
||||
import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance'
|
||||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
|
@ -19,7 +19,7 @@ export const useFetchTokens = (safeAddress: string): void => {
|
|||
batch(() => {
|
||||
// fetch tokens there to get symbols for tokens in TXs list
|
||||
dispatch(fetchTokens())
|
||||
dispatch(fetchCurrencyValues(safeAddress))
|
||||
dispatch(fetchSelectedCurrency(safeAddress))
|
||||
dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTr
|
|||
import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
export const useLoadSafe = (safeAddress: string): void => {
|
||||
export const useLoadSafe = (safeAddress?: string): void => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -22,13 +22,13 @@ export const useLoadSafe = (safeAddress: string): void => {
|
|||
return dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
dispatch(fetchSafeCreationTx(safeAddress))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return dispatch(addViewedSafe(safeAddress))
|
||||
})
|
||||
}
|
||||
}
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
|
||||
fetchData()
|
||||
}, [dispatch, safeAddress])
|
||||
|
|
|
@ -8,9 +8,9 @@ import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
|||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||
import { TIMEOUT } from 'src/utils/constants'
|
||||
|
||||
export const useSafeScheduledUpdates = (safeAddress: string): void => {
|
||||
export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
||||
const dispatch = useDispatch()
|
||||
const timer = useRef<number>(null)
|
||||
const timer = useRef<number>()
|
||||
|
||||
useEffect(() => {
|
||||
// using this variable to prevent setting a timeout when the component is already unmounted or the effect
|
||||
|
@ -29,7 +29,7 @@ export const useSafeScheduledUpdates = (safeAddress: string): void => {
|
|||
|
||||
if (mounted) {
|
||||
timer.current = setTimeout(() => {
|
||||
fetchSafeData(safeAddress)
|
||||
fetchSafeData(address)
|
||||
}, TIMEOUT * 3)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,875 @@
|
|||
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
||||
|
||||
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
|
||||
import { TransactionStatus, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
|
||||
import makeSafe from 'src/logic/safe/store/models/safe'
|
||||
import { List, Map, Record } from 'immutable'
|
||||
import { makeToken, TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import {
|
||||
buildTx,
|
||||
calculateTransactionStatus,
|
||||
calculateTransactionType,
|
||||
generateSafeTxHash,
|
||||
getRefundParams,
|
||||
isCancelTransaction,
|
||||
isCustomTransaction,
|
||||
isInnerTransaction,
|
||||
isModifySettingsTransaction,
|
||||
isMultiSendTransaction,
|
||||
isOutgoingTransaction,
|
||||
isPendingTransaction,
|
||||
isUpgradeTransaction,
|
||||
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getERC20DecimalsAndSymbol } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
describe('isInnerTransaction', () => {
|
||||
it('It should return true if the transaction recipient is our given safeAddress and the txValue is 0', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return false if the transaction recipient is our given safeAddress and the txValue is >0', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '100' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return false if the transaction recipient is not our given safeAddress', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return true if the transaction recipient is the given safeAddress and the txValue is 0', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ recipient: safeAddress, value: '0' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return false if the transaction recipient is the given safeAddress and the txValue is >0', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ recipient: safeAddress, value: '100' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return false if the transaction recipient is not the given safeAddress', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ recipient: safeAddress2, value: '100' })
|
||||
|
||||
// when
|
||||
const result = isInnerTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isCancelTransaction', () => {
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
it('It should return false if given a inner transaction with empty data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
|
||||
// when
|
||||
const result = isCancelTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return false if given a inner transaction without empty data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
|
||||
|
||||
// when
|
||||
const result = isCancelTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPendingTransaction', () => {
|
||||
it('It should return true if the transaction is on pending status', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ status: TransactionStatus.PENDING })
|
||||
const cancelTx = makeTransaction({ data: null })
|
||||
|
||||
// when
|
||||
const result = isPendingTransaction(transaction, cancelTx)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return true If the transaction is not pending status but the cancellation transaction is', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS })
|
||||
const cancelTx = makeTransaction({ status: TransactionStatus.PENDING })
|
||||
|
||||
// when
|
||||
const result = isPendingTransaction(transaction, cancelTx)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return true If the transaction and a cancellation transaction are not pending', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ status: TransactionStatus.CANCELLED })
|
||||
const cancelTx = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS })
|
||||
|
||||
// when
|
||||
const result = isPendingTransaction(transaction, cancelTx)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isModifySettingsTransaction', () => {
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
it('It should return true if given an inner transaction without empty data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
|
||||
|
||||
// when
|
||||
const result = isModifySettingsTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return false if given an inner transaction with empty data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
|
||||
// when
|
||||
const result = isModifySettingsTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMultiSendTransaction', () => {
|
||||
it('It should return true if given a transaction without value, the data has multisend data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' })
|
||||
|
||||
// when
|
||||
const result = isMultiSendTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return false if given a transaction without data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
|
||||
// when
|
||||
const result = isMultiSendTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return true if given a transaction without value, the data has not multisend substring', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'thisiswrongdata' })
|
||||
|
||||
// when
|
||||
const result = isMultiSendTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUpgradeTransaction', () => {
|
||||
it('If should return true if the transaction data is empty', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
|
||||
// when
|
||||
const result = isUpgradeTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return false if the transaction data is multisend transaction but does not have upgradeTx function signature encoded in data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' })
|
||||
|
||||
// when
|
||||
const result = isUpgradeTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return true if the transaction data is multisend transaction and has upgradeTx enconded in function signature data', () => {
|
||||
// given
|
||||
const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000`
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: upgradeTxData })
|
||||
|
||||
// when
|
||||
const result = isUpgradeTransaction(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOutgoingTransaction', () => {
|
||||
it('It should return true if the transaction recipient is not a safe address and data is not empty', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
|
||||
// when
|
||||
const result = isOutgoingTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('It should return true if the transaction has an address equal to the safe address', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
|
||||
|
||||
// when
|
||||
const result = isOutgoingTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return false if the transaction recipient is not a safe address and data is empty', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
|
||||
// when
|
||||
const result = isOutgoingTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
jest.mock('src/logic/tokens/utils/tokenHelpers')
|
||||
describe('isCustomTransaction', () => {
|
||||
afterAll(() => {
|
||||
jest.unmock('src/logic/tokens/utils/tokenHelpers')
|
||||
})
|
||||
it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||
|
||||
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
|
||||
|
||||
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
|
||||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
|
||||
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||
|
||||
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
|
||||
|
||||
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
|
||||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
|
||||
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return false if is outgoing transaction, not SendERC20Transaction, isUpgradeTransaction and not isSendERC721Transaction', async () => {
|
||||
// given
|
||||
const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000`
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||
|
||||
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
|
||||
|
||||
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => true)
|
||||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return false if is outgoing transaction, is not SendERC20Transaction, not isUpgradeTransaction and isSendERC721Transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||
|
||||
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
|
||||
|
||||
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
|
||||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => true)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
|
||||
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRefundParams', () => {
|
||||
it('It should return null if given a transaction with the gasPrice == 0', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice: '0' })
|
||||
|
||||
// when
|
||||
const result = await getRefundParams(transaction, getERC20DecimalsAndSymbol)
|
||||
|
||||
// then
|
||||
expect(result).toBe(null)
|
||||
})
|
||||
it('It should return 0.000000000000020000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 18 decimals', async () => {
|
||||
// given
|
||||
const gasPrice = '100'
|
||||
const baseGas = 100
|
||||
const safeTxGas = 100
|
||||
const decimals = 18
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas })
|
||||
const feeString = (Number(gasPrice) * (Number(baseGas) + Number(safeTxGas))).toString().padStart(decimals, '0')
|
||||
const whole = feeString.slice(0, feeString.length - decimals) || '0'
|
||||
const fraction = feeString.slice(feeString.length - decimals)
|
||||
|
||||
const expectedResult = {
|
||||
fee: `${whole}.${fraction}`,
|
||||
symbol: 'ETH',
|
||||
}
|
||||
|
||||
const getTokenInfoMock = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
symbol: 'ETH',
|
||||
decimals,
|
||||
}
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await getRefundParams(transaction, getTokenInfoMock)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(getTokenInfoMock).toBeCalled()
|
||||
})
|
||||
it('Given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 1 decimal, returns 2000.0', async () => {
|
||||
// given
|
||||
const gasPrice = '100'
|
||||
const baseGas = 100
|
||||
const safeTxGas = 100
|
||||
const decimals = 1
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas })
|
||||
|
||||
const expectedResult = {
|
||||
fee: `2000.0`,
|
||||
symbol: 'ETH',
|
||||
}
|
||||
|
||||
const getTokenInfoMock = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
symbol: 'ETH',
|
||||
decimals,
|
||||
}
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await getRefundParams(transaction, getTokenInfoMock)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(getTokenInfoMock).toBeCalled()
|
||||
})
|
||||
it('It should return 0.50000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 400 and 5 decimals', async () => {
|
||||
// given
|
||||
const gasPrice = '100'
|
||||
const baseGas = 100
|
||||
const safeTxGas = 400
|
||||
const decimals = 5
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas })
|
||||
|
||||
const expectedResult = {
|
||||
fee: `0.50000`,
|
||||
symbol: 'ETH',
|
||||
}
|
||||
|
||||
const getTokenInfoMock = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
symbol: 'ETH',
|
||||
decimals,
|
||||
}
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await getRefundParams(transaction, getTokenInfoMock)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(getTokenInfoMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDecodedParams', () => {
|
||||
it('', () => {
|
||||
// given
|
||||
// when
|
||||
// then
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTransactionCancelled', () => {
|
||||
it('', () => {
|
||||
// given
|
||||
// when
|
||||
// then
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateTransactionStatus', () => {
|
||||
it('It should return SUCCESS if the tx is executed and successful', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ isExecuted: true, isSuccessful: true })
|
||||
const safe = makeSafe()
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.SUCCESS)
|
||||
})
|
||||
it('It should return CANCELLED if the tx is cancelled and successful', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ cancelled: true })
|
||||
const safe = makeSafe()
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.CANCELLED)
|
||||
})
|
||||
it('It should return AWAITING_EXECUTION if the tx has an amount of confirmations equal to the safe threshold', () => {
|
||||
// given
|
||||
const makeUser = Record({
|
||||
owner: '',
|
||||
type: '',
|
||||
hash: '',
|
||||
signature: '',
|
||||
})
|
||||
const transaction = makeTransaction({ cancelled: true, confirmations: List([makeUser(), makeUser(), makeUser()]) })
|
||||
const safe = makeSafe({ threshold: 3 })
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.CANCELLED)
|
||||
})
|
||||
it('It should return SUCCESS if the tx is the creation transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ creationTx: true, confirmations: List() })
|
||||
const safe = makeSafe({ threshold: 3 })
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.SUCCESS)
|
||||
})
|
||||
it('It should return PENDING if the tx is pending', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ confirmations: List(), isPending: true })
|
||||
const safe = makeSafe({ threshold: 3 })
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.PENDING)
|
||||
})
|
||||
it('It should return PENDING if the tx has no confirmations', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ confirmations: List(), isPending: false })
|
||||
const safe = makeSafe({ threshold: 3 })
|
||||
const currentUser = safeAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.PENDING)
|
||||
})
|
||||
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const makeUser = Record({
|
||||
owner: '',
|
||||
type: '',
|
||||
hash: '',
|
||||
signature: '',
|
||||
})
|
||||
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
|
||||
const safe = makeSafe({
|
||||
threshold: 3,
|
||||
owners: List([
|
||||
{ name: '', address: userAddress },
|
||||
{ name: '', address: userAddress2 },
|
||||
]),
|
||||
})
|
||||
const currentUser = userAddress
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS)
|
||||
})
|
||||
it('It should return AWAITING_YOUR_CONFIRMATION if the tx has confirmations bellow the threshold, the user is owner and not signed', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const makeUser = Record({
|
||||
owner: '',
|
||||
type: '',
|
||||
hash: '',
|
||||
signature: '',
|
||||
})
|
||||
|
||||
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
|
||||
const safe = makeSafe({
|
||||
threshold: 3,
|
||||
owners: List([
|
||||
{ name: '', address: userAddress },
|
||||
{ name: '', address: userAddress2 },
|
||||
]),
|
||||
})
|
||||
const currentUser = userAddress2
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.AWAITING_YOUR_CONFIRMATION)
|
||||
})
|
||||
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is not owner', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const makeUser = Record({
|
||||
owner: '',
|
||||
type: '',
|
||||
hash: '',
|
||||
signature: '',
|
||||
})
|
||||
|
||||
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
|
||||
const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) })
|
||||
const currentUser = userAddress2
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS)
|
||||
})
|
||||
it('It should return FAILED if the tx is not successful', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const makeUser = Record({
|
||||
owner: '',
|
||||
type: '',
|
||||
hash: '',
|
||||
signature: '',
|
||||
})
|
||||
|
||||
const transaction = makeTransaction({
|
||||
confirmations: List([makeUser({ owner: userAddress })]),
|
||||
isSuccessful: false,
|
||||
})
|
||||
const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) })
|
||||
const currentUser = userAddress2
|
||||
|
||||
// when
|
||||
const result = calculateTransactionStatus(transaction, safe, currentUser)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionStatus.FAILED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateTransactionType', () => {
|
||||
it('It should return TOKEN If the tx is a token transfer transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ isTokenTransfer: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.TOKEN)
|
||||
})
|
||||
it('It should return COLLECTIBLE If the tx is a collectible transfer transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ isCollectibleTransfer: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.COLLECTIBLE)
|
||||
})
|
||||
it('It should return SETTINGS If the tx is a modifySettings transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ modifySettingsTx: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.SETTINGS)
|
||||
})
|
||||
|
||||
it('It should return CANCELLATION If the tx is a cancellation transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ isCancellationTx: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.CANCELLATION)
|
||||
})
|
||||
|
||||
it('It should return CUSTOM If the tx is a custom transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ customTx: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.CUSTOM)
|
||||
})
|
||||
it('It should return CUSTOM If the tx is a creation transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ creationTx: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.CREATION)
|
||||
})
|
||||
it('It should return UPGRADE If the tx is an upgrade transaction', () => {
|
||||
// given
|
||||
const transaction = makeTransaction({ upgradeTx: true })
|
||||
|
||||
// when
|
||||
const result = calculateTransactionType(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toBe(TransactionTypes.UPGRADE)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildTx', () => {
|
||||
it('Returns a valid transaction', async () => {
|
||||
// given
|
||||
const cancelTx1 = makeTransaction()
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
|
||||
const userAddress = 'address1'
|
||||
const cancellationTxs = List([cancelTx1])
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
|
||||
const outgoingTxs = List([cancelTx1])
|
||||
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
|
||||
const expectedTx = makeTransaction({
|
||||
baseGas: 0,
|
||||
blockNumber: 0,
|
||||
cancelled: false,
|
||||
confirmations: List([]),
|
||||
creationTx: false,
|
||||
customTx: false,
|
||||
data: EMPTY_DATA,
|
||||
dataDecoded: null,
|
||||
decimals: 18,
|
||||
decodedParams: null,
|
||||
executionDate: '',
|
||||
executionTxHash: '',
|
||||
executor: '',
|
||||
gasPrice: '',
|
||||
gasToken: ZERO_ADDRESS,
|
||||
isCancellationTx: false,
|
||||
isCollectibleTransfer: false,
|
||||
isExecuted: false,
|
||||
isSuccessful: false,
|
||||
isTokenTransfer: false,
|
||||
modifySettingsTx: false,
|
||||
multiSendTx: false,
|
||||
nonce: 0,
|
||||
operation: 0,
|
||||
origin: '',
|
||||
recipient: safeAddress2,
|
||||
refundParams: null,
|
||||
refundReceiver: ZERO_ADDRESS,
|
||||
safeTxGas: 0,
|
||||
safeTxHash: '',
|
||||
setupData: '',
|
||||
status: TransactionStatus.FAILED,
|
||||
submissionDate: '',
|
||||
symbol: 'ETH',
|
||||
upgradeTx: false,
|
||||
value: '0',
|
||||
fee: '',
|
||||
})
|
||||
|
||||
// when
|
||||
const txResult = await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser: userAddress,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe: safeInstance,
|
||||
tx: transaction,
|
||||
txCode: null,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(txResult).toStrictEqual(expectedTx)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateStoredTransactionsStatus', () => {
|
||||
it('', () => {
|
||||
// given
|
||||
// when
|
||||
// then
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateSafeTxHash', () => {
|
||||
it('It should return a safe transaction hash', () => {
|
||||
// given
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const userAddress3 = 'address3'
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
const txArgs = {
|
||||
baseGas: 100,
|
||||
data: '',
|
||||
gasPrice: '1000',
|
||||
gasToken: '',
|
||||
nonce: 0,
|
||||
operation: 0,
|
||||
refundReceiver: userAddress,
|
||||
safeInstance,
|
||||
safeTxGas: 1000,
|
||||
sender: userAddress2,
|
||||
sigs: '',
|
||||
to: userAddress3,
|
||||
valueInWei: '5000',
|
||||
}
|
||||
|
||||
// when
|
||||
const result = generateSafeTxHash(safeAddress, txArgs)
|
||||
|
||||
// then
|
||||
expect(result).toBe('0x21e6ebc992f959dd0a2a6ce6034c414043c598b7f446c274efb3527c30dec254')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { SafeRecordProps } from '../models/safe'
|
||||
|
||||
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
||||
|
||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
||||
safe,
|
||||
}))
|
|
@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
|
|||
return List(owners)
|
||||
}
|
||||
|
||||
export const addSafe = createAction(ADD_SAFE, (safe) => ({
|
||||
export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
|
||||
safe,
|
||||
loadedFromStorage,
|
||||
}))
|
||||
|
||||
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
|
||||
const state = getState()
|
||||
const safeList = safesListSelector(state)
|
||||
|
||||
dispatch(addSafe(safe))
|
||||
dispatch(addSafe(safe, true))
|
||||
|
||||
if (safeList.size === 0) {
|
||||
dispatch(setDefaultSafe(safe.address))
|
||||
|
|
|
@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'
|
|||
|
||||
import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { Transaction } from '../../models/types/transactions'
|
||||
import { Transaction } from '../../models/types/transactions.d'
|
||||
|
||||
export type ServiceUriParams = {
|
||||
safeAddress: string
|
||||
|
@ -30,8 +30,8 @@ const getAllTransactionsUri = (safeAddress: string): string => {
|
|||
|
||||
const fetchAllTransactions = async (
|
||||
urlParams: ServiceUriParams,
|
||||
eTag: string | null,
|
||||
): Promise<{ responseEtag: string; results: Transaction[]; count?: number }> => {
|
||||
eTag?: string,
|
||||
): Promise<{ responseEtag?: string; results: Transaction[]; count?: number }> => {
|
||||
const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams
|
||||
try {
|
||||
const url = getAllTransactionsUri(safeAddress)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { Transaction } from '../../models/types/transactions'
|
||||
import { Transaction } from '../../models/types/transactions.d'
|
||||
|
||||
export const LOAD_MORE_TRANSACTIONS = 'LOAD_MORE_TRANSACTIONS'
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import semverSatisfies from 'semver/functions/satisfies'
|
|||
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 {
|
||||
|
@ -100,7 +101,7 @@ interface CreateTransactionArgs {
|
|||
navigateToTransactionsTab?: boolean
|
||||
notifiedTransaction: string
|
||||
operation?: number
|
||||
origin?: string
|
||||
origin?: string | null
|
||||
safeAddress: string
|
||||
to: string
|
||||
txData?: string
|
||||
|
@ -110,6 +111,7 @@ interface CreateTransactionArgs {
|
|||
|
||||
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
|
||||
type ConfirmEventHandler = (safeTxHash: string) => void
|
||||
type ErrorEventHandler = () => void
|
||||
|
||||
const createTransaction = (
|
||||
{
|
||||
|
@ -124,6 +126,7 @@ const createTransaction = (
|
|||
origin = null,
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
onError?: ErrorEventHandler,
|
||||
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
|
@ -169,6 +172,7 @@ const createTransaction = (
|
|||
sender: from,
|
||||
sigs,
|
||||
}
|
||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
|
@ -176,20 +180,19 @@ const createTransaction = (
|
|||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
|
||||
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 })
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
onUserConfirm?.(safeTxHash)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
const tx = isExecution
|
||||
? await getExecutionTransaction(txArgs)
|
||||
: await getApprovalTransaction(safeInstance, safeTxHash)
|
||||
|
@ -205,6 +208,7 @@ const createTransaction = (
|
|||
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)
|
||||
|
@ -240,6 +244,8 @@ const createTransaction = (
|
|||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
|
||||
onError?.()
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
|
|||
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
|
||||
import addSafe from 'src/logic/safe/store/actions/addSafe'
|
||||
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
|
||||
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
|
@ -17,8 +16,9 @@ import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/mod
|
|||
import { Action, Dispatch } from 'redux'
|
||||
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { latestMasterContractVersionSelector } from '../selectors'
|
||||
|
||||
const buildOwnersFrom = (safeOwners: string[], localSafe: SafeRecordProps): List<SafeOwner> => {
|
||||
const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => {
|
||||
const ownersList = safeOwners.map((ownerAddress) => {
|
||||
const convertedAdd = checksumAddress(ownerAddress)
|
||||
|
||||
|
@ -85,7 +85,7 @@ export const buildSafe = async (
|
|||
needsUpdate,
|
||||
featuresEnabled,
|
||||
balances: Map(),
|
||||
latestIncomingTxBlock: null,
|
||||
latestIncomingTxBlock: 0,
|
||||
activeAssets: Set(),
|
||||
activeTokens: Set(),
|
||||
blacklistedAssets: Set(),
|
||||
|
@ -114,11 +114,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
|||
])
|
||||
|
||||
// Converts from [ { address, ownerName} ] to address array
|
||||
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined
|
||||
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : []
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
name: localSafe?.name,
|
||||
modules: buildModulesLinkedList(modules?.array, modules?.next),
|
||||
nonce: Number(remoteNonce),
|
||||
threshold: Number(remoteThreshold),
|
||||
|
@ -126,30 +127,27 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
|||
)
|
||||
|
||||
// If the remote owners does not contain a local address, we remove that local owner
|
||||
if (localOwners) {
|
||||
localOwners.forEach((localAddress) => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (remoteOwnerIndex === -1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
|
||||
}
|
||||
})
|
||||
localOwners.forEach((localAddress) => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (remoteOwnerIndex === -1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
|
||||
}
|
||||
})
|
||||
|
||||
// If the remote has an owner that we don't have locally, we add it
|
||||
remoteOwners.forEach((remoteAddress) => {
|
||||
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (localOwnerIndex === -1) {
|
||||
dispatch(
|
||||
addSafeOwner({
|
||||
safeAddress,
|
||||
ownerAddress: remoteAddress,
|
||||
ownerName: 'UNKNOWN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
// If the remote has an owner that we don't have locally, we add it
|
||||
remoteOwners.forEach((remoteAddress) => {
|
||||
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (localOwnerIndex === -1) {
|
||||
dispatch(
|
||||
addSafeOwner({
|
||||
safeAddress,
|
||||
ownerAddress: remoteAddress,
|
||||
ownerName: 'UNKNOWN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default (safeAdd: string) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppReduxState,
|
||||
|
@ -157,12 +155,15 @@ export default (safeAdd: string) => async (
|
|||
try {
|
||||
const safeAddress = checksumAddress(safeAdd)
|
||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
||||
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
|
||||
const latestMasterContractVersion = latestMasterContractVersionSelector(getState())
|
||||
const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
|
||||
|
||||
dispatch(addSafe(safeProps))
|
||||
// `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call
|
||||
// and `addSafe` will only add a newly non-existent safe
|
||||
// For the case where the safe does not exist in the localStorage,
|
||||
// `updateSafe` uses a default `notSetValue` to add the Safe to the store
|
||||
dispatch(updateSafe(safeProps))
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while updating Safe information: ', err)
|
||||
|
||||
return Promise.resolve()
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { addSafe } from './addSafe'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
import { SAFES_KEY } from 'src/logic/safe/utils'
|
||||
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { buildSafe } from 'src/logic/safe/store/reducer/safe'
|
||||
|
||||
import { loadFromStorage } from 'src/utils/storage'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
import { addSafe } from './addSafe'
|
||||
|
||||
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const safes = await loadFromStorage(SAFES_KEY)
|
||||
const safes = await loadFromStorage<Record<string, SafeRecordProps>>(SAFES_KEY)
|
||||
|
||||
if (safes) {
|
||||
Object.values(safes).forEach((safeProps) => {
|
||||
dispatch(addSafe(buildSafe(safeProps)))
|
||||
dispatch(addSafe(buildSafe(safeProps), true))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { fromJS } from 'immutable'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
|
@ -20,9 +19,9 @@ import {
|
|||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { makeConfirmation } from '../models/confirmation'
|
||||
import { storeTx } from './createTransaction'
|
||||
import { TransactionStatus } from '../models/types/transaction'
|
||||
import { TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
|
||||
|
||||
const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async (
|
||||
dispatch,
|
||||
|
@ -34,7 +33,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(null, lastTx, safeInstance)
|
||||
const nonce = await getNewTxNonce(undefined, lastTx, safeInstance)
|
||||
const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
|
||||
|
@ -79,12 +78,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
|
||||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
||||
|
||||
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))
|
||||
|
@ -105,9 +106,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
|
||||
const txToMock: TxToMock = {
|
||||
...txArgs,
|
||||
confirmations: txArgs.confirmations, // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
|
||||
value: txArgs.valueInWei,
|
||||
submissionDate: txArgs.submissionDate,
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
||||
|
@ -123,10 +122,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash }),
|
||||
storeTx(
|
||||
mockedTx.updateIn(
|
||||
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
|
||||
(previous) => previous.push(from),
|
||||
),
|
||||
mockedTx.withMutations((record) => {
|
||||
record
|
||||
.updateIn(
|
||||
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
|
||||
(previous) => previous.push(from),
|
||||
)
|
||||
.set('status', TransactionStatus.PENDING)
|
||||
}),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
state,
|
||||
|
@ -175,16 +178,20 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
: TransactionStatus.FAILED,
|
||||
)
|
||||
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
|
||||
.updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear())
|
||||
})
|
||||
: mockedTx.withMutations((record) => {
|
||||
record
|
||||
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
|
||||
previous.pop(),
|
||||
)
|
||||
.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
|
||||
})
|
||||
: mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
|
||||
|
||||
await storeTx(
|
||||
toStoreTx.withMutations((record) => {
|
||||
record
|
||||
.set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })]))
|
||||
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
|
||||
previous.pop(from),
|
||||
)
|
||||
toStoreTx.update('confirmations', (confirmations) => {
|
||||
const index = confirmations.findIndex(({ owner }) => owner === from)
|
||||
return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations
|
||||
}),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
|
|
|
@ -27,7 +27,7 @@ async function fetchTransactions(
|
|||
txType: TransactionTypes.INCOMING | TransactionTypes.OUTGOING,
|
||||
safeAddress: string,
|
||||
eTag: string | null,
|
||||
): Promise<{ eTag: string; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
|
||||
): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
|
||||
try {
|
||||
const url = getServiceUrl(txType, safeAddress)
|
||||
const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined)
|
||||
|
|
|
@ -39,8 +39,9 @@ export default (safeAddress: string): ThunkAction<Promise<void>, AppReduxState,
|
|||
}
|
||||
|
||||
const incomingTransactions = await loadIncomingTransactions(safeAddress)
|
||||
const safeIncomingTxs = incomingTransactions.get(safeAddress)
|
||||
|
||||
if (incomingTransactions.get(safeAddress).size) {
|
||||
if (safeIncomingTxs?.size) {
|
||||
dispatch(addIncomingTransactions(incomingTransactions))
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -45,7 +45,12 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
|||
const batch = new web3ReadOnly.BatchRequest()
|
||||
|
||||
const whenTxsValues = txs.map((tx) => {
|
||||
const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }]
|
||||
const methods = [
|
||||
'symbol',
|
||||
'decimals',
|
||||
{ method: 'getTransaction', args: [tx.transactionHash], type: 'eth' },
|
||||
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
|
||||
]
|
||||
|
||||
return generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
|
@ -59,17 +64,17 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
|
|||
batch.execute()
|
||||
|
||||
return Promise.all(whenTxsValues).then((txsValues) =>
|
||||
txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [
|
||||
txsValues.map(([tx, symbol, decimals, { gasPrice }, { gasUsed }]) => [
|
||||
tx,
|
||||
symbol === null ? 'ETH' : symbol,
|
||||
decimals === null ? '18' : decimals,
|
||||
new bn(gas).div(gasPrice).toFixed(),
|
||||
new bn(gasPrice).times(gasUsed),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
let previousETag = null
|
||||
export const loadIncomingTransactions = async (safeAddress: string) => {
|
||||
let previousETag: string | null = null
|
||||
export const loadIncomingTransactions = async (safeAddress: string): Promise<Map<string, List<any>>> => {
|
||||
const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag)
|
||||
previousETag = eTag
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ export type TxServiceModel = {
|
|||
blockNumber?: number | null
|
||||
confirmations: ConfirmationServiceModel[]
|
||||
confirmationsRequired: number
|
||||
creationTx?: boolean | null
|
||||
data?: string | null
|
||||
data: string | null
|
||||
dataDecoded?: DataDecoded
|
||||
ethGasPrice: string
|
||||
executionDate?: string | null
|
||||
|
@ -40,15 +39,15 @@ export type TxServiceModel = {
|
|||
isExecuted: boolean
|
||||
isSuccessful: boolean
|
||||
modified: string
|
||||
nonce?: number | null
|
||||
nonce: number
|
||||
operation: number
|
||||
origin?: string | null
|
||||
origin: string | null
|
||||
refundReceiver: string
|
||||
safe: string
|
||||
safeTxGas: number
|
||||
safeTxHash: string
|
||||
signatures: string
|
||||
submissionDate?: string | null
|
||||
submissionDate: string | null
|
||||
to: string
|
||||
transactionHash?: string | null
|
||||
value: string
|
||||
|
@ -78,7 +77,7 @@ export type BatchProcessTxsProps = OutgoingTxs & {
|
|||
*/
|
||||
const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => {
|
||||
return outgoingTxs.reduce(
|
||||
(acc, transaction) => {
|
||||
(acc: { cancellationTxs: Record<number, TxServiceModel>; outgoingTxs: TxServiceModel[] }, transaction) => {
|
||||
if (
|
||||
isCancelTransaction(transaction, safeAddress) &&
|
||||
outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress))
|
||||
|
@ -164,7 +163,7 @@ const batchProcessOutgoingTransactions = async ({
|
|||
// outgoing transactions
|
||||
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
|
||||
|
||||
const outgoing = []
|
||||
const outgoing: Transaction[] = []
|
||||
for (const [tx, txCode] of outgoingTxsWithData) {
|
||||
outgoing.push(
|
||||
await buildTx({
|
||||
|
@ -182,7 +181,7 @@ const batchProcessOutgoingTransactions = async ({
|
|||
return { cancel, outgoing }
|
||||
}
|
||||
|
||||
let previousETag = null
|
||||
let previousETag: string | null = null
|
||||
export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
|
||||
const defaultResponse = {
|
||||
cancel: Map(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions'
|
||||
import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
|
||||
export const isMultiSigTx = (tx: Transaction): boolean => {
|
||||
return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
TransactionTypes,
|
||||
TransactionTypeValues,
|
||||
TxArgs,
|
||||
RefundParams,
|
||||
} from 'src/logic/safe/store/models/types/transaction'
|
||||
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions'
|
||||
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
|
||||
|
@ -34,7 +35,7 @@ import { TypedDataUtils } from 'eth-sig-util'
|
|||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
|
||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export const isEmptyData = (data?: string | null): boolean => {
|
||||
return !data || data === EMPTY_DATA
|
||||
|
@ -65,15 +66,15 @@ export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: str
|
|||
}
|
||||
|
||||
export const isMultiSendTransaction = (tx: TxServiceModel): boolean => {
|
||||
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
|
||||
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
|
||||
return (
|
||||
!isEmptyData(tx.data) &&
|
||||
isMultiSendTransaction(tx) &&
|
||||
tx.data.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8)
|
||||
tx.data.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8)
|
||||
tx.data?.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8)
|
||||
tx.data?.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -83,24 +84,24 @@ export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string):
|
|||
|
||||
export const isCustomTransaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode: string,
|
||||
txCode: string | null,
|
||||
safeAddress: string,
|
||||
knownTokens: Map<string, Token>,
|
||||
): Promise<boolean> => {
|
||||
return (
|
||||
isOutgoingTransaction(tx, safeAddress) &&
|
||||
!(await isSendERC20Transaction(tx, txCode, knownTokens)) &&
|
||||
!isUpgradeTransaction(tx) &&
|
||||
!isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
)
|
||||
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
|
||||
const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens)
|
||||
const isUpgrade = isUpgradeTransaction(tx)
|
||||
const isErc721 = isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
|
||||
return isOutgoing && !isErc20 && !isUpgrade && !isErc721
|
||||
}
|
||||
|
||||
export const getRefundParams = async (
|
||||
tx: TxServiceModel,
|
||||
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
|
||||
): Promise<any> => {
|
||||
): Promise<RefundParams | null> => {
|
||||
const txGasPrice = Number(tx.gasPrice)
|
||||
let refundParams = null
|
||||
let refundParams: RefundParams | null = null
|
||||
|
||||
if (txGasPrice > 0) {
|
||||
let refundSymbol = 'ETH'
|
||||
|
@ -273,7 +274,6 @@ export const buildTx = async ({
|
|||
blockNumber: tx.blockNumber,
|
||||
cancelled: isTxCancelled,
|
||||
confirmations,
|
||||
creationTx: tx.creationTx,
|
||||
customTx: isCustomTx,
|
||||
data: tx.data ? tx.data : EMPTY_DATA,
|
||||
dataDecoded: tx.dataDecoded,
|
||||
|
@ -282,6 +282,7 @@ export const buildTx = async ({
|
|||
executionDate: tx.executionDate,
|
||||
executionTxHash: tx.transactionHash,
|
||||
executor: tx.executor,
|
||||
fee: tx.fee,
|
||||
gasPrice: tx.gasPrice,
|
||||
gasToken: tx.gasToken || ZERO_ADDRESS,
|
||||
isCancellationTx,
|
||||
|
@ -315,6 +316,7 @@ export type TxToMock = TxArgs & {
|
|||
safeTxHash: string
|
||||
value: string
|
||||
submissionDate: string
|
||||
dataDecoded: DataDecoded | null
|
||||
}
|
||||
|
||||
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
|
||||
|
@ -325,7 +327,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
|
|||
|
||||
return buildTx({
|
||||
cancellationTxs,
|
||||
currentUser: null,
|
||||
currentUser: undefined,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
|
@ -344,7 +346,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe
|
|||
dispatch(
|
||||
addOrUpdateTransactions({
|
||||
safeAddress,
|
||||
transactions: transactions.withMutations((list) =>
|
||||
transactions: transactions.withMutations((list: any[]) =>
|
||||
list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))),
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Set } from 'immutable'
|
||||
import updateAssetsList from './updateAssetsList'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
// the selector uses ownProps argument/router props to get the address of the safe
|
||||
// so in order to use it I had to recreate the same structure
|
||||
// const generateMatchProps = (safeAddress: string) => ({
|
||||
// match: {
|
||||
// params: {
|
||||
// [SAFE_PARAM_ADDRESS]: safeAddress,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
const updateActiveAssets = (safeAddress, activeAssets) => async (dispatch) => {
|
||||
dispatch(updateSafe({ address: safeAddress, activeAssets }))
|
||||
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateAssetsList({ safeAddress, activeAssets }))
|
||||
}
|
||||
|
||||
export default updateActiveAssets
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Set } from 'immutable'
|
||||
import updateTokensList from './updateTokensList'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
// the selector uses ownProps argument/router props to get the address of the safe
|
||||
// so in order to use it I had to recreate the same structure
|
||||
|
@ -10,8 +12,8 @@ import updateSafe from './updateSafe'
|
|||
// },
|
||||
// })
|
||||
|
||||
const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => {
|
||||
dispatch(updateSafe({ address: safeAddress, activeTokens }))
|
||||
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateTokensList({ safeAddress, activeTokens }))
|
||||
}
|
||||
|
||||
export default updateActiveTokens
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST'
|
||||
|
||||
const updateAssetsList = createAction(UPDATE_ASSETS_LIST)
|
||||
|
||||
export default updateAssetsList
|
|
@ -1,7 +1,9 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Set } from 'immutable'
|
||||
import updateAssetsList from './updateAssetsList'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
const updateBlacklistedAssets = (safeAddress, blacklistedAssets) => async (dispatch) => {
|
||||
dispatch(updateSafe({ address: safeAddress, blacklistedAssets }))
|
||||
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateAssetsList({ safeAddress, blacklistedAssets }))
|
||||
}
|
||||
|
||||
export default updateBlacklistedAssets
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Set } from 'immutable'
|
||||
import updateTokensList from './updateTokensList'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => {
|
||||
dispatch(updateSafe({ address: safeAddress, blacklistedTokens }))
|
||||
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateTokensList({ safeAddress, blacklistedTokens }))
|
||||
}
|
||||
|
||||
export default updateBlacklistedTokens
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST'
|
||||
|
||||
const updateTokenList = createAction(UPDATE_TOKENS_LIST)
|
||||
|
||||
export default updateTokenList
|
|
@ -4,7 +4,7 @@ import axios from 'axios'
|
|||
|
||||
import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory'
|
||||
|
||||
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel> => {
|
||||
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | null> => {
|
||||
try {
|
||||
const url = buildTxServiceUrl(safeAddress)
|
||||
const response = await axios.get(url, { params: { limit: 1 } })
|
||||
|
@ -17,23 +17,22 @@ export const getLastTx = async (safeAddress: string): Promise<TxServiceModel> =>
|
|||
}
|
||||
|
||||
export const getNewTxNonce = async (
|
||||
txNonce: string | null,
|
||||
lastTx: TxServiceModel,
|
||||
txNonce: string | undefined,
|
||||
lastTx: TxServiceModel | null,
|
||||
safeInstance: GnosisSafe,
|
||||
): Promise<string> => {
|
||||
if (!Number.isInteger(Number.parseInt(txNonce, 10))) {
|
||||
return lastTx === null
|
||||
? // use current's safe nonce as fallback
|
||||
(await safeInstance.methods.nonce().call()).toString()
|
||||
: `${lastTx.nonce + 1}`
|
||||
if (txNonce) {
|
||||
return txNonce
|
||||
}
|
||||
return txNonce
|
||||
|
||||
// use current's safe nonce as fallback
|
||||
return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString()
|
||||
}
|
||||
|
||||
export const shouldExecuteTransaction = async (
|
||||
safeInstance: GnosisSafe,
|
||||
nonce: string,
|
||||
lastTx: TxServiceModel,
|
||||
lastTx: TxServiceModel | null,
|
||||
): Promise<boolean> => {
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
|
||||
|
@ -45,7 +44,7 @@ export const shouldExecuteTransaction = async (
|
|||
// by the user using the exec button.
|
||||
const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted
|
||||
|
||||
return isFirstTransaction || canExecuteCurrentTransaction
|
||||
return isFirstTransaction || !!canExecuteCurrentTransaction
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -85,7 +85,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
|||
const safes = safesMapSelector(state)
|
||||
const currentSafe = safes.get(safeAddress)
|
||||
|
||||
if (!isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
|
||||
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -103,8 +103,8 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
|||
}
|
||||
case ADD_INCOMING_TRANSACTIONS: {
|
||||
action.payload.forEach((incomingTransactions, safeAddress) => {
|
||||
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress)
|
||||
const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : []
|
||||
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)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
|
@ -12,17 +11,30 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
|
|||
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
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 { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
|
||||
const watchedActions = [
|
||||
ADD_SAFE,
|
||||
UPDATE_SAFE,
|
||||
REMOVE_SAFE,
|
||||
ADD_OR_UPDATE_SAFE,
|
||||
ADD_SAFE_OWNER,
|
||||
REMOVE_SAFE_OWNER,
|
||||
REPLACE_SAFE_OWNER,
|
||||
EDIT_SAFE_OWNER,
|
||||
ACTIVATE_TOKEN_FOR_ALL_SAFES,
|
||||
UPDATE_TOKENS_LIST,
|
||||
UPDATE_ASSETS_LIST,
|
||||
SET_DEFAULT_SAFE,
|
||||
]
|
||||
|
||||
|
@ -48,6 +60,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
const safes = safesMapSelector(state)
|
||||
const addressBook = addressBookSelector(state)
|
||||
await saveSafes(safes.toJSON())
|
||||
|
||||
switch (action.type) {
|
||||
|
@ -56,19 +69,60 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||
break
|
||||
}
|
||||
case ADD_SAFE: {
|
||||
const { safe, loadedFromStorage } = action.payload
|
||||
const safeAlreadyLoaded =
|
||||
loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address))
|
||||
|
||||
safe.owners.forEach((owner) => {
|
||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
||||
|
||||
const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
||||
checksumEntry,
|
||||
addressBook,
|
||||
safeAlreadyLoaded,
|
||||
)
|
||||
|
||||
if (!ownerWasAlreadyInAddressBook) {
|
||||
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
|
||||
}
|
||||
const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address))
|
||||
if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) {
|
||||
dispatch(updateAddressBookEntry(checksumEntry))
|
||||
}
|
||||
})
|
||||
const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
||||
{ address: safe.address, name: safe.name },
|
||||
addressBook,
|
||||
safeAlreadyLoaded,
|
||||
)
|
||||
|
||||
if (!safeWasAlreadyInAddressBook) {
|
||||
dispatch(
|
||||
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
|
||||
notifyEntryUpdate: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ADD_OR_UPDATE_SAFE: {
|
||||
const { safe } = action.payload
|
||||
const ownersArray = safe.owners.toJS()
|
||||
// Adds the owners to the address book
|
||||
ownersArray.forEach((owner) => {
|
||||
dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true })))
|
||||
safe.owners.forEach((owner) => {
|
||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
||||
if (isValidAddressBookName(checksumEntry.name)) {
|
||||
dispatch(addOrUpdateAddressBookEntry(checksumEntry))
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case UPDATE_SAFE: {
|
||||
const { activeTokens } = action.payload
|
||||
const { activeTokens, name, address } = action.payload
|
||||
if (activeTokens) {
|
||||
recalculateActiveTokens(state)
|
||||
}
|
||||
if (name) {
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
|
||||
}
|
||||
break
|
||||
}
|
||||
case SET_DEFAULT_SAFE: {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { List, Map, RecordOf } from 'immutable'
|
|||
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'
|
||||
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
||||
export enum TransactionTypes {
|
||||
INCOMING = 'incoming',
|
||||
|
@ -33,6 +33,7 @@ export enum PendingActionType {
|
|||
REJECT = 'reject',
|
||||
}
|
||||
export type PendingActionValues = PendingActionType[keyof PendingActionType]
|
||||
export type RefundParams = { fee: string; symbol: string }
|
||||
|
||||
export type TransactionProps = {
|
||||
baseGas: number
|
||||
|
@ -43,7 +44,7 @@ export type TransactionProps = {
|
|||
creator: string
|
||||
creationTx: boolean
|
||||
customTx: boolean
|
||||
data?: string | null
|
||||
data: string | null
|
||||
dataDecoded: DataDecoded | null
|
||||
decimals?: (number | string) | null
|
||||
decodedParams: DecodedParams | null
|
||||
|
@ -51,7 +52,7 @@ export type TransactionProps = {
|
|||
executionTxHash?: string | null
|
||||
executor: string
|
||||
factoryAddress: string
|
||||
fee?: string // It will be replace with the new TXs types.
|
||||
fee: string | null // It will be replace with the new TXs types.
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
isCancellationTx: boolean
|
||||
|
@ -63,18 +64,18 @@ export type TransactionProps = {
|
|||
masterCopy: string
|
||||
modifySettingsTx: boolean
|
||||
multiSendTx: boolean
|
||||
nonce?: number | null
|
||||
nonce: number
|
||||
operation: number
|
||||
origin: string | null
|
||||
ownersWithPendingActions: Map<PendingActionValues, List<any>>
|
||||
recipient: string
|
||||
refundParams: any
|
||||
refundParams: RefundParams | null
|
||||
refundReceiver: string
|
||||
safeTxGas: number
|
||||
safeTxHash: string
|
||||
setupData: string
|
||||
status?: TransactionStatus
|
||||
submissionDate?: string | null
|
||||
status: TransactionStatus
|
||||
submissionDate: string | null
|
||||
symbol?: string | null
|
||||
transactionHash: string | null
|
||||
transfers?: Transfer[]
|
||||
|
@ -87,7 +88,7 @@ export type Transaction = RecordOf<TransactionProps>
|
|||
|
||||
export type TxArgs = {
|
||||
baseGas: number
|
||||
data?: string | null
|
||||
data: string
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
nonce: number
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { Transaction } from '../models/types/transactions'
|
||||
import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
import {
|
||||
LOAD_MORE_TRANSACTIONS,
|
||||
LoadMoreTransactionsAction,
|
||||
} from 'src/logic/safe/store/actions/allTransactions/pagination'
|
||||
|
||||
export const TRANSACTIONS = 'allTransactions'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Map, Set } from 'immutable'
|
||||
import { Map, Set, List } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
||||
|
@ -11,10 +11,14 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
|
|||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
||||
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 { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
||||
|
@ -37,11 +41,37 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
|||
blacklistedTokens,
|
||||
activeAssets,
|
||||
blacklistedAssets,
|
||||
latestIncomingTxBlock: null,
|
||||
latestIncomingTxBlock: 0,
|
||||
modules: null,
|
||||
}
|
||||
}
|
||||
|
||||
const updateSafeProps = (prevSafe, safe) => {
|
||||
return prevSafe.withMutations((record) => {
|
||||
// Every property is updated individually to overcome the issue with nested data being overwritten
|
||||
const safeProperties = Object.keys(safe)
|
||||
|
||||
// We check each safe property sent in action.payload
|
||||
safeProperties.forEach((key) => {
|
||||
if (safe[key] && typeof safe[key] === 'object') {
|
||||
if (safe[key].length >= 0) {
|
||||
// If type is array we update the array
|
||||
record.update(key, () => safe[key])
|
||||
} else if (safe[key].size >= 0) {
|
||||
// If type is Immutable List we replace current List
|
||||
// If type is Object we do a merge
|
||||
List.isList(safe[key])
|
||||
? record.update(key, (current) => current.set(safe[key]))
|
||||
: record.update(key, (current) => current.merge(safe[key]))
|
||||
}
|
||||
} else {
|
||||
// By default we overwrite the value. This is for strings, numbers and unset values
|
||||
record.set(key, safe[key])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
|
@ -50,8 +80,8 @@ export default handleActions(
|
|||
|
||||
return state.updateIn(
|
||||
['safes', safeAddress],
|
||||
makeSafe({ name: 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => prevSafe.merge(safe),
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
},
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
|
||||
|
@ -65,7 +95,7 @@ export default handleActions(
|
|||
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
|
||||
const activeTokens = safeActiveTokens.add(tokenAddress)
|
||||
|
||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
|
||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -77,11 +107,24 @@ export default handleActions(
|
|||
// with initial props and it would overwrite existing ones
|
||||
|
||||
if (state.hasIn(['safes', safe.address])) {
|
||||
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
|
||||
return state
|
||||
}
|
||||
|
||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
},
|
||||
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const { safe } = action.payload
|
||||
|
||||
if (!state.hasIn(['safes', safe.address])) {
|
||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
}
|
||||
|
||||
return state.updateIn(
|
||||
['safes', safe.address],
|
||||
makeSafe({ name: 'LOADED SAFE', address: safe.address }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
},
|
||||
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
|
@ -90,6 +133,14 @@ export default handleActions(
|
|||
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
const addressFound = state
|
||||
.getIn(['safes', safeAddress])
|
||||
.owners.find((owner) => sameAddress(owner.address, ownerAddress))
|
||||
|
||||
if (addressFound) {
|
||||
return state
|
||||
}
|
||||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
prevSafe.merge({
|
||||
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
||||
|
@ -127,6 +178,24 @@ export default handleActions(
|
|||
return prevSafe.merge({ owners: updatedOwners })
|
||||
})
|
||||
},
|
||||
[UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => {
|
||||
// Only activeTokens or blackListedTokens is required
|
||||
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
|
||||
|
||||
const key = activeTokens ? 'activeTokens' : 'blacklistedTokens'
|
||||
const list = activeTokens ?? blacklistedTokens
|
||||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
|
||||
},
|
||||
[UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => {
|
||||
// Only activeAssets or blackListedAssets is required
|
||||
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
|
||||
|
||||
const key = activeAssets ? 'activeAssets' : 'blacklistedAssets'
|
||||
const list = activeAssets ?? blacklistedAssets
|
||||
|
||||
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) =>
|
||||
state.set('latestMasterContractVersion', action.payload),
|
||||
|
|
|
@ -12,11 +12,11 @@ export const allTransactionsSelector = createSelector(getTransactionsStateSelect
|
|||
export const safeAllTransactionsSelector = createSelector(
|
||||
safeParamAddressFromStateSelector,
|
||||
allTransactionsSelector,
|
||||
(safeAddress, transactions) => transactions[safeAddress]?.transactions || [],
|
||||
(safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.transactions : []),
|
||||
)
|
||||
|
||||
export const safeTotalTransactionsAmountSelector = createSelector(
|
||||
safeParamAddressFromStateSelector,
|
||||
allTransactionsSelector,
|
||||
(safeAddress, transactions) => transactions[safeAddress]?.totalTransactionsCount || 0,
|
||||
(safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.totalTransactionsCount : 0),
|
||||
)
|
||||
|
|
|
@ -36,7 +36,7 @@ const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELL
|
|||
|
||||
const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => {
|
||||
export const safeParamAddressFromStateSelector = (state: AppReduxState): string => {
|
||||
const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, {
|
||||
path: `${SAFELIST_ADDRESS}/:safeAddress`,
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ export const safeParamAddressFromStateSelector = (state: AppReduxState): string
|
|||
return checksumAddress(match.params.safeAddress)
|
||||
}
|
||||
|
||||
return null
|
||||
return ''
|
||||
}
|
||||
|
||||
export const safeParamAddressSelector = (
|
||||
|
@ -177,16 +177,16 @@ export const safeBlacklistedAssetsSelector = createSelector(
|
|||
)
|
||||
|
||||
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
||||
safes.get(safeAddress).get('activeAssets')
|
||||
safes.get(safeAddress)?.get('activeAssets') || Set()
|
||||
|
||||
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
||||
safes.get(safeAddress).get('blacklistedAssets')
|
||||
safes.get(safeAddress)?.get('blacklistedAssets') || Set()
|
||||
|
||||
const baseSafe = makeSafe()
|
||||
|
||||
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
|
||||
safe: SafeRecord,
|
||||
): SafeRecordProps[K] | null => (safe ? safe.get(field, baseSafe.get(field)) : null)
|
||||
): SafeRecordProps[K] | undefined => (safe ? safe.get(field, baseSafe.get(field)) : undefined)
|
||||
|
||||
export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { Set, Map } from 'immutable'
|
||||
import { aNewStore } from 'src/store'
|
||||
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
describe('Feature > Balances', () => {
|
||||
let store
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
beforeEach(async () => {
|
||||
store = aNewStore()
|
||||
})
|
||||
|
||||
it('It should return an updated balance when updates active tokens', async () => {
|
||||
// given
|
||||
const tokensAmount = '100'
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
name: 'OmiseGo',
|
||||
symbol: 'OMG',
|
||||
decimals: 18,
|
||||
logoUri:
|
||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||
})
|
||||
const balances = Map({
|
||||
[token.address]: tokensAmount,
|
||||
})
|
||||
const expectedResult = '100'
|
||||
|
||||
// when
|
||||
store.dispatch(updateSafe({ address: safeAddress, balances }))
|
||||
store.dispatch(updateActiveTokens(safeAddress, Set([token.address])))
|
||||
|
||||
const safe = safesMapSelector(store.getState()).get(safeAddress)
|
||||
const balanceResult = safe?.get('balances').get(token.address)
|
||||
const activeTokens = safe?.get('activeTokens')
|
||||
const tokenIsActive = activeTokens?.has(token.address)
|
||||
|
||||
// then
|
||||
expect(balanceResult).toBe(expectedResult)
|
||||
expect(tokenIsActive).toBe(true)
|
||||
})
|
||||
|
||||
it('The store should have an updated ether balance after updating the value', async () => {
|
||||
// given
|
||||
const etherAmount = '1'
|
||||
const expectedResult = '1'
|
||||
|
||||
// when
|
||||
store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount }))
|
||||
const safe = safesMapSelector(store.getState()).get(safeAddress)
|
||||
const balanceResult = safe?.get('ethBalance')
|
||||
|
||||
// then
|
||||
expect(balanceResult).toBe(expectedResult)
|
||||
})
|
||||
})
|
|
@ -1,8 +1,13 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
|
||||
export const getAwaitingTransactions = (allTransactions = List([]), cancellationTxs, userAccount: string) => {
|
||||
export const getAwaitingTransactions = (
|
||||
allTransactions: List<Transaction>,
|
||||
cancellationTxs,
|
||||
userAccount: string,
|
||||
): List<Transaction> => {
|
||||
return allTransactions.filter((tx) => {
|
||||
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ const estimateDataGasCosts = (data: string): number => {
|
|||
return accumulator + 16
|
||||
}
|
||||
|
||||
return data.match(/.{2}/g).reduce(reducer, 0)
|
||||
return data.match(/.{2}/g)?.reduce(reducer, 0)
|
||||
}
|
||||
|
||||
export const estimateTxGasCosts = async (
|
||||
|
@ -38,6 +38,11 @@ export const estimateTxGasCosts = async (
|
|||
try {
|
||||
const web3 = getWeb3()
|
||||
const from = await getAccountFrom(web3)
|
||||
|
||||
if (!from) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const safeInstance = (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe
|
||||
const nonce = await safeInstance.methods.nonce().call()
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getIncomingTxServiceUriTo, getTxServiceHost } from 'src/config'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
export const buildIncomingTxServiceUrl = (safeAddress) => {
|
||||
export const buildIncomingTxServiceUrl = (safeAddress: string): string => {
|
||||
const host = getTxServiceHost()
|
||||
const address = checksumAddress(safeAddress)
|
||||
const base = getIncomingTxServiceUriTo(address)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { AbstractProvider } from 'web3-core'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
|
||||
const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet"
|
||||
|
||||
|
@ -59,7 +60,7 @@ const generateTypedDataFrom = async ({
|
|||
}
|
||||
|
||||
export const getEIP712Signer = (version?: string) => async (txArgs) => {
|
||||
const web3: any = getWeb3()
|
||||
const web3 = getWeb3()
|
||||
const typedData = await generateTypedDataFrom(txArgs)
|
||||
|
||||
let method = 'eth_signTypedData_v3'
|
||||
|
@ -80,13 +81,14 @@ export const getEIP712Signer = (version?: string) => async (txArgs) => {
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
web3.currentProvider.sendAsync(signedTypedData, (err, signature) => {
|
||||
const provider = web3.currentProvider as AbstractProvider
|
||||
provider.sendAsync(signedTypedData, (err, signature) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (signature.result == null) {
|
||||
if (signature?.result == null) {
|
||||
reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,26 +4,13 @@ import { AbstractProvider } from 'web3-core/types'
|
|||
|
||||
const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED'
|
||||
|
||||
export const ethSigner = async ({
|
||||
baseGas,
|
||||
data,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
nonce,
|
||||
operation,
|
||||
refundReceiver,
|
||||
safeInstance,
|
||||
safeTxGas,
|
||||
sender,
|
||||
to,
|
||||
valueInWei,
|
||||
}): Promise<string> => {
|
||||
type EthSignerArgs = {
|
||||
safeTxHash: string
|
||||
sender: string
|
||||
}
|
||||
|
||||
export const ethSigner = async ({ safeTxHash, sender }: EthSignerArgs): Promise<string> => {
|
||||
const web3 = await getWeb3()
|
||||
const txHash = await safeInstance.methods
|
||||
.getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce)
|
||||
.call({
|
||||
from: sender,
|
||||
})
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const provider = web3.currentProvider as AbstractProvider
|
||||
|
@ -31,7 +18,7 @@ export const ethSigner = async ({
|
|||
{
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sign',
|
||||
params: [sender, txHash],
|
||||
params: [sender, safeTxHash],
|
||||
id: new Date().getTime(),
|
||||
},
|
||||
async function (err, signature) {
|
||||
|
@ -39,7 +26,7 @@ export const ethSigner = async ({
|
|||
return reject(err)
|
||||
}
|
||||
|
||||
if (signature.result == null) {
|
||||
if (signature?.result == null) {
|
||||
reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ethSigner } from './ethSigner'
|
|||
const SIGNERS = {
|
||||
EIP712_V3: getEIP712Signer('v3'),
|
||||
EIP712_V4: getEIP712Signer('v4'),
|
||||
EIP712: getEIP712Signer() as any,
|
||||
EIP712: getEIP712Signer(),
|
||||
ETH_SIGN: ethSigner,
|
||||
}
|
||||
|
||||
|
@ -18,13 +18,13 @@ const getSignersByWallet = (isHW) =>
|
|||
|
||||
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
|
||||
|
||||
export const tryOffchainSigning = async (txArgs, isHW) => {
|
||||
export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boolean): Promise<string> => {
|
||||
let signature
|
||||
|
||||
const signerByWallet = getSignersByWallet(isHW)
|
||||
for (const signingFunc of signerByWallet) {
|
||||
try {
|
||||
signature = await signingFunc(txArgs)
|
||||
signature = await signingFunc({ ...txArgs, safeTxHash })
|
||||
|
||||
break
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NonPayableTransactionObject } from 'src/types/contracts/types.d'
|
||||
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
|
||||
export const CALL = 0
|
||||
export const DELEGATE_CALL = 1
|
||||
|
|
|
@ -2,20 +2,20 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
|||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
|
||||
export const SAFES_KEY = 'SAFES'
|
||||
export const TX_KEY = 'TX'
|
||||
export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
|
||||
|
||||
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
|
||||
const safes = await loadFromStorage(SAFES_KEY)
|
||||
if (!safes) {
|
||||
return undefined
|
||||
}
|
||||
const safe = safes[safeAddress]
|
||||
type StoredSafes = Record<string, SafeRecordProps>
|
||||
|
||||
return safe ? safe.name : undefined
|
||||
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
|
||||
return loadFromStorage<StoredSafes>(SAFES_KEY)
|
||||
}
|
||||
|
||||
export const saveSafes = async (safes) => {
|
||||
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
|
||||
const safes = await loadStoredSafes()
|
||||
return safes?.[safeAddress]?.name
|
||||
}
|
||||
|
||||
export const saveSafes = async (safes: StoredSafes): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(SAFES_KEY, safes)
|
||||
} catch (err) {
|
||||
|
@ -23,9 +23,9 @@ export const saveSafes = async (safes) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | null> => {
|
||||
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
|
||||
return storedSafes[safeAddress] || null
|
||||
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | undefined> => {
|
||||
const storedSafes = await loadStoredSafes()
|
||||
return storedSafes?.[safeAddress]
|
||||
}
|
||||
|
||||
export const getDefaultSafe = async (): Promise<string> => {
|
||||
|
|
|
@ -11,13 +11,15 @@ export const FEATURES = [
|
|||
{ name: 'ERC1155', validVersion: '>=1.1.1' },
|
||||
]
|
||||
|
||||
export const safeNeedsUpdate = (currentVersion: string, latestVersion: string): boolean => {
|
||||
type Feature = typeof FEATURES[number]
|
||||
|
||||
export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => {
|
||||
if (!currentVersion || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
const current = semverValid(currentVersion)
|
||||
const latest = semverValid(latestVersion)
|
||||
const current = semverValid(currentVersion) as string
|
||||
const latest = semverValid(latestVersion) as string
|
||||
|
||||
return latest ? semverLessThan(current, latest) : false
|
||||
}
|
||||
|
@ -26,7 +28,7 @@ export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<s
|
|||
gnosisSafeInstance.methods.VERSION().call()
|
||||
|
||||
export const enabledFeatures = (version: string): string[] =>
|
||||
FEATURES.reduce((acc, feature) => {
|
||||
FEATURES.reduce((acc: string[], feature: Feature) => {
|
||||
if (semverSatisfies(version, feature.validVersion)) {
|
||||
acc.push(feature.name)
|
||||
}
|
||||
|
@ -44,11 +46,11 @@ export const checkIfSafeNeedsUpdate = async (
|
|||
lastSafeVersion: string,
|
||||
): Promise<SafeVersionInfo> => {
|
||||
if (!gnosisSafeInstance || !lastSafeVersion) {
|
||||
return null
|
||||
throw new Error('checkIfSafeNeedsUpdate: No Safe Instance or version provided')
|
||||
}
|
||||
const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance)
|
||||
const current = semverValid(safeMasterVersion)
|
||||
const latest = semverValid(lastSafeVersion)
|
||||
const current = semverValid(safeMasterVersion) as string
|
||||
const latest = semverValid(lastSafeVersion) as string
|
||||
const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion)
|
||||
|
||||
return { current, latest, needUpdate }
|
||||
|
@ -69,7 +71,7 @@ export const getCurrentMasterContractLastVersion = async (): Promise<string> =>
|
|||
|
||||
export const getSafeVersionInfo = async (safeAddress: string): Promise<SafeVersionInfo> => {
|
||||
try {
|
||||
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeMaster = getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastSafeVersion = await getCurrentMasterContractLastVersion()
|
||||
return checkIfSafeNeedsUpdate(safeMaster, lastSafeVersion)
|
||||
} catch (err) {
|
||||
|
|
|
@ -48,7 +48,7 @@ const extractDataFromResult = (currentTokens: TokenState) => (
|
|||
if (tokenAddress === null) {
|
||||
acc.ethBalance = humanReadableValue(balance, 18)
|
||||
} else {
|
||||
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token.decimals)) })
|
||||
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token?.decimals)) })
|
||||
|
||||
if (currentTokens && !currentTokens.get(tokenAddress)) {
|
||||
acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token }))
|
||||
|
@ -57,7 +57,7 @@ const extractDataFromResult = (currentTokens: TokenState) => (
|
|||
|
||||
acc.currencyList = acc.currencyList.push(
|
||||
makeBalanceCurrency({
|
||||
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null,
|
||||
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : undefined,
|
||||
tokenAddress,
|
||||
balanceInBaseCurrency: balanceUsd,
|
||||
balanceInSelectedCurrency: balanceUsd,
|
||||
|
|
|
@ -12,8 +12,10 @@ import { fetchTokenList } from 'src/logic/tokens/api'
|
|||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { store } from 'src/store'
|
||||
import { AppReduxState, store } from 'src/store'
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
import { AnyAction } from 'redux'
|
||||
|
||||
const createStandardTokenContract = async () => {
|
||||
const web3 = getWeb3()
|
||||
|
@ -43,7 +45,7 @@ export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
|||
|
||||
export const getERC721TokenContract = ensureOnce(createERC721TokenContract)
|
||||
|
||||
export const containsMethodByHash = async (contractAddress, methodHash) => {
|
||||
export const containsMethodByHash = async (contractAddress: string, methodHash: string): Promise<boolean> => {
|
||||
const web3 = getWeb3()
|
||||
const byteCode = await web3.eth.getCode(contractAddress)
|
||||
|
||||
|
@ -57,11 +59,7 @@ const getTokenValues = (tokenAddress) =>
|
|||
methods: ['decimals', 'name', 'symbol'],
|
||||
})
|
||||
|
||||
export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
|
||||
if (!tokenAddress) {
|
||||
return null
|
||||
}
|
||||
|
||||
export const getTokenInfos = async (tokenAddress: string): Promise<Token | undefined> => {
|
||||
const { tokens } = store.getState()
|
||||
const localToken = tokens.get(tokenAddress)
|
||||
|
||||
|
@ -74,7 +72,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
|
|||
const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
|
||||
|
||||
if (tokenDecimals === null) {
|
||||
return null
|
||||
return undefined
|
||||
}
|
||||
|
||||
const token = makeToken({
|
||||
|
@ -91,7 +89,10 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
|
|||
return token
|
||||
}
|
||||
|
||||
export const fetchTokens = () => async (dispatch, getState) => {
|
||||
export const fetchTokens = () => async (
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const currentSavedTokens = tokensSelector(getState())
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ export type TokenProps = {
|
|||
name: string
|
||||
symbol: string
|
||||
decimals: number | string
|
||||
logoUri?: string | null
|
||||
logoUri: string
|
||||
balance?: number | string
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { formatAmount, formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
||||
|
||||
describe('formatAmount', () => {
|
||||
it('Given 0 returns 0', () => {
|
||||
it('Given 0 returns 0', () => {
|
||||
// given
|
||||
const input = '0'
|
||||
const expectedResult = '0'
|
||||
|
@ -13,7 +12,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given 1 returns 1', () => {
|
||||
it('Given 1 returns 1', () => {
|
||||
// given
|
||||
const input = '1'
|
||||
const expectedResult = '1'
|
||||
|
@ -24,7 +23,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = '19797.899'
|
||||
const expectedResult = '19,797.899'
|
||||
|
@ -35,7 +34,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number > 0.001 && < 1000 returns the same number as string', () => {
|
||||
it('Given number > 0.001 && < 1000 returns the same number as string', () => {
|
||||
// given
|
||||
const input = 999
|
||||
const expectedResult = '999'
|
||||
|
@ -45,7 +44,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => {
|
||||
it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => {
|
||||
// given
|
||||
const input = 9999
|
||||
const expectedResult = '9,999'
|
||||
|
@ -55,7 +54,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => {
|
||||
it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => {
|
||||
// given
|
||||
const input = 99999
|
||||
const expectedResult = '99,999'
|
||||
|
@ -65,7 +64,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => {
|
||||
it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => {
|
||||
// given
|
||||
const input = 999999
|
||||
const expectedResult = '999,999'
|
||||
|
@ -75,7 +74,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => {
|
||||
it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => {
|
||||
// given
|
||||
const input = 9999999
|
||||
const expectedResult = '9,999,999'
|
||||
|
@ -85,7 +84,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number < 0.001 returns < 0.001', () => {
|
||||
it('Given number < 0.001 returns < 0.001', () => {
|
||||
// given
|
||||
const input = 0.000001
|
||||
const expectedResult = '< 0.001'
|
||||
|
@ -95,7 +94,7 @@ describe('formatAmount', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given number > 10 ** 15 returns > 1000T', () => {
|
||||
it('Given number > 10 ** 15 returns > 1000T', () => {
|
||||
// given
|
||||
const input = 10 ** 15 * 2
|
||||
const expectedResult = '> 1000T'
|
||||
|
@ -109,7 +108,7 @@ describe('formatAmount', () => {
|
|||
})
|
||||
|
||||
describe('FormatsAmountsInUsFormat', () => {
|
||||
it('Given 0 returns 0.00', () => {
|
||||
it('Given 0 returns 0.00', () => {
|
||||
// given
|
||||
const input = 0
|
||||
const expectedResult = '0.00'
|
||||
|
@ -120,7 +119,7 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given 1 returns 1.00', () => {
|
||||
it('Given 1 returns 1.00', () => {
|
||||
// given
|
||||
const input = 1
|
||||
const expectedResult = '1.00'
|
||||
|
@ -131,9 +130,9 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => {
|
||||
it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => {
|
||||
// given
|
||||
const input = 311137.30
|
||||
const input = 311137.3
|
||||
const expectedResult = '311,137.30'
|
||||
|
||||
// when
|
||||
|
@ -142,7 +141,7 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797.899
|
||||
const expectedResult = '19,797.899'
|
||||
|
@ -153,7 +152,7 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => {
|
||||
it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797899.479
|
||||
const expectedResult = '19,797,899.479'
|
||||
|
@ -164,7 +163,7 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => {
|
||||
it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => {
|
||||
// given
|
||||
const input = 19797899479.999
|
||||
const expectedResult = '19,797,899,479.999'
|
||||
|
@ -176,4 +175,3 @@ describe('FormatsAmountsInUsFormat', () => {
|
|||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
||||
|
||||
describe('isTokenTransfer', () => {
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
it('It should return false if the transaction has no value but but "transfer" function signature is encoded in the data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9059cbb' })
|
||||
const expectedResult = true
|
||||
// when
|
||||
const result = isTokenTransfer(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if the transaction has no value but and no "transfer" function signature encoded in data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9055cbb' })
|
||||
const expectedResult = false
|
||||
// when
|
||||
const result = isTokenTransfer(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if the transaction has empty data', () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
|
||||
const expectedResult = false
|
||||
// when
|
||||
const result = isTokenTransfer(transaction)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
jest.mock('src/logic/tokens/store/actions/fetchTokens')
|
||||
jest.mock('src/logic/contracts/generateBatchRequests')
|
||||
jest.mock('console')
|
||||
describe('getERC20DecimalsAndSymbol', () => {
|
||||
afterAll(() => {
|
||||
jest.unmock('src/logic/tokens/store/actions/fetchTokens')
|
||||
jest.unmock('src/logic/contracts/generateBatchRequests')
|
||||
jest.unmock('console')
|
||||
})
|
||||
it('It should return DAI information from the store if given a DAI address', async () => {
|
||||
// given
|
||||
const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa'
|
||||
const decimals = Number(18)
|
||||
const symbol = 'DAI'
|
||||
const token = makeToken({
|
||||
address: tokenAddress,
|
||||
name: 'Dai',
|
||||
symbol,
|
||||
decimals,
|
||||
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
||||
balance: 0,
|
||||
})
|
||||
const expectedResult = {
|
||||
decimals,
|
||||
symbol,
|
||||
}
|
||||
|
||||
const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens')
|
||||
const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => token)
|
||||
|
||||
// when
|
||||
const result = await getERC20DecimalsAndSymbol(tokenAddress)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return default value decimals: 18, symbol: UNKNOWN if given a token address and if there is an error fetching the data', async () => {
|
||||
// given
|
||||
const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa'
|
||||
const decimals = Number(18)
|
||||
const symbol = 'UNKNOWN'
|
||||
|
||||
const expectedResult = {
|
||||
decimals,
|
||||
symbol,
|
||||
}
|
||||
|
||||
const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens')
|
||||
const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => {
|
||||
throw new Error()
|
||||
})
|
||||
console.error = jest.fn()
|
||||
const spyConsole = jest.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
// when
|
||||
const result = await getERC20DecimalsAndSymbol(tokenAddress)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(spyConsole).toHaveBeenCalled()
|
||||
})
|
||||
it("It should fetch token information from the blockchain if given a token address and if the token doesn't exist in redux store", async () => {
|
||||
// given
|
||||
const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa'
|
||||
const decimals = Number(18)
|
||||
const symbol = 'DAI'
|
||||
const expectedResult = {
|
||||
decimals,
|
||||
symbol,
|
||||
}
|
||||
|
||||
const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens')
|
||||
const generateBatchRequests = require('src/logic/contracts/generateBatchRequests')
|
||||
const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null)
|
||||
|
||||
const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [decimals, symbol])
|
||||
|
||||
// when
|
||||
const result = await getERC20DecimalsAndSymbol(tokenAddress)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
expect(spyTokenInfos).toHaveBeenCalled()
|
||||
expect(spyGenerateBatchRequest).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isERC721Contract', () => {
|
||||
afterAll(() => {
|
||||
jest.unmock('src/logic/tokens/store/actions/fetchTokens')
|
||||
})
|
||||
beforeEach(() => {
|
||||
jest.mock('src/logic/tokens/store/actions/fetchTokens')
|
||||
})
|
||||
it('It should return false if given non-erc721 contract address', async () => {
|
||||
// given
|
||||
const contractAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' // DAI Address
|
||||
const expectedResult = false
|
||||
|
||||
const ERC721Contract = {
|
||||
at: () => {
|
||||
throw new Error('Contract is not ERC721')
|
||||
},
|
||||
}
|
||||
|
||||
const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens')
|
||||
const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract)
|
||||
|
||||
// when
|
||||
const result = await isERC721Contract(contractAddress)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
expect(standardContractSpy).toHaveBeenCalled
|
||||
})
|
||||
it('It should return true if given a Erc721 contract address', async () => {
|
||||
// given
|
||||
const contractAddress = '0x014d5883274ab3a9708b0f1e4263df6e90160a30' // dummy ft Address
|
||||
const ERC721Contract = {
|
||||
at: (address) => address === contractAddress,
|
||||
}
|
||||
const expectedResult = true
|
||||
|
||||
const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens')
|
||||
const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract)
|
||||
|
||||
// when
|
||||
const result = await isERC721Contract(contractAddress)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
expect(standardContractSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -35,18 +35,18 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
|
|||
// } catch {
|
||||
// return 'Not a token address'
|
||||
// }
|
||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') as string })
|
||||
|
||||
return call !== '0x'
|
||||
}
|
||||
|
||||
export const isTokenTransfer = (tx: TxServiceModel): boolean => {
|
||||
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
||||
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isSendERC721Transaction = (
|
||||
tx: TxServiceModel,
|
||||
txCode: string,
|
||||
txCode: string | null,
|
||||
knownTokens: Map<string, Token>,
|
||||
): boolean => {
|
||||
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
|
||||
|
@ -78,7 +78,7 @@ export const getERC20DecimalsAndSymbol = async (
|
|||
try {
|
||||
const storedTokenInfo = await getTokenInfos(tokenAddress)
|
||||
|
||||
if (storedTokenInfo === null) {
|
||||
if (!storedTokenInfo) {
|
||||
const [tokenDecimals, tokenSymbol] = await generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
address: tokenAddress,
|
||||
|
@ -96,7 +96,7 @@ export const getERC20DecimalsAndSymbol = async (
|
|||
|
||||
export const isSendERC20Transaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode: string,
|
||||
txCode: string | null,
|
||||
knownTokens: Map<string, Token>,
|
||||
): Promise<boolean> => {
|
||||
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
|
||||
|
@ -118,8 +118,8 @@ export const isERC721Contract = async (contractAddress: string): Promise<boolean
|
|||
let isERC721 = false
|
||||
|
||||
try {
|
||||
isERC721 = true
|
||||
await ERC721Token.at(contractAddress)
|
||||
isERC721 = true
|
||||
} catch (error) {
|
||||
console.warn('Asset not found')
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { List } from 'immutable'
|
|||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
export const sameAddress = (firstAddress: string, secondAddress: string): boolean => {
|
||||
export const sameAddress = (firstAddress: string | undefined, secondAddress: string | undefined): boolean => {
|
||||
if (!firstAddress) {
|
||||
return false
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export const shortVersionOf = (value: string, cut: number): string => {
|
|||
}
|
||||
|
||||
const final = value.length - cut
|
||||
if (value.length < final) {
|
||||
if (value.length <= cut) {
|
||||
return value
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ const isSmartContractWallet = async (web3Provider: Web3, account: string): Promi
|
|||
}
|
||||
|
||||
export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet'): Promise<ProviderProps> => {
|
||||
const account = await getAccountFrom(web3Instance)
|
||||
const account = (await getAccountFrom(web3Instance)) || ''
|
||||
const network = await getNetworkIdFrom(web3Instance)
|
||||
const smartContractWallet = await isSmartContractWallet(web3Instance, account)
|
||||
const hardwareWallet = isHardwareWallet(providerName)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue