commit
8ff417e99b
16
.travis.yml
16
.travis.yml
|
@ -72,7 +72,7 @@ after_success:
|
|||
- yarn coveralls
|
||||
|
||||
deploy:
|
||||
# Development environment
|
||||
# Development environment only on rinkeby
|
||||
- provider: s3
|
||||
bucket: $DEV_BUCKET_NAME
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
|
@ -83,6 +83,7 @@ deploy:
|
|||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: development
|
||||
condition: $REACT_APP_NETWORK = rinkeby
|
||||
|
||||
# Staging environment
|
||||
- provider: s3
|
||||
|
@ -95,19 +96,6 @@ deploy:
|
|||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: master
|
||||
|
||||
# EWC testing on staging
|
||||
- provider: s3
|
||||
bucket: $STAGING_BUCKET_NAME
|
||||
access_key_id: $AWS_ACCESS_KEY_ID
|
||||
secret_access_key: $AWS_SECRET_ACCESS_KEY
|
||||
skip_cleanup: true
|
||||
local_dir: build
|
||||
upload_dir: current/app
|
||||
region: $AWS_DEFAULT_REGION
|
||||
on:
|
||||
branch: release/v2.14.0
|
||||
condition: $REACT_APP_NETWORK = energy_web_chain
|
||||
|
||||
# Prepare production deployment
|
||||
- provider: s3
|
||||
|
|
|
@ -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
|
||||
```
|
37
package.json
37
package.json
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { List } from 'immutable'
|
||||
import memoize from 'lodash.memoize'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import memoize from 'lodash.memoize'
|
||||
|
||||
type ValidatorReturnType = string | undefined
|
||||
type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
||||
|
@ -62,7 +64,11 @@ export const mustBeEthereumAddress = memoize(
|
|||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
|
||||
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
|
||||
const errorMessage = `Address should be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
}`
|
||||
|
||||
return startsWith0x && isAddress ? undefined : errorMessage
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -70,9 +76,11 @@ export const mustBeEthereumContractAddress = memoize(
|
|||
async (address: string): Promise<ValidatorReturnType> => {
|
||||
const contractCode = await getWeb3().eth.getCode(address)
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === ''
|
||||
? 'Address should be a valid Ethereum contract address or ENS name'
|
||||
: undefined
|
||||
const errorMessage = `Address should be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
}`
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import memoize from 'lodash.memoize'
|
||||
|
||||
import networks from 'src/config/networks'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures, Wallets, GasPriceOracle } from 'src/config/networks/network.d'
|
||||
import {
|
||||
EnvironmentSettings,
|
||||
ETHEREUM_NETWORK,
|
||||
FEATURES,
|
||||
GasPriceOracle,
|
||||
NetworkSettings,
|
||||
SafeFeatures,
|
||||
Wallets,
|
||||
} from 'src/config/networks/network.d'
|
||||
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
|
||||
|
@ -90,6 +98,16 @@ export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: s
|
|||
|
||||
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig().disabledFeatures || []
|
||||
|
||||
/**
|
||||
* Checks if a particular feature is enabled in the current network configuration
|
||||
* @params {FEATURES} feature
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isFeatureEnabled = memoize((feature: FEATURES): boolean => {
|
||||
const disabledFeatures = getNetworkConfigDisabledFeatures()
|
||||
return !disabledFeatures.some((disabledFeature) => disabledFeature === feature)
|
||||
})
|
||||
|
||||
export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || []
|
||||
|
||||
export const getNetworkInfo = (): NetworkSettings => getConfig().network
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import EwcLogo from 'src/config/assets/token_ewc.svg'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0
|
||||
// once the oracle is fixed we need to remove the fixed value
|
||||
|
@ -44,7 +44,7 @@ const mainnet: NetworkConfig = {
|
|||
logoUri: EwcLogo,
|
||||
},
|
||||
},
|
||||
disabledWallets:[
|
||||
disabledWallets: [
|
||||
WALLETS.TREZOR,
|
||||
WALLETS.LEDGER,
|
||||
WALLETS.COINBASE,
|
||||
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
|
@ -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] },
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export type TransactionProps = {
|
|||
isCollectibleTransfer: boolean
|
||||
isExecuted: boolean
|
||||
isPending?: boolean
|
||||
isSuccessful: boolean
|
||||
isSuccessful?: boolean
|
||||
isTokenTransfer: boolean
|
||||
masterCopy: string
|
||||
modifySettingsTx: boolean
|
||||
|
|
|
@ -241,7 +241,13 @@ export const METHOD_TO_ID = {
|
|||
|
||||
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
export const TOKEN_TRANSFER_METHODS_NAMES = {
|
||||
TRANSFER: 'transfer',
|
||||
TRANSFER_FROM: 'transferFrom',
|
||||
SAFE_TRANSFER_FROM: 'safeTransferFrom',
|
||||
} as const
|
||||
|
||||
type TokenMethods = typeof TOKEN_TRANSFER_METHODS_NAMES[keyof typeof TOKEN_TRANSFER_METHODS_NAMES]
|
||||
|
||||
type SafeDecodedParams = {
|
||||
[key in SafeMethods]?: Record<string, string>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
|||
|
||||
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
|
||||
import { LATEST_SAFE_VERSION } from 'src/utils/constants'
|
||||
import { getNetworkConfigDisabledFeatures } from 'src/config'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
|
||||
type FeatureConfigByVersion = {
|
||||
|
@ -41,9 +41,8 @@ const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, ver
|
|||
}
|
||||
|
||||
export const enabledFeatures = (version?: string): FEATURES[] => {
|
||||
const disabledFeatures = getNetworkConfigDisabledFeatures()
|
||||
return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => {
|
||||
if (!disabledFeatures.includes(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
|
||||
if (isFeatureEnabled(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
|
||||
acc.push(feature.name)
|
||||
}
|
||||
return acc
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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' && (
|
||||
|
|
|
@ -1,246 +1,204 @@
|
|||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import MuiTextField from '@material-ui/core/TextField'
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles'
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Autocomplete, { AutocompleteProps } from '@material-ui/lab/Autocomplete'
|
||||
import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
useTextFieldInputStyle,
|
||||
useTextFieldLabelStyle,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
export interface AddressBookProps {
|
||||
fieldMutator: (address: string) => void
|
||||
isCustomTx?: boolean
|
||||
pristine: boolean
|
||||
pristine?: boolean
|
||||
recipientAddress?: string
|
||||
setSelectedEntry: (
|
||||
entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name? }> | null,
|
||||
) => void
|
||||
setIsValidAddress: (valid: boolean) => void
|
||||
setSelectedEntry: Dispatch<SetStateAction<{ address: string; name: string }> | null>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const textFieldLabelStyle = makeStyles(() => ({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
fontSize: '15px',
|
||||
width: '500px',
|
||||
},
|
||||
}))
|
||||
|
||||
const textFieldInputStyle = makeStyles(() => ({
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
width: '420px',
|
||||
},
|
||||
}))
|
||||
|
||||
const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
|
||||
const abFlags = await Promise.all(
|
||||
addressBook.map(
|
||||
async ({ address }: AddressBookEntry): Promise<boolean> => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return addressBook.filter((_, index) => abFlags[index])
|
||||
export interface BaseAddressBookInputProps extends AddressBookProps {
|
||||
addressBookEntries: AddressBookEntry[]
|
||||
setSelectedEntry: (args: { address: string; name: string } | null) => void
|
||||
setValidationText: Dispatch<SetStateAction<string | undefined>>
|
||||
validationText: string | undefined
|
||||
}
|
||||
|
||||
const AddressBookInput = ({
|
||||
const BaseAddressBookInput = ({
|
||||
addressBookEntries,
|
||||
fieldMutator,
|
||||
isCustomTx,
|
||||
pristine,
|
||||
recipientAddress,
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
}: AddressBookProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [isValidForm, setIsValidForm] = useState(true)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
const [inputTouched, setInputTouched] = useState(false)
|
||||
const [blurred, setBlurred] = useState(pristine)
|
||||
const [adbkList, setADBKList] = useState<AddressBookEntry[]>([])
|
||||
setValidationText,
|
||||
validationText,
|
||||
}: BaseAddressBookInputProps): ReactElement => {
|
||||
const updateAddressInfo = (addressEntry: AddressBookEntry): void => {
|
||||
setSelectedEntry(addressEntry)
|
||||
fieldMutator(addressEntry.address)
|
||||
}
|
||||
|
||||
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
||||
const validateAddress = (address: string): AddressBookEntry | string | undefined => {
|
||||
const addressErrorMessage = mustBeEthereumAddress(address)
|
||||
setIsValidAddress(!addressErrorMessage)
|
||||
|
||||
const onAddressInputChanged = async (value: string): Promise<void> => {
|
||||
const normalizedAddress = trimSpaces(value)
|
||||
const isENSDomain = isValidEnsName(normalizedAddress)
|
||||
setInputAddValue(normalizedAddress)
|
||||
let resolvedAddress = normalizedAddress
|
||||
let addressErrorMessage
|
||||
if (inputTouched && !normalizedAddress) {
|
||||
setIsValidForm(false)
|
||||
setValidationText('Required')
|
||||
setIsValidAddress(false)
|
||||
if (addressErrorMessage) {
|
||||
setValidationText(addressErrorMessage)
|
||||
return
|
||||
}
|
||||
if (normalizedAddress) {
|
||||
if (isENSDomain) {
|
||||
resolvedAddress = await getAddressFromENS(normalizedAddress)
|
||||
setInputAddValue(resolvedAddress)
|
||||
|
||||
const filteredEntries = filterAddressEntries(addressBookEntries, { inputValue: address })
|
||||
return filteredEntries.length === 1 ? filteredEntries[0] : address
|
||||
}
|
||||
|
||||
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
||||
switch (reason) {
|
||||
case 'select-option': {
|
||||
const { address, name } = value as AddressBookEntry
|
||||
updateAddressInfo({ address, name })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addressErrorMessage = mustBeEthereumAddress(resolvedAddress)
|
||||
if (isCustomTx && addressErrorMessage === undefined) {
|
||||
addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress)
|
||||
}
|
||||
const onInputChange: AutocompleteProps<AddressBookEntry, false, false, true>['onInputChange'] = async (
|
||||
_,
|
||||
value,
|
||||
reason,
|
||||
) => {
|
||||
switch (reason) {
|
||||
case 'input': {
|
||||
const normalizedValue = trimSpaces(value)
|
||||
|
||||
// First removes the entries that are not contracts if the operation is custom tx
|
||||
const adbkToFilter = isCustomTx ? await filterAddressBookWithContractAddresses(addressBook) : addressBook
|
||||
// Then Filters the entries based on the input of the user
|
||||
const filteredADBK = adbkToFilter.filter((adbkEntry) => {
|
||||
const { address, name } = adbkEntry
|
||||
return (
|
||||
name.toLowerCase().includes(normalizedAddress.toLowerCase()) ||
|
||||
address.toLowerCase().includes(resolvedAddress.toLowerCase())
|
||||
)
|
||||
})
|
||||
setADBKList(filteredADBK)
|
||||
if (!addressErrorMessage) {
|
||||
// base case if isENSDomain we set the domain as the name
|
||||
// if address does not exist in address book we use blank name
|
||||
let addressName = isENSDomain ? normalizedAddress : ''
|
||||
|
||||
// if address is valid, and is in the address book, then we use the stored values
|
||||
if (filteredADBK.length === 1) {
|
||||
const addressBookContact = filteredADBK[0]
|
||||
addressName = addressBookContact.name ?? addressName
|
||||
if (!normalizedValue) {
|
||||
break
|
||||
}
|
||||
|
||||
setSelectedEntry({
|
||||
name: addressName,
|
||||
address: resolvedAddress,
|
||||
})
|
||||
// ENS-enabled resolve/validation
|
||||
if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
|
||||
const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue)
|
||||
|
||||
const validatedAddress = validateAddress(address)
|
||||
|
||||
if (!validatedAddress) {
|
||||
fieldMutator('')
|
||||
break
|
||||
}
|
||||
|
||||
const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
break
|
||||
}
|
||||
|
||||
// ETH address validation
|
||||
const validatedAddress = validateAddress(normalizedValue)
|
||||
|
||||
if (!validatedAddress) {
|
||||
fieldMutator('')
|
||||
break
|
||||
}
|
||||
|
||||
const newEntry =
|
||||
typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
setIsValidForm(addressErrorMessage === undefined)
|
||||
setValidationText(addressErrorMessage)
|
||||
fieldMutator(resolvedAddress)
|
||||
setIsValidAddress(addressErrorMessage === undefined)
|
||||
}
|
||||
|
||||
const labelStyles = useTextFieldLabelStyle()
|
||||
const inputStyles = useTextFieldInputStyle()
|
||||
|
||||
return (
|
||||
<Autocomplete<AddressBookEntry, false, false, true>
|
||||
closeIcon={null}
|
||||
openOnFocus={false}
|
||||
filterOptions={filterAddressEntries}
|
||||
freeSolo
|
||||
onChange={onChange}
|
||||
onInputChange={onInputChange}
|
||||
options={addressBookEntries}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
autoFocus={true}
|
||||
error={!!validationText}
|
||||
fullWidth
|
||||
id="filled-error-helper-text"
|
||||
variant="filled"
|
||||
label={validationText ? validationText : 'Recipient'}
|
||||
InputLabelProps={{ shrink: true, required: true, classes: labelStyles }}
|
||||
InputProps={{ ...params.InputProps, classes: inputStyles }}
|
||||
/>
|
||||
)}
|
||||
getOptionLabel={({ address }) => address}
|
||||
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showIdenticon />}
|
||||
role="listbox"
|
||||
style={{ display: 'flex', flexGrow: 1 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const AddressBookInput = (props: AddressBookProps): ReactElement => {
|
||||
const addressBookEntries = useSelector(addressBookSelector)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
|
||||
return (
|
||||
<BaseAddressBookInput
|
||||
addressBookEntries={addressBookEntries}
|
||||
setValidationText={setValidationText}
|
||||
validationText={validationText}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContractsAddressBookInput = ({
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
...props
|
||||
}: AddressBookProps): ReactElement => {
|
||||
const addressBookEntries = useSelector(addressBookSelector)
|
||||
const [filteredEntries, setFilteredEntries] = useState<AddressBookEntry[]>([])
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const filterAdbkContractAddresses = async (): Promise<void> => {
|
||||
if (!isCustomTx) {
|
||||
setADBKList(addressBook)
|
||||
return
|
||||
}
|
||||
|
||||
const filteredADBK = await filterAddressBookWithContractAddresses(addressBook)
|
||||
setADBKList(filteredADBK)
|
||||
const filterContractAddresses = async (): Promise<void> => {
|
||||
const filteredADBK = await filterContractAddressBookEntries(addressBookEntries)
|
||||
setFilteredEntries(filteredADBK)
|
||||
}
|
||||
filterAdbkContractAddresses()
|
||||
}, [addressBook, isCustomTx])
|
||||
filterContractAddresses()
|
||||
}, [addressBookEntries])
|
||||
|
||||
const labelStyling = textFieldLabelStyle()
|
||||
const txInputStyling = textFieldInputStyle()
|
||||
|
||||
let statusClasses = ''
|
||||
if (!isValidForm) {
|
||||
statusClasses = 'isInvalid'
|
||||
}
|
||||
if (isValidForm && inputTouched) {
|
||||
statusClasses = 'isValid'
|
||||
const onSetSelectedEntry = async (selectedEntry) => {
|
||||
if (selectedEntry?.address) {
|
||||
// verify if `address` is a contract
|
||||
const contractAddressErrorMessage = await mustBeEthereumContractAddress(selectedEntry.address)
|
||||
setIsValidAddress(!contractAddressErrorMessage)
|
||||
setValidationText(contractAddressErrorMessage ?? '')
|
||||
setSelectedEntry(selectedEntry)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
closeIcon={null}
|
||||
openOnFocus={false}
|
||||
filterOptions={(optionsArray, { inputValue }) =>
|
||||
optionsArray.filter((item) => {
|
||||
const inputLowerCase = inputValue.toLowerCase()
|
||||
const foundName = item.name.toLowerCase().includes(inputLowerCase)
|
||||
const foundAddress = item.address?.toLowerCase().includes(inputLowerCase)
|
||||
return foundName || foundAddress
|
||||
})
|
||||
}
|
||||
freeSolo
|
||||
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
|
||||
id="free-solo-demo"
|
||||
onChange={(_, value: AddressBookEntry) => {
|
||||
let address = ''
|
||||
let name = ''
|
||||
if (value) {
|
||||
address = value.address
|
||||
name = value.name
|
||||
}
|
||||
setSelectedEntry({ address, name })
|
||||
fieldMutator(address)
|
||||
}}
|
||||
onClose={() => setBlurred(true)}
|
||||
onOpen={() => {
|
||||
setSelectedEntry(null)
|
||||
setBlurred(false)
|
||||
}}
|
||||
open={!blurred}
|
||||
options={adbkList}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
// eslint-disable-next-line
|
||||
autoFocus={!blurred || pristine}
|
||||
error={!isValidForm}
|
||||
fullWidth
|
||||
id="filled-error-helper-text"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
required: true,
|
||||
classes: labelStyling,
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
classes: {
|
||||
...txInputStyling,
|
||||
},
|
||||
className: statusClasses,
|
||||
}}
|
||||
label={!isValidForm ? validationText : 'Recipient'}
|
||||
onChange={(event) => {
|
||||
setInputTouched(true)
|
||||
onAddressInputChanged(event.target.value)
|
||||
}}
|
||||
value={{ address: inputAddValue }}
|
||||
variant="filled"
|
||||
/>
|
||||
)}
|
||||
renderOption={(adbkEntry) => {
|
||||
const { address, name } = adbkEntry
|
||||
|
||||
if (!address) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.itemOptionList}>
|
||||
<div className={classes.identicon}>
|
||||
<Identicon address={address} diameter={32} />
|
||||
</div>
|
||||
<div className={classes.adbkEntryName}>
|
||||
<span>{name}</span>
|
||||
<span>{address}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
role="listbox"
|
||||
style={{ display: 'flex', flexGrow: 1 }}
|
||||
value={{ address: inputAddValue, name: '' }}
|
||||
/>
|
||||
</>
|
||||
<BaseAddressBookInput
|
||||
addressBookEntries={filteredEntries}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={onSetSelectedEntry}
|
||||
setValidationText={setValidationText}
|
||||
validationText={validationText}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressBookInput
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import { createStyles } from '@material-ui/core'
|
||||
import { createStyles, makeStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
itemOptionList: {
|
||||
display: 'flex',
|
||||
},
|
||||
export const useTextFieldLabelStyle = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
fontSize: '15px',
|
||||
width: '500px',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
adbkEntryName: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontSize: '14px',
|
||||
},
|
||||
identicon: {
|
||||
display: 'flex',
|
||||
padding: '5px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
})
|
||||
export const useTextFieldInputStyle = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
width: '420px',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState } from 'react'
|
|||
import { useFormState, useField } from 'react-final-form'
|
||||
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import {
|
||||
|
@ -82,11 +82,10 @@ const EthAddressInput = ({
|
|||
validate={validate}
|
||||
/>
|
||||
) : (
|
||||
<AddressBookInput
|
||||
<ContractsAddressBookInput
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
setIsValidAddress={() => {}}
|
||||
fieldMutator={onScannedValue}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -25,7 +25,7 @@ import Row from 'src/components/layout/Row'
|
|||
import ScanQRModal from 'src/components/ScanQRModal'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
||||
import ArrowDown from '../../assets/arrow-down.svg'
|
||||
|
@ -147,9 +147,13 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
|||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry(null)
|
||||
if (e.key === 'Tab') {
|
||||
return
|
||||
}
|
||||
setSelectedEntry(null)
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedEntry(null)
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex={0}
|
||||
|
@ -193,9 +197,8 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
|||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
<ContractsAddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createMuiTheme } from '@material-ui/core/styles'
|
||||
import { rgba } from 'polished'
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
||||
|
||||
import {
|
||||
boldFont,
|
||||
|
@ -407,7 +407,7 @@ const theme = createMuiTheme({
|
|||
MuiCheckbox: {
|
||||
colorSecondary: {
|
||||
'&$disabled': {
|
||||
color: rgba(secondary, 0.5),
|
||||
color: fade(secondary, 0.5),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import GoogleAnalytics, { EventArgs } from 'react-ga'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
|
||||
import { getGoogleAnalyticsTrackingID } from 'src/config'
|
||||
import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
|
||||
|
@ -15,11 +16,17 @@ export const loadGoogleAnalytics = (): void => {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('Loading google analytics...')
|
||||
const trackingID = getGoogleAnalyticsTrackingID()
|
||||
const networkInfo = getNetworkInfo()
|
||||
if (!trackingID) {
|
||||
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
|
||||
} else {
|
||||
GoogleAnalytics.initialize(trackingID)
|
||||
GoogleAnalytics.set({ anonymizeIp: true })
|
||||
GoogleAnalytics.set({
|
||||
anonymizeIp: true,
|
||||
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
|
||||
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
|
||||
appVersion: process.env.REACT_APP_APP_VERSION,
|
||||
})
|
||||
analyticsLoaded = true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue