Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2020-12-14 14:53:54 -03:00
commit 0814eb528e
47 changed files with 1602 additions and 1880 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ yalc.lock
# testing
/coverage/
src/types/contracts/
tsconfig.tsbuildinfo

View File

@ -1,6 +1,5 @@
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
sudo: required
dist: bionic
dist: focal
language: node_js
node_js:
- '12'
@ -51,7 +50,7 @@ before_script:
before_install:
# Needed to deploy pull request and releases
- sudo apt-get update
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev libudev-dev
- sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user
script:
- yarn lint:check

View File

@ -1,7 +1,7 @@
import Web3 from 'web3'
const window = global.window || {}
window.web3 = window.web3 || {}
window.web3 = {}
window.web3.currentProvider = new Web3.providers.HttpProvider('http://localhost:8545')
global.window = window

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.16.2",
"version": "2.17.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -48,7 +48,7 @@
"husky": {
"hooks": {
"pre-commit": "lint-staged --allow-empty",
"pre-push": "tsc"
"pre-push": "tsc --noEmit --incremental"
}
},
"lint-staged": {
@ -149,6 +149,7 @@
}
},
"resolutions": {
"@babel/core": "^7.12.0",
"sass-loader": "^9.0.0"
},
"browserslist": {
@ -164,9 +165,10 @@
]
},
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#3f0689f",
"@gnosis.pm/safe-apps-sdk": "1.0.0-beta.4",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#ff29c3c",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid-singleton": "5.34.0",
"@material-ui/core": "^4.11.0",
@ -212,14 +214,13 @@
"react-dom": "16.13.1",
"react-final-form": "^6.5.2",
"react-final-form-listeners": "^1.0.2",
"react-ga": "3.2.1",
"react-ga": "3.3.0",
"react-hot-loader": "4.13.0",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",
"react-scripts": "^4.0.1",
"react-window": "^1.8.6",
"recompose": "^0.30.0",
"redux": "4.0.5",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
@ -232,7 +233,7 @@
"web3-utils": "^1.2.11"
},
"devDependencies": {
"@rescripts/cli": "^0.0.14",
"@rescripts/cli": "^0.0.15",
"@sentry/cli": "^1.59.0",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
@ -241,7 +242,7 @@
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.2.2",
"@typechain/web3-v1": "^1.0.0",
"@typechain/web3-v1": "^2.0.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.16",
"@types/lodash.memoize": "^4.1.6",

View File

@ -80,8 +80,8 @@ const App: React.FC = ({ children }) => {
const granted = useSelector(grantedSelector)
const sidebarItems = useSidebarItems()
useLoadSafe(safeAddress)
useSafeScheduledUpdates(safeAddress)
const safeLoaded = useLoadSafe(safeAddress)
useSafeScheduledUpdates(safeLoaded, safeAddress)
const sendFunds = safeActionsState.sendFunds
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''

View File

@ -9,11 +9,11 @@ import { Link } from 'react-router-dom'
import Provider from './Provider'
import Spacer from 'src/components/Spacer'
import openHoc from 'src/components/hoc/OpenHoc'
import Col from 'src/components/layout/Col'
import Img from 'src/components/layout/Img'
import Row from 'src/components/layout/Row'
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
import { useStateHandler } from 'src/logic/hooks/useStateHandler'
import SafeLogo from '../assets/gnosis-safe-multisig-logo.svg'
@ -55,41 +55,45 @@ const styles = () => ({
},
})
const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInfo, toggle }) => (
<Row className={classes.summary}>
<Col className={classes.logo} middle="xs" start="xs">
<Link to="/">
<Img alt="Gnosis Team Safe" height={36} src={SafeLogo} testId="heading-gnosis-logo" />
</Link>
</Col>
<Spacer />
<Provider
info={providerInfo}
open={open}
toggle={toggle}
render={(providerRef) => (
<Popper
anchorEl={providerRef.current}
className={classes.popper}
open={open}
placement="bottom"
popperOptions={{ positionFixed: true }}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<>
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
<List className={classes.root} component="div">
{providerDetails}
</List>
</ClickAwayListener>
</>
</Grow>
)}
</Popper>
)}
/>
</Row>
))
const Layout = ({ classes, providerDetails, providerInfo }) => {
const { clickAway, open, toggle } = useStateHandler()
return (
<Row className={classes.summary}>
<Col className={classes.logo} middle="xs" start="xs">
<Link to="/">
<Img alt="Gnosis Team Safe" height={36} src={SafeLogo} testId="heading-gnosis-logo" />
</Link>
</Col>
<Spacer />
<Provider
info={providerInfo}
open={open}
toggle={toggle}
render={(providerRef) => (
<Popper
anchorEl={providerRef.current}
className={classes.popper}
open={open}
placement="bottom"
popperOptions={{ positionFixed: true }}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<>
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
<List className={classes.root} component="div">
{providerDetails}
</List>
</ClickAwayListener>
</>
</Grow>
)}
</Popper>
)}
/>
</Row>
)
}
export default withStyles(styles as any)(Layout)

View File

@ -44,6 +44,7 @@ const IconContainer = styled.div`
justify-content: space-evenly;
`
const StyledButton = styled(Button)`
padding: 0 18px;
*:first-child {
margin: 0 4px 0 0;
}

View File

@ -63,21 +63,17 @@ export const onboardUser = async (): Promise<boolean> => {
return walletSelected && onboard.walletCheck()
}
const ConnectButton = (props): React.ReactElement => (
<Button
color="primary"
minWidth={140}
onClick={async () => {
const walletSelected = await onboard.walletSelect()
export const onConnectButtonClick = async () => {
const walletSelected = await onboard.walletSelect()
// perform wallet checks only if user selected a wallet
if (walletSelected) {
await onboard.walletCheck()
}
}}
variant="contained"
{...props}
>
// perform wallet checks only if user selected a wallet
if (walletSelected) {
await onboard.walletCheck()
}
}
const ConnectButton = (props): React.ReactElement => (
<Button color="primary" minWidth={140} onClick={onConnectButtonClick} variant="contained" {...props}>
Connect
</Button>
)

View File

@ -1,22 +0,0 @@
import * as React from 'react'
import Bold from 'src/components/layout/Bold'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph/index'
import Row from 'src/components/layout/Row'
import { CreateSafe } from 'src/routes/welcome/components/Layout'
const NoSafe = ({ provider, text }) => (
<Row>
<Col center="xs" margin="md" sm={10} smOffset={2} start="sm" xs={12}>
<Paragraph size="lg">
<Bold>{text}</Bold>
</Paragraph>
</Col>
<Col center="xs" margin="md" sm={10} smOffset={2} start="sm" xs={12}>
<CreateSafe provider={provider} />
</Col>
</Row>
)
export default NoSafe

View File

@ -166,22 +166,16 @@ describe('Forms > Validators', () => {
})
describe('uniqueAddress validator', () => {
it('Returns undefined for an address not contained in the passed array', async () => {
it('Returns undefined if `addresses` does not contains the provided address', async () => {
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
expect(uniqueAddress(addresses)()).toBeUndefined()
expect(uniqueAddress(addresses)('0x2D6F2B448b0F711Eb81f2929566504117d67E44F')).toBeUndefined()
})
it('Returns an error message for an array with duplicated values', async () => {
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
it('Returns an error message if address is in the `addresses` list already', async () => {
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0x2D6F2B448b0F711Eb81f2929566504117d67E44F']
expect(uniqueAddress(addresses)()).toEqual(ADDRESS_REPEATED_ERROR)
})
it('Returns an error message for an array with duplicated checksum and not checksum values', async () => {
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae']
expect(uniqueAddress(addresses)()).toEqual(ADDRESS_REPEATED_ERROR)
expect(uniqueAddress(addresses)('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toEqual(ADDRESS_REPEATED_ERROR)
})
})

View File

@ -1,8 +1,10 @@
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { List } from 'immutable'
import memoize from 'lodash.memoize'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d'
import { List } from 'immutable'
type ValidatorReturnType = string | undefined
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
@ -87,17 +89,9 @@ export const minMaxLength = (minLen: number, maxLen: number) => (value: string):
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
export const uniqueAddress = (addresses: string[] | List<string>): GenericValidatorType => (): ValidatorReturnType => {
// @ts-expect-error both list and array have signatures for map but TS thinks they're not compatible
const lowercaseAddresses = addresses.map((address) => address.toLowerCase())
const uniqueAddresses = new Set(lowercaseAddresses)
const lengthPropName = 'size' in addresses ? 'size' : 'length'
if (uniqueAddresses.size !== addresses?.[lengthPropName]) {
return ADDRESS_REPEATED_ERROR
}
return undefined
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
}
export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>

View File

@ -1,6 +0,0 @@
import { withStateHandlers } from 'recompose'
export default withStateHandlers(() => ({ open: false }), {
toggle: ({ open }) => () => ({ open: !open }),
clickAway: () => () => ({ open: false }),
})

View File

@ -7,19 +7,6 @@
padding: 12px 0 0 0;
}
@media only screen and (max-width: #{$screenLg}px) {
.page {
padding: 72px $lg 0 $lg;
}
}
@media only screen and (min-width: #{$screenLg}px) and (max-width: 1360px) {
.page {
padding: 96px 120px 0 120px;
}
}
.center {
align-self: center;
}

View File

@ -11,6 +11,9 @@ import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
import { SENTRY_DSN } from './utils/constants'
import { disableMMAutoRefreshWarning } from './utils/mm_warnings'
disableMMAutoRefreshWarning()
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })

View File

@ -8,9 +8,10 @@ import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
export const addressBookAddressesListSelector = createSelector(addressBookSelector, (addressBook): string[] => {
export const addressBookAddressesListSelector = (state: AppReduxState): string[] => {
const addressBook = addressBookSelector(state)
return addressBook.map((entry) => entry.address)
})
}
export const getNameFromAddressBookSelector = createSelector(
addressBookSelector,

View File

@ -64,5 +64,5 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
}
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
return !!method?.payable
return Boolean(method?.payable) || method.stateMutability === 'payable'
}

View File

@ -57,6 +57,10 @@ const generateBatchRequests = <ReturnValues>({
if (type !== undefined) {
request = web3[type][method].request(...args, resolver)
} else {
if (address === null) {
resolve()
return
}
request = contractInstance.methods[method](...args).call.request(resolver)
}

View File

@ -0,0 +1,17 @@
import { useState } from 'react'
type ReturnValue = {
open: boolean
toggle: () => void
clickAway: () => void
}
export const useStateHandler = (openInitialValue = false): ReturnValue => {
const [open, setOpen] = useState(openInitialValue)
return {
open,
toggle: () => setOpen((open) => !open),
clickAway: () => setOpen(false),
}
}

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
@ -10,26 +10,26 @@ 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): boolean => {
const dispatch = useDispatch<Dispatch>()
const [isSafeLoaded, setIsSafeLoaded] = useState(false)
useEffect(() => {
const fetchData = () => {
const fetchData = async () => {
if (safeAddress) {
dispatch(fetchLatestMasterContractVersion())
.then(() => {
dispatch(fetchSafe(safeAddress))
return dispatch(fetchSafeTokens(safeAddress))
})
.then(() => {
dispatch(fetchSafeCreationTx(safeAddress))
dispatch(fetchTransactions(safeAddress))
return dispatch(addViewedSafe(safeAddress))
})
await dispatch(fetchLatestMasterContractVersion())
await dispatch(fetchSafe(safeAddress))
setIsSafeLoaded(true)
await dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchSafeCreationTx(safeAddress))
dispatch(fetchTransactions(safeAddress))
dispatch(addViewedSafe(safeAddress))
}
}
dispatch(loadAddressBookFromStorage())
fetchData()
}, [dispatch, safeAddress])
return isSafeLoaded
}

View File

@ -8,7 +8,7 @@ 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 = (safeLoaded: boolean, safeAddress?: string): void => {
const dispatch = useDispatch()
const timer = useRef<number>()
@ -34,7 +34,7 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
}
}
if (safeAddress) {
if (safeAddress && safeLoaded) {
fetchSafeData(safeAddress)
}
@ -42,5 +42,5 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
mounted = false
clearTimeout(timer.current)
}
}, [dispatch, safeAddress])
}, [dispatch, safeAddress, safeLoaded])
}

View File

@ -57,6 +57,7 @@ export interface CreateTransactionArgs {
type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void
type ErrorEventHandler = () => void
export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001
const createTransaction = (
{
@ -210,20 +211,21 @@ const createTransaction = (
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
: notificationsQueue.afterExecutionError.message
console.error(`Error creating the TX: `, err)
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
dispatch(enqueueSnackbar(errorMsg))
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
console.error(`Error creating the TX - an attempt to get the error message: ${errMsg}`)
if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
console.error(`Error creating the TX - an attempt to get the error message: ${errMsg}`)
}
}
return txHash

View File

@ -97,7 +97,7 @@ const processTransaction = ({
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
dispatch(closeSnackbarAction(beforeExecutionKey))
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
await saveTxToHistory({ ...txArgs, signature })
// TODO: while we wait for the tx to be stored in the service and later update the tx info
@ -130,7 +130,7 @@ const processTransaction = ({
.send(sendParams)
.once('transactionHash', async (hash: string) => {
txHash = hash
dispatch(closeSnackbarAction(beforeExecutionKey))
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
@ -141,19 +141,19 @@ const processTransaction = ({
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
dispatch(closeSnackbarAction(pendingExecutionKey))
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
await storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error(e)
}
})
.on('error', (error) => {
dispatch(closeSnackbarAction(pendingExecutionKey))
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
if (pendingExecutionKey) {
dispatch(closeSnackbarAction(pendingExecutionKey))
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
dispatch(
@ -178,17 +178,16 @@ const processTransaction = ({
const errorMsg = err.message
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
: notificationsQueue.afterExecutionError.message
console.error(err)
if (txHash !== undefined) {
dispatch(closeSnackbarAction(beforeExecutionKey))
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) {
dispatch(closeSnackbarAction(pendingExecutionKey))
}
if (pendingExecutionKey) {
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
}
dispatch(enqueueSnackbar(errorMsg))
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
if (txHash) {
const executeData = safeInstance.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)
console.error(`Error executing the TX: ${errMsg}`)

View File

@ -72,14 +72,13 @@ export const safeTransactionsSelector = createSelector(
},
)
export const addressBookQueryParamsSelector = (state: AppReduxState): string | null => {
export const addressBookQueryParamsSelector = (state: AppReduxState): string | undefined => {
const { location } = state.router
let entryAddressToEditOrCreateNew = null
if (location && location.query) {
if (location?.query) {
const { entryAddress } = location.query
entryAddressToEditOrCreateNew = entryAddress
return entryAddress
}
return entryAddressToEditOrCreateNew
}
export const safeCancellationTransactionsSelector = createSelector(

View File

@ -1,4 +1,4 @@
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
import { AbiItem } from 'web3-utils'
import { MultiSend } from 'src/types/contracts/MultiSend.d'
import { getWeb3 } from 'src/logic/wallets/getWeb3'

View File

@ -1,5 +1,6 @@
import { getEIP712Signer } from './EIP712Signer'
import { ethSigner } from './ethSigner'
import { METAMASK_REJECT_CONFIRM_TX_ERROR_CODE } from 'src/logic/safe/store/actions/createTransaction'
// 1. we try to sign via EIP-712 if user's wallet supports it
// 2. If not, try to use eth_sign (Safe version has to be >1.1.1)
@ -29,9 +30,8 @@ export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boole
break
} catch (err) {
console.error(err)
// Metamask sign request error code
if (err.code === 4001) {
throw new Error('User denied sign request')
if (err.code === METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
throw err
}
}
}

View File

@ -35,7 +35,7 @@ const httpProviderOptions = {
export const web3ReadOnly = new Web3(
process.env.NODE_ENV !== 'test'
? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
: window.web3?.currentProvider || 'ws://localhost:8545',
: 'ws://localhost:8545',
)
let web3 = web3ReadOnly

View File

@ -1,5 +1,5 @@
import closeSnackbar from 'src/logic/notifications/store/actions/closeSnackbar'
import { WALLET_PROVIDER, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
import { fetchProvider } from 'src/logic/wallets/store/actions'
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
@ -29,9 +29,6 @@ const providerWatcherMware = (store) => (next) => async (action) => {
clearInterval(watcherInterval)
}
if (currentProviderProps.name.toUpperCase() === WALLET_PROVIDER.METAMASK && (window as any).ethereum) {
;(window as any).ethereum.autoRefreshOnNetworkChange = false
}
saveToStorage(LAST_USED_PROVIDER_KEY, currentProviderProps.name)
watcherInterval = setInterval(async () => {

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Redirect, Route, Switch, withRouter } from 'react-router-dom'
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom'
import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes'
@ -19,8 +19,13 @@ const Load = React.lazy(() => import('./load/container/Load'))
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
const Routes = ({ location }) => {
const Routes = (): React.ReactElement => {
const [isInitialLoad, setInitialLoad] = useState(true)
const location = useLocation()
const matchSafeWithAction = useRouteMatch<{ safeAddress: string; safeAction: string }>({
path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction`,
})
const defaultSafe = useSelector(defaultSafeSelector)
const { trackPage } = useAnalytics()
@ -31,9 +36,18 @@ const Routes = ({ location }) => {
}, [location.pathname, isInitialLoad])
useEffect(() => {
const page = location.pathname + location.search
trackPage(page)
}, [location.pathname, location.search, trackPage])
if (matchSafeWithAction) {
// prevent logging safeAddress
let safePage = `${SAFELIST_ADDRESS}/SAFE_ADDRESS`
if (matchSafeWithAction.params?.safeAction) {
safePage += `/${matchSafeWithAction.params?.safeAction}`
}
trackPage(safePage)
} else {
const page = `${location.pathname}${location.search}`
trackPage(page)
}
}, [location, matchSafeWithAction, trackPage])
return (
<Switch>
@ -65,4 +79,4 @@ const Routes = ({ location }) => {
)
}
export default withRouter(Routes)
export default Routes

View File

@ -150,7 +150,6 @@ const Open = (): React.ReactElement => {
ReactGA.event({
category: 'User',
action: 'Created a safe',
value: safeAddress,
})
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)

View File

@ -1,10 +1,9 @@
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React from 'react'
import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { styles } from './style'
import { useStyles } from './style'
import Modal from 'src/components/Modal'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
@ -20,55 +19,69 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { Entry } from 'src/routes/safe/components/AddressBook/index'
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
export const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id'
const CreateEditEntryModalComponent = ({
classes,
const formMutators = {
setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
type CreateEditEntryModalProps = {
editEntryModalHandler: (entry: AddressBookEntry) => void
entryToEdit: Entry
isOpen: boolean
newEntryModalHandler: (entry: AddressBookEntry) => void
onClose: () => void
}
export const CreateEditEntryModal = ({
editEntryModalHandler,
entryToEdit,
isOpen,
newEntryModalHandler,
onClose,
}) => {
const onFormSubmitted = (values) => {
if (entryToEdit && !entryToEdit.entry.isNew) {
editEntryModalHandler(values)
} else {
}: CreateEditEntryModalProps): ReactElement => {
const classes = useStyles()
const { isNew, ...initialValues } = entryToEdit.entry
const onFormSubmitted = (values: AddressBookEntry) => {
if (isNew) {
newEntryModalHandler(values)
} else {
editEntryModalHandler(values)
}
}
const addressBookAddressesList = useSelector(addressBookAddressesListSelector)
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
const formMutators = {
setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
const storedAddresses = useSelector(addressBookAddressesListSelector)
const isUniqueAddress = uniqueAddress(storedAddresses)
return (
<Modal
description={entryToEdit ? 'Edit addressBook entry' : 'Create new addressBook entry'}
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
handleClose={onClose}
open={isOpen}
paperClassName={classes.smallerModalWindow}
title={entryToEdit ? 'Edit entry' : 'Create new entry'}
title={isNew ? 'Create new entry' : 'Edit entry'}
>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin weight="bolder">
{entryToEdit ? 'Edit entry' : 'Create entry'}
{isNew ? 'Create entry' : 'Edit entry'}
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
{(...args) => {
const formState = args[2]
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
@ -86,13 +99,11 @@ const CreateEditEntryModalComponent = ({
<Row margin="md">
<Col xs={11}>
<Field
className={classes.addressInput}
component={TextField}
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
name="name"
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
placeholder="Name"
testId={CREATE_ENTRY_INPUT_NAME_ID}
text={entryToEdit ? 'Entry*' : 'New entry*'}
text="Name"
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
@ -101,18 +112,16 @@ const CreateEditEntryModalComponent = ({
<Row margin="md">
<Col xs={11}>
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
disabled={!!entryToEdit}
disabled={!isNew}
fieldMutator={mutators.setOwnerAddress}
name="address"
placeholder="Owner address*"
placeholder="Address*"
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
text="Address*"
validators={[(value?: string) => (isNew ? isUniqueAddress(value) : undefined)]}
/>
</Col>
{!entryToEdit ? (
{isNew ? (
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
@ -131,8 +140,9 @@ const CreateEditEntryModalComponent = ({
testId={SAVE_NEW_ENTRY_BTN_ID}
type="submit"
variant="contained"
disabled={!formState.valid}
>
{entryToEdit ? 'Save' : 'Create'}
{isNew ? 'Create' : 'Save'}
</Button>
</Row>
</>
@ -142,5 +152,3 @@ const CreateEditEntryModalComponent = ({
</Modal>
)
}
export default withStyles(styles as any)(CreateEditEntryModalComponent)

View File

@ -1,26 +1,30 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { lg, md } from 'src/theme/variables'
export const styles = () => ({
heading: {
padding: lg,
justifyContent: 'space-between',
boxSizing: 'border-box',
},
manage: {
fontSize: lg,
},
container: {
padding: `${md} ${lg}`,
},
close: {
height: '35px',
width: '35px',
},
buttonRow: {
height: '84px',
justifyContent: 'center',
},
smallerModalWindow: {
height: 'auto',
},
})
export const useStyles = makeStyles(
createStyles({
heading: {
padding: lg,
justifyContent: 'space-between',
boxSizing: 'border-box',
},
manage: {
fontSize: lg,
},
container: {
padding: `${md} ${lg}`,
},
close: {
height: '35px',
width: '35px',
},
buttonRow: {
height: '84px',
justifyContent: 'center',
},
smallerModalWindow: {
height: 'auto',
},
}),
)

View File

@ -3,7 +3,7 @@ import TableContainer from '@material-ui/core/TableContainer'
import TableRow from '@material-ui/core/TableRow'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React, { useEffect, useState } from 'react'
import React, { ReactElement, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { styles } from './style'
@ -21,8 +21,8 @@ import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddr
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses'
import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
import {
AB_ADDRESS_ID,
@ -47,20 +47,24 @@ interface AddressBookSelectedEntry extends AddressBookEntry {
isNew?: boolean
}
const AddressBookTable = (): React.ReactElement => {
export type Entry = {
entry: AddressBookSelectedEntry
index?: number
isOwnerAddress?: boolean
}
const initialEntryState: Entry = { entry: { address: '', name: '', isNew: true } }
const AddressBookTable = (): ReactElement => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const autoColumns = columns.filter(({ custom }) => !custom)
const dispatch = useDispatch()
const safesList = useSelector(safesListSelector)
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
const addressBook = useSelector(addressBookSelector)
const granted = useSelector(grantedSelector)
const [selectedEntry, setSelectedEntry] = useState<{
entry?: AddressBookSelectedEntry
index?: number
isOwnerAddress?: boolean
} | null>(null)
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
@ -78,8 +82,9 @@ const AddressBookTable = (): React.ReactElement => {
useEffect(() => {
if (entryAddressToEditOrCreateNew) {
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd)
const address = checksumAddress(entryAddressToEditOrCreateNew)
const oldEntryIndex = addressBook.findIndex((entry) => sameAddress(entry.address, address))
if (oldEntryIndex >= 0) {
// Edit old entry
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
@ -88,7 +93,7 @@ const AddressBookTable = (): React.ReactElement => {
setSelectedEntry({
entry: {
name: '',
address: checksumEntryAdd,
address,
isNew: true,
},
})
@ -96,7 +101,7 @@ const AddressBookTable = (): React.ReactElement => {
}
}, [addressBook, entryAddressToEditOrCreateNew])
const newEntryModalHandler = (entry) => {
const newEntryModalHandler = (entry: AddressBookEntry) => {
setEditCreateEntryModalOpen(false)
const checksumEntries = {
...entry,
@ -105,8 +110,8 @@ const AddressBookTable = (): React.ReactElement => {
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
}
const editEntryModalHandler = (entry) => {
setSelectedEntry(null)
const editEntryModalHandler = (entry: AddressBookEntry) => {
setSelectedEntry(initialEntryState)
setEditCreateEntryModalOpen(false)
const checksumEntries = {
...entry,
@ -116,8 +121,8 @@ const AddressBookTable = (): React.ReactElement => {
}
const deleteEntryModalHandler = () => {
const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : ''
setSelectedEntry(null)
const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : ''
setSelectedEntry(initialEntryState)
setDeleteEntryModalOpen(false)
dispatch(removeAddressBookEntry(entryAddress))
}
@ -128,8 +133,8 @@ const AddressBookTable = (): React.ReactElement => {
<Col end="sm" xs={12}>
<ButtonLink
onClick={() => {
setSelectedEntry(null)
setEditCreateEntryModalOpen(!editCreateEntryModalOpen)
setSelectedEntry(initialEntryState)
setEditCreateEntryModalOpen(true)
}}
size="lg"
testId="manage-tokens-btn"

View File

@ -0,0 +1,106 @@
import { MutableRefObject, useEffect, useState } from 'react'
import {
getSDKVersion,
SDKMessageEvent,
MethodToResponse,
Methods,
ErrorResponse,
MessageFormatter,
METHODS,
} from '@gnosis.pm/safe-apps-sdk'
import { SafeApp } from './types.d'
type MessageHandler = (
msg: SDKMessageEvent,
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
class AppCommunicator {
private iframe: HTMLIFrameElement
private handlers = new Map<Methods, MessageHandler>()
private app: SafeApp
constructor(iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) {
this.iframe = iframeRef.current
this.app = app
window.addEventListener('message', this.handleIncomingMessage)
}
on = (method: Methods, handler: MessageHandler): void => {
this.handlers.set(method, handler)
}
private isValidMessage = (msg: SDKMessageEvent): boolean => {
// @ts-expect-error .parent doesn't exist on some possible types
const sentFromIframe = msg.source.parent === window.parent
const knownOrigin = this.app.url.includes(msg.origin)
const knownMethod = Object.values(METHODS).includes(msg.data.method)
return knownOrigin && sentFromIframe && knownMethod
}
private canHandleMessage = (msg: SDKMessageEvent): boolean => {
return Boolean(this.handlers.get(msg.data.method))
}
send = (data, requestId, error = false): void => {
const sdkVersion = getSDKVersion()
const msg = error
? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion)
: MessageFormatter.makeResponse(requestId, data, sdkVersion)
this.iframe.contentWindow?.postMessage(msg, this.app.url)
}
handleIncomingMessage = async (msg: SDKMessageEvent): Promise<void> => {
const validMessage = this.isValidMessage(msg)
const hasHandler = this.canHandleMessage(msg)
if (validMessage && hasHandler) {
const handler = this.handlers.get(msg.data.method)
try {
// @ts-expect-error Handler existence is checked in this.canHandleMessage
const response = await handler(msg)
// If response is not returned, it means the response will be send somewhere else
if (typeof response !== 'undefined') {
this.send(response, msg.data.id)
}
} catch (err) {
console.log({ err })
this.send(err.message, msg.data.id, true)
}
}
}
clear = (): void => {
window.removeEventListener('message', this.handleIncomingMessage)
}
}
const useAppCommunicator = (
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
app?: SafeApp,
): AppCommunicator | undefined => {
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
useEffect(() => {
let communicatorInstance
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
communicatorInstance = new AppCommunicator(iframeRef, app)
setCommunicator(communicatorInstance)
}
if (app && iframeRef.current !== null) {
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>, app)
}
return () => {
communicatorInstance?.clear()
}
}, [app, iframeRef])
return communicator
}
export { useAppCommunicator }

View File

@ -11,15 +11,10 @@ import {
Menu,
ButtonLink,
} from '@gnosis.pm/safe-react-components'
import { MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk'
import { useHistory, useRouteMatch } from 'react-router-dom'
import { useSelector } from 'react-redux'
import {
INTERFACE_MESSAGES,
Transaction,
RequestId,
LowercaseNetworks,
SendTransactionParams,
} from '@gnosis.pm/safe-apps-sdk'
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
import {
safeEthBalanceSelector,
@ -27,12 +22,15 @@ import {
safeNameSelector,
} from 'src/logic/safe/store/selectors'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { getNetworkName } from 'src/config'
import { getNetworkName, getTxServiceUrl } from 'src/config'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { isSameURL } from 'src/utils/url'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { staticAppsList } from 'src/routes/safe/components/Apps/utils'
import { LoadingContainer } from 'src/components/LoaderContainer/index'
import { TIMEOUT } from 'src/utils/constants'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import ConfirmTransactionModal from '../components/ConfirmTransactionModal'
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
@ -40,8 +38,7 @@ import { useLegalConsent } from '../hooks/useLegalConsent'
import LegalDisclaimer from './LegalDisclaimer'
import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils'
import { SafeApp, StoredSafeApp } from '../types.d'
import { LoadingContainer } from 'src/components/LoaderContainer/index'
import { TIMEOUT } from 'src/utils/constants'
import { useAppCommunicator } from '../communicator'
const OwnerDisclaimer = styled.div`
display: flex;
@ -72,11 +69,15 @@ const Breadcrumb = styled.div`
height: 51px;
`
export type TransactionParams = {
safeTxGas?: number
}
type ConfirmTransactionModalState = {
isOpen: boolean
txs: Transaction[]
requestId?: RequestId
params?: SendTransactionParams
params?: TransactionParams
}
type Props = {
@ -132,7 +133,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
}, [appIsLoading])
const openConfirmationModal = useCallback(
(txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) =>
(txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) =>
setConfirmTransactionModal({
isOpen: true,
txs,
@ -169,18 +170,78 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
})
}, [ethBalance, safeAddress, appUrl, sendMessageToIframe])
const communicator = useAppCommunicator(iframeRef, safeApp)
useEffect(() => {
communicator?.on('getEnvInfo', () => ({
txServiceUrl: getTxServiceUrl(),
}))
communicator?.on('getSafeInfo', () => ({
safeAddress,
network: NETWORK_NAME,
}))
communicator?.on('rpcCall', async (msg) => {
const params = msg.data.params as RPCPayload
try {
const response = new Promise<MethodToResponse['rpcCall']>((resolve, reject) => {
if (
web3ReadOnly.currentProvider !== null &&
typeof web3ReadOnly.currentProvider !== 'string' &&
'send' in web3ReadOnly.currentProvider
) {
web3ReadOnly.currentProvider?.send?.(
{
jsonrpc: '2.0',
method: params.call,
params: params.params,
id: '1',
},
(err, res) => {
if (err || res?.error) {
reject(err || res?.error)
}
resolve(res?.result)
},
)
}
})
return response
} catch (err) {
return err
}
})
communicator?.on('sendTransactions', (msg) => {
// @ts-expect-error explore ways to fix this
openConfirmationModal(msg.data.params.txs as Transaction[], msg.data.params.params, msg.data.id)
})
}, [communicator, openConfirmationModal, safeAddress])
const onUserTxConfirm = (safeTxHash: string) => {
// Safe Apps SDK V1 Handler
sendMessageToIframe(
{ messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } },
confirmTransactionModal.requestId,
)
// Safe Apps SDK V2 Handler
communicator?.send({ safeTxHash }, confirmTransactionModal.requestId)
}
const onTxReject = () => {
// Safe Apps SDK V1 Handler
sendMessageToIframe(
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
confirmTransactionModal.requestId,
)
// Safe Apps SDK V2 Handler
communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true)
}
const openRemoveModal = () => setIsRemoveModalOpen(true)

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
import { Transaction, SendTransactionParams } from '@gnosis.pm/safe-apps-sdk'
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
import styled from 'styled-components'
import { useDispatch } from 'react-redux'
@ -24,6 +24,7 @@ import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
import GasEstimationInfo from './GasEstimationInfo'
import { getNetworkInfo } from 'src/config'
import { TransactionParams } from './AppFrame'
const isTxValid = (t: Transaction): boolean => {
if (!['string', 'number'].includes(typeof t.value)) {
@ -70,7 +71,7 @@ type OwnProps = {
isOpen: boolean
app: SafeApp
txs: Transaction[]
params?: SendTransactionParams
params?: TransactionParams
safeAddress: string
safeName: string
ethBalance: string

View File

@ -9,8 +9,7 @@ import {
RequestId,
Transaction,
LowercaseNetworks,
SendTransactionParams,
} from '@gnosis.pm/safe-apps-sdk'
} from '@gnosis.pm/safe-apps-sdk-v1'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useCallback, MutableRefObject } from 'react'
import { getNetworkName, getTxServiceUrl } from 'src/config/'
@ -19,7 +18,7 @@ import {
safeNameSelector,
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { TransactionParams } from '../components/AppFrame'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
type InterfaceMessageProps<T extends InterfaceMessageIds> = {
@ -31,19 +30,11 @@ type ReturnType = {
sendMessageToIframe: <T extends InterfaceMessageIds>(message: InterfaceMessageProps<T>, requestId?: RequestId) => void
}
interface CustomMessageEvent extends MessageEvent {
data: {
requestId: RequestId
messageId: SDKMessageIds
data: SDKMessageToPayload[SDKMessageIds]
}
}
const NETWORK_NAME = getNetworkName()
const useIframeMessageHandler = (
selectedApp: SafeApp | undefined,
openConfirmationModal: (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => void,
openConfirmationModal: (txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) => void,
closeModal: () => void,
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
): ReturnType => {
@ -58,6 +49,7 @@ const useIframeMessageHandler = (
const requestWithMessage = {
...message,
requestId: requestId || Math.trunc(window.performance.now()),
version: '0.4.2',
}
if (iframeRef && selectedApp) {
@ -93,44 +85,6 @@ const useIframeMessageHandler = (
break
}
case SDK_MESSAGES.SEND_TRANSACTIONS_V2: {
const payload = messagePayload as SDKMessageToPayload[typeof SDK_MESSAGES.SEND_TRANSACTIONS_V2]
if (payload) {
openConfirmationModal(payload.txs, payload.params, requestId)
}
break
}
case SDK_MESSAGES.RPC_CALL: {
const payload = messagePayload as SDKMessageToPayload['RPC_CALL']
if (
web3ReadOnly.currentProvider !== null &&
typeof web3ReadOnly.currentProvider !== 'string' &&
'send' in web3ReadOnly.currentProvider
) {
web3ReadOnly.currentProvider?.send?.(
{
jsonrpc: '2.0',
method: payload?.call,
params: payload?.params,
id: '1',
},
(err, res) => {
if (!err) {
const rpcCallMsg = {
messageId: INTERFACE_MESSAGES.RPC_CALL_RESPONSE,
data: res,
}
sendMessageToIframe(rpcCallMsg, requestId)
}
},
)
}
break
}
case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: {
const safeInfoMessage = {
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
@ -157,7 +111,13 @@ const useIframeMessageHandler = (
}
}
}
const onIframeMessage = async (message: CustomMessageEvent) => {
const onIframeMessage = async (
message: MessageEvent<{
requestId: RequestId
messageId: SDKMessageIds
data: SDKMessageToPayload[SDKMessageIds]
}>,
) => {
if (message.origin === window.origin) {
return
}

View File

@ -1,10 +1,10 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import AppFrame from './components/AppFrame'
import AppsList from './components/AppsList'
import { useLocation } from 'react-router-dom'
const useQuery = () => {
return new URLSearchParams(useLocation().search)
}

View File

@ -32,7 +32,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
},
// Aave
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX1NUtvm9WjbvT79sTdeg3sw1NxZAM273y44nBy5d2jZb`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQ3w2ezp2zx3u2LYQHyuNzMrLDJFjyL1rjAFTjNMcQ4cK`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET],
},
@ -95,7 +95,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
},
// TX-Builder
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmXdrr9hRbXSaqMb71iKnEp66PwwsAbJDR9XdwByUYSTxB`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmYES1Se6i6679z3PfQ62bydgVVEoSRUabvjB35DfUGPGA`,
disabled: false,
networks: [
ETHEREUM_NETWORK.MAINNET,

View File

@ -7,7 +7,7 @@ import SettingsDescription from './SettingsDescription'
import CustomDescription from './CustomDescription'
import TransferDescription from './TransferDescription'
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import { getRawTxAmount, getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import Block from 'src/components/layout/Block'
import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
@ -33,8 +33,9 @@ const UpgradeDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement =
const TransferDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => {
const amountWithSymbol = getTxAmount(tx, false)
const { recipient, isTokenTransfer = false } = getTxData(tx)
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer }} />
const rawAmount = getRawTxAmount(tx)
const { recipient, isTokenTransfer = false, tokenAddress } = getTxData(tx)
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer, rawAmount, tokenAddress }} />
}
const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {

View File

@ -3,9 +3,7 @@ import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
import NoSafe from 'src/components/NoSafe'
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { FEATURES } from 'src/config/networks/network.d'
@ -35,14 +33,8 @@ const Container = (): React.ReactElement => {
onClose: () => {},
})
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const provider = useSelector(providerNameSelector)
const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
if (!safeAddress) {
return <NoSafe provider={provider} text="Safe not found" />
}
if (!featuresEnabled) {
return (
<LoadingContainer>

View File

@ -1,25 +0,0 @@
@import "src/theme/variables.scss";
.safe {
justify-content: center;
justify-items: center;
margin-top: $xl;
}
.summary {
display: flex;
justify-content: space-around;
}
.safeActions {
display: flex;
justify-content: center;
}
.learnMoreLink {
color: $secondary;
}
.connectWallet {
text-align: center;
}

View File

@ -1,118 +1,189 @@
import OpenInNew from '@material-ui/icons/OpenInNew'
import React from 'react'
import styled from 'styled-components'
import {
Card,
Button,
Title,
Text,
Divider,
ButtonLink,
Dot,
Icon,
Link as LinkSRC,
} from '@gnosis.pm/safe-react-components'
import styles from './Layout.module.scss'
import ConnectButton from 'src/components/ConnectButton'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Heading from 'src/components/layout/Heading'
import Img from 'src/components/layout/Img'
import Link from 'src/components/layout/Link'
import Block from 'src/components/layout/Block'
import { LOAD_ADDRESS, OPEN_ADDRESS } from 'src/routes/routes'
import { marginButtonImg, secondary } from 'src/theme/variables'
import { onConnectButtonClick } from 'src/components/ConnectButton'
import PlusIcon from '../assets/new.svg'
import SafeIcon from '../assets/safe.svg'
const Wrapper = styled.div`
display: flex;
flex-direction: row;
margin: 24px 0 0 0;
`
const StyledCardDouble = styled(Card)`
display: flex;
padding: 0;
`
const StyledCard = styled(Card)`
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 20px 0 0;
max-width: 27%;
height: 276px;
`
const CardsCol = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 24px;
width: 50%;
`
const StyledButton = styled(Button)`
margin-top: auto;
text-decoration: none;
`
const TitleWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
margin: 0 0 16px 0;
const openIconStyle = {
height: '13px',
color: secondary,
marginBottom: '-2px',
h5 {
color: white;
}
`
const StyledTitle = styled(Title)`
margin: 0 0 0 16px;
`
const StyledTitleOnly = styled(Title)`
margin: 0 0 16px 0;
`
const StyledButtonLink = styled(ButtonLink)`
margin: 16px 0 16px -8px;
`
type Props = {
isOldMultisigMigration?: boolean
provider: any
}
const buttonStyle = {
marginLeft: marginButtonImg,
}
export const CreateSafe = ({ provider, size }: any) => (
<Button
color="primary"
component={Link}
disabled={!provider}
minHeight={42}
minWidth={240}
size={size || 'medium'}
to={OPEN_ADDRESS}
variant="contained"
testId="create-new-safe-btn"
>
<Img alt="Safe" height={14} src={PlusIcon} />
<div style={buttonStyle}>Create new Safe</div>
</Button>
)
export const LoadSafe = ({ provider, size }) => (
<Button
color="primary"
component={Link}
disabled={!provider}
minWidth={240}
size={size || 'medium'}
to={LOAD_ADDRESS}
variant="outlined"
testId="load-existing-safe-btn"
>
<Img alt="Safe" height={14} src={SafeIcon} />
<div style={buttonStyle}>Load existing Safe</div>
</Button>
)
const Welcome = ({ isOldMultisigMigration, provider }: any) => {
const headingText = isOldMultisigMigration ? (
<>
We will replicate the owner structure from your existing Gnosis MultiSig
<br />
to let you test the new interface.
<br />
As soon as you feel comfortable, start moving funds to your new Safe.
<br />{' '}
</>
) : (
<>
Gnosis Safe Multisig is the most secure way to manage crypto funds
<br />
collectively. It is an improvement of the Gnosis MultiSig, which is used by more than 3000 teams
<br /> and stores over $1B USD worth of digital assets. Gnosis Safe Multisig features a modular
<br /> design, formally verified smart contracts and vastly improved user experience.{' '}
</>
)
const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElement => {
return (
<Block className={styles.safe}>
<Heading align="center" margin="lg" tag="h1" weight="bold">
Welcome to
<br />
Gnosis Safe Multisig
</Heading>
<Heading align="center" margin="xl" tag="h3">
{headingText}
<a
className={styles.learnMoreLink}
href="https://gnosis-safe.io/teams"
rel="noopener noreferrer"
target="_blank"
>
Learn more
<OpenInNew style={openIconStyle} />
</a>
</Heading>
{provider ? (
<>
<Block className={styles.safeActions} margin="md">
<CreateSafe provider={provider} size="large" />
</Block>
<Block className={styles.safeActions} margin="md">
<LoadSafe provider={provider} size="large" />
</Block>
</>
) : (
<Block className={styles.connectWallet} margin="md">
<Heading align="center" margin="md" tag="h3">
Get Started by Connecting a Wallet
</Heading>
<ConnectButton minHeight={42} minWidth={240} data-testid="connect-btn" />
</Block>
)}
<Block>
{/* Title */}
<Title size="md" strong>
Welcome to Gnosis Safe Multisig.
</Title>
{/* Subtitle */}
<Title size="xs">
{isOldMultisigMigration ? (
<>
We will replicate the owner structure from your existing Gnosis MultiSig to let you test the new interface.
As soon as you feel comfortable, start moving funds to your new Safe.
</>
) : (
<>
Gnosis Safe Multisig is the most trusted platform to manage digital assets. <br /> Here is how to get
started:{' '}
</>
)}
</Title>
<>
<Wrapper>
{/* Connect wallet */}
<StyledCard>
<TitleWrapper>
<Dot color="primary">
{!provider ? <Title size="xs">1</Title> : <Icon color="white" type="check" size="md" />}
</Dot>
<StyledTitle size="sm" strong withoutMargin>
Connect wallet
</StyledTitle>
</TitleWrapper>
<Text size="xl">
Gnosis Safe Multisig supports a wide range of wallets that you can choose to be one of the authentication
factors.
</Text>
<StyledButtonLink textSize="xl" color="primary" iconType="externalLink" iconSize="sm">
<LinkSRC
size="xl"
href="https://help.gnosis-safe.io/en/articles/4689442-why-do-i-need-to-connect-a-wallet"
target="_blank"
rel="noopener noreferrer"
title="More info about: Why do I need to connect wallet?"
>
Why do I need to connect wallet?
</LinkSRC>
</StyledButtonLink>
<StyledButton
size="lg"
color="primary"
variant="contained"
onClick={onConnectButtonClick}
disabled={provider}
data-testid="connect-btn"
>
<Text size="xl" color="white">
Connect wallet
</Text>
</StyledButton>
</StyledCard>
<StyledCardDouble disabled={!provider}>
{/* Create safe */}
<CardsCol>
<TitleWrapper>
<Dot color="primary">
<Title size="xs">2</Title>
</Dot>
<StyledTitle size="sm" strong withoutMargin>
Create Safe
</StyledTitle>
</TitleWrapper>
<Text size="xl">
Create a new Safe Multisig that is controlled by one or multiple owners. <br />
You will be required to pay a network fee for creating your new Safe.
</Text>
<StyledButton size="lg" color="primary" variant="contained" component={Link} to={OPEN_ADDRESS}>
<Text size="xl" color="white">
+ Create new Safe
</Text>
</StyledButton>
</CardsCol>
<Divider orientation="vertical" />
{/* Load safe */}
<CardsCol>
<StyledTitleOnly size="sm" strong withoutMargin>
Load existing Safe
</StyledTitleOnly>
<Text size="xl">
Already have a Safe? Do you want to access your Safe Multisig from a different device? Easily load your
Safe Multisig using your Safe address.
</Text>
<StyledButton
variant="bordered"
iconType="safe"
iconSize="sm"
size="lg"
color="secondary"
component={Link}
to={LOAD_ADDRESS}
>
<Text size="xl" color="secondary">
Load existing Safe
</Text>
</StyledButton>
</CardsCol>
</StyledCardDouble>
</Wrapper>
</>
</Block>
)
}

View File

@ -1,10 +1,10 @@
//
function useTestAccountAt(index = 0) {
(window as any).testAccountIndex = index
window.testAccountIndex = index
}
function resetTestAccount() {
delete (window as any).testAccountIndex
delete window.testAccountIndex
}
export { useTestAccountAt, resetTestAccount }

View File

@ -1,8 +1,11 @@
import Web3 from 'web3'
export {}
declare global {
interface Window {
web3?: Web3
testAccountIndex?: string
ethereum?: {
autoRefreshOnNetworkChange: boolean
isMetaMask: boolean
}
testAccountIndex?: string | number
}
}
declare module '@openzeppelin/contracts/build/contracts/ERC721'

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import GoogleAnalytics, { EventArgs } from 'react-ga'
import ReactGA, { EventArgs } from 'react-ga'
import { getNetworkInfo } from 'src/config'
import { getGoogleAnalyticsTrackingID } from 'src/config'
@ -20,8 +20,8 @@ export const loadGoogleAnalytics = (): void => {
if (!trackingID) {
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
} else {
GoogleAnalytics.initialize(trackingID)
GoogleAnalytics.set({
ReactGA.initialize(trackingID)
ReactGA.set({
anonymizeIp: true,
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
@ -50,22 +50,19 @@ export const useAnalytics = (): UseAnalyticsResponse => {
fetchCookiesFromStorage()
}, [])
const trackPage = useCallback(
(page) => {
if (!analyticsAllowed || !analyticsLoaded) {
return
}
GoogleAnalytics.pageview(page)
},
[analyticsAllowed],
)
const trackPage = (page) => {
if (!analyticsAllowed || !analyticsLoaded) {
return
}
ReactGA.pageview(page)
}
const trackEvent = useCallback(
(event: EventArgs) => {
if (!analyticsAllowed || !analyticsLoaded) {
return
}
GoogleAnalytics.event(event)
ReactGA.event(event)
},
[analyticsAllowed],
)

6
src/utils/mm_warnings.ts Normal file
View File

@ -0,0 +1,6 @@
// https://docs.metamask.io/guide/ethereum-provider.html#ethereum-autorefreshonnetworkchange
export const disableMMAutoRefreshWarning = (): void => {
if (window.ethereum && window.ethereum.isMetaMask) {
window.ethereum.autoRefreshOnNetworkChange = false
}
}

2310
yarn.lock

File diff suppressed because it is too large Load Diff