Merge branch 'development' of github.com:gnosis/safe-react into development
This commit is contained in:
commit
0814eb528e
|
@ -12,3 +12,4 @@ yalc.lock
|
|||
# testing
|
||||
/coverage/
|
||||
src/types/contracts/
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
17
package.json
17
package.json
|
@ -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",
|
||||
|
|
|
@ -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) : ''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { withStateHandlers } from 'recompose'
|
||||
|
||||
export default withStateHandlers(() => ({ open: false }), {
|
||||
toggle: ({ open }) => () => ({ open: !open }),
|
||||
clickAway: () => () => ({ open: false }),
|
||||
})
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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] })
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -150,7 +150,6 @@ const Open = (): React.ReactElement => {
|
|||
ReactGA.event({
|
||||
category: 'User',
|
||||
action: 'Created a safe',
|
||||
value: safeAddress,
|
||||
})
|
||||
|
||||
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue