Merge branch 'development' into release/v2.11.1
This commit is contained in:
commit
e5468704d4
|
@ -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
|
||||
|
|
48
package.json
48
package.json
|
@ -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.12.0",
|
||||
"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.6",
|
||||
"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",
|
||||
|
@ -236,15 +236,15 @@
|
|||
"@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",
|
||||
"@testing-library/user-event": "12.1.3",
|
||||
"@typechain/web3-v1": "^1.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.9",
|
||||
"@types/jest": "^26.0.12",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "14.6.0",
|
||||
"@types/react": "^16.9.47",
|
||||
"@types/node": "14.6.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",
|
||||
|
@ -255,26 +255,26 @@
|
|||
"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.2.13",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"prettier": "2.1.1",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-docgen-typescript-loader": "^3.7.2",
|
||||
"truffle": "5.1.36",
|
||||
"truffle": "5.1.41",
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -6,12 +6,10 @@ export interface AddressBookEntryProps {
|
|||
isOwner: boolean
|
||||
}
|
||||
|
||||
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
|
||||
|
||||
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
|
||||
address: '',
|
||||
name: '',
|
||||
isOwner: false,
|
||||
})
|
||||
|
||||
export type AddressBookEntry = RecordOf<AddressBookEntryProps>
|
||||
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import {
|
||||
AddressBookEntryRecord,
|
||||
AddressBookEntryProps,
|
||||
makeAddressBookEntry,
|
||||
} from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
|
||||
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 { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||
|
||||
export type AddressBookCollection = List<AddressBookEntry>
|
||||
export type AddressBookCollection = List<AddressBookEntryRecord>
|
||||
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
|
||||
|
||||
export const buildAddressBook = (storedAdbk) => {
|
||||
let addressBookBuilt = Map([])
|
||||
export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map<string, AddressBookCollection> => {
|
||||
let addressBookBuilt: Map<string, AddressBookCollection> = Map([])
|
||||
Object.entries(storedAdbk).forEach((adbkProps: any) => {
|
||||
const safeAddress = checksumAddress(adbkProps[0])
|
||||
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
|
||||
const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry)
|
||||
const adbkSafeEntries = List(adbkRecords)
|
||||
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
|
||||
})
|
||||
|
@ -55,7 +59,7 @@ export default handleActions(
|
|||
const safeAddressBook = state.getIn(['addressBook', safeAddress])
|
||||
|
||||
if (safeAddressBook) {
|
||||
const adbkAddressList = getAddressesListFromAdbk(safeAddressBook)
|
||||
const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook)
|
||||
const found = adbkAddressList.includes(entry.address)
|
||||
if (!found) {
|
||||
const updatedSafeAdbkList = safeAddressBook.push(entry)
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
|
||||
|
@ -13,8 +14,8 @@ export const getAddressBook = createSelector(
|
|||
addressBookMapSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(addressBook, safeAddress) => {
|
||||
let result = List([])
|
||||
if (addressBook) {
|
||||
let result: List<AddressBookEntryRecord> = List([])
|
||||
if (addressBook && safeAddress) {
|
||||
result = addressBook.get(safeAddress, List())
|
||||
}
|
||||
return result
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { Map, List } from 'immutable'
|
||||
import {
|
||||
getAddressBookFromStorage,
|
||||
getAddressesListFromSafeAddressBook,
|
||||
getNameFromSafeAddressBook,
|
||||
getOwnersWithNameFromAddressBook,
|
||||
saveAddressBook,
|
||||
} from 'src/logic/addressBook/utils/index'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const getMockAddressBookEntry = (
|
||||
address: string,
|
||||
name: string = 'test',
|
||||
isOwner: boolean = false,
|
||||
): AddressBookEntryRecord =>
|
||||
makeAddressBookEntry({
|
||||
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 = List([entry1, entry2, entry3])
|
||||
const expectedResult = [entry1.address, entry2.address, entry3.address]
|
||||
|
||||
// when
|
||||
const result = getAddressesListFromSafeAddressBook(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 = List([entry1, entry2, entry3])
|
||||
const expectedResult = entry2.name
|
||||
|
||||
// when
|
||||
const result = getNameFromSafeAddressBook(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 = List([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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('saveAddressBook', () => {
|
||||
const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
it('It should save a given addressBook to the localStorage', async () => {
|
||||
// given
|
||||
const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) })
|
||||
|
||||
// when
|
||||
// @ts-ignore
|
||||
await saveAddressBook(addressBook)
|
||||
const storedAdBk = await getAddressBookFromStorage()
|
||||
// @ts-ignore
|
||||
let result = buildAddressBook(storedAdBk)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(addressBook)
|
||||
})
|
||||
})
|
|
@ -1,27 +1,28 @@
|
|||
import { List } from 'immutable'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { AddressBookEntryProps } from './../model/addressBook'
|
||||
import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { AddressBookCollection } from '../store/reducer/addressBook'
|
||||
import { AddressBookMap } from '../store/reducer/types/addressBook'
|
||||
|
||||
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
|
||||
return await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook) => {
|
||||
export const saveAddressBook = async (addressBook: AddressBookMap): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON())
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS())
|
||||
} catch (err) {
|
||||
console.error('Error storing addressBook in localstorage', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address)
|
||||
export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] =>
|
||||
Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address)
|
||||
|
||||
export const getNameFromAdbk = (addressBook, userAddress) => {
|
||||
export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => {
|
||||
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
|
||||
if (entry) {
|
||||
return entry.name
|
||||
|
@ -30,18 +31,17 @@ export const getNameFromAdbk = (addressBook, userAddress) => {
|
|||
}
|
||||
|
||||
export const getOwnersWithNameFromAddressBook = (
|
||||
addressBook: AddressBookEntryProps,
|
||||
addressBook: AddressBookCollection,
|
||||
ownerList: List<SafeOwner>,
|
||||
): List<SafeOwner> | [] => {
|
||||
if (!ownerList) {
|
||||
return []
|
||||
}
|
||||
const ownersListWithAdbkNames = ownerList.map((owner) => {
|
||||
const ownerName = getNameFromAdbk(addressBook, owner.address)
|
||||
return ownerList.map((owner) => {
|
||||
const ownerName = getNameFromSafeAddressBook(addressBook, owner.address)
|
||||
return {
|
||||
address: owner.address,
|
||||
name: ownerName || owner.name,
|
||||
}
|
||||
})
|
||||
return ownersListWithAdbkNames
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
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'
|
||||
beforeEach(async () => {
|
||||
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)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(axios.get).toHaveBeenCalled()
|
||||
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/`, { 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(() => {
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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 RejectEventHandler = () => void
|
||||
|
||||
const createTransaction = (
|
||||
{
|
||||
|
@ -124,6 +126,7 @@ const createTransaction = (
|
|||
origin = null,
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
onUserReject?: RejectEventHandler,
|
||||
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
|
@ -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,21 @@ const createTransaction = (
|
|||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
|
||||
// Different wallets return different error messages in this case. This is an assumption that if
|
||||
// error message includes "user" word, the tx was rejected by user
|
||||
|
||||
let errorIncludesUserWord = false
|
||||
if (typeof error === 'string') {
|
||||
errorIncludesUserWord = (error as string).includes('User') || (error as string).includes('user')
|
||||
}
|
||||
if (error.message) {
|
||||
errorIncludesUserWord = error.message.includes('User') || error.message.includes('user')
|
||||
}
|
||||
|
||||
if (errorIncludesUserWord) {
|
||||
onUserReject?.()
|
||||
}
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Action, Dispatch } from 'redux'
|
|||
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
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(),
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
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) => {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -85,6 +84,8 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
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(),
|
||||
|
|
|
@ -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,4 +1,6 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Set } from 'immutable'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
// 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,7 +12,7 @@ import updateSafe from './updateSafe'
|
|||
// },
|
||||
// })
|
||||
|
||||
const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => {
|
||||
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateSafe({ address: safeAddress, activeTokens }))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import updateSafe from './updateSafe'
|
||||
import { Dispatch } from 'redux'
|
||||
import { Set } from 'immutable'
|
||||
|
||||
const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => {
|
||||
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
|
||||
dispatch(updateSafe({ address: safeAddress, blacklistedTokens }))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ 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 viewedSafes = state.currentSession['viewedSafes']
|
||||
const recurringUser = viewedSafes?.includes(safeAddress)
|
||||
|
||||
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -37,7 +37,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
|||
blacklistedTokens,
|
||||
activeAssets,
|
||||
blacklistedAssets,
|
||||
latestIncomingTxBlock: null,
|
||||
latestIncomingTxBlock: 0,
|
||||
modules: null,
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,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,7 +77,7 @@ 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))
|
||||
|
|
|
@ -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,62 @@
|
|||
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(updateActiveTokens(safeAddress, Set([token.address])))
|
||||
store.dispatch(updateSafe({ address: safeAddress, balances }))
|
||||
|
||||
const safe = safesMapSelector(store.getState()).get(safeAddress)
|
||||
//@ts-ignore
|
||||
const balanceResult = safe.get('balances').get(token.address)
|
||||
//@ts-ignore
|
||||
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)
|
||||
//@ts-ignore
|
||||
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)
|
||||
|
|
|
@ -39,7 +39,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
|
||||
}
|
||||
|
|
|
@ -2,11 +2,18 @@ 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'
|
||||
|
||||
type StoredSafes = Record<string, SafeRecordProps>
|
||||
|
||||
export const loadStoredSafes = async (): Promise<StoredSafes | undefined> => {
|
||||
const safes = await loadFromStorage<StoredSafes>(SAFES_KEY)
|
||||
|
||||
return safes
|
||||
}
|
||||
|
||||
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
|
||||
const safes = await loadFromStorage(SAFES_KEY)
|
||||
const safes = await loadStoredSafes()
|
||||
if (!safes) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -23,9 +30,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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -16,8 +16,7 @@ export const loadLastUsedProvider = async (): Promise<string | undefined> => {
|
|||
return lastUsedProvider
|
||||
}
|
||||
|
||||
let watcherInterval = null
|
||||
|
||||
let watcherInterval
|
||||
const providerWatcherMware = (store) => (next) => async (action) => {
|
||||
const handledAction = next(action)
|
||||
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
//@ts-nocheck
|
||||
import {
|
||||
isUserAnOwner,
|
||||
isUserAnOwnerOfAnySafe,
|
||||
isValidEnsName,
|
||||
sameAddress,
|
||||
shortVersionOf,
|
||||
} from 'src/logic/wallets/ethAddresses'
|
||||
import makeSafe from 'src/logic/safe/store/models/safe'
|
||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||
import { List } from 'immutable'
|
||||
|
||||
describe('Utility function: sameAddress', () => {
|
||||
it('It should return false if no address given', () => {
|
||||
// given
|
||||
const safeAddress = null
|
||||
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
|
||||
// when
|
||||
const result = sameAddress(safeAddress, safeAddress2)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return false if not second address given', () => {
|
||||
// given
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const safeAddress2 = null
|
||||
|
||||
// when
|
||||
const result = sameAddress(safeAddress, safeAddress2)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('It should return true if two equal addresses given', () => {
|
||||
// given
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
|
||||
// when
|
||||
const result = sameAddress(safeAddress, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it('If should return false if two different addresses given', () => {
|
||||
// given
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
|
||||
// when
|
||||
const result = sameAddress(safeAddress, safeAddress2)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility function: shortVersionOf', () => {
|
||||
it('It should return Unknown if no address given', () => {
|
||||
// given
|
||||
const safeAddress = null
|
||||
const cut = 5
|
||||
const expectedResult = 'Unknown'
|
||||
|
||||
// when
|
||||
const result = shortVersionOf(safeAddress, cut)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return 0x344...f0503 if given 0x344B941b1aAE2e4Be73987212FC4741687Bf0503 and a cut = 5', () => {
|
||||
// given
|
||||
const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
const cut = 5
|
||||
const expectedResult = `0x344...f0503`
|
||||
|
||||
// when
|
||||
const result = shortVersionOf(safeAddress, cut)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('If should return the same address if a cut value bigger than the address length given', () => {
|
||||
// given
|
||||
const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
const cut = safeAddress.length
|
||||
const expectedResult = safeAddress
|
||||
|
||||
// when
|
||||
const result = shortVersionOf(safeAddress, cut)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility function: isUserAnOwner', () => {
|
||||
it("Should return false if there's no Safe", () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const safeInstance = null
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isUserAnOwner(safeInstance, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it("Should return false if there's no `userAccount`", () => {
|
||||
// given
|
||||
const userAddress = null
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isUserAnOwner(safeInstance, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Should return false if there are no owners for the Safe', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const owners = null
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isUserAnOwner(safeInstance, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it("Should return true if `userAccount` is not in the list of Safe's owners", () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isUserAnOwner(safeInstance, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it("Should return false if `userAccount` is not in the list of Safe's owners", () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isUserAnOwner(safeInstance, userAddress2)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility function: isUserAnOwnerOfAnySafe', () => {
|
||||
it('Should return true if given a list of safes, one of them has an owner equal to the userAccount', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const owners1 = List([makeOwner({ address: userAddress })])
|
||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
||||
const safeInstance = makeSafe({ owners: owners1 })
|
||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||
const safesList = List([safeInstance, safeInstance2])
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isUserAnOwnerOfAnySafe(safesList, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return false if given a list of safes, none of them has an owner equal to the userAccount', () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const userAddress3 = 'address3'
|
||||
const owners1 = List([makeOwner({ address: userAddress3 })])
|
||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
||||
const safeInstance = makeSafe({ owners: owners1 })
|
||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||
const safesList = List([safeInstance, safeInstance2])
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isUserAnOwnerOfAnySafe(safesList, userAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility function: isValidEnsName', () => {
|
||||
it('If should return false if given no ens name', () => {
|
||||
// given
|
||||
const ensName = null
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return false for an ens without extension in format [value].[eth|test|xyz|luxe]', () => {
|
||||
// given
|
||||
const ensName = 'test'
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return false for an ens without the format [value].[eth|test|xyz|luxe]', () => {
|
||||
// given
|
||||
const ensName = 'test.et12312'
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return true for an ens in format [value].eth', () => {
|
||||
// given
|
||||
const ensName = 'test.eth'
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return true for ens in format [value].test', () => {
|
||||
// given
|
||||
const ensName = 'test.test'
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return true for an ens in the format [value].xyz', () => {
|
||||
// given
|
||||
const ensName = 'test.xyz'
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('It should return true for an ens in format [value].luxe', () => {
|
||||
// given
|
||||
const ensName = 'test.luxe'
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidEnsName(ensName)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
})
|
Binary file not shown.
After Width: | Height: | Size: 986 KiB |
|
@ -1,15 +1,16 @@
|
|||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3'
|
||||
import contractDataImage from './images/contractData.png'
|
||||
|
||||
const USER_ENABLED_LEDGER_TX_DATA = 'USER_ENABLED_LEDGER_TX_DATA'
|
||||
function transactionDataCheck(): any {
|
||||
return async (stateAndHelpers) => {
|
||||
const { wallet } = stateAndHelpers
|
||||
const isTransactionDataEnabled = await loadFromStorage<boolean>(USER_ENABLED_LEDGER_TX_DATA)
|
||||
if (wallet && wallet.name === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
|
||||
if (wallet && wallet.name.toUpperCase() === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
|
||||
return {
|
||||
heading: 'Allow Transaction Data', // edit modal heading here
|
||||
description: 'Please allow transaction data on your Ledger device.', // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
|
||||
description: `<div><p><strong>Important</strong>: In order to sign transactions with your Ledger device, you will have to activate the "Contract Data" setting in the Ethereum app on your Ledger.</p><img style="width:100%" src=${contractDataImage} /></div>`, // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
|
||||
eventCode: 'allowTransactionData',
|
||||
button: {
|
||||
text: 'Done',
|
|
@ -10,7 +10,13 @@ import { StepperPageFormProps } from 'src/components/Stepper'
|
|||
import AddressInput from 'src/components/forms/AddressInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { mustBeEthereumAddress, noErrorsOn, required } from 'src/components/forms/validator'
|
||||
import {
|
||||
mustBeEthereumAddress,
|
||||
noErrorsOn,
|
||||
required,
|
||||
composeValidators,
|
||||
minMaxLength,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
|
@ -109,7 +115,7 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
|||
placeholder="Name of the Safe"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={required}
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId="load-safe-name-field"
|
||||
/>
|
||||
</Col>
|
||||
|
@ -120,6 +126,8 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
|||
fieldMutator={(val) => {
|
||||
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
|
||||
}}
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
inputAdornment={
|
||||
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
|
||||
endAdornment: (
|
||||
|
@ -156,12 +164,15 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
|||
)
|
||||
}
|
||||
|
||||
const DetailsPage = () => (controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement => (
|
||||
<>
|
||||
<OpenPaper controls={controls}>
|
||||
<DetailsForm errors={errors} form={form} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
const DetailsPage = () =>
|
||||
function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls}>
|
||||
<DetailsForm errors={errors} form={form} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailsPage
|
||||
|
|
|
@ -8,7 +8,7 @@ import Identicon from 'src/components/Identicon'
|
|||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { required } from 'src/components/forms/validator'
|
||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
|
@ -79,7 +79,7 @@ const calculateSafeValues = (owners, threshold, values) => {
|
|||
}
|
||||
|
||||
const OwnerListComponent = (props) => {
|
||||
const [owners, setOwners] = useState([])
|
||||
const [owners, setOwners] = useState<string[]>([])
|
||||
const { classes, updateInitialProps, values } = props
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -132,7 +132,7 @@ const OwnerListComponent = (props) => {
|
|||
placeholder="Owner Name*"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
validate={required}
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId={`load-safe-owner-name-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -156,12 +156,15 @@ const OwnerListComponent = (props) => {
|
|||
|
||||
const OwnerListPage = withStyles(styles as any)(OwnerListComponent)
|
||||
|
||||
const OwnerList = ({ updateInitialProps }, network) => (controls, { values }) => (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
const OwnerList = ({ updateInitialProps }, network) =>
|
||||
function LoadSafeOwnerList(controls, { values }): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OwnerList
|
||||
|
|
|
@ -6,12 +6,11 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from '../components/fields'
|
|||
|
||||
import Page from 'src/components/layout/Page'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils'
|
||||
import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils'
|
||||
import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import { history } from 'src/store'
|
||||
import { loadFromStorage } from 'src/utils/storage'
|
||||
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
@ -27,7 +26,7 @@ export const loadSafe = async (
|
|||
const safeProps = await buildSafe(safeAddress, safeName)
|
||||
safeProps.owners = owners
|
||||
|
||||
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
|
||||
const storedSafes = (await loadStoredSafes()) || {}
|
||||
|
||||
storedSafes[safeAddress] = safeProps
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import * as React from 'react'
|
||||
|
||||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { required } from 'src/components/forms/validator'
|
||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { FIELD_NAME } from 'src/routes/open/components/fields'
|
||||
import { secondary, sm } from 'src/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
const styles = createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
maxWidth: '440px',
|
||||
|
@ -28,51 +28,56 @@ const styles = () => ({
|
|||
},
|
||||
})
|
||||
|
||||
const SafeName = ({ classes, safeName }) => (
|
||||
<>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="md">
|
||||
You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet
|
||||
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block className={classes.root} margin="lg">
|
||||
<Field
|
||||
component={TextField}
|
||||
defaultValue={safeName}
|
||||
name={FIELD_NAME}
|
||||
placeholder="Name of the new Safe"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={required}
|
||||
testId="create-safe-name-field"
|
||||
/>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph className={classes.links} color="primary" noMargin size="md">
|
||||
By continuing you consent to the{' '}
|
||||
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
terms of use
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://gnosis-safe.io/privacy" rel="noopener noreferrer" target="_blank">
|
||||
privacy policy
|
||||
</a>
|
||||
.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
const useSafeNameStyles = makeStyles(styles)
|
||||
|
||||
const SafeNameForm = withStyles(styles as any)(SafeName)
|
||||
const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
|
||||
const classes = useSafeNameStyles()
|
||||
|
||||
const SafeNamePage = () => (controls, { values }) => {
|
||||
const { safeName } = values
|
||||
return (
|
||||
<OpenPaper controls={controls}>
|
||||
<SafeNameForm safeName={safeName} />
|
||||
</OpenPaper>
|
||||
<>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="md">
|
||||
You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new
|
||||
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block className={classes.root} margin="lg">
|
||||
<Field
|
||||
component={TextField}
|
||||
defaultValue={safeName}
|
||||
name={FIELD_NAME}
|
||||
placeholder="Name of the new Safe"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId="create-safe-name-field"
|
||||
/>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph className={classes.links} color="primary" noMargin size="md">
|
||||
By continuing you consent to the{' '}
|
||||
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
||||
terms of use
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://gnosis-safe.io/privacy" rel="noopener noreferrer" target="_blank">
|
||||
privacy policy
|
||||
</a>
|
||||
.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeNamePage
|
||||
const SafeNamePageComponent = () =>
|
||||
function SafeNamePage(controls, { values }): React.ReactElement {
|
||||
const { safeName } = values
|
||||
return (
|
||||
<OpenPaper controls={controls}>
|
||||
<SafeNameForm safeName={safeName} />
|
||||
</OpenPaper>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeNamePageComponent
|
||||
|
|
|
@ -16,7 +16,14 @@ import AddressInput from 'src/components/forms/AddressInput'
|
|||
import Field from 'src/components/forms/Field'
|
||||
import SelectField from 'src/components/forms/SelectField'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { composeValidators, minValue, mustBeInteger, noErrorsOn, required } from 'src/components/forms/validator'
|
||||
import {
|
||||
composeValidators,
|
||||
minValue,
|
||||
mustBeInteger,
|
||||
noErrorsOn,
|
||||
required,
|
||||
minMaxLength,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
|
@ -129,7 +136,7 @@ const SafeOwners = (props) => {
|
|||
placeholder="Owner Name*"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
validate={required}
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId={`create-safe-owner-name-field-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -138,6 +145,8 @@ const SafeOwners = (props) => {
|
|||
fieldMutator={(val) => {
|
||||
form.mutators.setValue(addressName, val)
|
||||
}}
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
inputAdornment={
|
||||
noErrorsOn(addressName, errors) && {
|
||||
endAdornment: (
|
||||
|
@ -217,18 +226,21 @@ const SafeOwners = (props) => {
|
|||
|
||||
const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners))
|
||||
|
||||
const SafeOwnersPage = ({ updateInitialProps }) => (controls, { errors, form, values }) => (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<SafeOwnersForm
|
||||
errors={errors}
|
||||
form={form}
|
||||
otherAccounts={getAccountsFrom(values)}
|
||||
updateInitialProps={updateInitialProps}
|
||||
values={values}
|
||||
/>
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
const SafeOwnersPage = ({ updateInitialProps }) =>
|
||||
function OpenSafeOwnersPage(controls, { errors, form, values }) {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<SafeOwnersForm
|
||||
errors={errors}
|
||||
form={form}
|
||||
otherAccounts={getAccountsFrom(values)}
|
||||
updateInitialProps={updateInitialProps}
|
||||
values={values}
|
||||
/>
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeOwnersPage
|
||||
|
|
|
@ -161,7 +161,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
|
|||
pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
|
||||
state: {
|
||||
name,
|
||||
tx: pendingCreation.txHash,
|
||||
tx: pendingCreation?.txHash,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
|
|||
|
||||
const onRetry = async () => {
|
||||
const values = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
delete values.txHash
|
||||
delete values?.txHash
|
||||
await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values)
|
||||
setSafeCreationPendingInfo(values)
|
||||
createSafeProxy()
|
||||
|
|
|
@ -105,7 +105,7 @@ const BackButton = styled(Button)`
|
|||
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [safeCreationTxHash, setSafeCreationTxHash] = useState()
|
||||
const [safeCreationTxHash, setSafeCreationTxHash] = useState('')
|
||||
const [createdSafeAddress, setCreatedSafeAddress] = useState()
|
||||
|
||||
const [error, setError] = useState(false)
|
||||
|
@ -242,7 +242,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
useEffect(() => {
|
||||
let interval
|
||||
|
||||
const awaitUntilSafeIsDeployed = async () => {
|
||||
const awaitUntilSafeIsDeployed = async (safeCreationTxHash: string) => {
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash)
|
||||
|
@ -283,7 +283,9 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
return
|
||||
}
|
||||
|
||||
awaitUntilSafeIsDeployed()
|
||||
if (typeof safeCreationTxHash === 'string') {
|
||||
awaitUntilSafeIsDeployed(safeCreationTxHash)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
|
@ -294,7 +296,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
return <Loader size="sm" />
|
||||
}
|
||||
|
||||
let FooterComponent = null
|
||||
let FooterComponent
|
||||
if (error) {
|
||||
FooterComponent = ErrorFooter
|
||||
} else if (steps[stepIndex].footerComponent) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils'
|
||||
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
|
||||
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
|
||||
|
@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({
|
|||
}
|
||||
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const addressBookAddressesList = getAddressesListFromAdbk(addressBook)
|
||||
const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook)
|
||||
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
|
||||
|
||||
const formMutators = {
|
||||
|
|
|
@ -41,6 +41,7 @@ import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bi
|
|||
import RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
|
||||
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
|
||||
const AddressBookTable = ({ classes }) => {
|
||||
const columns = generateColumns()
|
||||
|
@ -49,10 +50,15 @@ const AddressBookTable = ({ classes }) => {
|
|||
const safesList = useSelector(safesListSelector)
|
||||
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const [selectedEntry, setSelectedEntry] = useState(null)
|
||||
const [selectedEntry, setSelectedEntry] = useState<any>(null)
|
||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'AddressBook' })
|
||||
}, [trackEvent])
|
||||
|
||||
useEffect(() => {
|
||||
if (entryAddressToEditOrCreateNew) {
|
||||
|
@ -64,7 +70,7 @@ const AddressBookTable = ({ classes }) => {
|
|||
if (entryAddressToEditOrCreateNew) {
|
||||
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
|
||||
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
|
||||
if (key >= 0) {
|
||||
if (key && key >= 0) {
|
||||
// Edit old entry
|
||||
const value = addressBook.get(key)
|
||||
setSelectedEntry({ entry: value, index: key })
|
||||
|
|
|
@ -38,7 +38,7 @@ const Transactions = (): React.ReactElement => {
|
|||
{transactionsByPage.map((tx: Transaction, index) => {
|
||||
let txHash = ''
|
||||
if ('transactionHash' in tx) {
|
||||
txHash = tx.transactionHash
|
||||
txHash = tx.transactionHash as string
|
||||
}
|
||||
if ('txHash' in tx) {
|
||||
txHash = tx.txHash
|
||||
|
|
|
@ -14,7 +14,7 @@ const AppAgreement = (): React.ReactElement => {
|
|||
const { visited } = useFormState({ subscription: { visited: true } })
|
||||
|
||||
// trick to prevent having the field validated by default. Not sure why this happens in this form
|
||||
const validate = !visited.agreementAccepted ? undefined : required
|
||||
const validate = !visited?.agreementAccepted ? undefined : required
|
||||
|
||||
return (
|
||||
<Field
|
||||
|
|
|
@ -28,7 +28,7 @@ export const appUrlResolver = createDecorator({
|
|||
},
|
||||
})
|
||||
|
||||
export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): React.ReactElement => {
|
||||
export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): null => {
|
||||
const {
|
||||
input: { value: appUrl },
|
||||
} = useField('appUrl', { subscription: { value: true } })
|
||||
|
@ -52,7 +52,7 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
|
|||
const { visited } = useFormState({ subscription: { visited: true } })
|
||||
|
||||
// trick to prevent having the field validated by default. Not sure why this happens in this form
|
||||
const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
|
||||
const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
|
||||
|
||||
return (
|
||||
<Field label="App URL" name="appUrl" placeholder="App URL" type="text" component={TextField} validate={validate} />
|
||||
|
|
|
@ -9,14 +9,14 @@ interface SubmitButtonStatusProps {
|
|||
onSubmitButtonStatusChange: (disabled: boolean) => void
|
||||
}
|
||||
|
||||
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => {
|
||||
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): null => {
|
||||
const { valid, validating, visited } = useFormState({
|
||||
subscription: { valid: true, validating: true, visited: true },
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
// if non visited, fields were not evaluated yet. Then, the default value is considered invalid
|
||||
const fieldsVisited = visited.agreementAccepted && visited.appUrl
|
||||
const fieldsVisited = visited?.agreementAccepted && visited.appUrl
|
||||
|
||||
onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo))
|
||||
}, [validating, valid, visited, onSubmitButtonStatusChange, appInfo])
|
||||
|
|
|
@ -40,7 +40,7 @@ const INITIAL_VALUES: AddAppFormValues = {
|
|||
}
|
||||
|
||||
const APP_INFO: SafeApp = {
|
||||
id: undefined,
|
||||
id: '',
|
||||
url: '',
|
||||
name: '',
|
||||
iconUrl: appsIconSvg,
|
||||
|
|
|
@ -54,7 +54,7 @@ const AppFrame = forwardRef<HTMLIFrameElement, AppFrameProps>(function AppFrameC
|
|||
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
|
||||
|
||||
if (!selectedApp) {
|
||||
return null
|
||||
return <div />
|
||||
}
|
||||
|
||||
if (!consentReceived) {
|
||||
|
|
|
@ -31,7 +31,7 @@ const isTxValid = (t: Transaction): boolean => {
|
|||
}
|
||||
|
||||
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
|
||||
return isAddressValid && t.data && typeof t.data === 'string'
|
||||
return isAddressValid && !!t.data && typeof t.data === 'string'
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
|
@ -71,6 +71,7 @@ type OwnProps = {
|
|||
ethBalance: string
|
||||
onCancel: () => void
|
||||
onUserConfirm: (safeTxHash: string) => void
|
||||
onUserTxReject: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
|
@ -84,7 +85,8 @@ const ConfirmTransactionModal = ({
|
|||
onCancel,
|
||||
onUserConfirm,
|
||||
onClose,
|
||||
}: OwnProps): React.ReactElement => {
|
||||
onUserTxReject,
|
||||
}: OwnProps): React.ReactElement | null => {
|
||||
const dispatch = useDispatch()
|
||||
if (!isOpen) {
|
||||
return null
|
||||
|
@ -111,6 +113,7 @@ const ConfirmTransactionModal = ({
|
|||
navigateToTransactionsTab: false,
|
||||
},
|
||||
handleUserConfirmation,
|
||||
onUserTxReject,
|
||||
),
|
||||
)
|
||||
onClose()
|
||||
|
@ -133,27 +136,25 @@ const ConfirmTransactionModal = ({
|
|||
<>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => {
|
||||
return (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>{humanReadableValue(tx.value, 18)} ETH</Bold>
|
||||
</div>
|
||||
{txs.map((tx, index) => (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>{humanReadableValue(tx.value, 18)} ETH</Bold>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ type Props = {
|
|||
onAppRemoved: (appId: string) => void
|
||||
}
|
||||
|
||||
type AppListItem = SafeApp & { checked: boolean }
|
||||
|
||||
const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true)
|
||||
|
@ -28,7 +30,7 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props):
|
|||
|
||||
const closeModal = () => setIsOpen(false)
|
||||
|
||||
const getItemList = () =>
|
||||
const getItemList = (): AppListItem[] =>
|
||||
appList.map((a) => {
|
||||
return { ...a, checked: !a.disabled }
|
||||
})
|
||||
|
|
|
@ -42,7 +42,7 @@ const useAppList = (): UseAppListReturnType => {
|
|||
}
|
||||
})
|
||||
|
||||
let apps = []
|
||||
let apps: SafeApp[] = []
|
||||
// using the appURL to recover app info
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
try {
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
INTERFACE_MESSAGES,
|
||||
RequestId,
|
||||
Transaction,
|
||||
LowercaseNetworks,
|
||||
} from '@gnosis.pm/safe-apps-sdk'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||
|
@ -44,7 +45,7 @@ const useIframeMessageHandler = (
|
|||
selectedApp: SafeApp | undefined,
|
||||
openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void,
|
||||
closeModal: () => void,
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement>,
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||
): ReturnType => {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
|
@ -60,8 +61,8 @@ const useIframeMessageHandler = (
|
|||
requestId: requestId || Math.trunc(window.performance.now()),
|
||||
}
|
||||
|
||||
if (iframeRef?.current && selectedApp) {
|
||||
iframeRef.current.contentWindow.postMessage(requestWithMessage, selectedApp.url)
|
||||
if (iframeRef && selectedApp) {
|
||||
iframeRef.current?.contentWindow?.postMessage(requestWithMessage, selectedApp.url)
|
||||
}
|
||||
},
|
||||
[iframeRef, selectedApp],
|
||||
|
@ -77,7 +78,9 @@ const useIframeMessageHandler = (
|
|||
|
||||
switch (msg.data.messageId) {
|
||||
case SDK_MESSAGES.SEND_TRANSACTIONS: {
|
||||
openConfirmationModal(msg.data.data, requestId)
|
||||
if (msg.data.data) {
|
||||
openConfirmationModal(msg.data.data, requestId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -85,9 +88,9 @@ const useIframeMessageHandler = (
|
|||
const message = {
|
||||
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
|
||||
data: {
|
||||
safeAddress,
|
||||
network: network,
|
||||
ethBalance,
|
||||
safeAddress: safeAddress as string,
|
||||
network: network.toLowerCase() as LowercaseNetworks,
|
||||
ethBalance: ethBalance as string,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -104,7 +107,7 @@ const useIframeMessageHandler = (
|
|||
if (message.origin === window.origin) {
|
||||
return
|
||||
}
|
||||
if (!selectedApp.url.includes(message.origin)) {
|
||||
if (!selectedApp?.url.includes(message.origin)) {
|
||||
console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
|
||||
import { INTERFACE_MESSAGES, Transaction, RequestId } from '@gnosis.pm/safe-apps-sdk'
|
||||
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk'
|
||||
import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
@ -7,6 +7,7 @@ import styled, { css } from 'styled-components'
|
|||
import ManageApps from './components/ManageApps'
|
||||
import AppFrame from './components/AppFrame'
|
||||
import { useAppList } from './hooks/useAppList'
|
||||
import { SafeApp } from './types.d'
|
||||
|
||||
import LCL from 'src/components/ListContentLayout'
|
||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
import { isSameURL } from 'src/utils/url'
|
||||
import { useIframeMessageHandler } from './hooks/useIframeMessageHandler'
|
||||
import ConfirmTransactionModal from './components/ConfirmTransactionModal'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
|
||||
const centerCSS = css`
|
||||
display: flex;
|
||||
|
@ -62,8 +64,9 @@ const Apps = (): React.ReactElement => {
|
|||
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
|
||||
INITIAL_CONFIRM_TX_MODAL_STATE,
|
||||
)
|
||||
const iframeRef = useRef<HTMLIFrameElement>()
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
const { trackEvent } = useAnalytics()
|
||||
const granted = useSelector(grantedSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
|
@ -99,6 +102,13 @@ const Apps = (): React.ReactElement => {
|
|||
)
|
||||
}
|
||||
|
||||
const onUserTxReject = () => {
|
||||
sendMessageToIframe(
|
||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
|
||||
confirmTransactionModal.requestId,
|
||||
)
|
||||
}
|
||||
|
||||
const onSelectApp = useCallback(
|
||||
(appId) => {
|
||||
if (selectedAppId === appId) {
|
||||
|
@ -111,6 +121,7 @@ const Apps = (): React.ReactElement => {
|
|||
[selectedAppId],
|
||||
)
|
||||
|
||||
// Auto Select app first App
|
||||
useEffect(() => {
|
||||
const selectFirstEnabledApp = () => {
|
||||
const firstEnabledApp = appList.find((a) => !a.disabled)
|
||||
|
@ -124,7 +135,14 @@ const Apps = (): React.ReactElement => {
|
|||
if (initialSelect || currentAppWasDisabled) {
|
||||
selectFirstEnabledApp()
|
||||
}
|
||||
}, [appList, selectedApp, selectedAppId])
|
||||
}, [appList, selectedApp, selectedAppId, trackEvent])
|
||||
|
||||
// track GA
|
||||
useEffect(() => {
|
||||
if (selectedApp) {
|
||||
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name })
|
||||
}
|
||||
}, [selectedApp, trackEvent])
|
||||
|
||||
const handleIframeLoad = useCallback(() => {
|
||||
const iframe = iframeRef.current
|
||||
|
@ -136,14 +154,14 @@ const Apps = (): React.ReactElement => {
|
|||
sendMessageToIframe({
|
||||
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
|
||||
data: {
|
||||
safeAddress,
|
||||
network,
|
||||
ethBalance,
|
||||
safeAddress: safeAddress as string,
|
||||
network: network.toLowerCase() as LowercaseNetworks,
|
||||
ethBalance: ethBalance as string,
|
||||
},
|
||||
})
|
||||
}, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe])
|
||||
|
||||
if (loadingAppList || !appList.length) {
|
||||
if (loadingAppList || !appList.length || !safeAddress) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Loader size="md" />
|
||||
|
@ -189,14 +207,15 @@ const Apps = (): React.ReactElement => {
|
|||
</CenteredMT>
|
||||
<ConfirmTransactionModal
|
||||
isOpen={confirmTransactionModal.isOpen}
|
||||
app={selectedApp}
|
||||
app={selectedApp as SafeApp}
|
||||
safeAddress={safeAddress}
|
||||
ethBalance={ethBalance}
|
||||
safeName={safeName}
|
||||
ethBalance={ethBalance as string}
|
||||
safeName={safeName as string}
|
||||
txs={confirmTransactionModal.txs}
|
||||
onCancel={closeConfirmationModal}
|
||||
onClose={closeConfirmationModal}
|
||||
onUserConfirm={onUserTxConfirm}
|
||||
onUserTxReject={onUserTxReject}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type SafeApp = {
|
||||
id: string | undefined
|
||||
id: string
|
||||
url: string
|
||||
name: string
|
||||
iconUrl: string
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue