Merge branch 'development' of github.com:gnosis/safe-react into development
This commit is contained in:
commit
433fe9be79
16
.travis.yml
16
.travis.yml
|
@ -72,7 +72,7 @@ after_success:
|
|||
- yarn coveralls
|
||||
|
||||
deploy:
|
||||
# Development environment
|
||||
# Development environment only on rinkeby
|
||||
- provider: s3
|
||||
bucket: $DEV_BUCKET_NAME
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
|
@ -83,6 +83,7 @@ deploy:
|
|||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: development
|
||||
condition: $REACT_APP_NETWORK = rinkeby
|
||||
|
||||
# Staging environment
|
||||
- provider: s3
|
||||
|
@ -95,19 +96,6 @@ deploy:
|
|||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: master
|
||||
|
||||
# EWC testing on staging
|
||||
- provider: s3
|
||||
bucket: $STAGING_BUCKET_NAME
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||
skip_cleanup: true
|
||||
local_dir: build
|
||||
upload_dir: current/app
|
||||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: release/v2.14.0
|
||||
condition: $REACT_APP_NETWORK = energy_web_chain
|
||||
|
||||
# Prepare production deployment
|
||||
- provider: s3
|
||||
|
|
|
@ -72,7 +72,8 @@ export enum FEATURES {
|
|||
ERC721 = 'ERC721',
|
||||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION'
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP'
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -235,7 +236,7 @@ const rinkeby: NetworkConfig = {
|
|||
address: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
decimals: ?,
|
||||
decimals: 0,
|
||||
logoUri: '',
|
||||
},
|
||||
},
|
||||
|
|
41
package.json
41
package.json
|
@ -168,18 +168,18 @@
|
|||
"dependencies": {
|
||||
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#3f0689f",
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8d8508e",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#03ff672d6f73366297986d58631f9582fe2ed4a3",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.26.0",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.28.0",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"@sentry/react": "^5.27.1",
|
||||
"@sentry/tracing": "^5.27.1",
|
||||
"@truffle/contract": "4.2.26",
|
||||
"@sentry/react": "^5.27.3",
|
||||
"@sentry/tracing": "^5.27.3",
|
||||
"@truffle/contract": "4.2.28",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.20.0",
|
||||
"axios": "0.21.0",
|
||||
"bignumber.js": "9.0.1",
|
||||
"bnc-onboard": "1.14.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -190,7 +190,7 @@
|
|||
"date-fns": "2.16.1",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
"electron-log": "4.2.4",
|
||||
"electron-log": "4.3.0",
|
||||
"electron-settings": "^4.0.2",
|
||||
"electron-updater": "4.3.5",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
|
@ -208,7 +208,6 @@
|
|||
"lodash.memoize": "^4.1.2",
|
||||
"material-ui-search-bar": "^1.0.0",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"polished": "3.6.7",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.13.6",
|
||||
"react": "16.13.1",
|
||||
|
@ -229,13 +228,13 @@
|
|||
"reselect": "^4.0.0",
|
||||
"semver": "7.3.2",
|
||||
"styled-components": "^5.2.0",
|
||||
"web3": "1.2.9",
|
||||
"web3-core": "^1.2.11",
|
||||
"web3-eth-contract": "^1.2.11",
|
||||
"web3-utils": "^1.2.11"
|
||||
"web3": "^1.3.0",
|
||||
"web3-core": "^1.3.0",
|
||||
"web3-eth-contract": "^1.3.0",
|
||||
"web3-utils": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/cli": "^1.58.0",
|
||||
"@sentry/cli": "^1.59.0",
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addons": "^5.3.19",
|
||||
|
@ -247,20 +246,20 @@
|
|||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "^14.14.5",
|
||||
"@types/react": "^16.9.54",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/react": "^16.9.55",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "4.6.0",
|
||||
"@typescript-eslint/parser": "4.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.6.1",
|
||||
"@typescript-eslint/parser": "4.6.1",
|
||||
"autoprefixer": "9.8.6",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "9.3.1",
|
||||
"electron-builder": "22.8.1",
|
||||
"electron": "9.3.3",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-notarize": "1.0.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "6.14.0",
|
||||
|
@ -271,7 +270,7 @@
|
|||
"eslint-plugin-sort-destructure-keys": "1.3.5",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.4.2",
|
||||
"lint-staged": "^10.5.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.1.2",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { List } from 'immutable'
|
||||
import memoize from 'lodash.memoize'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import memoize from 'lodash.memoize'
|
||||
|
||||
type ValidatorReturnType = string | undefined
|
||||
type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
||||
|
@ -62,7 +64,11 @@ export const mustBeEthereumAddress = memoize(
|
|||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
|
||||
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
|
||||
const errorMessage = `Address should be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
}`
|
||||
|
||||
return startsWith0x && isAddress ? undefined : errorMessage
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -70,9 +76,11 @@ export const mustBeEthereumContractAddress = memoize(
|
|||
async (address: string): Promise<ValidatorReturnType> => {
|
||||
const contractCode = await getWeb3().eth.getCode(address)
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === ''
|
||||
? 'Address should be a valid Ethereum contract address or ENS name'
|
||||
: undefined
|
||||
const errorMessage = `Address should be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
}`
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import memoize from 'lodash.memoize'
|
||||
|
||||
import networks from 'src/config/networks'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures, Wallets, GasPriceOracle } from 'src/config/networks/network.d'
|
||||
import {
|
||||
EnvironmentSettings,
|
||||
ETHEREUM_NETWORK,
|
||||
FEATURES,
|
||||
GasPriceOracle,
|
||||
NetworkSettings,
|
||||
SafeFeatures,
|
||||
Wallets,
|
||||
} from 'src/config/networks/network.d'
|
||||
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
|
||||
|
@ -90,6 +98,16 @@ export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: s
|
|||
|
||||
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig().disabledFeatures || []
|
||||
|
||||
/**
|
||||
* Checks if a particular feature is enabled in the current network configuration
|
||||
* @params {FEATURES} feature
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isFeatureEnabled = memoize((feature: FEATURES): boolean => {
|
||||
const disabledFeatures = getNetworkConfigDisabledFeatures()
|
||||
return !disabledFeatures.some((disabledFeature) => disabledFeature === feature)
|
||||
})
|
||||
|
||||
export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || []
|
||||
|
||||
export const getNetworkInfo = (): NetworkSettings => getConfig().network
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import EwcLogo from 'src/config/assets/token_ewc.svg'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0
|
||||
// once the oracle is fixed we need to remove the fixed value
|
||||
|
@ -44,7 +44,7 @@ const mainnet: NetworkConfig = {
|
|||
logoUri: EwcLogo,
|
||||
},
|
||||
},
|
||||
disabledWallets:[
|
||||
disabledWallets: [
|
||||
WALLETS.TREZOR,
|
||||
WALLETS.LEDGER,
|
||||
WALLETS.COINBASE,
|
||||
|
@ -59,8 +59,11 @@ const mainnet: NetworkConfig = {
|
|||
WALLETS.WALLET_CONNECT,
|
||||
WALLETS.WALLET_LINK,
|
||||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE
|
||||
]
|
||||
WALLETS.LATTICE,
|
||||
],
|
||||
disabledFeatures: [
|
||||
FEATURES.ENS_LOOKUP,
|
||||
],
|
||||
}
|
||||
|
||||
export default mainnet
|
||||
|
|
|
@ -23,7 +23,8 @@ export enum FEATURES {
|
|||
ERC721 = 'ERC721',
|
||||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION'
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP',
|
||||
}
|
||||
|
||||
type Token = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import EwcLogo from 'src/config/assets/token_ewc.svg'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
|
||||
|
@ -41,7 +41,7 @@ const mainnet: NetworkConfig = {
|
|||
logoUri: EwcLogo,
|
||||
},
|
||||
},
|
||||
disabledWallets:[
|
||||
disabledWallets: [
|
||||
WALLETS.TREZOR,
|
||||
WALLETS.LEDGER,
|
||||
WALLETS.COINBASE,
|
||||
|
@ -56,8 +56,11 @@ const mainnet: NetworkConfig = {
|
|||
WALLETS.WALLET_CONNECT,
|
||||
WALLETS.WALLET_LINK,
|
||||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE
|
||||
]
|
||||
WALLETS.LATTICE,
|
||||
],
|
||||
disabledFeatures: [
|
||||
FEATURES.ENS_LOOKUP,
|
||||
],
|
||||
}
|
||||
|
||||
export default mainnet
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d'
|
||||
import xDaiLogo from 'src/config/assets/token_xdai.svg'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
|
||||
|
@ -36,7 +36,7 @@ const xDai: NetworkConfig = {
|
|||
logoUri: xDaiLogo,
|
||||
},
|
||||
},
|
||||
disabledWallets:[
|
||||
disabledWallets: [
|
||||
WALLETS.TREZOR,
|
||||
WALLETS.LEDGER,
|
||||
WALLETS.COINBASE,
|
||||
|
@ -50,8 +50,11 @@ const xDai: NetworkConfig = {
|
|||
WALLETS.WALLET_CONNECT,
|
||||
WALLETS.WALLET_LINK,
|
||||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE
|
||||
]
|
||||
WALLETS.LATTICE,
|
||||
],
|
||||
disabledFeatures: [
|
||||
FEATURES.ENS_LOOKUP,
|
||||
],
|
||||
}
|
||||
|
||||
export default xDai
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { List } from 'immutable'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
|
||||
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
|
||||
|
||||
|
@ -138,3 +139,39 @@ export const checkIfEntryWasDeletedFromAddressBook = (
|
|||
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
|
||||
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtered list of AddressBookEntries whose addresses are contracts
|
||||
* @param {Array<AddressBookEntry>} addressBook
|
||||
* @returns Array<AddressBookEntry>
|
||||
*/
|
||||
export const filterContractAddressBookEntries = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
|
||||
const abFlags = await Promise.all(
|
||||
addressBook.map(
|
||||
async ({ address }: AddressBookEntry): Promise<boolean> => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return addressBook.filter((_, index) => abFlags[index])
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the AddressBookEntries by `address` or `name` based on the `inputValue`
|
||||
* @param {Array<AddressBookEntry>} addressBookEntries
|
||||
* @param {Object} filterParams
|
||||
* @param {String} filterParams.inputValue
|
||||
* @return Array<AddressBookEntry>
|
||||
*/
|
||||
export const filterAddressEntries = (
|
||||
addressBookEntries: AddressBookEntry[],
|
||||
{ inputValue }: { inputValue: string },
|
||||
): AddressBookEntry[] =>
|
||||
addressBookEntries.filter(({ address, name }) => {
|
||||
const inputLowerCase = inputValue.toLowerCase()
|
||||
const foundName = name.toLowerCase().includes(inputLowerCase)
|
||||
const foundAddress = address?.toLowerCase().includes(inputLowerCase)
|
||||
|
||||
return foundName || foundAddress
|
||||
})
|
||||
|
|
|
@ -16,6 +16,10 @@ export const nftAssetsListSelector = createSelector(nftAssets, (assets): NFTAsse
|
|||
return assets ? Object.values(assets) : []
|
||||
})
|
||||
|
||||
export const nftAssetsListAddressesSelector = createSelector(nftAssetsListSelector, (assets): string[] => {
|
||||
return Array.from(new Set(assets.map((nftAsset) => nftAsset.address)))
|
||||
})
|
||||
|
||||
export const availableNftAssetsAddresses = createSelector(nftTokensSelector, (userNftTokens): string[] => {
|
||||
return Array.from(new Set(userNftTokens.map((nftToken) => nftToken.assetAddress)))
|
||||
})
|
||||
|
|
|
@ -281,7 +281,6 @@ describe('isCustomTransaction', () => {
|
|||
it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
|
@ -299,7 +298,7 @@ describe('isCustomTransaction', () => {
|
|||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
const result = await isCustomTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
|
@ -309,7 +308,6 @@ describe('isCustomTransaction', () => {
|
|||
it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
|
@ -327,7 +325,7 @@ describe('isCustomTransaction', () => {
|
|||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
const result = await isCustomTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
|
@ -338,7 +336,6 @@ describe('isCustomTransaction', () => {
|
|||
// given
|
||||
const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000`
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
|
@ -356,7 +353,7 @@ describe('isCustomTransaction', () => {
|
|||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
const result = await isCustomTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
|
@ -365,7 +362,6 @@ describe('isCustomTransaction', () => {
|
|||
it('It should return false if is outgoing transaction, is not SendERC20Transaction, not isUpgradeTransaction and isSendERC721Transaction', async () => {
|
||||
// given
|
||||
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
|
||||
const txCode = ''
|
||||
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
|
||||
const token = makeToken({
|
||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
||||
|
@ -383,7 +379,7 @@ describe('isCustomTransaction', () => {
|
|||
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => true)
|
||||
|
||||
// when
|
||||
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
|
||||
const result = await isCustomTransaction(transaction, safeAddress)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
|
@ -839,11 +835,9 @@ describe('buildTx', () => {
|
|||
const txResult = await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser: userAddress,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe: safeInstance,
|
||||
tx: transaction,
|
||||
txCode: undefined,
|
||||
})
|
||||
|
||||
// then
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { fromJS, List, Map } from 'immutable'
|
||||
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
|
@ -9,7 +8,6 @@ import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
|
|||
import { store } from 'src/store'
|
||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
|
||||
import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
import { DataDecoded } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
|
||||
|
@ -65,7 +63,6 @@ export type OutgoingTxs = {
|
|||
|
||||
export type BatchProcessTxsProps = OutgoingTxs & {
|
||||
currentUser?: string
|
||||
knownTokens: Map<string, Token>
|
||||
safe: SafeRecord
|
||||
}
|
||||
|
||||
|
@ -138,7 +135,6 @@ const batchRequestContractCode = (transactions: TxServiceModel[]): Promise<Batch
|
|||
const batchProcessOutgoingTransactions = async ({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
}: BatchProcessTxsProps): Promise<{
|
||||
|
@ -150,15 +146,13 @@ const batchProcessOutgoingTransactions = async ({
|
|||
const cancellationTxsWithData = cancelTxsValues.length ? await batchRequestContractCode(cancelTxsValues) : []
|
||||
|
||||
const cancel = {}
|
||||
for (const [tx, txCode] of cancellationTxsWithData) {
|
||||
for (const [tx] of cancellationTxsWithData) {
|
||||
cancel[`${tx.nonce}`] = await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -166,16 +160,14 @@ const batchProcessOutgoingTransactions = async ({
|
|||
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
|
||||
|
||||
const outgoing: Transaction[] = []
|
||||
for (const [tx, txCode] of outgoingTxsWithData) {
|
||||
for (const [tx] of outgoingTxsWithData) {
|
||||
outgoing.push(
|
||||
await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -195,7 +187,6 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
|
|||
return defaultResponse
|
||||
}
|
||||
|
||||
const knownTokens: TokenState = state[TOKEN_REDUCER_ID]
|
||||
const currentUser: string = state[PROVIDER_REDUCER_ID].get('account')
|
||||
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
|
||||
|
||||
|
@ -215,7 +206,6 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
|
|||
const { cancel, outgoing } = await batchProcessOutgoingTransactions({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { List } from 'immutable'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import {
|
||||
getERC20DecimalsAndSymbol,
|
||||
getERC721Symbol,
|
||||
|
@ -33,7 +32,6 @@ import {
|
|||
TxServiceModel,
|
||||
} from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { TypedDataUtils } from 'eth-sig-util'
|
||||
import { Token } from 'src/logic/tokens/store/model/token'
|
||||
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
|
||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
|
@ -83,16 +81,11 @@ export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress?: string):
|
|||
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isCustomTransaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode?: string,
|
||||
safeAddress?: string,
|
||||
knownTokens?: TokenState,
|
||||
): Promise<boolean> => {
|
||||
export const isCustomTransaction = async (tx: TxServiceModel, safeAddress?: string): Promise<boolean> => {
|
||||
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
|
||||
const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens)
|
||||
const isErc20 = await isSendERC20Transaction(tx)
|
||||
const isUpgrade = isUpgradeTransaction(tx)
|
||||
const isErc721 = isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
const isErc721 = isSendERC721Transaction(tx)
|
||||
|
||||
return isOutgoing && !isErc20 && !isUpgrade && !isErc721
|
||||
}
|
||||
|
@ -232,27 +225,24 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues
|
|||
|
||||
export type BuildTx = BatchProcessTxsProps & {
|
||||
tx: TxServiceModel
|
||||
txCode?: string
|
||||
}
|
||||
|
||||
export const buildTx = async ({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
}: BuildTx): Promise<Transaction> => {
|
||||
const safeAddress = safe.address
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
|
||||
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
|
||||
const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
const isSendERC20Tx = await isSendERC20Transaction(tx, txCode, knownTokens)
|
||||
const isSendERC721Tx = isSendERC721Transaction(tx)
|
||||
const isSendERC20Tx = await isSendERC20Transaction(tx)
|
||||
const isMultiSendTx = isMultiSendTransaction(tx)
|
||||
const isUpgradeTx = isUpgradeTransaction(tx)
|
||||
const isCustomTx = await isCustomTransaction(tx, txCode, safeAddress, knownTokens)
|
||||
const isCustomTx = await isCustomTransaction(tx, safeAddress)
|
||||
const isCancellationTx = isCancelTransaction(tx, safeAddress)
|
||||
const refundParams = await getRefundParams(tx, getERC20DecimalsAndSymbol)
|
||||
const decodedParams = getDecodedParams(tx)
|
||||
|
@ -323,7 +313,6 @@ export type TxToMock = TxArgs & {
|
|||
}
|
||||
|
||||
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
|
||||
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
|
||||
const safe = safeSelector(state)
|
||||
const cancellationTxs = safeCancellationTransactionsSelector(state)
|
||||
const outgoingTxs = safeTransactionsSelector(state)
|
||||
|
@ -335,11 +324,9 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
|
|||
return buildTx({
|
||||
cancellationTxs,
|
||||
currentUser: undefined,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx: (tx as unknown) as TxServiceModel,
|
||||
txCode: EMPTY_DATA,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -241,7 +241,13 @@ export const METHOD_TO_ID = {
|
|||
|
||||
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
export const TOKEN_TRANSFER_METHODS_NAMES = {
|
||||
TRANSFER: 'transfer',
|
||||
TRANSFER_FROM: 'transferFrom',
|
||||
SAFE_TRANSFER_FROM: 'safeTransferFrom',
|
||||
} as const
|
||||
|
||||
type TokenMethods = typeof TOKEN_TRANSFER_METHODS_NAMES[keyof typeof TOKEN_TRANSFER_METHODS_NAMES]
|
||||
|
||||
type SafeDecodedParams = {
|
||||
[key in SafeMethods]?: Record<string, string>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
|||
|
||||
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
|
||||
import { LATEST_SAFE_VERSION } from 'src/utils/constants'
|
||||
import { getNetworkConfigDisabledFeatures } from 'src/config'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
|
||||
type FeatureConfigByVersion = {
|
||||
|
@ -41,9 +41,8 @@ const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, ver
|
|||
}
|
||||
|
||||
export const enabledFeatures = (version?: string): FEATURES[] => {
|
||||
const disabledFeatures = getNetworkConfigDisabledFeatures()
|
||||
return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => {
|
||||
if (!disabledFeatures.includes(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
|
||||
if (isFeatureEnabled(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
|
||||
acc.push(feature.name)
|
||||
}
|
||||
return acc
|
||||
|
|
|
@ -203,4 +203,32 @@ describe('isERC721Contract', () => {
|
|||
// then
|
||||
expect(txValue).toEqual(expectedResult)
|
||||
})
|
||||
|
||||
it('It should return the right conversion from token to unit with exceeding decimals', () => {
|
||||
// given
|
||||
const decimals = Number(18)
|
||||
|
||||
const expectedResult = '333333333333333398'
|
||||
const VALUE = '0.33333333333333339878798333'
|
||||
|
||||
// when
|
||||
const txValue = toTokenUnit(VALUE, decimals)
|
||||
|
||||
// then
|
||||
expect(txValue).toEqual(expectedResult)
|
||||
})
|
||||
|
||||
it('It should return the right conversion from token to unit with exact decimals', () => {
|
||||
// given
|
||||
const decimals = Number(18)
|
||||
|
||||
const expectedResult = '333333333333333399'
|
||||
const VALUE = '0.333333333333333399'
|
||||
|
||||
// when
|
||||
const txValue = toTokenUnit(VALUE, decimals)
|
||||
|
||||
// then
|
||||
expect(txValue).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,13 +7,5 @@ export const humanReadableValue = (value: number | string, decimals = 18): strin
|
|||
export const fromTokenUnit = (amount: number | string, decimals: string | number): string =>
|
||||
new BigNumber(amount).times(`1e-${decimals}`).toFixed()
|
||||
|
||||
export const toTokenUnit = (amount: number | string, decimals: string | number): string => {
|
||||
const amountBN = new BigNumber(amount).times(`1e${decimals}`)
|
||||
const [, amountDecimalPlaces] = amount.toString().split('.')
|
||||
|
||||
if (amountDecimalPlaces?.length >= +decimals) {
|
||||
return amountBN.toFixed(+decimals, BigNumber.ROUND_DOWN)
|
||||
}
|
||||
|
||||
return amountBN.toFixed()
|
||||
}
|
||||
export const toTokenUnit = (amount: number | string, decimals: string | number): string =>
|
||||
new BigNumber(amount).times(`1e${decimals}`).toFixed(0, BigNumber.ROUND_DOWN)
|
||||
|
|
|
@ -8,11 +8,14 @@ import {
|
|||
getERC721TokenContract,
|
||||
} from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
import { store } from 'src/store'
|
||||
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
|
||||
|
||||
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
||||
|
||||
|
@ -42,14 +45,17 @@ export const isTokenTransfer = (tx: TxServiceModel): boolean => {
|
|||
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isSendERC721Transaction = (tx: TxServiceModel, txCode?: string, knownTokens?: TokenState): boolean => {
|
||||
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
|
||||
// but no proper ERC721 standard implemented
|
||||
return (
|
||||
(txCode?.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) &&
|
||||
tx.to !== '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85') ||
|
||||
(isTokenTransfer(tx) && !knownTokens?.get(tx.to))
|
||||
)
|
||||
export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
|
||||
let hasERC721Transfer = false
|
||||
|
||||
if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) {
|
||||
hasERC721Transfer = tx.dataDecoded.parameters.findIndex((param) => sameString(param.name, 'tokenId')) !== -1
|
||||
}
|
||||
|
||||
// Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this
|
||||
const state = store.getState()
|
||||
const knownAssets = nftAssetsListAddressesSelector(state)
|
||||
return knownAssets.includes(tx.to) || hasERC721Transfer
|
||||
}
|
||||
|
||||
export const getERC721Symbol = async (contractAddress: string): Promise<string> => {
|
||||
|
@ -59,6 +65,12 @@ export const getERC721Symbol = async (contractAddress: string): Promise<string>
|
|||
const tokenInstance = await ERC721token.at(contractAddress)
|
||||
tokenSymbol = tokenInstance.symbol()
|
||||
} catch (err) {
|
||||
// If the contract address is an ENS token contract, we know that the ERC721 standard is not proper implemented
|
||||
// The method symbol() is missing
|
||||
const ENS_TOKEN_CONTRACT = '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85'
|
||||
if (sameString(contractAddress, ENS_TOKEN_CONTRACT)) {
|
||||
return 'ENS'
|
||||
}
|
||||
console.error(`Failed to retrieve token symbol for ERC721 token ${contractAddress}`)
|
||||
}
|
||||
return tokenSymbol
|
||||
|
@ -89,12 +101,8 @@ export const getERC20DecimalsAndSymbol = async (
|
|||
return tokenInfo
|
||||
}
|
||||
|
||||
export const isSendERC20Transaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode?: string,
|
||||
knownTokens?: TokenState,
|
||||
): Promise<boolean> => {
|
||||
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
|
||||
export const isSendERC20Transaction = async (tx: TxServiceModel): Promise<boolean> => {
|
||||
let isSendTokenTx = !isSendERC721Transaction(tx) && isTokenTransfer(tx)
|
||||
|
||||
if (isSendTokenTx) {
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
|
||||
|
|
|
@ -1,246 +1,204 @@
|
|||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import MuiTextField from '@material-ui/core/TextField'
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles'
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Autocomplete, { AutocompleteProps } from '@material-ui/lab/Autocomplete'
|
||||
import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
useTextFieldInputStyle,
|
||||
useTextFieldLabelStyle,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
export interface AddressBookProps {
|
||||
fieldMutator: (address: string) => void
|
||||
isCustomTx?: boolean
|
||||
pristine: boolean
|
||||
pristine?: boolean
|
||||
recipientAddress?: string
|
||||
setSelectedEntry: (
|
||||
entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name? }> | null,
|
||||
) => void
|
||||
setIsValidAddress: (valid: boolean) => void
|
||||
setSelectedEntry: Dispatch<SetStateAction<{ address: string; name: string }> | null>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const textFieldLabelStyle = makeStyles(() => ({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
fontSize: '15px',
|
||||
width: '500px',
|
||||
},
|
||||
}))
|
||||
|
||||
const textFieldInputStyle = makeStyles(() => ({
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
width: '420px',
|
||||
},
|
||||
}))
|
||||
|
||||
const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
|
||||
const abFlags = await Promise.all(
|
||||
addressBook.map(
|
||||
async ({ address }: AddressBookEntry): Promise<boolean> => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return addressBook.filter((_, index) => abFlags[index])
|
||||
export interface BaseAddressBookInputProps extends AddressBookProps {
|
||||
addressBookEntries: AddressBookEntry[]
|
||||
setSelectedEntry: (args: { address: string; name: string } | null) => void
|
||||
setValidationText: Dispatch<SetStateAction<string | undefined>>
|
||||
validationText: string | undefined
|
||||
}
|
||||
|
||||
const AddressBookInput = ({
|
||||
const BaseAddressBookInput = ({
|
||||
addressBookEntries,
|
||||
fieldMutator,
|
||||
isCustomTx,
|
||||
pristine,
|
||||
recipientAddress,
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
}: AddressBookProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [isValidForm, setIsValidForm] = useState(true)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
const [inputTouched, setInputTouched] = useState(false)
|
||||
const [blurred, setBlurred] = useState(pristine)
|
||||
const [adbkList, setADBKList] = useState<AddressBookEntry[]>([])
|
||||
setValidationText,
|
||||
validationText,
|
||||
}: BaseAddressBookInputProps): ReactElement => {
|
||||
const updateAddressInfo = (addressEntry: AddressBookEntry): void => {
|
||||
setSelectedEntry(addressEntry)
|
||||
fieldMutator(addressEntry.address)
|
||||
}
|
||||
|
||||
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
||||
const validateAddress = (address: string): AddressBookEntry | string | undefined => {
|
||||
const addressErrorMessage = mustBeEthereumAddress(address)
|
||||
setIsValidAddress(!addressErrorMessage)
|
||||
|
||||
const onAddressInputChanged = async (value: string): Promise<void> => {
|
||||
const normalizedAddress = trimSpaces(value)
|
||||
const isENSDomain = isValidEnsName(normalizedAddress)
|
||||
setInputAddValue(normalizedAddress)
|
||||
let resolvedAddress = normalizedAddress
|
||||
let addressErrorMessage
|
||||
if (inputTouched && !normalizedAddress) {
|
||||
setIsValidForm(false)
|
||||
setValidationText('Required')
|
||||
setIsValidAddress(false)
|
||||
if (addressErrorMessage) {
|
||||
setValidationText(addressErrorMessage)
|
||||
return
|
||||
}
|
||||
if (normalizedAddress) {
|
||||
if (isENSDomain) {
|
||||
resolvedAddress = await getAddressFromENS(normalizedAddress)
|
||||
setInputAddValue(resolvedAddress)
|
||||
|
||||
const filteredEntries = filterAddressEntries(addressBookEntries, { inputValue: address })
|
||||
return filteredEntries.length === 1 ? filteredEntries[0] : address
|
||||
}
|
||||
|
||||
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
||||
switch (reason) {
|
||||
case 'select-option': {
|
||||
const { address, name } = value as AddressBookEntry
|
||||
updateAddressInfo({ address, name })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addressErrorMessage = mustBeEthereumAddress(resolvedAddress)
|
||||
if (isCustomTx && addressErrorMessage === undefined) {
|
||||
addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress)
|
||||
}
|
||||
const onInputChange: AutocompleteProps<AddressBookEntry, false, false, true>['onInputChange'] = async (
|
||||
_,
|
||||
value,
|
||||
reason,
|
||||
) => {
|
||||
switch (reason) {
|
||||
case 'input': {
|
||||
const normalizedValue = trimSpaces(value)
|
||||
|
||||
// First removes the entries that are not contracts if the operation is custom tx
|
||||
const adbkToFilter = isCustomTx ? await filterAddressBookWithContractAddresses(addressBook) : addressBook
|
||||
// Then Filters the entries based on the input of the user
|
||||
const filteredADBK = adbkToFilter.filter((adbkEntry) => {
|
||||
const { address, name } = adbkEntry
|
||||
return (
|
||||
name.toLowerCase().includes(normalizedAddress.toLowerCase()) ||
|
||||
address.toLowerCase().includes(resolvedAddress.toLowerCase())
|
||||
)
|
||||
})
|
||||
setADBKList(filteredADBK)
|
||||
if (!addressErrorMessage) {
|
||||
// base case if isENSDomain we set the domain as the name
|
||||
// if address does not exist in address book we use blank name
|
||||
let addressName = isENSDomain ? normalizedAddress : ''
|
||||
|
||||
// if address is valid, and is in the address book, then we use the stored values
|
||||
if (filteredADBK.length === 1) {
|
||||
const addressBookContact = filteredADBK[0]
|
||||
addressName = addressBookContact.name ?? addressName
|
||||
if (!normalizedValue) {
|
||||
break
|
||||
}
|
||||
|
||||
setSelectedEntry({
|
||||
name: addressName,
|
||||
address: resolvedAddress,
|
||||
})
|
||||
// ENS-enabled resolve/validation
|
||||
if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
|
||||
const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue)
|
||||
|
||||
const validatedAddress = validateAddress(address)
|
||||
|
||||
if (!validatedAddress) {
|
||||
fieldMutator('')
|
||||
break
|
||||
}
|
||||
|
||||
const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
break
|
||||
}
|
||||
|
||||
// ETH address validation
|
||||
const validatedAddress = validateAddress(normalizedValue)
|
||||
|
||||
if (!validatedAddress) {
|
||||
fieldMutator('')
|
||||
break
|
||||
}
|
||||
|
||||
const newEntry =
|
||||
typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
setIsValidForm(addressErrorMessage === undefined)
|
||||
setValidationText(addressErrorMessage)
|
||||
fieldMutator(resolvedAddress)
|
||||
setIsValidAddress(addressErrorMessage === undefined)
|
||||
}
|
||||
|
||||
const labelStyles = useTextFieldLabelStyle()
|
||||
const inputStyles = useTextFieldInputStyle()
|
||||
|
||||
return (
|
||||
<Autocomplete<AddressBookEntry, false, false, true>
|
||||
closeIcon={null}
|
||||
openOnFocus={false}
|
||||
filterOptions={filterAddressEntries}
|
||||
freeSolo
|
||||
onChange={onChange}
|
||||
onInputChange={onInputChange}
|
||||
options={addressBookEntries}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
autoFocus={true}
|
||||
error={!!validationText}
|
||||
fullWidth
|
||||
id="filled-error-helper-text"
|
||||
variant="filled"
|
||||
label={validationText ? validationText : 'Recipient'}
|
||||
InputLabelProps={{ shrink: true, required: true, classes: labelStyles }}
|
||||
InputProps={{ ...params.InputProps, classes: inputStyles }}
|
||||
/>
|
||||
)}
|
||||
getOptionLabel={({ address }) => address}
|
||||
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showIdenticon />}
|
||||
role="listbox"
|
||||
style={{ display: 'flex', flexGrow: 1 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const AddressBookInput = (props: AddressBookProps): ReactElement => {
|
||||
const addressBookEntries = useSelector(addressBookSelector)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
|
||||
return (
|
||||
<BaseAddressBookInput
|
||||
addressBookEntries={addressBookEntries}
|
||||
setValidationText={setValidationText}
|
||||
validationText={validationText}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContractsAddressBookInput = ({
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
...props
|
||||
}: AddressBookProps): ReactElement => {
|
||||
const addressBookEntries = useSelector(addressBookSelector)
|
||||
const [filteredEntries, setFilteredEntries] = useState<AddressBookEntry[]>([])
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const filterAdbkContractAddresses = async (): Promise<void> => {
|
||||
if (!isCustomTx) {
|
||||
setADBKList(addressBook)
|
||||
return
|
||||
}
|
||||
|
||||
const filteredADBK = await filterAddressBookWithContractAddresses(addressBook)
|
||||
setADBKList(filteredADBK)
|
||||
const filterContractAddresses = async (): Promise<void> => {
|
||||
const filteredADBK = await filterContractAddressBookEntries(addressBookEntries)
|
||||
setFilteredEntries(filteredADBK)
|
||||
}
|
||||
filterAdbkContractAddresses()
|
||||
}, [addressBook, isCustomTx])
|
||||
filterContractAddresses()
|
||||
}, [addressBookEntries])
|
||||
|
||||
const labelStyling = textFieldLabelStyle()
|
||||
const txInputStyling = textFieldInputStyle()
|
||||
|
||||
let statusClasses = ''
|
||||
if (!isValidForm) {
|
||||
statusClasses = 'isInvalid'
|
||||
}
|
||||
if (isValidForm && inputTouched) {
|
||||
statusClasses = 'isValid'
|
||||
const onSetSelectedEntry = async (selectedEntry) => {
|
||||
if (selectedEntry?.address) {
|
||||
// verify if `address` is a contract
|
||||
const contractAddressErrorMessage = await mustBeEthereumContractAddress(selectedEntry.address)
|
||||
setIsValidAddress(!contractAddressErrorMessage)
|
||||
setValidationText(contractAddressErrorMessage ?? '')
|
||||
setSelectedEntry(selectedEntry)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
closeIcon={null}
|
||||
openOnFocus={false}
|
||||
filterOptions={(optionsArray, { inputValue }) =>
|
||||
optionsArray.filter((item) => {
|
||||
const inputLowerCase = inputValue.toLowerCase()
|
||||
const foundName = item.name.toLowerCase().includes(inputLowerCase)
|
||||
const foundAddress = item.address?.toLowerCase().includes(inputLowerCase)
|
||||
return foundName || foundAddress
|
||||
})
|
||||
}
|
||||
freeSolo
|
||||
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
|
||||
id="free-solo-demo"
|
||||
onChange={(_, value: AddressBookEntry) => {
|
||||
let address = ''
|
||||
let name = ''
|
||||
if (value) {
|
||||
address = value.address
|
||||
name = value.name
|
||||
}
|
||||
setSelectedEntry({ address, name })
|
||||
fieldMutator(address)
|
||||
}}
|
||||
onClose={() => setBlurred(true)}
|
||||
onOpen={() => {
|
||||
setSelectedEntry(null)
|
||||
setBlurred(false)
|
||||
}}
|
||||
open={!blurred}
|
||||
options={adbkList}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
// eslint-disable-next-line
|
||||
autoFocus={!blurred || pristine}
|
||||
error={!isValidForm}
|
||||
fullWidth
|
||||
id="filled-error-helper-text"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
required: true,
|
||||
classes: labelStyling,
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
classes: {
|
||||
...txInputStyling,
|
||||
},
|
||||
className: statusClasses,
|
||||
}}
|
||||
label={!isValidForm ? validationText : 'Recipient'}
|
||||
onChange={(event) => {
|
||||
setInputTouched(true)
|
||||
onAddressInputChanged(event.target.value)
|
||||
}}
|
||||
value={{ address: inputAddValue }}
|
||||
variant="filled"
|
||||
/>
|
||||
)}
|
||||
renderOption={(adbkEntry) => {
|
||||
const { address, name } = adbkEntry
|
||||
|
||||
if (!address) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.itemOptionList}>
|
||||
<div className={classes.identicon}>
|
||||
<Identicon address={address} diameter={32} />
|
||||
</div>
|
||||
<div className={classes.adbkEntryName}>
|
||||
<span>{name}</span>
|
||||
<span>{address}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
role="listbox"
|
||||
style={{ display: 'flex', flexGrow: 1 }}
|
||||
value={{ address: inputAddValue, name: '' }}
|
||||
/>
|
||||
</>
|
||||
<BaseAddressBookInput
|
||||
addressBookEntries={filteredEntries}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={onSetSelectedEntry}
|
||||
setValidationText={setValidationText}
|
||||
validationText={validationText}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressBookInput
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import { createStyles } from '@material-ui/core'
|
||||
import { createStyles, makeStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
itemOptionList: {
|
||||
display: 'flex',
|
||||
},
|
||||
export const useTextFieldLabelStyle = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
fontSize: '15px',
|
||||
width: '500px',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
adbkEntryName: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontSize: '14px',
|
||||
},
|
||||
identicon: {
|
||||
display: 'flex',
|
||||
padding: '5px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
})
|
||||
export const useTextFieldInputStyle = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
width: '420px',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState } from 'react'
|
|||
import { useFormState, useField } from 'react-final-form'
|
||||
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import {
|
||||
|
@ -82,11 +82,10 @@ const EthAddressInput = ({
|
|||
validate={validate}
|
||||
/>
|
||||
) : (
|
||||
<AddressBookInput
|
||||
<ContractsAddressBookInput
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
setIsValidAddress={() => {}}
|
||||
fieldMutator={onScannedValue}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -25,7 +25,7 @@ import Row from 'src/components/layout/Row'
|
|||
import ScanQRModal from 'src/components/ScanQRModal'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
||||
import ArrowDown from '../../assets/arrow-down.svg'
|
||||
|
@ -147,9 +147,13 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
|||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry(null)
|
||||
if (e.key === 'Tab') {
|
||||
return
|
||||
}
|
||||
setSelectedEntry(null)
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedEntry(null)
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
|
@ -193,9 +197,8 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
|||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
<ContractsAddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
|
@ -19,17 +20,17 @@ import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
|||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { CollectibleSelectField } from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/CollectibleSelectField'
|
||||
import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/TokenSelectField'
|
||||
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { NFTToken } from 'src/logic/collectibles/sources/collectibles'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
import ArrowDown from 'src/routes/safe/components/Balances/SendModal/screens/assets/arrow-down.svg'
|
||||
|
||||
import { CollectibleSelectField } from './CollectibleSelectField'
|
||||
import { styles } from './style'
|
||||
import { NFTToken } from 'src/logic/collectibles/sources/collectibles'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import TokenSelectField from './TokenSelectField'
|
||||
|
||||
const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
|
@ -71,9 +72,27 @@ const SendCollectible = ({
|
|||
const nftAssets = useSelector(safeActiveSelectorMap)
|
||||
const nftTokens = useSelector(nftTokensSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {
|
||||
const defaultEntry = { address: '', name: '' }
|
||||
|
||||
// if there's nothing to lookup for, we return the default entry
|
||||
if (!initialValues?.recipientAddress && !recipientAddress) {
|
||||
return defaultEntry
|
||||
}
|
||||
|
||||
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
|
||||
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
|
||||
const addressBookEntry = addressBook.find(({ address }) => {
|
||||
return sameAddress(predefinedAddress, address)
|
||||
})
|
||||
|
||||
// if found in the Address Book, then we return the entry
|
||||
if (addressBookEntry) {
|
||||
return addressBookEntry
|
||||
}
|
||||
|
||||
// otherwise we return the default entry
|
||||
return defaultEntry
|
||||
})
|
||||
const [pristine, setPristine] = useState(true)
|
||||
const [isValidAddress, setIsValidAddress] = useState(false)
|
||||
|
@ -123,7 +142,7 @@ const SendCollectible = ({
|
|||
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
|
||||
mutators.setRecipient(scannedAddress)
|
||||
setSelectedEntry({
|
||||
name: scannedName,
|
||||
name: scannedName ?? '',
|
||||
address: scannedAddress,
|
||||
})
|
||||
closeQrModal()
|
||||
|
@ -151,9 +170,13 @@ const SendCollectible = ({
|
|||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry({ address: '', name: 'string' })
|
||||
if (e.key === 'Tab') {
|
||||
return
|
||||
}
|
||||
setSelectedEntry({ address: '', name: '' })
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedEntry({ address: '', name: '' })
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
|
@ -200,7 +223,6 @@ const SendCollectible = ({
|
|||
<AddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
pristine={pristine}
|
||||
recipientAddress={recipientAddress}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
|
|
|
@ -3,16 +3,14 @@ import InputAdornment from '@material-ui/core/InputAdornment'
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { OnChange } from 'react-final-form-listeners'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { composeValidators, maxValue, minValue, mustBeFloat, required } from 'src/components/forms/validator'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
|
@ -23,9 +21,10 @@ import Row from 'src/components/layout/Row'
|
|||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
|
||||
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
@ -33,7 +32,7 @@ import { sm } from 'src/theme/variables'
|
|||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
|
@ -75,15 +74,32 @@ const SendFunds = ({
|
|||
const classes = useStyles()
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {
|
||||
const defaultEntry = { address: '', name: '' }
|
||||
|
||||
// if there's nothing to lookup for, we return the default entry
|
||||
if (!initialValues?.recipientAddress && !recipientAddress) {
|
||||
return defaultEntry
|
||||
}
|
||||
|
||||
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
|
||||
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
|
||||
const addressBookEntry = addressBook.find(({ address }) => {
|
||||
return sameAddress(predefinedAddress, address)
|
||||
})
|
||||
|
||||
// if found in the Address Book, then we return the entry
|
||||
if (addressBookEntry) {
|
||||
return addressBookEntry
|
||||
}
|
||||
|
||||
// otherwise we return the default entry
|
||||
return defaultEntry
|
||||
})
|
||||
const [pristine, setPristine] = useState(true)
|
||||
const [isValidAddress, setIsValidAddress] = useState(false)
|
||||
|
||||
React.useMemo(() => {
|
||||
useEffect(() => {
|
||||
if (selectedEntry === null && pristine) {
|
||||
setPristine(false)
|
||||
}
|
||||
|
@ -152,9 +168,13 @@ const SendFunds = ({
|
|||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry({ address: '', name: 'string' })
|
||||
if (e.key === 'Tab') {
|
||||
return
|
||||
}
|
||||
setSelectedEntry({ address: '', name: '' })
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedEntry({ address: '', name: '' })
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
|
@ -165,52 +185,29 @@ const SendFunds = ({
|
|||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={selectedEntry.address} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Block>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry({ address: '', name: 'string' })}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.name}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry({ address: '', name: 'string' })}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.address}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<CopyBtn content={selectedEntry.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(selectedEntry.address)} />
|
||||
</Block>
|
||||
</Col>
|
||||
<EthHashInfo
|
||||
hash={selectedEntry.address}
|
||||
name={selectedEntry.name}
|
||||
showIdenticon
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(selectedEntry.address)}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
pristine={pristine}
|
||||
recipientAddress={recipientAddress}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
pristine={pristine}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
|
@ -256,15 +253,7 @@ const SendFunds = ({
|
|||
maxValue(selectedTokenRecord?.balance || 0),
|
||||
)}
|
||||
/>
|
||||
<OnChange name="token">
|
||||
{() => {
|
||||
setSelectedEntry({
|
||||
name: selectedEntry?.name,
|
||||
address: selectedEntry?.address,
|
||||
})
|
||||
mutators.onTokenChange()
|
||||
}}
|
||||
</OnChange>
|
||||
<OnChange name="token">{() => mutators.onTokenChange()}</OnChange>
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
|
|
|
@ -93,8 +93,8 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }
|
|||
<Paragraph color="disabled" noMargin size="md" style={{ marginLeft: sm, marginRight: sm }}>
|
||||
{ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={safeAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Row>
|
||||
</Block>
|
||||
|
|
|
@ -29,77 +29,142 @@ interface TxData {
|
|||
upgradeTx?: boolean
|
||||
}
|
||||
|
||||
export const getTxData = (tx: Transaction): TxData => {
|
||||
const getTxDataForModifySettingsTxs = (tx: Transaction): TxData => {
|
||||
const txData: TxData = {}
|
||||
|
||||
if (tx.decodedParams) {
|
||||
if (tx.isTokenTransfer) {
|
||||
const { to } = tx.decodedParams.transfer || {}
|
||||
txData.recipient = to
|
||||
txData.isTokenTransfer = true
|
||||
} else if (tx.isCollectibleTransfer) {
|
||||
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
|
||||
const { to, value } = safeTransferFrom || transferFrom || transfer || {}
|
||||
txData.recipient = to
|
||||
txData.tokenId = value
|
||||
txData.isCollectibleTransfer = true
|
||||
} else if (tx.modifySettingsTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.modifySettingsTx = true
|
||||
if (!tx.modifySettingsTx || !tx.decodedParams) {
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.REMOVE_OWNER
|
||||
txData.removedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]) {
|
||||
const { _threshold } = tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.CHANGE_THRESHOLD
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD
|
||||
txData.addedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]) {
|
||||
const { newOwner, oldOwner } = tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.SWAP_OWNER
|
||||
txData.removedOwner = oldOwner
|
||||
txData.addedOwner = newOwner
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.ENABLE_MODULE
|
||||
txData.module = module
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.DISABLE_MODULE
|
||||
txData.module = module
|
||||
}
|
||||
} else if (tx.multiSendTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
} else {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
}
|
||||
} else if (tx.customTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
} else if (Number(tx.value) > 0) {
|
||||
txData.recipient = tx.recipient
|
||||
} else if (tx.isCancellationTx) {
|
||||
txData.cancellationTx = true
|
||||
} else if (tx.creationTx) {
|
||||
txData.creationTx = true
|
||||
} else if (tx.upgradeTx) {
|
||||
txData.upgradeTx = true
|
||||
txData.data = `The contract of this Safe is upgraded to Version ${getSafeVersion(tx.data)}`
|
||||
} else {
|
||||
txData.recipient = tx.recipient
|
||||
txData.recipient = tx.recipient
|
||||
txData.modifySettingsTx = true
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.REMOVE_OWNER
|
||||
txData.removedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
|
||||
return txData
|
||||
}
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]) {
|
||||
const { _threshold } = tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.CHANGE_THRESHOLD
|
||||
txData.newThreshold = _threshold
|
||||
return txData
|
||||
}
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD
|
||||
txData.addedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]) {
|
||||
const { newOwner, oldOwner } = tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.SWAP_OWNER
|
||||
txData.removedOwner = oldOwner
|
||||
txData.addedOwner = newOwner
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.ENABLE_MODULE
|
||||
txData.module = module
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.DISABLE_MODULE
|
||||
txData.module = module
|
||||
return txData
|
||||
}
|
||||
|
||||
return txData
|
||||
}
|
||||
|
||||
const getTxDataForTxsWithDecodedParams = (tx: Transaction): TxData => {
|
||||
const txData: TxData = {}
|
||||
|
||||
if (!tx.decodedParams) {
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.isTokenTransfer) {
|
||||
const { to } = tx.decodedParams.transfer || {}
|
||||
txData.recipient = to
|
||||
txData.isTokenTransfer = true
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.isCollectibleTransfer) {
|
||||
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
|
||||
const { to, value } = safeTransferFrom || transferFrom || transfer || {}
|
||||
txData.recipient = to
|
||||
txData.tokenId = value
|
||||
txData.isCollectibleTransfer = true
|
||||
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.modifySettingsTx) {
|
||||
return getTxDataForModifySettingsTxs(tx)
|
||||
}
|
||||
|
||||
if (tx.multiSendTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
return txData
|
||||
}
|
||||
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
|
||||
return txData
|
||||
}
|
||||
|
||||
// @todo (agustin) this function does not makes much sense
|
||||
// it should be refactored to simplify unnecessary if's checks and re-asigning props to the txData object
|
||||
export const getTxData = (tx: Transaction): TxData => {
|
||||
const txData: TxData = {}
|
||||
|
||||
if (tx.decodedParams) {
|
||||
return getTxDataForTxsWithDecodedParams(tx)
|
||||
}
|
||||
|
||||
if (tx.customTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
return txData
|
||||
}
|
||||
if (Number(tx.value) > 0) {
|
||||
txData.recipient = tx.recipient
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.isCancellationTx) {
|
||||
txData.cancellationTx = true
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.creationTx) {
|
||||
txData.creationTx = true
|
||||
return txData
|
||||
}
|
||||
|
||||
if (tx.upgradeTx) {
|
||||
txData.upgradeTx = true
|
||||
txData.data = `The contract of this Safe is upgraded to Version ${getSafeVersion(tx.data)}`
|
||||
|
||||
return txData
|
||||
}
|
||||
txData.recipient = tx.recipient
|
||||
|
||||
return txData
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createMuiTheme } from '@material-ui/core/styles'
|
||||
import { rgba } from 'polished'
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
||||
|
||||
import {
|
||||
boldFont,
|
||||
|
@ -407,7 +407,7 @@ const theme = createMuiTheme({
|
|||
MuiCheckbox: {
|
||||
colorSecondary: {
|
||||
'&$disabled': {
|
||||
color: rgba(secondary, 0.5),
|
||||
color: fade(secondary, 0.5),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import GoogleAnalytics, { EventArgs } from 'react-ga'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
|
||||
import { getGoogleAnalyticsTrackingID } from 'src/config'
|
||||
import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
|
||||
|
@ -15,11 +16,17 @@ export const loadGoogleAnalytics = (): void => {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('Loading google analytics...')
|
||||
const trackingID = getGoogleAnalyticsTrackingID()
|
||||
const networkInfo = getNetworkInfo()
|
||||
if (!trackingID) {
|
||||
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
|
||||
} else {
|
||||
GoogleAnalytics.initialize(trackingID)
|
||||
GoogleAnalytics.set({ anonymizeIp: true })
|
||||
GoogleAnalytics.set({
|
||||
anonymizeIp: true,
|
||||
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
|
||||
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
|
||||
appVersion: process.env.REACT_APP_APP_VERSION,
|
||||
})
|
||||
analyticsLoaded = true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue