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
|
# testing
|
||||||
/coverage/
|
/coverage/
|
||||||
src/types/contracts/
|
src/types/contracts/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
|
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
|
||||||
sudo: required
|
dist: focal
|
||||||
dist: bionic
|
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- '12'
|
- '12'
|
||||||
|
@ -51,7 +50,7 @@ before_script:
|
||||||
before_install:
|
before_install:
|
||||||
# Needed to deploy pull request and releases
|
# Needed to deploy pull request and releases
|
||||||
- sudo apt-get update
|
- 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
|
- pip install awscli --upgrade --user
|
||||||
script:
|
script:
|
||||||
- yarn lint:check
|
- yarn lint:check
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Web3 from 'web3'
|
import Web3 from 'web3'
|
||||||
|
|
||||||
const window = global.window || {}
|
const window = global.window || {}
|
||||||
window.web3 = window.web3 || {}
|
window.web3 = {}
|
||||||
window.web3.currentProvider = new Web3.providers.HttpProvider('http://localhost:8545')
|
window.web3.currentProvider = new Web3.providers.HttpProvider('http://localhost:8545')
|
||||||
|
|
||||||
global.window = window
|
global.window = window
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "2.16.2",
|
"version": "2.17.0",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"pre-commit": "lint-staged --allow-empty",
|
"pre-commit": "lint-staged --allow-empty",
|
||||||
"pre-push": "tsc"
|
"pre-push": "tsc --noEmit --incremental"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
@ -149,6 +149,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
"@babel/core": "^7.12.0",
|
||||||
"sass-loader": "^9.0.0"
|
"sass-loader": "^9.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
@ -164,9 +165,10 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-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",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.34.0",
|
"@ledgerhq/hw-transport-node-hid-singleton": "5.34.0",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
|
@ -212,14 +214,13 @@
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-final-form": "^6.5.2",
|
"react-final-form": "^6.5.2",
|
||||||
"react-final-form-listeners": "^1.0.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-hot-loader": "4.13.0",
|
||||||
"react-qr-reader": "^2.2.1",
|
"react-qr-reader": "^2.2.1",
|
||||||
"react-redux": "7.2.2",
|
"react-redux": "7.2.2",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-scripts": "^4.0.1",
|
"react-scripts": "^4.0.1",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"recompose": "^0.30.0",
|
|
||||||
"redux": "4.0.5",
|
"redux": "4.0.5",
|
||||||
"redux-actions": "^2.6.5",
|
"redux-actions": "^2.6.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
|
@ -232,7 +233,7 @@
|
||||||
"web3-utils": "^1.2.11"
|
"web3-utils": "^1.2.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rescripts/cli": "^0.0.14",
|
"@rescripts/cli": "^0.0.15",
|
||||||
"@sentry/cli": "^1.59.0",
|
"@sentry/cli": "^1.59.0",
|
||||||
"@storybook/addon-actions": "^5.3.19",
|
"@storybook/addon-actions": "^5.3.19",
|
||||||
"@storybook/addon-links": "^5.3.19",
|
"@storybook/addon-links": "^5.3.19",
|
||||||
|
@ -241,7 +242,7 @@
|
||||||
"@storybook/react": "^5.3.19",
|
"@storybook/react": "^5.3.19",
|
||||||
"@testing-library/jest-dom": "5.11.6",
|
"@testing-library/jest-dom": "5.11.6",
|
||||||
"@testing-library/react": "11.2.2",
|
"@testing-library/react": "11.2.2",
|
||||||
"@typechain/web3-v1": "^1.0.0",
|
"@typechain/web3-v1": "^2.0.0",
|
||||||
"@types/history": "4.6.2",
|
"@types/history": "4.6.2",
|
||||||
"@types/jest": "^26.0.16",
|
"@types/jest": "^26.0.16",
|
||||||
"@types/lodash.memoize": "^4.1.6",
|
"@types/lodash.memoize": "^4.1.6",
|
||||||
|
|
|
@ -80,8 +80,8 @@ const App: React.FC = ({ children }) => {
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const sidebarItems = useSidebarItems()
|
const sidebarItems = useSidebarItems()
|
||||||
|
|
||||||
useLoadSafe(safeAddress)
|
const safeLoaded = useLoadSafe(safeAddress)
|
||||||
useSafeScheduledUpdates(safeAddress)
|
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
||||||
|
|
||||||
const sendFunds = safeActionsState.sendFunds
|
const sendFunds = safeActionsState.sendFunds
|
||||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { Link } from 'react-router-dom'
|
||||||
import Provider from './Provider'
|
import Provider from './Provider'
|
||||||
|
|
||||||
import Spacer from 'src/components/Spacer'
|
import Spacer from 'src/components/Spacer'
|
||||||
import openHoc from 'src/components/hoc/OpenHoc'
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
|
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'
|
import SafeLogo from '../assets/gnosis-safe-multisig-logo.svg'
|
||||||
|
|
||||||
|
@ -55,41 +55,45 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInfo, toggle }) => (
|
const Layout = ({ classes, providerDetails, providerInfo }) => {
|
||||||
<Row className={classes.summary}>
|
const { clickAway, open, toggle } = useStateHandler()
|
||||||
<Col className={classes.logo} middle="xs" start="xs">
|
|
||||||
<Link to="/">
|
return (
|
||||||
<Img alt="Gnosis Team Safe" height={36} src={SafeLogo} testId="heading-gnosis-logo" />
|
<Row className={classes.summary}>
|
||||||
</Link>
|
<Col className={classes.logo} middle="xs" start="xs">
|
||||||
</Col>
|
<Link to="/">
|
||||||
<Spacer />
|
<Img alt="Gnosis Team Safe" height={36} src={SafeLogo} testId="heading-gnosis-logo" />
|
||||||
<Provider
|
</Link>
|
||||||
info={providerInfo}
|
</Col>
|
||||||
open={open}
|
<Spacer />
|
||||||
toggle={toggle}
|
<Provider
|
||||||
render={(providerRef) => (
|
info={providerInfo}
|
||||||
<Popper
|
open={open}
|
||||||
anchorEl={providerRef.current}
|
toggle={toggle}
|
||||||
className={classes.popper}
|
render={(providerRef) => (
|
||||||
open={open}
|
<Popper
|
||||||
placement="bottom"
|
anchorEl={providerRef.current}
|
||||||
popperOptions={{ positionFixed: true }}
|
className={classes.popper}
|
||||||
>
|
open={open}
|
||||||
{({ TransitionProps }) => (
|
placement="bottom"
|
||||||
<Grow {...TransitionProps}>
|
popperOptions={{ positionFixed: true }}
|
||||||
<>
|
>
|
||||||
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
|
{({ TransitionProps }) => (
|
||||||
<List className={classes.root} component="div">
|
<Grow {...TransitionProps}>
|
||||||
{providerDetails}
|
<>
|
||||||
</List>
|
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
|
||||||
</ClickAwayListener>
|
<List className={classes.root} component="div">
|
||||||
</>
|
{providerDetails}
|
||||||
</Grow>
|
</List>
|
||||||
)}
|
</ClickAwayListener>
|
||||||
</Popper>
|
</>
|
||||||
)}
|
</Grow>
|
||||||
/>
|
)}
|
||||||
</Row>
|
</Popper>
|
||||||
))
|
)}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(Layout)
|
export default withStyles(styles as any)(Layout)
|
||||||
|
|
|
@ -44,6 +44,7 @@ const IconContainer = styled.div`
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
`
|
`
|
||||||
const StyledButton = styled(Button)`
|
const StyledButton = styled(Button)`
|
||||||
|
padding: 0 18px;
|
||||||
*:first-child {
|
*:first-child {
|
||||||
margin: 0 4px 0 0;
|
margin: 0 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,21 +63,17 @@ export const onboardUser = async (): Promise<boolean> => {
|
||||||
return walletSelected && onboard.walletCheck()
|
return walletSelected && onboard.walletCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectButton = (props): React.ReactElement => (
|
export const onConnectButtonClick = async () => {
|
||||||
<Button
|
const walletSelected = await onboard.walletSelect()
|
||||||
color="primary"
|
|
||||||
minWidth={140}
|
|
||||||
onClick={async () => {
|
|
||||||
const walletSelected = await onboard.walletSelect()
|
|
||||||
|
|
||||||
// perform wallet checks only if user selected a wallet
|
// perform wallet checks only if user selected a wallet
|
||||||
if (walletSelected) {
|
if (walletSelected) {
|
||||||
await onboard.walletCheck()
|
await onboard.walletCheck()
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
variant="contained"
|
|
||||||
{...props}
|
const ConnectButton = (props): React.ReactElement => (
|
||||||
>
|
<Button color="primary" minWidth={140} onClick={onConnectButtonClick} variant="contained" {...props}>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</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', () => {
|
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']
|
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
|
||||||
|
|
||||||
expect(uniqueAddress(addresses)()).toBeUndefined()
|
expect(uniqueAddress(addresses)('0x2D6F2B448b0F711Eb81f2929566504117d67E44F')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Returns an error message for an array with duplicated values', async () => {
|
it('Returns an error message if address is in the `addresses` list already', async () => {
|
||||||
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
|
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0x2D6F2B448b0F711Eb81f2929566504117d67E44F']
|
||||||
|
|
||||||
expect(uniqueAddress(addresses)()).toEqual(ADDRESS_REPEATED_ERROR)
|
expect(uniqueAddress(addresses)('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { List } from 'immutable'
|
||||||
import memoize from 'lodash.memoize'
|
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 { isFeatureEnabled } from 'src/config'
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
type ValidatorReturnType = string | undefined
|
type ValidatorReturnType = string | undefined
|
||||||
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
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 ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||||
|
|
||||||
export const uniqueAddress = (addresses: string[] | List<string>): GenericValidatorType => (): ValidatorReturnType => {
|
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
|
||||||
// @ts-expect-error both list and array have signatures for map but TS thinks they're not compatible
|
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
||||||
const lowercaseAddresses = addresses.map((address) => address.toLowerCase())
|
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||||
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 composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>
|
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;
|
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 {
|
.center {
|
||||||
align-self: 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 loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
|
||||||
import { store } from 'src/store'
|
import { store } from 'src/store'
|
||||||
import { SENTRY_DSN } from './utils/constants'
|
import { SENTRY_DSN } from './utils/constants'
|
||||||
|
import { disableMMAutoRefreshWarning } from './utils/mm_warnings'
|
||||||
|
|
||||||
|
disableMMAutoRefreshWarning()
|
||||||
|
|
||||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
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 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)
|
return addressBook.map((entry) => entry.address)
|
||||||
})
|
}
|
||||||
|
|
||||||
export const getNameFromAddressBookSelector = createSelector(
|
export const getNameFromAddressBookSelector = createSelector(
|
||||||
addressBookSelector,
|
addressBookSelector,
|
||||||
|
|
|
@ -64,5 +64,5 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
|
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) {
|
if (type !== undefined) {
|
||||||
request = web3[type][method].request(...args, resolver)
|
request = web3[type][method].request(...args, resolver)
|
||||||
} else {
|
} else {
|
||||||
|
if (address === null) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
request = contractInstance.methods[method](...args).call.request(resolver)
|
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 { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
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 fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
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 dispatch = useDispatch<Dispatch>()
|
||||||
|
const [isSafeLoaded, setIsSafeLoaded] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = () => {
|
const fetchData = async () => {
|
||||||
if (safeAddress) {
|
if (safeAddress) {
|
||||||
dispatch(fetchLatestMasterContractVersion())
|
await dispatch(fetchLatestMasterContractVersion())
|
||||||
.then(() => {
|
await dispatch(fetchSafe(safeAddress))
|
||||||
dispatch(fetchSafe(safeAddress))
|
setIsSafeLoaded(true)
|
||||||
return dispatch(fetchSafeTokens(safeAddress))
|
await dispatch(fetchSafeTokens(safeAddress))
|
||||||
})
|
dispatch(fetchSafeCreationTx(safeAddress))
|
||||||
.then(() => {
|
dispatch(fetchTransactions(safeAddress))
|
||||||
dispatch(fetchSafeCreationTx(safeAddress))
|
dispatch(addViewedSafe(safeAddress))
|
||||||
dispatch(fetchTransactions(safeAddress))
|
|
||||||
return dispatch(addViewedSafe(safeAddress))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatch(loadAddressBookFromStorage())
|
dispatch(loadAddressBookFromStorage())
|
||||||
|
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [dispatch, safeAddress])
|
}, [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 fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
|
|
||||||
export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: string): void => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const timer = useRef<number>()
|
const timer = useRef<number>()
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safeAddress) {
|
if (safeAddress && safeLoaded) {
|
||||||
fetchSafeData(safeAddress)
|
fetchSafeData(safeAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,5 +42,5 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
||||||
mounted = false
|
mounted = false
|
||||||
clearTimeout(timer.current)
|
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 CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||||
type ConfirmEventHandler = (safeTxHash: string) => void
|
type ConfirmEventHandler = (safeTxHash: string) => void
|
||||||
type ErrorEventHandler = () => void
|
type ErrorEventHandler = () => void
|
||||||
|
export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001
|
||||||
|
|
||||||
const createTransaction = (
|
const createTransaction = (
|
||||||
{
|
{
|
||||||
|
@ -210,20 +211,21 @@ const createTransaction = (
|
||||||
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
||||||
: notificationsQueue.afterExecutionError.message
|
: notificationsQueue.afterExecutionError.message
|
||||||
|
|
||||||
console.error(`Error creating the TX: `, err)
|
|
||||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||||
|
|
||||||
if (pendingExecutionKey) {
|
if (pendingExecutionKey) {
|
||||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(enqueueSnackbar(errorMsg))
|
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
|
||||||
|
|
||||||
const executeDataUsedSignatures = safeInstance.methods
|
if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
|
||||||
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
const executeDataUsedSignatures = safeInstance.methods
|
||||||
.encodeABI()
|
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
||||||
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
|
.encodeABI()
|
||||||
console.error(`Error creating the TX - an attempt to get the error message: ${errMsg}`)
|
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
|
return txHash
|
||||||
|
|
|
@ -97,7 +97,7 @@ const processTransaction = ({
|
||||||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||||
|
|
||||||
if (signature) {
|
if (signature) {
|
||||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||||
|
|
||||||
await saveTxToHistory({ ...txArgs, signature })
|
await saveTxToHistory({ ...txArgs, signature })
|
||||||
// TODO: while we wait for the tx to be stored in the service and later update the tx info
|
// 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)
|
.send(sendParams)
|
||||||
.once('transactionHash', async (hash: string) => {
|
.once('transactionHash', async (hash: string) => {
|
||||||
txHash = hash
|
txHash = hash
|
||||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||||
|
|
||||||
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
||||||
|
|
||||||
|
@ -141,19 +141,19 @@ const processTransaction = ({
|
||||||
])
|
])
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||||
await storeTx({ transaction: tx, safeAddress, dispatch, state })
|
await storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||||
storeTx({ transaction: tx, safeAddress, dispatch, state })
|
storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||||
console.error('Processing transaction error: ', error)
|
console.error('Processing transaction error: ', error)
|
||||||
})
|
})
|
||||||
.then(async (receipt) => {
|
.then(async (receipt) => {
|
||||||
if (pendingExecutionKey) {
|
if (pendingExecutionKey) {
|
||||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -178,17 +178,16 @@ const processTransaction = ({
|
||||||
const errorMsg = err.message
|
const errorMsg = err.message
|
||||||
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
||||||
: notificationsQueue.afterExecutionError.message
|
: notificationsQueue.afterExecutionError.message
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
if (txHash !== undefined) {
|
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
|
||||||
|
|
||||||
if (pendingExecutionKey) {
|
if (pendingExecutionKey) {
|
||||||
dispatch(closeSnackbarAction(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 executeData = safeInstance.methods.approveHash(txHash).encodeABI()
|
||||||
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)
|
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)
|
||||||
console.error(`Error executing the TX: ${errMsg}`)
|
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
|
const { location } = state.router
|
||||||
let entryAddressToEditOrCreateNew = null
|
|
||||||
if (location && location.query) {
|
if (location?.query) {
|
||||||
const { entryAddress } = location.query
|
const { entryAddress } = location.query
|
||||||
entryAddressToEditOrCreateNew = entryAddress
|
return entryAddress
|
||||||
}
|
}
|
||||||
return entryAddressToEditOrCreateNew
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const safeCancellationTransactionsSelector = createSelector(
|
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 { AbiItem } from 'web3-utils'
|
||||||
import { MultiSend } from 'src/types/contracts/MultiSend.d'
|
import { MultiSend } from 'src/types/contracts/MultiSend.d'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getEIP712Signer } from './EIP712Signer'
|
import { getEIP712Signer } from './EIP712Signer'
|
||||||
import { ethSigner } from './ethSigner'
|
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
|
// 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)
|
// 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
|
break
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// Metamask sign request error code
|
if (err.code === METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
|
||||||
if (err.code === 4001) {
|
throw err
|
||||||
throw new Error('User denied sign request')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ const httpProviderOptions = {
|
||||||
export const web3ReadOnly = new Web3(
|
export const web3ReadOnly = new Web3(
|
||||||
process.env.NODE_ENV !== 'test'
|
process.env.NODE_ENV !== 'test'
|
||||||
? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
|
? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
|
||||||
: window.web3?.currentProvider || 'ws://localhost:8545',
|
: 'ws://localhost:8545',
|
||||||
)
|
)
|
||||||
|
|
||||||
let web3 = web3ReadOnly
|
let web3 = web3ReadOnly
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import closeSnackbar from 'src/logic/notifications/store/actions/closeSnackbar'
|
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 { fetchProvider } from 'src/logic/wallets/store/actions'
|
||||||
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
|
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
|
||||||
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
|
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
|
||||||
|
@ -29,9 +29,6 @@ const providerWatcherMware = (store) => (next) => async (action) => {
|
||||||
clearInterval(watcherInterval)
|
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)
|
saveToStorage(LAST_USED_PROVIDER_KEY, currentProviderProps.name)
|
||||||
|
|
||||||
watcherInterval = setInterval(async () => {
|
watcherInterval = setInterval(async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
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'
|
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 SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
|
||||||
|
|
||||||
const Routes = ({ location }) => {
|
const Routes = (): React.ReactElement => {
|
||||||
const [isInitialLoad, setInitialLoad] = useState(true)
|
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 defaultSafe = useSelector(defaultSafeSelector)
|
||||||
const { trackPage } = useAnalytics()
|
const { trackPage } = useAnalytics()
|
||||||
|
|
||||||
|
@ -31,9 +36,18 @@ const Routes = ({ location }) => {
|
||||||
}, [location.pathname, isInitialLoad])
|
}, [location.pathname, isInitialLoad])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const page = location.pathname + location.search
|
if (matchSafeWithAction) {
|
||||||
trackPage(page)
|
// prevent logging safeAddress
|
||||||
}, [location.pathname, location.search, trackPage])
|
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 (
|
return (
|
||||||
<Switch>
|
<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({
|
ReactGA.event({
|
||||||
category: 'User',
|
category: 'User',
|
||||||
action: 'Created a safe',
|
action: 'Created a safe',
|
||||||
value: safeAddress,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { useStyles } from './style'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
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 Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
|
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_NAME_ID = 'create-entry-input-name'
|
||||||
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
|
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
|
||||||
export const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id'
|
export const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id'
|
||||||
|
|
||||||
const CreateEditEntryModalComponent = ({
|
const formMutators = {
|
||||||
classes,
|
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,
|
editEntryModalHandler,
|
||||||
entryToEdit,
|
entryToEdit,
|
||||||
isOpen,
|
isOpen,
|
||||||
newEntryModalHandler,
|
newEntryModalHandler,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}: CreateEditEntryModalProps): ReactElement => {
|
||||||
const onFormSubmitted = (values) => {
|
const classes = useStyles()
|
||||||
if (entryToEdit && !entryToEdit.entry.isNew) {
|
|
||||||
editEntryModalHandler(values)
|
const { isNew, ...initialValues } = entryToEdit.entry
|
||||||
} else {
|
|
||||||
|
const onFormSubmitted = (values: AddressBookEntry) => {
|
||||||
|
if (isNew) {
|
||||||
newEntryModalHandler(values)
|
newEntryModalHandler(values)
|
||||||
|
} else {
|
||||||
|
editEntryModalHandler(values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addressBookAddressesList = useSelector(addressBookAddressesListSelector)
|
const storedAddresses = useSelector(addressBookAddressesListSelector)
|
||||||
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
|
const isUniqueAddress = uniqueAddress(storedAddresses)
|
||||||
|
|
||||||
const formMutators = {
|
|
||||||
setOwnerAddress: (args, state, utils) => {
|
|
||||||
utils.changeValue(state, 'address', () => args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
description={entryToEdit ? 'Edit addressBook entry' : 'Create new addressBook entry'}
|
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
paperClassName={classes.smallerModalWindow}
|
paperClassName={classes.smallerModalWindow}
|
||||||
title={entryToEdit ? 'Edit entry' : 'Create new entry'}
|
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||||
>
|
>
|
||||||
<Row align="center" className={classes.heading} grow>
|
<Row align="center" className={classes.heading} grow>
|
||||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||||
{entryToEdit ? 'Edit entry' : 'Create entry'}
|
{isNew ? 'Create entry' : 'Edit entry'}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<IconButton disableRipple onClick={onClose}>
|
<IconButton disableRipple onClick={onClose}>
|
||||||
<Close className={classes.close} />
|
<Close className={classes.close} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
|
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
||||||
{(...args) => {
|
{(...args) => {
|
||||||
|
const formState = args[2]
|
||||||
const mutators = args[3]
|
const mutators = args[3]
|
||||||
const handleScan = (value, closeQrModal) => {
|
const handleScan = (value, closeQrModal) => {
|
||||||
let scannedAddress = value
|
let scannedAddress = value
|
||||||
|
@ -86,13 +99,11 @@ const CreateEditEntryModalComponent = ({
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<Field
|
<Field
|
||||||
className={classes.addressInput}
|
|
||||||
component={TextField}
|
component={TextField}
|
||||||
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
|
|
||||||
name="name"
|
name="name"
|
||||||
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
|
placeholder="Name"
|
||||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||||
text={entryToEdit ? 'Entry*' : 'New entry*'}
|
text="Name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||||
/>
|
/>
|
||||||
|
@ -101,18 +112,16 @@ const CreateEditEntryModalComponent = ({
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<AddressInput
|
<AddressInput
|
||||||
className={classes.addressInput}
|
disabled={!isNew}
|
||||||
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
|
|
||||||
disabled={!!entryToEdit}
|
|
||||||
fieldMutator={mutators.setOwnerAddress}
|
fieldMutator={mutators.setOwnerAddress}
|
||||||
name="address"
|
name="address"
|
||||||
placeholder="Owner address*"
|
placeholder="Address*"
|
||||||
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
||||||
text="Owner address*"
|
text="Address*"
|
||||||
validators={entryToEdit ? undefined : [entryDoesntExist]}
|
validators={[(value?: string) => (isNew ? isUniqueAddress(value) : undefined)]}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
{!entryToEdit ? (
|
{isNew ? (
|
||||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||||
<ScanQRWrapper handleScan={handleScan} />
|
<ScanQRWrapper handleScan={handleScan} />
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -131,8 +140,9 @@ const CreateEditEntryModalComponent = ({
|
||||||
testId={SAVE_NEW_ENTRY_BTN_ID}
|
testId={SAVE_NEW_ENTRY_BTN_ID}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={!formState.valid}
|
||||||
>
|
>
|
||||||
{entryToEdit ? 'Save' : 'Create'}
|
{isNew ? 'Create' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
@ -142,5 +152,3 @@ const CreateEditEntryModalComponent = ({
|
||||||
</Modal>
|
</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'
|
import { lg, md } from 'src/theme/variables'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const useStyles = makeStyles(
|
||||||
heading: {
|
createStyles({
|
||||||
padding: lg,
|
heading: {
|
||||||
justifyContent: 'space-between',
|
padding: lg,
|
||||||
boxSizing: 'border-box',
|
justifyContent: 'space-between',
|
||||||
},
|
boxSizing: 'border-box',
|
||||||
manage: {
|
},
|
||||||
fontSize: lg,
|
manage: {
|
||||||
},
|
fontSize: lg,
|
||||||
container: {
|
},
|
||||||
padding: `${md} ${lg}`,
|
container: {
|
||||||
},
|
padding: `${md} ${lg}`,
|
||||||
close: {
|
},
|
||||||
height: '35px',
|
close: {
|
||||||
width: '35px',
|
height: '35px',
|
||||||
},
|
width: '35px',
|
||||||
buttonRow: {
|
},
|
||||||
height: '84px',
|
buttonRow: {
|
||||||
justifyContent: 'center',
|
height: '84px',
|
||||||
},
|
justifyContent: 'center',
|
||||||
smallerModalWindow: {
|
},
|
||||||
height: 'auto',
|
smallerModalWindow: {
|
||||||
},
|
height: 'auto',
|
||||||
})
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
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 { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses'
|
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
||||||
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
||||||
import {
|
import {
|
||||||
AB_ADDRESS_ID,
|
AB_ADDRESS_ID,
|
||||||
|
@ -47,20 +47,24 @@ interface AddressBookSelectedEntry extends AddressBookEntry {
|
||||||
isNew?: boolean
|
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 classes = useStyles()
|
||||||
const columns = generateColumns()
|
const columns = generateColumns()
|
||||||
const autoColumns = columns.filter((c) => !c.custom)
|
const autoColumns = columns.filter(({ custom }) => !custom)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safesList = useSelector(safesListSelector)
|
const safesList = useSelector(safesListSelector)
|
||||||
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
|
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
|
||||||
const addressBook = useSelector(addressBookSelector)
|
const addressBook = useSelector(addressBookSelector)
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const [selectedEntry, setSelectedEntry] = useState<{
|
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
|
||||||
entry?: AddressBookSelectedEntry
|
|
||||||
index?: number
|
|
||||||
isOwnerAddress?: boolean
|
|
||||||
} | null>(null)
|
|
||||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||||
|
@ -78,8 +82,9 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (entryAddressToEditOrCreateNew) {
|
if (entryAddressToEditOrCreateNew) {
|
||||||
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
|
const address = checksumAddress(entryAddressToEditOrCreateNew)
|
||||||
const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd)
|
const oldEntryIndex = addressBook.findIndex((entry) => sameAddress(entry.address, address))
|
||||||
|
|
||||||
if (oldEntryIndex >= 0) {
|
if (oldEntryIndex >= 0) {
|
||||||
// Edit old entry
|
// Edit old entry
|
||||||
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
|
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
|
||||||
|
@ -88,7 +93,7 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
setSelectedEntry({
|
setSelectedEntry({
|
||||||
entry: {
|
entry: {
|
||||||
name: '',
|
name: '',
|
||||||
address: checksumEntryAdd,
|
address,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -96,7 +101,7 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
}
|
}
|
||||||
}, [addressBook, entryAddressToEditOrCreateNew])
|
}, [addressBook, entryAddressToEditOrCreateNew])
|
||||||
|
|
||||||
const newEntryModalHandler = (entry) => {
|
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
||||||
setEditCreateEntryModalOpen(false)
|
setEditCreateEntryModalOpen(false)
|
||||||
const checksumEntries = {
|
const checksumEntries = {
|
||||||
...entry,
|
...entry,
|
||||||
|
@ -105,8 +110,8 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const editEntryModalHandler = (entry) => {
|
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
||||||
setSelectedEntry(null)
|
setSelectedEntry(initialEntryState)
|
||||||
setEditCreateEntryModalOpen(false)
|
setEditCreateEntryModalOpen(false)
|
||||||
const checksumEntries = {
|
const checksumEntries = {
|
||||||
...entry,
|
...entry,
|
||||||
|
@ -116,8 +121,8 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteEntryModalHandler = () => {
|
const deleteEntryModalHandler = () => {
|
||||||
const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
||||||
setSelectedEntry(null)
|
setSelectedEntry(initialEntryState)
|
||||||
setDeleteEntryModalOpen(false)
|
setDeleteEntryModalOpen(false)
|
||||||
dispatch(removeAddressBookEntry(entryAddress))
|
dispatch(removeAddressBookEntry(entryAddress))
|
||||||
}
|
}
|
||||||
|
@ -128,8 +133,8 @@ const AddressBookTable = (): React.ReactElement => {
|
||||||
<Col end="sm" xs={12}>
|
<Col end="sm" xs={12}>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEntry(null)
|
setSelectedEntry(initialEntryState)
|
||||||
setEditCreateEntryModalOpen(!editCreateEntryModalOpen)
|
setEditCreateEntryModalOpen(true)
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
testId="manage-tokens-btn"
|
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,
|
Menu,
|
||||||
ButtonLink,
|
ButtonLink,
|
||||||
} from '@gnosis.pm/safe-react-components'
|
} from '@gnosis.pm/safe-react-components'
|
||||||
|
import { MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk'
|
||||||
import { useHistory, useRouteMatch } from 'react-router-dom'
|
import { useHistory, useRouteMatch } from 'react-router-dom'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import {
|
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
INTERFACE_MESSAGES,
|
|
||||||
Transaction,
|
|
||||||
RequestId,
|
|
||||||
LowercaseNetworks,
|
|
||||||
SendTransactionParams,
|
|
||||||
} from '@gnosis.pm/safe-apps-sdk'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
safeEthBalanceSelector,
|
safeEthBalanceSelector,
|
||||||
|
@ -27,12 +22,15 @@ import {
|
||||||
safeNameSelector,
|
safeNameSelector,
|
||||||
} from 'src/logic/safe/store/selectors'
|
} from 'src/logic/safe/store/selectors'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
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 { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
import { isSameURL } from 'src/utils/url'
|
import { isSameURL } from 'src/utils/url'
|
||||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||||
import { staticAppsList } from 'src/routes/safe/components/Apps/utils'
|
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 ConfirmTransactionModal from '../components/ConfirmTransactionModal'
|
||||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||||
|
@ -40,8 +38,7 @@ import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||||
import LegalDisclaimer from './LegalDisclaimer'
|
import LegalDisclaimer from './LegalDisclaimer'
|
||||||
import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils'
|
import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils'
|
||||||
import { SafeApp, StoredSafeApp } from '../types.d'
|
import { SafeApp, StoredSafeApp } from '../types.d'
|
||||||
import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
import { useAppCommunicator } from '../communicator'
|
||||||
import { TIMEOUT } from 'src/utils/constants'
|
|
||||||
|
|
||||||
const OwnerDisclaimer = styled.div`
|
const OwnerDisclaimer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -72,11 +69,15 @@ const Breadcrumb = styled.div`
|
||||||
height: 51px;
|
height: 51px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export type TransactionParams = {
|
||||||
|
safeTxGas?: number
|
||||||
|
}
|
||||||
|
|
||||||
type ConfirmTransactionModalState = {
|
type ConfirmTransactionModalState = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
txs: Transaction[]
|
txs: Transaction[]
|
||||||
requestId?: RequestId
|
requestId?: RequestId
|
||||||
params?: SendTransactionParams
|
params?: TransactionParams
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -132,7 +133,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
||||||
}, [appIsLoading])
|
}, [appIsLoading])
|
||||||
|
|
||||||
const openConfirmationModal = useCallback(
|
const openConfirmationModal = useCallback(
|
||||||
(txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) =>
|
(txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) =>
|
||||||
setConfirmTransactionModal({
|
setConfirmTransactionModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
txs,
|
txs,
|
||||||
|
@ -169,18 +170,78 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
||||||
})
|
})
|
||||||
}, [ethBalance, safeAddress, appUrl, sendMessageToIframe])
|
}, [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) => {
|
const onUserTxConfirm = (safeTxHash: string) => {
|
||||||
|
// Safe Apps SDK V1 Handler
|
||||||
sendMessageToIframe(
|
sendMessageToIframe(
|
||||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } },
|
{ messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } },
|
||||||
confirmTransactionModal.requestId,
|
confirmTransactionModal.requestId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Safe Apps SDK V2 Handler
|
||||||
|
communicator?.send({ safeTxHash }, confirmTransactionModal.requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTxReject = () => {
|
const onTxReject = () => {
|
||||||
|
// Safe Apps SDK V1 Handler
|
||||||
sendMessageToIframe(
|
sendMessageToIframe(
|
||||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
|
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
|
||||||
confirmTransactionModal.requestId,
|
confirmTransactionModal.requestId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Safe Apps SDK V2 Handler
|
||||||
|
communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRemoveModal = () => setIsRemoveModalOpen(true)
|
const openRemoveModal = () => setIsRemoveModalOpen(true)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
|
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 styled from 'styled-components'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
||||||
|
|
||||||
import GasEstimationInfo from './GasEstimationInfo'
|
import GasEstimationInfo from './GasEstimationInfo'
|
||||||
import { getNetworkInfo } from 'src/config'
|
import { getNetworkInfo } from 'src/config'
|
||||||
|
import { TransactionParams } from './AppFrame'
|
||||||
|
|
||||||
const isTxValid = (t: Transaction): boolean => {
|
const isTxValid = (t: Transaction): boolean => {
|
||||||
if (!['string', 'number'].includes(typeof t.value)) {
|
if (!['string', 'number'].includes(typeof t.value)) {
|
||||||
|
@ -70,7 +71,7 @@ type OwnProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
app: SafeApp
|
app: SafeApp
|
||||||
txs: Transaction[]
|
txs: Transaction[]
|
||||||
params?: SendTransactionParams
|
params?: TransactionParams
|
||||||
safeAddress: string
|
safeAddress: string
|
||||||
safeName: string
|
safeName: string
|
||||||
ethBalance: string
|
ethBalance: string
|
||||||
|
|
|
@ -9,8 +9,7 @@ import {
|
||||||
RequestId,
|
RequestId,
|
||||||
Transaction,
|
Transaction,
|
||||||
LowercaseNetworks,
|
LowercaseNetworks,
|
||||||
SendTransactionParams,
|
} from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
} from '@gnosis.pm/safe-apps-sdk'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||||
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
||||||
|
@ -19,7 +18,7 @@ import {
|
||||||
safeNameSelector,
|
safeNameSelector,
|
||||||
safeParamAddressFromStateSelector,
|
safeParamAddressFromStateSelector,
|
||||||
} from 'src/logic/safe/store/selectors'
|
} 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'
|
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
|
||||||
|
|
||||||
type InterfaceMessageProps<T extends InterfaceMessageIds> = {
|
type InterfaceMessageProps<T extends InterfaceMessageIds> = {
|
||||||
|
@ -31,19 +30,11 @@ type ReturnType = {
|
||||||
sendMessageToIframe: <T extends InterfaceMessageIds>(message: InterfaceMessageProps<T>, requestId?: RequestId) => void
|
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 NETWORK_NAME = getNetworkName()
|
||||||
|
|
||||||
const useIframeMessageHandler = (
|
const useIframeMessageHandler = (
|
||||||
selectedApp: SafeApp | undefined,
|
selectedApp: SafeApp | undefined,
|
||||||
openConfirmationModal: (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => void,
|
openConfirmationModal: (txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) => void,
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||||
): ReturnType => {
|
): ReturnType => {
|
||||||
|
@ -58,6 +49,7 @@ const useIframeMessageHandler = (
|
||||||
const requestWithMessage = {
|
const requestWithMessage = {
|
||||||
...message,
|
...message,
|
||||||
requestId: requestId || Math.trunc(window.performance.now()),
|
requestId: requestId || Math.trunc(window.performance.now()),
|
||||||
|
version: '0.4.2',
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iframeRef && selectedApp) {
|
if (iframeRef && selectedApp) {
|
||||||
|
@ -93,44 +85,6 @@ const useIframeMessageHandler = (
|
||||||
break
|
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: {
|
case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: {
|
||||||
const safeInfoMessage = {
|
const safeInfoMessage = {
|
||||||
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
|
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) {
|
if (message.origin === window.origin) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import AppFrame from './components/AppFrame'
|
import AppFrame from './components/AppFrame'
|
||||||
import AppsList from './components/AppsList'
|
import AppsList from './components/AppsList'
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
|
|
||||||
const useQuery = () => {
|
const useQuery = () => {
|
||||||
return new URLSearchParams(useLocation().search)
|
return new URLSearchParams(useLocation().search)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
||||||
},
|
},
|
||||||
// Aave
|
// Aave
|
||||||
{
|
{
|
||||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX1NUtvm9WjbvT79sTdeg3sw1NxZAM273y44nBy5d2jZb`,
|
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQ3w2ezp2zx3u2LYQHyuNzMrLDJFjyL1rjAFTjNMcQ4cK`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||||
},
|
},
|
||||||
|
@ -95,7 +95,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
||||||
},
|
},
|
||||||
// TX-Builder
|
// TX-Builder
|
||||||
{
|
{
|
||||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmXdrr9hRbXSaqMb71iKnEp66PwwsAbJDR9XdwByUYSTxB`,
|
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmYES1Se6i6679z3PfQ62bydgVVEoSRUabvjB35DfUGPGA`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
networks: [
|
networks: [
|
||||||
ETHEREUM_NETWORK.MAINNET,
|
ETHEREUM_NETWORK.MAINNET,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SettingsDescription from './SettingsDescription'
|
||||||
import CustomDescription from './CustomDescription'
|
import CustomDescription from './CustomDescription'
|
||||||
import TransferDescription from './TransferDescription'
|
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 Block from 'src/components/layout/Block'
|
||||||
import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
|
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 TransferDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
||||||
const amountWithSymbol = getTxAmount(tx, false)
|
const amountWithSymbol = getTxAmount(tx, false)
|
||||||
const { recipient, isTokenTransfer = false } = getTxData(tx)
|
const rawAmount = getRawTxAmount(tx)
|
||||||
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer }} />
|
const { recipient, isTokenTransfer = false, tokenAddress } = getTxData(tx)
|
||||||
|
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer, rawAmount, tokenAddress }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
||||||
|
|
|
@ -3,9 +3,7 @@ import React, { useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
|
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import NoSafe from 'src/components/NoSafe'
|
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
|
||||||
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
||||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
@ -35,14 +33,8 @@ const Container = (): React.ReactElement => {
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
|
||||||
const provider = useSelector(providerNameSelector)
|
|
||||||
const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||||
|
|
||||||
if (!safeAddress) {
|
|
||||||
return <NoSafe provider={provider} text="Safe not found" />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!featuresEnabled) {
|
if (!featuresEnabled) {
|
||||||
return (
|
return (
|
||||||
<LoadingContainer>
|
<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 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 Link from 'src/components/layout/Link'
|
||||||
|
import Block from 'src/components/layout/Block'
|
||||||
import { LOAD_ADDRESS, OPEN_ADDRESS } from 'src/routes/routes'
|
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'
|
const Wrapper = styled.div`
|
||||||
import SafeIcon from '../assets/safe.svg'
|
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 = {
|
h5 {
|
||||||
height: '13px',
|
color: white;
|
||||||
color: secondary,
|
}
|
||||||
marginBottom: '-2px',
|
`
|
||||||
|
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 = {
|
const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElement => {
|
||||||
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.{' '}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Block className={styles.safe}>
|
<Block>
|
||||||
<Heading align="center" margin="lg" tag="h1" weight="bold">
|
{/* Title */}
|
||||||
Welcome to
|
<Title size="md" strong>
|
||||||
<br />
|
Welcome to Gnosis Safe Multisig.
|
||||||
Gnosis Safe Multisig
|
</Title>
|
||||||
</Heading>
|
|
||||||
<Heading align="center" margin="xl" tag="h3">
|
{/* Subtitle */}
|
||||||
{headingText}
|
<Title size="xs">
|
||||||
<a
|
{isOldMultisigMigration ? (
|
||||||
className={styles.learnMoreLink}
|
<>
|
||||||
href="https://gnosis-safe.io/teams"
|
We will replicate the owner structure from your existing Gnosis MultiSig to let you test the new interface.
|
||||||
rel="noopener noreferrer"
|
As soon as you feel comfortable, start moving funds to your new Safe.
|
||||||
target="_blank"
|
</>
|
||||||
>
|
) : (
|
||||||
Learn more
|
<>
|
||||||
<OpenInNew style={openIconStyle} />
|
Gnosis Safe Multisig is the most trusted platform to manage digital assets. <br /> Here is how to get
|
||||||
</a>
|
started:{' '}
|
||||||
</Heading>
|
</>
|
||||||
{provider ? (
|
)}
|
||||||
<>
|
</Title>
|
||||||
<Block className={styles.safeActions} margin="md">
|
|
||||||
<CreateSafe provider={provider} size="large" />
|
<>
|
||||||
</Block>
|
<Wrapper>
|
||||||
<Block className={styles.safeActions} margin="md">
|
{/* Connect wallet */}
|
||||||
<LoadSafe provider={provider} size="large" />
|
<StyledCard>
|
||||||
</Block>
|
<TitleWrapper>
|
||||||
</>
|
<Dot color="primary">
|
||||||
) : (
|
{!provider ? <Title size="xs">1</Title> : <Icon color="white" type="check" size="md" />}
|
||||||
<Block className={styles.connectWallet} margin="md">
|
</Dot>
|
||||||
<Heading align="center" margin="md" tag="h3">
|
<StyledTitle size="sm" strong withoutMargin>
|
||||||
Get Started by Connecting a Wallet
|
Connect wallet
|
||||||
</Heading>
|
</StyledTitle>
|
||||||
<ConnectButton minHeight={42} minWidth={240} data-testid="connect-btn" />
|
</TitleWrapper>
|
||||||
</Block>
|
<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>
|
</Block>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
//
|
//
|
||||||
function useTestAccountAt(index = 0) {
|
function useTestAccountAt(index = 0) {
|
||||||
(window as any).testAccountIndex = index
|
window.testAccountIndex = index
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTestAccount() {
|
function resetTestAccount() {
|
||||||
delete (window as any).testAccountIndex
|
delete window.testAccountIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useTestAccountAt, resetTestAccount }
|
export { useTestAccountAt, resetTestAccount }
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import Web3 from 'web3'
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
web3?: Web3
|
ethereum?: {
|
||||||
testAccountIndex?: string
|
autoRefreshOnNetworkChange: boolean
|
||||||
|
isMetaMask: boolean
|
||||||
|
}
|
||||||
|
testAccountIndex?: string | number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import GoogleAnalytics, { EventArgs } from 'react-ga'
|
import ReactGA, { EventArgs } from 'react-ga'
|
||||||
import { getNetworkInfo } from 'src/config'
|
import { getNetworkInfo } from 'src/config'
|
||||||
|
|
||||||
import { getGoogleAnalyticsTrackingID } from 'src/config'
|
import { getGoogleAnalyticsTrackingID } from 'src/config'
|
||||||
|
@ -20,8 +20,8 @@ export const loadGoogleAnalytics = (): void => {
|
||||||
if (!trackingID) {
|
if (!trackingID) {
|
||||||
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
|
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
|
||||||
} else {
|
} else {
|
||||||
GoogleAnalytics.initialize(trackingID)
|
ReactGA.initialize(trackingID)
|
||||||
GoogleAnalytics.set({
|
ReactGA.set({
|
||||||
anonymizeIp: true,
|
anonymizeIp: true,
|
||||||
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
|
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
|
||||||
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
|
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
|
||||||
|
@ -50,22 +50,19 @@ export const useAnalytics = (): UseAnalyticsResponse => {
|
||||||
fetchCookiesFromStorage()
|
fetchCookiesFromStorage()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const trackPage = useCallback(
|
const trackPage = (page) => {
|
||||||
(page) => {
|
if (!analyticsAllowed || !analyticsLoaded) {
|
||||||
if (!analyticsAllowed || !analyticsLoaded) {
|
return
|
||||||
return
|
}
|
||||||
}
|
ReactGA.pageview(page)
|
||||||
GoogleAnalytics.pageview(page)
|
}
|
||||||
},
|
|
||||||
[analyticsAllowed],
|
|
||||||
)
|
|
||||||
|
|
||||||
const trackEvent = useCallback(
|
const trackEvent = useCallback(
|
||||||
(event: EventArgs) => {
|
(event: EventArgs) => {
|
||||||
if (!analyticsAllowed || !analyticsLoaded) {
|
if (!analyticsAllowed || !analyticsLoaded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GoogleAnalytics.event(event)
|
ReactGA.event(event)
|
||||||
},
|
},
|
||||||
[analyticsAllowed],
|
[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