Merge pull request #1597 from gnosis/release/v2.15.0

Release v2.15.0
This commit is contained in:
Daniel Sanchez 2020-11-16 13:31:25 +01:00 committed by GitHub
commit 8ff417e99b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 3098 additions and 1887 deletions

View File

@ -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

360
docs/networks.md Normal file
View File

@ -0,0 +1,360 @@
# Network Configuration
## Network configuration structure
We have currently this structure for the network configuration:
- This is the main configuration that you need to provide in order to add a new network.
```typescript
export interface NetworkConfig {
network: NetworkSettings
disabledFeatures?: SafeFeatures
disabledWallets?: Wallets
environment: SafeEnvironments
}
```
#### NetworkSettings
- It contains the Ethereum compatible network id, the network name, information about the native coin of that network and a boolean to indicate if the network is a testnet or a production network.
```typescript
export type NetworkSettings = {
id: ETHEREUM_NETWORK,
backgroundColor: string,
textColor: string,
label: string,
isTestNet: boolean,
nativeCoin: Token,
}
```
- Currently supported Ethereum compatible networks:
```typescript
export enum ETHEREUM_NETWORK {
MAINNET = 1,
MORDEN = 2,
ROPSTEN = 3,
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
XDAI = 100,
ENERGY_WEB_CHAIN = 246,
VOLTA = 73799,
UNKNOWN = 0,
LOCAL = 4447,
}
```
- This is the structure to define the native coin:
```typescript
type Token = {
address: string
name: string
symbol: string
decimals: number
logoUri?: string
}
```
#### SafeFeatures
It's an array that contains a list of features that should be disabled for the network. It's empty by default.
```typescript
export type SafeFeatures = FEATURES[]
export enum FEATURES {
ERC721 = 'ERC721',
ERC1155 = 'ERC1155',
SAFE_APPS = 'SAFE_APPS',
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
ENS_LOOKUP = 'ENS_LOOKUP'
}
```
#### Wallets
It's an array that contains a list of wallets that will be disabled for the network. It's empty by default.
```typescript
export type Wallets = WALLETS[]
```
```typescript
export enum WALLETS {
METAMASK = 'metamask',
WALLET_CONNECT = 'walletConnect',
TREZOR = 'trezor',
LEDGER = 'ledger',
TRUST = 'trust',
DAPPER = 'dapper',
FORTMATIC = 'fortmatic',
PORTIS = 'portis',
AUTHEREUM = 'authereum',
TORUS = 'torus',
UNILOGIN = 'unilogin',
COINBASE = 'coinbase',
WALLET_LINK = 'walletLink',
OPERA = 'opera',
OPERA_TOUCH = 'operaTouch'
}
```
#### SafeEnviroments
If the network has different enviroments, you can add them here, otherwise you should only add production settings
```typescript
type SafeEnvironments = {
dev?: EnvironmentSettings
staging?: EnvironmentSettings
production: EnvironmentSettings
}
```
We use a transaction service (**txServiceUrl**) to fetch transactions and balances of each safe and also to POST messages with the created transactions, this should be provided by Gnosis.
The **networkExplorer** parameters are used to provide information related to the networkExplorer used for the given network (Blockscout for xDai, Etherscan for mainnet, etc). This is used for link transaction hashes and addresses to the given network explorer.
```typescript
export type EnvironmentSettings = GasPrice & {
txServiceUrl: string
relayApiUrl?: string
safeAppsUrl: string
rpcServiceUrl: string
networkExplorerName: string
networkExplorerUrl: string
networkExplorerApiUrl: string
}
```
The **gasPrice** is used to indicate a fixed amount for some networks (like xDai), otherwise you can provide an oracle we can use to fetch the current gas price.
```typescript
type GasPrice = {
gasPrice: number
gasPriceOracle?: GasPriceOracle
} | {
gasPrice?: number
// for infura there's a REST API Token required stored in: `REACT_APP_INFURA_TOKEN`
gasPriceOracle: GasPriceOracle
}
```
```typescript
export type GasPriceOracle = {
url: string
// Different gas api providers can use a different name to reflect different gas levels based on tx speed
// For example in ethGasStation for ETHEREUM_MAINNET = safeLow | average | fast
gasParameter: string
}
```
### Enviroment variables:
- **REACT_APP_NETWORK**: name of the used network (ex: xDai, mainnet, rinkeby)
- **REACT_APP_GOOGLE_ANALYTICS**: Used for enabling google analytics
- **REACT_APP_PORTIS_ID**: Portis ID for enabling it on given network
- **REACT_APP_FORTMATIC_KEY**: Formatic yey for given network
- **REACT_APP_BLOCKNATIVE_KEY**: Blocknative key for given network
---
## How to add a network
1) In case that it is not already supported, add the network on the **ETHEREUM_NETWORK** enum in [`src/config/networks/network.d.ts`](/src/config/networks/network.d.ts)
```typescript
export enum ETHEREUM_NETWORK {
MAINNET = 1,
MORDEN = 2,
ROPSTEN = 3,
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
XDAI = 100,
ENERGY_WEB_CHAIN = 246,
VOLTA = 73799,
UNKNOWN = 0,
LOCAL = 4447,
}
```
2) Add **env variables**:
* REACT_APP_NETWORK
* REACT_APP_GOOGLE_ANALYTICS
* REACT_APP_PORTIS_ID
* REACT_APP_FORTMATIC_KEY
* REACT_APP_BLOCKNATIVE_KEY
3) Add the **NetworkSettings** in [`src/config/networks`](/src/config/networks) as `<network_name>.ts`:
```typescript
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: '',
safeAppsUrl: '',
gasPriceOracleUrl: '',
gasPriceOracle: {
url: '',
gasParameter: '',
},
rpcServiceUrl: '',
networkExplorerName: '',
networkExplorerUrl: '',
networkExplorerApiUrl: '',
}
const rinkeby: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: '',
},
production: {
...baseConfig,
txServiceUrl: '',
safeAppsUrl: '',
},
},
network: {
id: ETHEREUM_NETWORK.<NETWORK_NAME>,
backgroundColor: '',
textColor: '',
label: '',
isTestNet: true/false,
nativeCoin: {
address: '',
name: '',
symbol: '',
decimals: 0,
logoUri: '',
},
},
disabledFeatures: [],
disabledWallets: []
}
export default <NETWORK_NAME>
```
## Configuration example (xDai) - fixed gas price
1) **ETHEREUM_NETWORK**
```typescript
export enum ETHEREUM_NETWORK {
MAINNET = 1,
MORDEN = 2,
ROPSTEN = 3,
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
XDAI = 100, -> ADDED XDAI
ENERGY_WEB_CHAIN = 246,
VOLTA = 73799,
UNKNOWN = 0,
LOCAL = 4447,
}
```
2) **Network file** [xdai](/src/config/networks/xdai.ts)
```typescript
import { ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const xDai: NetworkConfig = {
environment: {
production: {
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
gasPrice: 1e9,
rpcServiceUrl: 'https://dai.poa.network/',
networkExplorerName: 'Blockscout',
networkExplorerUrl: 'https://blockscout.com/poa/xdai',
networkExplorerApiUrl: 'https://blockscout.com/poa/xdai/api',
},
},
network: {
id: ETHEREUM_NETWORK.XDAI,
backgroundColor: '#48A8A6',
textColor: '#ffffff',
label: 'xDai',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'xDai',
symbol: 'xDai',
decimals: 18,
logoUri: xDaiLogo,
},
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER
]
}
export default xDai
```
## Configuration example (Mainnet) - gas price retrieven by oracle
**Network file** [mainnet](/src/config/networks/mainnet.ts)
```typescript
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
url: 'https://ethgasstation.info/json/ethgasAPI.json',
gasParameter: 'average',
},
rpcServiceUrl: 'https://mainnet.infura.io:443/v3',
networkExplorerName: 'Etherscan',
networkExplorerUrl: 'https://etherscan.io',
networkExplorerApiUrl: 'https://api.etherscan.io/api',
}
const mainnet: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.MAINNET,
backgroundColor: '#E8E7E6',
textColor: '#001428',
label: 'Mainnet',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: EtherLogo,
},
}
}
export default mainnet
```

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.14.1",
"version": "2.15.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -168,20 +168,20 @@
"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",
"@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.13.2",
"bnc-onboard": "1.14.0",
"classnames": "^2.2.6",
"concurrently": "^5.3.0",
"connected-react-router": "6.8.0",
@ -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": "1.2.11",
"web3-core": "^1.2.11",
"web3-eth-contract": "^1.2.11",
"web3-utils": "^1.2.11"
},
"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",

View File

@ -101,6 +101,10 @@ Give an example
Add additional notes about how to deploy this on a live system
## Configuring the app for running on different networks
[Please check the network configuration documentation](./docs/networks.md)
## Built With
* [Truffle React Box](https://github.com/truffle-box/react-box) - The web framework used

View File

@ -0,0 +1,7 @@
<svg width="41px" height="41px" viewBox="0 0 41 41" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<!-- Generated by Pixelmator Pro 1.8 -->
<defs>
<image id="image" width="41px" height="41px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAKaADAAQAAAABAAAAKQAAAADAIIRfAAAFp0lEQVRYCdVYbUyVZRi+DwqEiimO9NBwwEnIlqwgk5D5AZsJLlrQ0rnVUFvrB8TmDwJqY8aYQA3b+tOf5mbFxjI/Go1+tMH6Ia0NB9iPwlMgoHw4VCZw+JDzdF2PvKeX46FzDnA8dG/Xnq/7ue/rfT7u53kfkf+BWBbJcT36xQNJQDqwA9gKbAZocwjoBX4HfgX+AP4CRoGAyzp4+Bi4DtwHnIDyAuqMAX8DpwB+YEDkaVj9CLgJeCPlrX0ANkg2Flg2eRGWOH3enPvbPgKbGQCXx6JlNXp+AkwC/hLwVZ+2a4EwwG9Zgx7fAL46W6red/DF9e6zcAS/BmaApTr3tf8D+LoAhANeheujEvDV+HLrfeaVIRReAiaCSHIKvvcCC0oMWgKxi/0d7XvgEWdmGWIqHEf+KVM5WNkn4fhdT865s/oBf786UPq3wWWTO1GeJoFyuFi7n5pJRqJgX4Eke8ApChCuyWcA3l6CJomvFagtL+x29x+NChsrSTIRWMtCsOTZw8Vi3Znl7p6nXjIrebq8ArgO+aioKMnMzJRt27ZJSEiI9PX1SUtLi/T29lJfoqOjpbCwUMLCwsRiscj09LR0dXVJY2Oj3LvH6IFjIzxcysvLdfnMmTO6bvv27XKsoED3YUVNba1Yc45jvVpk9ZpI2fTcy7LjnQ/VXXuH9F/5yeDDu+pX1P8Z0IsbBJTdbldOp1PBuZqamlKzs7Oqra3NtfiTkpLU2NiYejAzo8bHx5XD4VCU1tZWBXJaLzIyUo2OjqrOzk5Xv5ycHCf1Jicndf+EBJvKu2BXeeevq7d+vKXe/OGGeuN8l0p5v9J8R/2NBCldgIqLi1O3bt7UBkpLS1VaWppKTUlReXl5Kjs72+XMIHnp4kXdJzk5WZ09e5b+VWVlpVeSFRUVKj4+XoWGhqqITZudxOv1Hc6dH9Toctja9S5f4MW7p57uLczk5uaKNSZGysrKpLq6mlVa2q5eNbLz0vGJCenp6dF1J0+elKNHj8r+/fvn6Xgq3BkZke7ubt00MzKkp3ViuF9N3L4ljrmyqZ++xXNNakWMiGAwpLm52aTjW/bQoUOCkZH+fp4H/ssvFW9bnNM8tj0LdzfPa02QKTeLWfbt3SsOjJr7KKXt2iWc5qamJjl37pwMDAzI6dOnzV3n5zEAnsRqtcpAt12qK095auZ/lJ7uPqS29vZ2vfOysrIEm8DVwQLST0REuMhztLnPEmw2Qs8CNpjk5+dLR0eHq99DPVfx3wwiglkyMjJk48aNOoqY6+fy5KbjJH875fLly1qRa7KqqkrS09MlJSVFhyO2u0tDQ4MOQyUlJTo9dqzAXcVjOTY2VrAhZTfsFxcXS11dnQwODsrFS5c86WtubDgC6OHZsGGDDhsMP4bMINTAiELc1DqJiYmIAPdVfX29LiNe6j7UP3L4sELsVAxBiJnzQ1B2tg5Bhl2mhu1XDxx4OD1zPAw+SN8D9HT/iZT/xesYjPfs2SOpqanCL6YMDQ0JlwLXnFEuLCxyBXcG84MHDwrClDgmJ2XVqlWCWChFRUUyPkazD6Xz2jXLiRMndAH8BAS1bS6R4eFhQ82cOlBwrR/e3/i6sNDXBKue61Ff17iV+fTxLbDS5HsQ4r+5Sziag0CwRs3dL8nxFqTFCIoczS/n6lZCwksFb+ePyFbU8Avcv+pxlxnAbY+wM1XwasTz6XETM/xNw3eOic+C2ZogkvwCvucfSQvQ5OMRdxafPowvDHQ6C1+NQATgs/AX9zwQaHKG/Sb40tcynxnOKYYj5dsMn+cMY8udcg1yiv0aQejPE66PfcBdYLkJchdnA8smcbBUBfCQXSpZhrla4D/DDNoXLfxZ58vCDcCf1zfqss/ngN/vTT5tdxh2F5Llo8IOgLH1eYCHgbH4OZX8l7gGXAF4m+El5g7AmfBL/gFhgZO179JGyAAAAABJRU5ErkJggg=="/>
</defs>
<use id="image-1" xlink:href="#image" x="0px" y="0px" width="41px" height="41px"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -3,6 +3,7 @@ import metamaskIcon from './icon-metamask.png'
import walletConnectIcon from './icon-wallet-connect.svg'
import trezorIcon from './icon-trezor.svg'
import ledgerIcon from './icon-ledger.svg'
import latticeIcon from './icon-lattice.svg'
import dapperIcon from './icon-dapper.png'
import fortmaticIcon from './icon-fortmatic.svg'
import portisIcon from './icon-portis.svg'
@ -42,6 +43,10 @@ const WALLET_ICONS: WalletObjectsProps<IconValue> = {
src: ledgerIcon,
height: 25,
},
[WALLET_PROVIDER.LATTICE]: {
src: latticeIcon,
height: 41,
},
[WALLET_PROVIDER.DAPPER]: {
src: dapperIcon,
height: 25,

View File

@ -4,7 +4,7 @@ import React from 'react'
import Button from 'src/components/layout/Button'
import { getNetworkId } from 'src/config'
import { getWeb3, setWeb3 } from 'src/logic/wallets/getWeb3'
import { fetchProvider } from 'src/logic/wallets/store/actions'
import { fetchProvider, removeProvider } from 'src/logic/wallets/store/actions'
import transactionDataCheck from 'src/logic/wallets/transactionDataCheck'
import { getSupportedWallets } from 'src/logic/wallets/utils/walletList'
import { store } from 'src/store'
@ -39,6 +39,7 @@ export const onboard = Onboard({
if (!address && lastUsedAddress) {
lastUsedAddress = ''
providerName = undefined
store.dispatch(removeProvider())
}
},
},

View File

@ -8,7 +8,7 @@ import CopyBtn from 'src/components/CopyBtn'
import Block from 'src/components/layout/Block'
import Span from 'src/components/layout/Span'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import EllipsisTransactionDetails from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails'
import { EllipsisTransactionDetails } from 'src/routes/safe/components/AddressBook/EllipsisTransactionDetails'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { getExplorerInfo } from 'src/config'
@ -19,9 +19,16 @@ interface EtherscanLinkProps {
cut?: number
knownAddress?: boolean
value: string
sendModalOpenHandler?: () => void
}
export const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
export const EtherscanLink = ({
className,
cut,
knownAddress,
value,
sendModalOpenHandler,
}: EtherscanLinkProps): React.ReactElement => {
const classes = useStyles()
return (
@ -31,7 +38,13 @@ export const EtherscanLink = ({ className, cut, knownAddress, value }: Etherscan
</Span>
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
<ExplorerButton explorerUrl={getExplorerInfo(value)} />
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
{knownAddress !== undefined ? (
<EllipsisTransactionDetails
address={value}
knownAddress={knownAddress}
sendModalOpenHandler={sendModalOpenHandler}
/>
) : null}
</Block>
)
}

View File

@ -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
},
)

View File

@ -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

View File

@ -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,
@ -58,8 +58,12 @@ const mainnet: NetworkConfig = {
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM
]
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
}
export default mainnet

View File

@ -15,14 +15,16 @@ export enum WALLETS {
COINBASE = 'coinbase',
WALLET_LINK = 'walletLink',
OPERA = 'opera',
OPERA_TOUCH = 'operaTouch'
OPERA_TOUCH = 'operaTouch',
LATTICE = 'lattice',
}
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 = {

View File

@ -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,
@ -55,8 +55,12 @@ const mainnet: NetworkConfig = {
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM
]
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
}
export default mainnet

View File

@ -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,
@ -49,8 +49,12 @@ const xDai: NetworkConfig = {
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM
]
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
}
export default xDai

View File

@ -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
})

View File

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
@ -16,10 +16,18 @@ 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)))
})
export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens): NFTToken[] =>
userNftTokens.sort((a, b) => a.name.localeCompare(b.name)),
)
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,

View File

@ -0,0 +1,68 @@
import { getTransferMethodByContractAddress } from 'src/logic/collectibles/utils'
jest.mock('src/config', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual('src/config')
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
getNetworkId: jest.fn().mockReturnValue(4),
}
})
describe('getTransferMethodByContractAddress', () => {
const config = require('src/config')
afterAll(() => {
jest.unmock('src/config')
})
it(`should return "transfer" method, if CK address is provided for MAINNET`, () => {
// Given
config.getNetworkId.mockReturnValue(1)
const contractAddress = '0x06012c8cf97bead5deae237070f9587f8e7a266d'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('transfer')
})
it(`should return "transfer" method, if CK address is provided for RINKEBY`, () => {
// Given
config.getNetworkId.mockReturnValue(4)
const contractAddress = '0x16baf0de678e52367adc69fd067e5edd1d33e3bf'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('transfer')
})
it(`should return "0x42842e0e" method, if CK address is provided any other network`, () => {
// Given
config.getNetworkId.mockReturnValue(100)
const contractAddress = '0x06012c8cf97bead5deae237070f9587f8e7a266d'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('0x42842e0e')
})
it(`should return "0x42842e0e" method, if non-CK address is provided`, () => {
// Given
config.getNetworkId.mockReturnValue(4)
const contractAddress = '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('0x42842e0e')
})
})

View File

@ -0,0 +1,131 @@
import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { CollectibleTx } from 'src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible'
import { store } from 'src/store'
import { sameString } from 'src/utils/strings'
// CryptoKitties Contract Addresses by network
// This is an exception made for a popular NFT that's not ERC721 standard-compatible,
// so we can allow the user to transfer the assets by using `transferFrom` instead of
// the standard `safeTransferFrom` method.
export const CK_ADDRESS = {
[ETHEREUM_NETWORK.MAINNET]: '0x06012c8cf97bead5deae237070f9587f8e7a266d',
[ETHEREUM_NETWORK.RINKEBY]: '0x16baf0de678e52367adc69fd067e5edd1d33e3bf',
}
// safeTransferFrom(address,address,uint256)
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
/**
* Verifies that a tx received by the transaction service is an ERC721 token-related transaction
* @param {TxServiceModel} tx
* @returns boolean
*/
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
}
/**
* Returns the symbol of the provided ERC721 contract
* @param {string} contractAddress
* @returns Promise<string>
*/
export const getERC721Symbol = async (contractAddress: string): Promise<string> => {
let tokenSymbol = 'UNKNOWN'
try {
const ERC721token = await getERC721TokenContract()
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 (sameAddress(contractAddress, ENS_TOKEN_CONTRACT)) {
return 'ENS'
}
console.error(`Failed to retrieve token symbol for ERC721 token ${contractAddress}`)
}
return tokenSymbol
}
/**
* Verifies if the provided contract is a valid ERC721
* @param {string} contractAddress
* @returns boolean
*/
export const isERC721Contract = async (contractAddress: string): Promise<boolean> => {
const ERC721Token = await getStandardTokenContract()
let isERC721 = false
try {
await ERC721Token.at(contractAddress)
isERC721 = true
} catch (error) {
console.warn('Asset not found')
}
return isERC721
}
/**
* Returns a method identifier based on the asset specified and the current network
* @param {string} contractAddress
* @returns string
*/
export const getTransferMethodByContractAddress = (contractAddress: string): string => {
if (sameAddress(contractAddress, CK_ADDRESS[getNetworkId()])) {
// on mainnet `transferFrom` seems to work fine but we can assure that `transfer` will work on both networks
// so that's the reason why we're falling back to `transfer` for CryptoKitties
return 'transfer'
}
return `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}`
}
/**
* Builds the encodedABI data for the transfer of an NFT token
* @param {CollectibleTx} tx
* @param {string} safeAddress
* @returns Promise<string>
*/
export const generateERC721TransferTxData = async (
tx: CollectibleTx,
safeAddress: string | undefined,
): Promise<string> => {
if (!safeAddress) {
throw new Error('Failed to build NFT transfer tx data. SafeAddress is not defined.')
}
const methodToCall = getTransferMethodByContractAddress(tx.assetAddress)
let transferParams = [tx.recipientAddress, tx.nftTokenId]
let NFTTokenContract
if (methodToCall.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) {
// we add the `from` param for the `safeTransferFrom` method call
transferParams = [safeAddress, ...transferParams]
NFTTokenContract = await getERC721TokenContract()
} else {
// we fallback to an ERC20 Token contract whose ABI implements the `transfer` method
NFTTokenContract = await getStandardTokenContract()
}
const tokenInstance = await NFTTokenContract.at(tx.assetAddress)
return tokenInstance.contract.methods[methodToCall](...transferParams).encodeABI()
}

View File

@ -35,7 +35,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
return {
method: METHOD_TO_ID[methodId],
parameters: [
{ name: 'oldOwner', type: 'address', value: decodedParameters[1] },
{ name: 'owner', type: 'address', value: decodedParameters[1] },
{ name: '_threshold', type: 'uint', value: decodedParameters[2] },
],
}

View File

@ -273,15 +273,16 @@ describe('isOutgoingTransaction', () => {
})
})
jest.mock('src/logic/collectibles/utils')
jest.mock('src/logic/tokens/utils/tokenHelpers')
describe('isCustomTransaction', () => {
afterAll(() => {
jest.unmock('src/logic/collectibles/utils')
jest.unmock('src/logic/tokens/utils/tokenHelpers')
})
it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
const txCode = ''
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
@ -293,23 +294,23 @@ describe('isCustomTransaction', () => {
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
// when
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
const txCode = ''
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
@ -321,24 +322,24 @@ describe('isCustomTransaction', () => {
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
// when
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
it('It should return false if is outgoing transaction, not SendERC20Transaction, isUpgradeTransaction and not isSendERC721Transaction', async () => {
// given
const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000`
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData })
const txCode = ''
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
@ -350,13 +351,14 @@ describe('isCustomTransaction', () => {
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => true)
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.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 +367,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',
@ -377,18 +378,19 @@ describe('isCustomTransaction', () => {
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
txHelpers.isSendERC721Transaction.mockImplementationOnce(() => true)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => true)
// when
const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens)
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
})
@ -839,11 +841,9 @@ describe('buildTx', () => {
const txResult = await buildTx({
cancellationTxs,
currentUser: userAddress,
knownTokens,
outgoingTxs,
safe: safeInstance,
tx: transaction,
txCode: undefined,
})
// then

View File

@ -1,6 +1,4 @@
import { push } from 'connected-react-router'
import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies'
import { ThunkAction } from 'redux-thunk'
@ -24,10 +22,11 @@ import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { removeTransaction } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import {
removeTxFromStore,
storeSignedTx,
storeExecutedTx,
} from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import {
generateSafeTxHash,
mockTransaction,
@ -35,68 +34,13 @@ import {
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions'
import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { AnyAction } from 'redux'
import { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store'
import { Dispatch, DispatchReturn } from './types'
export const removeTxFromStore = (
tx: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): void => {
if (tx.isCancellationTx) {
const newTxStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
const transactions = safeTransactionsSelector(state)
const txsToUpdate = transactions
.filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
.withMutations((list) => list.map((tx) => tx.set('status', newTxStatus)))
batch(() => {
dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
dispatch(removeCancellationTransaction({ safeAddress, transaction: tx }))
})
} else {
dispatch(removeTransaction({ safeAddress, transaction: tx }))
}
}
export const storeTx = async (
tx: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): Promise<void> => {
if (tx.isCancellationTx) {
let newTxStatus: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
if (tx.isExecuted) {
newTxStatus = TransactionStatus.CANCELLED
} else if (tx.status === TransactionStatus.PENDING) {
newTxStatus = tx.status
}
const transactions = safeTransactionsSelector(state)
const txsToUpdate = transactions
.filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
.withMutations((list) =>
list.map((tx) => tx.set('status', newTxStatus).set('cancelled', newTxStatus === TransactionStatus.CANCELLED)),
)
batch(() => {
dispatch(addOrUpdateCancellationTransactions({ safeAddress, transactions: Map({ [`${tx.nonce}`]: tx }) }))
dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
})
} else {
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([tx]) }))
}
}
interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean
notifiedTransaction: string
@ -228,15 +172,7 @@ const createTransaction = (
await Promise.all([
saveTxToHistory({ ...txArgs, txHash, origin }),
storeTx(
mockedTx.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
),
safeAddress,
dispatch,
state,
),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
@ -263,29 +199,8 @@ const createTransaction = (
),
)
const toStoreTx = isExecution
? mockedTx.withMutations((record) => {
record
.set('executionTxHash', receipt.transactionHash)
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set('status', receipt.status ? TransactionStatus.SUCCESS : TransactionStatus.FAILED)
})
: mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
await storeTx(
toStoreTx.withMutations((record) => {
record
.set('confirmations', List([makeConfirmation({ owner: from })]))
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(from),
)
}),
safeAddress,
dispatch,
state,
)
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash

View File

@ -1,3 +1,5 @@
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
import semverSatisfies from 'semver/functions/satisfies'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
@ -6,27 +8,41 @@ import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSign
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import {
isCancelTransaction,
mockTransaction,
TxToMock,
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { mockTransaction, TxToMock } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { AppReduxState } from 'src/store'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { storeTx } from './createTransaction'
import { TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async (
dispatch,
getState,
) => {
import { Dispatch, DispatchReturn } from './types'
interface ProcessTransactionArgs {
approveAndExecute: boolean
notifiedTransaction: string
safeAddress: string
tx: Transaction
userAddress: string
}
type ProcessTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
const processTransaction = ({
approveAndExecute,
notifiedTransaction,
safeAddress,
tx,
userAddress,
}: ProcessTransactionArgs): ProcessTransactionAction => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<DispatchReturn> => {
const state = getState()
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
@ -57,7 +73,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
safeInstance,
to: tx.recipient,
valueInWei: tx.value,
data: tx.data,
data: tx.data ?? EMPTY_DATA,
operation: tx.operation,
nonce: tx.nonce,
safeTxGas: tx.safeTxGas,
@ -121,30 +137,18 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
try {
await Promise.all([
saveTxToHistory({ ...txArgs, txHash }),
storeTx(
mockedTx.withMutations((record) => {
record
.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
)
.set('status', TransactionStatus.PENDING)
}),
safeAddress,
dispatch,
state,
),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
dispatch(closeSnackbarAction(pendingExecutionKey))
await storeTx(tx, safeAddress, dispatch, state)
await storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error(e)
}
})
.on('error', (error) => {
dispatch(closeSnackbarAction(pendingExecutionKey))
storeTx(tx, safeAddress, dispatch, state)
storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
@ -160,43 +164,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
),
)
const toStoreTx = isExecution
? mockedTx.withMutations((record) => {
record
.set('executionTxHash', receipt.transactionHash)
.set('blockNumber', receipt.blockNumber)
.set('executionDate', record.submissionDate)
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set(
'status',
receipt.status
? isCancelTransaction(record, safeAddress)
? TransactionStatus.CANCELLED
: TransactionStatus.SUCCESS
: TransactionStatus.FAILED,
)
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
.updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear())
})
: mockedTx.withMutations((record) => {
record
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(),
)
.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
})
await storeTx(
toStoreTx.update('confirmations', (confirmations) => {
const index = confirmations.findIndex(({ owner }) => owner === from)
return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations
}),
safeAddress,
dispatch,
state,
)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
dispatch(fetchTransactions(safeAddress))

View File

@ -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,
})

View File

@ -0,0 +1,169 @@
import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import { TransactionReceipt } from 'web3-core'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { removeTransaction } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
import { Transaction, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { AppReduxState } from 'src/store'
type SetPendingTransactionParams = {
transaction: Transaction
from: string
}
const setTxStatusAsPending = ({ transaction, from }: SetPendingTransactionParams): Transaction =>
transaction.withMutations((transaction) => {
transaction
// setting user as the one who has triggered the tx
// this allows to display the owner's "pending" status
.updateIn(['ownersWithPendingActions', transaction.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.push(from),
)
// global transaction status
.set('status', TransactionStatus.PENDING)
})
type SetOptimisticTransactionParams = {
transaction: Transaction
from: string
isExecution: boolean
receipt: TransactionReceipt
}
const updateTxBasedOnReceipt = ({
transaction,
from,
isExecution,
receipt,
}: SetOptimisticTransactionParams): Transaction => {
const txToStore = isExecution
? transaction.withMutations((tx) => {
tx.set('executionTxHash', receipt.transactionHash)
.set('blockNumber', receipt.blockNumber)
.set('executionDate', tx.submissionDate)
.set('fee', web3ReadOnly.utils.toWei(`${receipt.gasUsed}`, 'gwei'))
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set('status', receipt.status ? TransactionStatus.SUCCESS : TransactionStatus.FAILED)
})
: transaction.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
return txToStore.withMutations((tx) => {
const senderHasAlreadyConfirmed = tx.confirmations.findIndex(({ owner }) => sameAddress(owner, from)) !== -1
if (!senderHasAlreadyConfirmed) {
// updates confirmations status
tx.update('confirmations', (confirmations) => confirmations.push(makeConfirmation({ owner: from })))
}
tx.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear()).updateIn(
['ownersWithPendingActions', 'confirm'],
(prev) => prev.clear(),
)
})
}
type StoreTxParams = {
transaction: Transaction
safeAddress: string
dispatch: Dispatch
state: AppReduxState
}
export const storeTx = async ({ transaction, safeAddress, dispatch, state }: StoreTxParams): Promise<void> => {
if (transaction.isCancellationTx) {
// `transaction` is the Cancellation tx
// So we need to decide the `status` for the main transaction this `transaction` is cancelling
let status: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
// `cancelled`, will become true if its corresponding Cancellation tx was successfully executed
let cancelled = false
switch (transaction.status) {
case TransactionStatus.SUCCESS:
status = TransactionStatus.CANCELLED
cancelled = true
break
case TransactionStatus.PENDING:
status = TransactionStatus.PENDING
break
default:
break
}
const safeTransactions = safeTransactionsSelector(state)
const transactions = safeTransactions.withMutations((txs) => {
const txIndex = txs.findIndex(({ nonce }) => Number(nonce) === Number(transaction.nonce))
txs.update(txIndex, (tx) => tx.set('status', status).set('cancelled', cancelled))
})
batch(() => {
dispatch(
addOrUpdateCancellationTransactions({
safeAddress,
transactions: Map({ [`${transaction.nonce}`]: transaction }),
}),
)
dispatch(addOrUpdateTransactions({ safeAddress, transactions }))
})
} else {
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([transaction]) }))
}
}
type StoreSignedTxParams = StoreTxParams & {
from: string
isExecution: boolean
}
export const storeSignedTx = ({ transaction, from, isExecution, ...rest }: StoreSignedTxParams): Promise<void> =>
storeTx({
transaction: isExecution ? setTxStatusAsPending({ transaction, from }) : transaction,
...rest,
})
type StoreExecParams = StoreTxParams & {
from: string
isExecution: boolean
safeAddress: string
receipt: TransactionReceipt
}
export const storeExecutedTx = ({ safeAddress, dispatch, state, ...rest }: StoreExecParams): Promise<void> =>
storeTx({
transaction: updateTxBasedOnReceipt({ ...rest }),
safeAddress,
dispatch,
state,
})
export const removeTxFromStore = (
transaction: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): void => {
if (transaction.isCancellationTx) {
const safeTransactions = safeTransactionsSelector(state)
const transactions = safeTransactions.withMutations((txs) => {
const txIndex = txs.findIndex(({ nonce }) => Number(nonce) === Number(transaction.nonce))
txs[txIndex].set('status', TransactionStatus.AWAITING_YOUR_CONFIRMATION)
})
batch(() => {
dispatch(addOrUpdateTransactions({ safeAddress, transactions }))
dispatch(removeCancellationTransaction({ safeAddress, transaction }))
})
} else {
dispatch(removeTransaction({ safeAddress, transaction }))
}
}

View File

@ -1,12 +1,7 @@
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,
isSendERC20Transaction,
isSendERC721Transaction,
} from 'src/logic/tokens/utils/tokenHelpers'
import { getERC20DecimalsAndSymbol, isSendERC20Transaction } from 'src/logic/tokens/utils/tokenHelpers'
import { getERC721Symbol, isSendERC721Transaction } from 'src/logic/collectibles/utils'
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
@ -21,18 +16,18 @@ import {
TxArgs,
RefundParams,
} from 'src/logic/safe/store/models/types/transaction'
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
import { AppReduxState, store } from 'src/store'
import { safeSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import {
safeSelector,
safeTransactionsSelector,
safeCancellationTransactionsSelector,
} from 'src/logic/safe/store/selectors'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import {
BatchProcessTxsProps,
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'
@ -82,16 +77,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
}
@ -231,27 +221,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)
@ -322,19 +309,20 @@ 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: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List()
const safe = safeSelector(state)
const cancellationTxs = safeCancellationTransactionsSelector(state)
const outgoingTxs = safeTransactionsSelector(state)
if (!safe) {
throw new Error('Failed to recover Safe from the store')
}
return buildTx({
cancellationTxs,
currentUser: undefined,
knownTokens,
outgoingTxs,
safe,
tx: (tx as unknown) as TxServiceModel,
txCode: EMPTY_DATA,
})
}

View File

@ -59,7 +59,7 @@ export type TransactionProps = {
isCollectibleTransfer: boolean
isExecuted: boolean
isPending?: boolean
isSuccessful: boolean
isSuccessful?: boolean
isTokenTransfer: boolean
masterCopy: string
modifySettingsTx: boolean

View File

@ -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>

View File

@ -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

View File

@ -1,5 +1,6 @@
import { makeToken } from 'src/logic/tokens/store/model/token'
import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
import { getERC20DecimalsAndSymbol, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers'
import { isERC721Contract } from 'src/logic/collectibles/utils'
import { getMockedTxServiceModel } from 'src/test/utils/safeHelper'
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
@ -203,4 +204,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)
})
})

View File

@ -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)

View File

@ -2,20 +2,14 @@ import { AbiItem } from 'web3-utils'
import { getNetworkInfo } from 'src/config'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import {
getStandardTokenContract,
getTokenInfos,
getERC721TokenContract,
} from 'src/logic/tokens/store/actions/fetchTokens'
import { getTokenInfos } from 'src/logic/tokens/store/actions/fetchTokens'
import { isSendERC721Transaction } from 'src/logic/collectibles/utils'
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'
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
export const getEthAsToken = (balance: string | number): Token => {
const { nativeCoin } = getNetworkInfo()
return makeToken({
@ -42,28 +36,6 @@ 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 getERC721Symbol = async (contractAddress: string): Promise<string> => {
let tokenSymbol = 'UNKNOWN'
try {
const ERC721token = await getERC721TokenContract()
const tokenInstance = await ERC721token.at(contractAddress)
tokenSymbol = tokenInstance.symbol()
} catch (err) {
console.error(`Failed to retrieve token symbol for ERC721 token ${contractAddress}`)
}
return tokenSymbol
}
export const getERC20DecimalsAndSymbol = async (
tokenAddress: string,
): Promise<{ decimals: number; symbol: string }> => {
@ -89,12 +61,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)
@ -107,17 +75,3 @@ export const isSendERC20Transaction = async (
return isSendTokenTx
}
export const isERC721Contract = async (contractAddress: string): Promise<boolean> => {
const ERC721Token = await getStandardTokenContract()
let isERC721 = false
try {
await ERC721Token.at(contractAddress)
isERC721 = true
} catch (error) {
console.warn('Asset not found')
}
return isERC721
}

View File

@ -24,6 +24,7 @@ export const WALLET_PROVIDER = {
AUTHEREUM: 'AUTHEREUM',
LEDGER: 'LEDGER',
TREZOR: 'TREZOR',
LATTICE: 'LATTICE',
}
// With some wallets from web3connect you have to use their provider instance only for signing

View File

@ -40,6 +40,12 @@ const wallets: Wallet[] = [
},
{ walletName: WALLETS.TRUST, preferred: true, desktop: false },
{ walletName: WALLETS.DAPPER, desktop: false },
{
walletName: WALLETS.LATTICE,
rpcUrl,
appName: 'Gnosis Safe',
desktop: false,
},
{
walletName: WALLETS.FORTMATIC,
apiKey: FORTMATIC_KEY,

View File

@ -1,4 +1,4 @@
import { ClickAwayListener, Divider } from '@material-ui/core'
import { ClickAwayListener, createStyles, Divider } from '@material-ui/core'
import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem'
import { makeStyles } from '@material-ui/core/styles'
@ -11,26 +11,38 @@ import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { xs } from 'src/theme/variables'
const useStyles = makeStyles({
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
margin: `0 ${xs}`,
borderRadius: '50%',
transition: 'background-color .2s ease-in-out',
'&:hover': {
backgroundColor: '#F0EFEE',
const useStyles = makeStyles(
createStyles({
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
margin: `0 ${xs}`,
borderRadius: '50%',
transition: 'background-color .2s ease-in-out',
'&:hover': {
backgroundColor: '#F0EFEE',
},
outline: 'none',
},
outline: 'none',
},
increasedPopperZindex: {
zIndex: 2001,
},
})
increasedPopperZindex: {
zIndex: 2001,
},
}),
)
const EllipsisTransactionDetails = ({ address, knownAddress }) => {
type EllipsisTransactionDetailsProps = {
address: string
knownAddress?: boolean
sendModalOpenHandler?: () => void
}
export const EllipsisTransactionDetails = ({
address,
knownAddress,
sendModalOpenHandler,
}: EllipsisTransactionDetailsProps): React.ReactElement => {
const classes = useStyles()
const [anchorEl, setAnchorEl] = React.useState(null)
@ -51,10 +63,12 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
<div className={classes.container} role="menu" tabIndex={0}>
<MoreHorizIcon onClick={handleClick} onKeyDown={handleClick} />
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
<MenuItem disabled onClick={closeMenuHandler}>
Send Again
</MenuItem>
<Divider />
{sendModalOpenHandler ? (
<>
<MenuItem onClick={sendModalOpenHandler}>Send Again</MenuItem>
<Divider />
</>
) : null}
{knownAddress ? (
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
) : (
@ -65,5 +79,3 @@ const EllipsisTransactionDetails = ({ address, knownAddress }) => {
</ClickAwayListener>
)
}
export default EllipsisTransactionDetails

View File

@ -53,11 +53,11 @@ export const staticAppsList: Array<{ url: string; disabled: boolean; networks: n
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTBBaiDQyGa17DJ7DdviyHbc51fTVgf6Z5PW5w2YUTkgR`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
networks: [ETHEREUM_NETWORK.MAINNET],
},
// Sablier
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmboeZ9bae26Skg5xskCsXWjJuLjYk7aHgPh4BAnfRBDgo`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmfLqzEHz5TEupRLPuFp7prtcVAm6hKii5YZsVZWeM17Lr`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
},
@ -93,7 +93,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean; networks: n
},
// Wallet-Connect
{
url: `${gnosisAppsUrl}/walletConnect`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVWjxqMYuqZ4WvxKdrErcTt1Sx5JHxZosjYz9zHiHRAiq`,
disabled: false,
networks: [
ETHEREUM_NETWORK.MAINNET,

View File

@ -6,7 +6,7 @@ import { useSelector } from 'react-redux'
import Item from './components/Item'
import Paragraph from 'src/components/layout/Paragraph'
import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import { activeNftAssetsListSelector, orderedNFTAssets } from 'src/logic/collectibles/store/selectors'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import { fontColor, lg, screenSm, screenXs } from 'src/theme/variables'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
@ -81,7 +81,7 @@ const Collectibles = (): React.ReactElement => {
const classes = useStyles()
const [selectedToken, setSelectedToken] = React.useState<NFTToken | undefined>()
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
const nftTokens = useSelector(nftTokensSelector)
const nftTokens = useSelector(orderedNFTAssets)
const activeAssetsList = useSelector(activeNftAssetsListSelector)
const { trackEvent } = useAnalytics()

View File

@ -6,7 +6,6 @@ import React, { Suspense, useEffect, useState } from 'react'
import Modal from 'src/components/Modal'
import { CollectibleTx } from './screens/ReviewCollectible'
import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx'
import { SendFundsTx } from './screens/SendFunds'
import { ContractInteractionTx } from './screens/ContractInteraction'
import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx'
import { ReviewTxProp } from './screens/ReviewTx'
@ -53,6 +52,7 @@ type Props = {
onClose: () => void
recipientAddress?: string
selectedToken?: string | NFTToken
tokenAmount?: string
}
const SendModal = ({
@ -61,6 +61,7 @@ const SendModal = ({
onClose,
recipientAddress,
selectedToken,
tokenAmount,
}: Props): React.ReactElement => {
const classes = useStyles()
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
@ -119,11 +120,11 @@ const SendModal = ({
)}
{activeScreen === 'sendFunds' && (
<SendFunds
initialValues={tx as SendFundsTx}
onClose={onClose}
onNext={handleTxCreation}
recipientAddress={recipientAddress}
selectedToken={selectedToken as string}
amount={tokenAmount}
/>
)}
{activeScreen === 'reviewTx' && (
@ -161,7 +162,7 @@ const SendModal = ({
onClose={onClose}
onNext={handleSendCollectible}
recipientAddress={recipientAddress}
selectedToken={selectedToken as NFTToken}
selectedToken={selectedToken as NFTToken | undefined}
/>
)}
{activeScreen === 'reviewCollectible' && (

View File

@ -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

View File

@ -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',
},
}),
)

View File

@ -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}
/>
)}

View File

@ -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}

View File

@ -20,13 +20,12 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { safeSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
import { getERC721TokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH } from 'src/logic/tokens/utils/tokenHelpers'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import { sm } from 'src/theme/variables'
import { textShortener } from 'src/utils/strings'
import { generateERC721TransferTxData } from 'src/logic/collectibles/utils'
import ArrowDown from '../assets/arrow-down.svg'
@ -67,14 +66,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
const estimateGas = async () => {
try {
const methodToCall = `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}`
const transferParams = [tx.recipientAddress, tx.nftTokenId]
const params = [safeAddress, ...transferParams]
const ERC721Token = await getERC721TokenContract()
const tokenInstance = await ERC721Token.at(tx.assetAddress)
const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI()
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.recipientAddress, txData)
const txData = await generateERC721TransferTxData(tx, safeAddress)
const estimatedGasCosts = await estimateTxGasCosts(safeAddress ?? '', tx.recipientAddress, txData)
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const formattedGasCosts = formatAmount(gasCosts)
@ -92,7 +85,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
return () => {
isCurrent = false
}
}, [safeAddress, tx.assetAddress, tx.nftTokenId, tx.recipientAddress])
}, [safeAddress, tx])
const submitTx = async () => {
try {

View File

@ -13,10 +13,16 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import { textShortener } from 'src/utils/strings'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles'
const useSelectedCollectibleStyles = makeStyles(selectedTokenStyles)
const SelectedCollectible = ({ tokenId, tokens }) => {
type SelectedCollectibleProps = {
tokenId?: number | string
tokens: NFTToken[]
}
const SelectedCollectible = ({ tokenId, tokens }: SelectedCollectibleProps): React.ReactElement => {
const classes = useSelectedCollectibleStyles()
const token = tokenId && tokens ? tokens.find(({ tokenId: id }) => tokenId === id) : null
const shortener = textShortener({ charsStart: 40, charsEnd: 0 })
@ -31,7 +37,7 @@ const SelectedCollectible = ({ tokenId, tokens }) => {
<ListItemText
className={classes.tokenData}
primary={shortener(token.name)}
secondary={`token ID: ${shortener(token.tokenId)}`}
secondary={`token ID: ${shortener(token.tokenId.toString())}`}
/>
</>
) : (
@ -45,7 +51,12 @@ const SelectedCollectible = ({ tokenId, tokens }) => {
const useCollectibleSelectFieldStyles = makeStyles(selectStyles)
const CollectibleSelectField = ({ initialValue, tokens }) => {
type CollectibleSelectFieldProps = {
initialValue?: number | string
tokens: NFTToken[]
}
export const CollectibleSelectField = ({ initialValue, tokens }: CollectibleSelectFieldProps): React.ReactElement => {
const classes = useCollectibleSelectFieldStyles()
return (
@ -69,5 +80,3 @@ const CollectibleSelectField = ({ initialValue, tokens }) => {
</Field>
)
}
export default CollectibleSelectField

View File

@ -1,6 +1,7 @@
import { sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const selectedTokenStyles = () => ({
export const selectedTokenStyles = createStyles({
container: {
minHeight: '55px',
padding: 0,
@ -16,7 +17,7 @@ export const selectedTokenStyles = () => ({
},
})
export const selectStyles = () => ({
export const selectStyles = createStyles({
selectMenu: {
paddingRight: 0,
},

View File

@ -14,10 +14,16 @@ import Paragraph from 'src/components/layout/Paragraph'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import { textShortener } from 'src/utils/strings'
import { NFTAssets } from 'src/logic/collectibles/sources/collectibles'
const useSelectedTokenStyles = makeStyles(selectedTokenStyles)
const SelectedToken = ({ assetAddress, assets }) => {
type SelectedTokenProps = {
assetAddress?: string
assets: NFTAssets
}
const SelectedToken = ({ assetAddress, assets }: SelectedTokenProps): React.ReactElement => {
const classes = useSelectedTokenStyles()
const asset = assetAddress ? assets[assetAddress] : null
const shortener = textShortener({ charsStart: 40, charsEnd: 0 })
@ -32,7 +38,7 @@ const SelectedToken = ({ assetAddress, assets }) => {
<ListItemText
className={classes.tokenData}
primary={shortener(asset.name)}
secondary={`${formatAmount(asset.numberOfTokens)} ${asset.symbol}`}
secondary={`${formatAmount(asset.numberOfTokens.toString())} ${asset.symbol}`}
/>
</>
) : (
@ -46,7 +52,12 @@ const SelectedToken = ({ assetAddress, assets }) => {
const useTokenSelectFieldStyles = makeStyles(selectStyles)
const TokenSelectField = ({ assets, initialValue }) => {
type TokenSelectFieldProps = {
assets: NFTAssets
initialValue?: string
}
const TokenSelectField = ({ assets, initialValue }: TokenSelectFieldProps): React.ReactElement => {
const classes = useTokenSelectFieldStyles()
const assetsAddresses = Object.keys(assets)
@ -70,7 +81,7 @@ const TokenSelectField = ({ assets, initialValue }) => {
</ListItemIcon>
<ListItemText
primary={asset.name}
secondary={`Count: ${formatAmount(asset.numberOfTokens)} ${asset.symbol}`}
secondary={`Count: ${formatAmount(asset.numberOfTokens.toString())} ${asset.symbol}`}
/>
</MenuItem>
)

View File

@ -1,6 +1,7 @@
import { sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const selectedTokenStyles = () => ({
export const selectedTokenStyles = createStyles({
container: {
minHeight: '55px',
padding: 0,
@ -16,7 +17,7 @@ export const selectedTokenStyles = () => ({
},
})
export const selectStyles = () => ({
export const selectStyles = createStyles({
selectMenu: {
paddingRight: 0,
},

View File

@ -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) => {
@ -50,7 +51,7 @@ type SendCollectibleProps = {
onClose: () => void
onNext: (txInfo: SendCollectibleTxInfo) => void
recipientAddress?: string
selectedToken: NFTToken
selectedToken?: NFTToken
}
export type SendCollectibleTxInfo = {
@ -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}
/>
@ -220,7 +242,7 @@ const SendCollectible = ({
</Row>
<Row margin="sm">
<Col>
<TokenSelectField assets={nftAssets} initialValue={(selectedToken as any).assetAddress} />
<TokenSelectField assets={nftAssets} initialValue={selectedToken?.assetAddress} />
</Col>
</Row>
<Row margin="xs">
@ -232,7 +254,7 @@ const SendCollectible = ({
</Row>
<Row margin="md">
<Col>
<CollectibleSelectField initialValue={(selectedToken as any).tokenId} tokens={selectedNFTTokens} />
<CollectibleSelectField initialValue={selectedToken?.tokenId} tokens={selectedNFTTokens} />
</Col>
</Row>
</Block>

View File

@ -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) => {
@ -49,41 +48,50 @@ const formMutators = {
const useStyles = makeStyles(styles)
export type SendFundsTx = {
amount?: string
recipientAddress?: string
token?: string
}
type SendFundsProps = {
initialValues: SendFundsTx
onClose: () => void
onNext: (txInfo: unknown) => void
recipientAddress?: string
selectedToken?: string
amount?: string
}
const { nativeCoin } = getNetworkInfo()
const SendFunds = ({
initialValues,
onClose,
onNext,
recipientAddress,
selectedToken = '',
amount,
}: SendFundsProps): React.ReactElement => {
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: recipientAddress || '', name: '' }
// if there's nothing to lookup for, we return the default entry
if (!recipientAddress) {
return defaultEntry
}
const addressBookEntry = addressBook.find(({ address }) => {
return sameAddress(recipientAddress, 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)
}
@ -110,7 +118,11 @@ const SendFunds = ({
</IconButton>
</Row>
<Hairline />
<GnoForm formMutators={formMutators} initialValues={initialValues} onSubmit={handleSubmit}>
<GnoForm
formMutators={formMutators}
initialValues={{ amount, recipientAddress, token: selectedToken }}
onSubmit={handleSubmit}
>
{(...args) => {
const formState = args[2]
const mutators = args[3]
@ -152,9 +164,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 +181,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 +249,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>

View File

@ -1,5 +1,5 @@
import memoize from 'lodash.memoize'
import { isERC721Contract } from 'src/logic/tokens/utils/tokenHelpers'
import { isERC721Contract } from 'src/logic/collectibles/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
// eslint-disable-next-line

View File

@ -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>

View File

@ -13,10 +13,11 @@ type OwnerAddressTableCellProps = {
knownAddress?: boolean
showLinks: boolean
userName?: string
sendModalOpenHandler?: () => void
}
const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => {
const { address, knownAddress, showLinks, userName } = props
const { address, knownAddress, showLinks, userName, sendModalOpenHandler } = props
const [cut, setCut] = useState(0)
const { width } = useWindowDimensions()
@ -36,7 +37,12 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
{showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{userName && getValidAddressBookName(userName)}
<EtherscanLink knownAddress={knownAddress} value={address} cut={cut} />
<EtherscanLink
knownAddress={knownAddress}
value={address}
cut={cut}
sendModalOpenHandler={sendModalOpenHandler}
/>
</div>
) : (
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>

View File

@ -4,6 +4,9 @@ import Close from '@material-ui/icons/Close'
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { List } from 'immutable'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
@ -23,9 +26,10 @@ import {
} from 'src/logic/safe/store/selectors'
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
@ -37,6 +41,8 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
const safeName = useSelector(safeNameSelector)
const owners = useSelector(safeOwnersSelector)
const threshold = useSelector(safeThresholdSelector)
const addressBook = useSelector(addressBookSelector)
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
useEffect(() => {
let isCurrent = true
@ -106,7 +112,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
</Paragraph>
</Row>
<Hairline />
{owners?.map(
{ownersWithAddressBookName?.map(
(owner) =>
owner.address !== ownerAddress && (
<React.Fragment key={owner.address}>

View File

@ -1,17 +1,16 @@
import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types.d'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
export const OWNERS_TABLE_NAME_ID = 'name'
export const OWNERS_TABLE_ADDRESS_ID = 'address'
export const OWNERS_TABLE_ACTIONS_ID = 'actions'
export const getOwnerData = (owners) => {
const rows = owners.map((owner) => ({
export const getOwnerData = (owners: List<SafeOwner>): List<{ address: string; name: string }> => {
return owners.map((owner) => ({
[OWNERS_TABLE_NAME_ID]: owner.name,
[OWNERS_TABLE_ADDRESS_ID]: owner.address,
}))
return rows
}
export const generateColumns = (): List<TableColumn> => {

View File

@ -84,8 +84,8 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners)
const ownerData = getOwnerData(ownersAdbk)
const ownersWithAddressBookName = getOwnersWithNameFromAddressBook(addressBook, owners)
const ownerData = getOwnerData(ownersWithAddressBookName)
return (
<>

View File

@ -7,23 +7,59 @@ import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/sele
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
interface TransferDescriptionProps {
amount: string
amountWithSymbol: string
recipient: string
tokenAddress?: string
rawAmount?: string
isTokenTransfer: boolean
}
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
const TransferDescription = ({
amountWithSymbol = '',
recipient,
tokenAddress,
rawAmount,
isTokenTransfer,
}: TransferDescriptionProps): React.ReactElement => {
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient))
const [sendModalOpen, setSendModalOpen] = React.useState(false)
const sendModalOpenHandler = () => {
setSendModalOpen(true)
}
return (
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>Send {amount} to:</Bold>
{recipientName ? (
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
) : (
<EtherscanLink knownAddress={false} value={recipient} />
)}
</Block>
<>
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>Send {amountWithSymbol} to:</Bold>
{recipientName ? (
<OwnerAddressTableCell
address={recipient}
knownAddress
showLinks
userName={recipientName}
sendModalOpenHandler={isTokenTransfer ? sendModalOpenHandler : undefined}
/>
) : (
<EtherscanLink
knownAddress={false}
value={recipient}
sendModalOpenHandler={isTokenTransfer ? sendModalOpenHandler : undefined}
/>
)}
</Block>
<SendModal
activeScreenType="sendFunds"
isOpen={sendModalOpen}
onClose={() => setSendModalOpen(false)}
recipientAddress={recipient}
selectedToken={tokenAddress}
tokenAmount={rawAmount}
/>
</>
)
}

View File

@ -7,7 +7,7 @@ import SettingsDescription from './SettingsDescription'
import CustomDescription from './CustomDescription'
import TransferDescription from './TransferDescription'
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import { getRawTxAmount, getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
import Block from 'src/components/layout/Block'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
@ -30,8 +30,12 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
recipient,
removedOwner,
upgradeTx,
tokenAddress,
isTokenTransfer,
}: any = getTxData(tx)
const amount = getTxAmount(tx, false)
const amountWithSymbol = getTxAmount(tx, false)
const amount = getRawTxAmount(tx)
return (
<Block className={classes.txDataContainer}>
{modifySettingsTx && action && (
@ -43,10 +47,18 @@ const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
module={module}
/>
)}
{!upgradeTx && customTx && <CustomDescription amount={amount} data={data} recipient={recipient} storedTx={tx} />}
{!upgradeTx && customTx && (
<CustomDescription amount={amountWithSymbol} data={data} recipient={recipient} storedTx={tx} />
)}
{upgradeTx && <div>{data}</div>}
{!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && (
<TransferDescription amount={amount} recipient={recipient} />
<TransferDescription
amountWithSymbol={amountWithSymbol}
recipient={recipient}
tokenAddress={tokenAddress}
rawAmount={amount}
isTokenTransfer={isTokenTransfer}
/>
)}
</Block>
)

View File

@ -1,5 +1,7 @@
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { SAFE_METHODS_NAMES } from 'src/routes/safe/store/models/types/transactions.d'
import { sameString } from 'src/utils/strings'
import { getNetworkInfo } from 'src/config'
const getSafeVersion = (data) => {
const contractAddress = data.substr(340, 40).toLowerCase()
@ -27,79 +29,152 @@ interface TxData {
cancellationTx?: boolean
creationTx?: boolean
upgradeTx?: boolean
tokenAddress?: string
}
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
txData.tokenAddress = tx.recipient
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 = {}
const { nativeCoin } = getNetworkInfo()
if (sameString(tx.type, 'outgoing') && tx.symbol && sameString(tx.symbol, nativeCoin.symbol)) {
txData.isTokenTransfer = true
txData.tokenAddress = nativeCoin.address
}
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
}

View File

@ -1,11 +1,10 @@
import CircularProgress from '@material-ui/core/CircularProgress'
import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import React, { ReactElement } from 'react'
import AwaitingIcon from './assets/awaiting.svg'
import ErrorIcon from './assets/error.svg'
import OkIcon from './assets/ok.svg'
import { styles } from './style'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Img from 'src/components/layout/Img'
@ -19,7 +18,7 @@ const statusToIcon = {
awaiting_confirmations: AwaitingIcon,
awaiting_execution: AwaitingIcon,
pending: <CircularProgress size={14} />,
}
} as const
const statusToLabel = {
success: 'Success',
@ -29,15 +28,16 @@ const statusToLabel = {
awaiting_confirmations: 'Awaiting confirmations',
awaiting_execution: 'Awaiting execution',
pending: 'Pending',
}
} as const
const statusIconStyle = {
height: '14px',
width: '14px',
}
const Status = ({ classes, status }) => {
const Icon = statusToIcon[status]
const Status = ({ status }: { status: keyof typeof statusToLabel }): ReactElement => {
const classes = useStyles()
const Icon: typeof statusToIcon[keyof typeof statusToIcon] = statusToIcon[status]
return (
<Block className={`${classes.container} ${classes[status]}`}>
@ -49,4 +49,4 @@ const Status = ({ classes, status }) => {
)
}
export default withStyles(styles as any)(Status)
export default Status

View File

@ -1,49 +1,52 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { boldFont, disabled, error, extraSmallFontSize, lg, secondary, sm } from 'src/theme/variables'
export const styles = () => ({
container: {
display: 'flex',
fontSize: extraSmallFontSize,
fontWeight: boldFont,
padding: sm,
alignItems: 'center',
boxSizing: 'border-box',
height: lg,
marginTop: sm,
marginBottom: sm,
borderRadius: '3px',
},
success: {
backgroundColor: '#A1D2CA',
color: secondary,
},
cancelled: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
failed: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
awaiting_your_confirmation: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_confirmations: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_execution: {
backgroundColor: '#d4d5d3',
color: disabled,
},
pending: {
backgroundColor: '#fff3e2',
color: '#e8673c',
},
statusText: {
padding: '0 7px',
},
})
export const useStyles = makeStyles(
createStyles({
container: {
display: 'flex',
fontSize: extraSmallFontSize,
fontWeight: boldFont,
padding: sm,
alignItems: 'center',
boxSizing: 'border-box',
height: lg,
marginTop: sm,
marginBottom: sm,
borderRadius: '3px',
},
success: {
backgroundColor: '#A1D2CA',
color: secondary,
},
cancelled: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
failed: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
awaiting_your_confirmation: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_confirmations: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_execution: {
backgroundColor: '#d4d5d3',
color: disabled,
},
pending: {
backgroundColor: '#fff3e2',
color: '#e8673c',
},
statusText: {
padding: '0 7px',
},
}),
)

View File

@ -13,6 +13,7 @@ import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { getNetworkInfo } from 'src/config'
export const TX_TABLE_ID = 'id'
export const TX_TABLE_TYPE_ID = 'type'
@ -71,6 +72,25 @@ export const getTxAmount = (tx: Transaction, formatted = true): string => {
return getAmountWithSymbol({ decimals: decimals as string, symbol: symbol as string, value }, formatted)
}
export const getRawTxAmount = (tx: Transaction): string => {
const { decimals, decodedParams, isTokenTransfer } = tx
const { nativeCoin } = getNetworkInfo()
const { value } = isTokenTransfer && !!decodedParams?.transfer ? decodedParams.transfer : tx
if (tx.isCollectibleTransfer) {
return '1'
}
if (!isTokenTransfer && !(Number(value) > 0)) {
return NOT_AVAILABLE
}
const tokenDecimals = decimals ?? nativeCoin.decimals
const finalValue = new BigNumber(value).times(`1e-${tokenDecimals}`).toFixed()
return finalValue === 'NaN' ? NOT_AVAILABLE : finalValue
}
export interface TableData {
amount: string
cancelTx?: Transaction

View File

@ -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),
},
},
},

View File

@ -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
}
}

2385
yarn.lock

File diff suppressed because it is too large Load Diff