Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2020-11-05 12:53:30 -03:00
commit 79bf102166
61 changed files with 2011 additions and 1309 deletions

View File

@ -6,6 +6,7 @@ REACT_APP_GOOGLE_ANALYTICS=
REACT_APP_INFURA_TOKEN=
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
PUBLIC_URL=/app/
REACT_APP_SENTRY_DSN=
# For production environments
REACT_APP_BLOCKNATIVE_KEY=

View File

@ -1,4 +1,4 @@
if: (branch = development) OR (branch = master) OR (branch = release/2.14.0) OR (type = pull_request) OR (tag IS present)
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
sudo: required
dist: bionic
language: node_js
@ -10,30 +10,38 @@ matrix:
include:
- env:
- REACT_APP_NETWORK='mainnet'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_MAINNET}
- SENTRY_PROJECT=${SENTRY_PROJECT_MAINNET}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='rinkeby'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_RINKEBY}
- SENTRY_PROJECT=${SENTRY_PROJECT_RINKEBY}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
- env:
- REACT_APP_NETWORK='xdai'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
- STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_XDAI}
- SENTRY_PROJECT=${SENTRY_PROJECT_XDAI}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
if: (branch = master) OR tag IS present
- env:
- REACT_APP_NETWORK='volta'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
- STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
- SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
- env:
- REACT_APP_NETWORK='energy_web_chain'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
if: ((branch = master OR branch = release/2.14.0) AND NOT type = pull_request) OR tag IS present
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_EWC}
- SENTRY_PROJECT=${SENTRY_PROJECT_EWC}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
if: (branch = master AND NOT type = pull_request) OR tag IS present
cache:
yarn: true
before_script:
@ -49,7 +57,12 @@ script:
- yarn prettier:check
- yarn test:coverage
- yarn build
#- bash ./config/travis/build.sh
- if [[ $TRAVIS_BRANCH == "master" && $TRAVIS_PULL_REQUEST == "false" ]] || [ -n "$TRAVIS_TAG" ]; then
echo "Upload sentry source maps";
yarn sentry-upload-sourcemaps;
else
echo "Skip source map upload";
fi;
after_success:
# Pull Request - Deploy it to a review environment
# Travis doesn't do deploy step with pull requests builds
@ -93,7 +106,7 @@ deploy:
upload_dir: current/app
region: $AWS_DEFAULT_REGION
on:
branch: release/2.14.0
branch: release/v2.14.0
condition: $REACT_APP_NETWORK = energy_web_chain
# Prepare production deployment

View File

@ -1,10 +0,0 @@
#!/bin/bash
export NODE_ENV=production;
if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi
yarn lint:check
yarn prettier:check
yarn test:coverage
yarn build

359
docs/networks.md Normal file
View File

@ -0,0 +1,359 @@
# 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'
}
```
#### 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: ?,
logoUri: '',
},
},
disabledFeatures: [],
disabledWallets: []
}
export default <NETWORK_NAME>
```
## Configuration example (xDai) - fixed gas price
1) **ETHEREUM_NETWORK**
```typescript
export enum ETHEREUM_NETWORK {
MAINNET = 1,
MORDEN = 2,
ROPSTEN = 3,
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
XDAI = 100, -> ADDED XDAI
ENERGY_WEB_CHAIN = 246,
VOLTA = 73799,
UNKNOWN = 0,
LOCAL = 4447,
}
```
2) **Network file** [xdai](/src/config/networks/xdai.ts)
```typescript
import { ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
const xDai: NetworkConfig = {
environment: {
production: {
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
gasPrice: 1e9,
rpcServiceUrl: 'https://dai.poa.network/',
networkExplorerName: 'Blockscout',
networkExplorerUrl: 'https://blockscout.com/poa/xdai',
networkExplorerApiUrl: 'https://blockscout.com/poa/xdai/api',
},
},
network: {
id: ETHEREUM_NETWORK.XDAI,
backgroundColor: '#48A8A6',
textColor: '#ffffff',
label: 'xDai',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'xDai',
symbol: 'xDai',
decimals: 18,
logoUri: xDaiLogo,
},
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER
]
}
export default xDai
```
## Configuration example (Mainnet) - gas price retrieven by oracle
**Network file** [mainnet](/src/config/networks/mainnet.ts)
```typescript
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
gasPriceOracle: {
url: 'https://ethgasstation.info/json/ethgasAPI.json',
gasParameter: 'average',
},
rpcServiceUrl: 'https://mainnet.infura.io:443/v3',
networkExplorerName: 'Etherscan',
networkExplorerUrl: 'https://etherscan.io',
networkExplorerApiUrl: 'https://api.etherscan.io/api',
}
const mainnet: NetworkConfig = {
environment: {
dev: {
...baseConfig,
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
safeAppsUrl: 'https://apps.gnosis-safe.io',
},
},
network: {
id: ETHEREUM_NETWORK.MAINNET,
backgroundColor: '#E8E7E6',
textColor: '#001428',
label: 'Mainnet',
isTestNet: false,
nativeCoin: {
address: '0x000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: EtherLogo,
},
}
}
export default mainnet
```

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.13.1",
"version": "2.14.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -42,7 +42,8 @@
"test:coverage": "yarn test --coverage --watchAll=false",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"
"build-storybook": "build-storybook -s public",
"sentry-upload-sourcemaps": "sentry-cli --auth-token $SENTRY_AUTH_TOKEN releases -o $SENTRY_ORG -p $SENTRY_PROJECT files $npm_package_version upload-sourcemaps ./build/static/js/"
},
"husky": {
"hooks": {
@ -167,18 +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#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8d8508e",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.26.0",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
"@openzeppelin/contracts": "3.1.0",
"@sentry/react": "^5.27.1",
"@sentry/tracing": "^5.27.1",
"@truffle/contract": "4.2.26",
"async-sema": "^3.1.0",
"axios": "0.20.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",
@ -232,6 +235,7 @@
"web3-utils": "^1.2.11"
},
"devDependencies": {
"@sentry/cli": "^1.58.0",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addons": "^5.3.19",

View File

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

View File

@ -1,13 +1,13 @@
import React from 'react'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import Paragraph from 'src/components/layout/Paragraph'
import { border, xs } from 'src/theme/variables'
import styled from 'styled-components'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
const Wrapper = styled.div`
display: flex;
@ -61,7 +61,7 @@ const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactE
{safeAddress}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn value={safeAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
</div>
{ethBalance && (
<StyledBlock>

View File

@ -5,7 +5,6 @@ import QRCode from 'qrcode.react'
import React, { ReactElement } from 'react'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -15,7 +14,8 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { border, fontColor, lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
import { copyToClipboard } from 'src/utils/clipboard'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
const networkInfo = getNetworkInfo()
const useStyles = makeStyles(
@ -126,7 +126,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement =
{safeAddress}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn value={safeAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
</Block>
</Col>
<Hairline />

View File

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

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#B2B5B2" fill-rule="nonzero" d="M17 17v-2a1 1 0 0 1 2 0v2a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2a1 1 0 1 1 0 2H7v10h10z"/>
<path fill="#B2B5B2" d="M15.586 7H13a1 1 0 0 1 0-2h5a.997.997 0 0 1 1 1v5a1 1 0 0 1-2 0V8.414l-6.243 6.243a1 1 0 1 1-1.414-1.414L15.586 7z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

View File

@ -1,59 +0,0 @@
import Tooltip from '@material-ui/core/Tooltip'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import React from 'react'
import EtherscanOpenIcon from './img/etherscan-open.svg'
import Img from 'src/components/layout/Img'
import { xs } from 'src/theme/variables'
import { getExplorerInfo } from 'src/config'
const useStyles = makeStyles({
container: {
alignItems: 'center',
borderRadius: '50%',
display: 'flex',
justifyContent: 'center',
margin: `0 ${xs}`,
padding: '0',
transition: 'background-color .2s ease-in-out',
'&:hover': {
backgroundColor: '#F0EFEE',
},
},
increasedPopperZindex: {
zIndex: 2001,
},
})
interface EtherscanBtnProps {
className?: string
increaseZindex?: boolean
value: string
}
const EtherscanBtn = ({ className = '', increaseZindex = false, value }: EtherscanBtnProps): React.ReactElement => {
const classes = useStyles()
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
const explorerInfo = getExplorerInfo(value)
const { url } = explorerInfo()
return (
<Tooltip classes={customClasses} placement="top" title="Show details on Etherscan">
<a
aria-label="Show details on Etherscan"
className={cn(classes.container, className)}
onClick={(event) => event.stopPropagation()}
href={url}
rel="noopener noreferrer"
target="_blank"
>
<Img alt="Show on Etherscan" height={20} src={EtherscanOpenIcon} />
</a>
</Tooltip>
)
}
export default EtherscanBtn

View File

@ -5,11 +5,12 @@ import React from 'react'
import { styles } from './style'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
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 { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { getExplorerInfo } from 'src/config'
const useStyles = makeStyles(styles)
@ -20,7 +21,7 @@ interface EtherscanLinkProps {
value: string
}
const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
export const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkProps): React.ReactElement => {
const classes = useStyles()
return (
@ -29,10 +30,8 @@ const EtherscanLink = ({ className, cut, knownAddress, value }: EtherscanLinkPro
{cut ? shortVersionOf(value, cut) : value}
</Span>
<CopyBtn className={cn(classes.button, classes.firstButton)} content={value} />
<EtherscanBtn className={classes.button} value={value} />
<ExplorerButton explorerUrl={getExplorerInfo(value)} />
{knownAddress !== undefined ? <EllipsisTransactionDetails address={value} knownAddress={knownAddress} /> : null}
</Block>
)
}
export default EtherscanLink

View File

@ -0,0 +1,103 @@
import React from 'react'
import styled from 'styled-components'
import { Text, Link, Icon, FixedIcon, Title } from '@gnosis.pm/safe-react-components'
import { IS_PRODUCTION } from 'src/utils/constants'
const Wrapper = styled.div`
width: 100%;
margin-top: 50px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
`
const Content = styled.div`
width: 400px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
> * {
margin-top: 10px;
}
`
const LinkWrapper = styled.div`
display: inline-flex;
margin-bottom: 10px;
> :first-of-type {
margin-right: 5px;
}
`
const LinkContent = styled.div`
display: flex;
align-items: center;
> span {
margin-right: 5px;
}
`
type Props = {
error: Error
componentStack: string
resetError: () => void
}
const GlobalErrorBoundaryFallback = ({ error, componentStack }: Props): React.ReactElement => {
return (
<Wrapper>
<Content>
<Title size="md">Something went wrong, please try again.</Title>
<FixedIcon type="networkError" />
{IS_PRODUCTION && (
<div>
<Text size="xl" as="span">
In case the problem persists, please reach out to us via{' '}
</Text>
<LinkWrapper>
<a target="_blank" href="email: mailto:safe@gnosis.io" rel="noopener noreferrer">
<Text color="primary" size="lg" as="span">
Email
</Text>
</a>
<Icon type="externalLink" color="primary" size="sm" />
</LinkWrapper>
or{' '}
<LinkWrapper>
<a target="_blank" href="https://discordapp.com/invite/FPMRAwK" rel="noopener noreferrer">
<Text color="primary" size="lg" as="span">
Discord
</Text>
</a>
<Icon type="externalLink" color="primary" size="sm" />
</LinkWrapper>
</div>
)}
{!IS_PRODUCTION && (
<>
<Text size="xl" color="error">
{error.toString()}
</Text>
<Text size="md" color="error">
{componentStack}
</Text>
</>
)}
<Link size="lg" color="primary" href="/app/">
<LinkContent>
<Icon size="md" type="home" color="primary" />
Go to Home
</LinkContent>
</Link>
</Content>
</Wrapper>
)
}
export default GlobalErrorBoundaryFallback

View File

@ -4,10 +4,11 @@ import { ConnectedRouter } from 'connected-react-router'
import React from 'react'
import { Provider } from 'react-redux'
import { ThemeProvider } from 'styled-components'
import * as Sentry from '@sentry/react'
import Loader from 'src/components/Loader'
import App from 'src/components/App'
import GlobalErrorBoundary from 'src/components/GlobalErrorBoundary'
import AppRoutes from 'src/routes'
import { history, store } from 'src/store'
import theme from 'src/theme/mui'
@ -20,7 +21,11 @@ const Root = (): React.ReactElement => (
<ThemeProvider theme={styledTheme}>
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}>{<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>}</ConnectedRouter>
<ConnectedRouter history={history}>
<Sentry.ErrorBoundary fallback={GlobalErrorBoundary}>
<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>
</Sentry.ErrorBoundary>
</ConnectedRouter>
</MuiThemeProvider>
</Provider>
</ThemeProvider>

View File

@ -1,13 +1,16 @@
import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
import { EnvironmentSettings, ETHEREUM_NETWORK, 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
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps-ewc.staging.gnosisdev.com',
gasPriceOracle: {
url: 'https://station.energyweb.org',
gasParameter: 'standard',
},
gasPrice: 1e6,
rpcServiceUrl: 'https://rpc.energyweb.org',
networkExplorerName: 'Energy web explorer',
networkExplorerUrl: 'https://explorer.energyweb.org',
@ -21,11 +24,10 @@ const mainnet: NetworkConfig = {
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps.gnosis-safe.io',
safeAppsUrl: 'https://apps-ewc.gnosis-safe.io',
},
},
network: {
@ -41,7 +43,24 @@ const mainnet: NetworkConfig = {
decimals: 18,
logoUri: EwcLogo,
},
}
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER,
WALLETS.COINBASE,
WALLETS.DAPPER,
WALLETS.FORTMATIC,
WALLETS.OPERA,
WALLETS.OPERA_TOUCH,
WALLETS.PORTIS,
WALLETS.TORUS,
WALLETS.TRUST,
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM,
WALLETS.LATTICE
]
}
export default mainnet

View File

@ -15,7 +15,8 @@ export enum WALLETS {
COINBASE = 'coinbase',
WALLET_LINK = 'walletLink',
OPERA = 'opera',
OPERA_TOUCH = 'operaTouch'
OPERA_TOUCH = 'operaTouch',
LATTICE = 'lattice',
}
export enum FEATURES {

View File

@ -1,9 +1,9 @@
import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
safeAppsUrl: 'https://safe-apps-volta.staging.gnosisdev.com',
gasPriceOracle: {
url: 'https://station.energyweb.org',
gasParameter: 'standard',
@ -21,11 +21,10 @@ const mainnet: NetworkConfig = {
},
staging: {
...baseConfig,
safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com',
},
production: {
...baseConfig,
safeAppsUrl: 'https://apps.gnosis-safe.io',
safeAppsUrl: 'https://apps-volta.gnosis-safe.io',
},
},
network: {
@ -41,7 +40,24 @@ const mainnet: NetworkConfig = {
decimals: 18,
logoUri: EwcLogo,
},
}
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER,
WALLETS.COINBASE,
WALLETS.DAPPER,
WALLETS.FORTMATIC,
WALLETS.OPERA,
WALLETS.OPERA_TOUCH,
WALLETS.PORTIS,
WALLETS.TORUS,
WALLETS.TRUST,
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM,
WALLETS.LATTICE
]
}
export default mainnet

View File

@ -38,7 +38,19 @@ const xDai: NetworkConfig = {
},
disabledWallets:[
WALLETS.TREZOR,
WALLETS.LEDGER
WALLETS.LEDGER,
WALLETS.COINBASE,
WALLETS.DAPPER,
WALLETS.FORTMATIC,
WALLETS.OPERA,
WALLETS.OPERA_TOUCH,
WALLETS.TORUS,
WALLETS.TRUST,
WALLETS.UNILOGIN,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM,
WALLETS.LATTICE
]
}

View File

@ -1,6 +1,8 @@
import { BigNumber } from 'bignumber.js'
import React from 'react'
import ReactDOM from 'react-dom'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
import Root from 'src/components/Root'
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
@ -8,6 +10,7 @@ import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
import { SENTRY_DSN } from './utils/constants'
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
@ -16,6 +19,13 @@ store.dispatch(loadSafesFromStorage())
store.dispatch(loadDefaultSafe())
store.dispatch(loadCurrentSessionFromStorage())
Sentry.init({
dsn: SENTRY_DSN,
release: `safe-react@${process.env.REACT_APP_APP_VERSION}`,
integrations: [new Integrations.BrowserTracing()],
sampleRate: 1,
})
const root = document.getElementById('root')
if (root !== null) {

View File

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

View File

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

View File

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

View File

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

View File

@ -21,11 +21,12 @@ 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,
@ -323,9 +324,13 @@ 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,

View File

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

View File

@ -4,7 +4,6 @@ import { BigNumber } from 'bignumber.js'
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { getGasPrice, getGasPriceOracle } from 'src/config'
// const MAINNET_NETWORK = 1
export const EMPTY_DATA = '0x'
export const checkReceiptStatus = async (hash) => {
@ -28,16 +27,6 @@ export const checkReceiptStatus = async (hash) => {
}
export const calculateGasPrice = async (): Promise<string> => {
/*
const web3 = getWeb3()
const { network } = web3.version
const isMainnet = MAINNET_NETWORK === network
const url = isMainnet
? 'https://safe-relay.staging.gnosisdev.com/api/v1/gas-station/'
: 'https://safe-relay.dev.gnosisdev.com/'
*/
if (process.env.NODE_ENV === 'test') {
return '20000000000'
}
@ -61,7 +50,7 @@ export const calculateGasPrice = async (): Promise<string> => {
}
}
export const calculateGasOf = async (data, from, to) => {
export const calculateGasOf = async (data: string, from: string, to: string): Promise<number> => {
const web3 = getWeb3()
try {
const gas = await web3.eth.estimateGas({ data, from, to })

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
@ -24,6 +23,8 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { styles } from './styles'
import { getExplorerInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
const calculateSafeValues = (owners, threshold, values) => {
const initialValues = { ...values }
@ -112,7 +113,7 @@ const OwnerListComponent = (props) => {
{address}
</Paragraph>
<CopyBtn content={address} />
<EtherscanBtn value={address} />
<ExplorerButton explorerUrl={getExplorerInfo(address)} />
</Row>
</Col>
</Row>

View File

@ -3,7 +3,6 @@ import classNames from 'classnames'
import React from 'react'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
@ -16,6 +15,8 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
import { useStyles } from './styles'
import { getExplorerInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
const checkIfUserAddressIsAnOwner = (values: Record<string, string>, userAddress: string): boolean => {
let isOwner = false
@ -76,7 +77,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
{shortVersionOf(safeAddress, 4)}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn value={safeAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
</Row>
</Block>
<Block margin="lg">
@ -121,7 +122,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
{address}
</Paragraph>
<CopyBtn content={address} />
<EtherscanBtn value={address} />
<ExplorerButton explorerUrl={getExplorerInfo(address)} />
</Block>
</Block>
</Col>

View File

@ -2,9 +2,8 @@ import TableContainer from '@material-ui/core/TableContainer'
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
@ -18,6 +17,7 @@ import { getAccountsFrom, getNamesFrom } from 'src/routes/open/utils/safeDataExt
import { FIELD_CONFIRMATIONS, FIELD_NAME, getNumOwnersFrom } from '../fields'
import { useStyles } from './styles'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
type ReviewComponentProps = {
userAccount: string
@ -118,7 +118,7 @@ const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => {
{addresses[index]}
</Paragraph>
<CopyBtn content={addresses[index]} />
<EtherscanBtn value={addresses[index]} />
<ExplorerButton explorerUrl={getExplorerInfo(addresses[index])} />
</Block>
</Block>
</Col>

View File

@ -25,7 +25,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean; networks: n
},
// Aave
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmY1MUZo44UkT8EokYHs7xDvWEziYSn7n3c4ojVB6qo3SM`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX1NUtvm9WjbvT79sTdeg3sw1NxZAM273y44nBy5d2jZb`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET],
},

View File

@ -161,7 +161,7 @@ const SendModal = ({
onClose={onClose}
onNext={handleSendCollectible}
recipientAddress={recipientAddress}
selectedToken={selectedToken as NFTToken}
selectedToken={selectedToken as NFTToken | undefined}
/>
)}
{activeScreen === 'reviewCollectible' && (

View File

@ -4,10 +4,9 @@ import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -29,6 +28,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'
export type CustomTx = {
contractAddress?: string
@ -132,7 +132,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
{tx.contractAddress}
</Paragraph>
<CopyBtn content={tx.contractAddress as string} />
<EtherscanBtn value={tx.contractAddress as string} />
<ExplorerButton explorerUrl={getExplorerInfo(tx.contractAddress as string)} />
</Block>
</Col>
</Row>

View File

@ -8,7 +8,6 @@ import Close from '@material-ui/icons/Close'
import QRIcon from 'src/assets/icons/qrcode.svg'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextareaField from 'src/components/forms/TextareaField'
@ -32,7 +31,8 @@ import { sm } from 'src/theme/variables'
import ArrowDown from '../../assets/arrow-down.svg'
import { styles } from './style'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export interface CreatedTx {
contractAddress: string
@ -184,7 +184,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
</Paragraph>
</Block>
<CopyBtn content={selectedEntry.address} />
<EtherscanBtn value={selectedEntry.address} />
<ExplorerButton explorerUrl={getExplorerInfo(selectedEntry.address)} />
</Block>
</Col>
</Row>

View File

@ -5,9 +5,8 @@ import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -32,6 +31,7 @@ import { textShortener } from 'src/utils/strings'
import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
const { nativeCoin } = getNetworkInfo()
@ -153,7 +153,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
{tx.recipientAddress}
</Paragraph>
<CopyBtn content={tx.recipientAddress} />
<EtherscanBtn value={tx.recipientAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
</Block>
</Col>
</Row>

View File

@ -5,10 +5,9 @@ import { BigNumber } from 'bignumber.js'
import React, { useEffect, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -32,6 +31,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'
const useStyles = makeStyles(styles)
@ -162,7 +162,7 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
{tx.recipientAddress}
</Paragraph>
<CopyBtn content={tx.recipientAddress} />
<EtherscanBtn value={tx.recipientAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
</Block>
</Col>
</Row>

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import GnoForm from 'src/components/forms/GnoForm'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
@ -21,7 +20,7 @@ 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 { CollectibleSelectField } from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/CollectibleSelectField'
import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/TokenSelectField'
import { sm } from 'src/theme/variables'
@ -29,6 +28,8 @@ import ArrowDown from '../assets/arrow-down.svg'
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'
const formMutators = {
setMax: (args, state, utils) => {
@ -49,7 +50,7 @@ type SendCollectibleProps = {
onClose: () => void
onNext: (txInfo: SendCollectibleTxInfo) => void
recipientAddress?: string
selectedToken: NFTToken
selectedToken?: NFTToken
}
export type SendCollectibleTxInfo = {
@ -187,7 +188,7 @@ const SendCollectible = ({
</Paragraph>
</Block>
<CopyBtn content={selectedEntry.address} />
<EtherscanBtn value={selectedEntry.address} />
<ExplorerButton explorerUrl={getExplorerInfo(selectedEntry.address)} />
</Block>
</Col>
</Row>
@ -219,7 +220,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">
@ -231,7 +232,7 @@ const SendCollectible = ({
</Row>
<Row margin="md">
<Col>
<CollectibleSelectField initialValue={(selectedToken as any).tokenId} tokens={selectedNFTTokens} />
<CollectibleSelectField initialValue={selectedToken?.tokenId} tokens={selectedNFTTokens} />
</Col>
</Row>
</Block>

View File

@ -2,13 +2,12 @@ import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import React, { useState } from 'react'
import { OnChange } from 'react-final-form-listeners'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
@ -34,6 +33,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'
const formMutators = {
setMax: (args, state, utils) => {
@ -189,7 +189,7 @@ const SendFunds = ({
</Paragraph>
</Block>
<CopyBtn content={selectedEntry.address} />
<EtherscanBtn value={selectedEntry.address} />
<ExplorerButton explorerUrl={getExplorerInfo(selectedEntry.address)} />
</Block>
</Col>
</Row>

View File

@ -5,9 +5,8 @@ import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -21,6 +20,7 @@ import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn'
@ -118,7 +118,7 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
{owner.address}
</Paragraph>
<CopyBtn content={owner.address} />
<EtherscanBtn value={owner.address} />
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
</Block>
</Block>
</Col>
@ -146,7 +146,7 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
{values.ownerAddress}
</Paragraph>
<CopyBtn content={values.ownerAddress} />
<EtherscanBtn value={values.ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
</Block>
</Block>
</Col>

View File

@ -5,7 +5,6 @@ import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
@ -26,6 +25,8 @@ import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selector
import { sm } from 'src/theme/variables'
import { styles } from './style'
import { getExplorerInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input'
export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
@ -93,7 +94,7 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }
{ownerAddress}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn value={safeAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
</Block>
</Row>
</Block>

View File

@ -1,12 +1,12 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import EtherScanLink from 'src/components/EtherscanLink'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions'
import { EtherscanLink } from 'src/components/EtherscanLink'
type OwnerAddressTableCellProps = {
address: string
@ -36,7 +36,7 @@ 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} />
</div>
) : (
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>

View File

@ -5,7 +5,6 @@ import classNames from 'classnames/bind'
import React from 'react'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -15,6 +14,8 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { getExplorerInfo } from 'src/config'
export const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn'
@ -53,7 +54,7 @@ const CheckOwner = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) =>
{ownerAddress}
</Paragraph>
<CopyBtn content={ownerAddress} />
<EtherscanBtn value={ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
</Block>
</Block>
</Col>

View File

@ -5,9 +5,8 @@ import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -21,6 +20,7 @@ import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn'
@ -119,7 +119,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
{owner.address}
</Paragraph>
<CopyBtn content={owner.address} />
<EtherscanBtn value={owner.address} />
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
</Block>
</Block>
</Col>
@ -148,7 +148,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
{ownerAddress}
</Paragraph>
<CopyBtn content={ownerAddress} />
<EtherscanBtn value={ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
</Block>
</Block>
</Col>

View File

@ -6,7 +6,6 @@ import React from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import AddressInput from 'src/components/forms/AddressInput'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
@ -23,6 +22,8 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import { safeOwnersSelector } from 'src/logic/safe/store/selectors'
import { styles } from './style'
import { getExplorerInfo } from 'src/config'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input'
export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid'
@ -94,7 +95,7 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
{ownerAddress}
</Paragraph>
<CopyBtn content={ownerAddress} />
<EtherscanBtn value={ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
</Block>
</Block>
</Col>

View File

@ -4,10 +4,12 @@ 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 { getNetworkInfo } from 'src/config'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
@ -24,6 +26,8 @@ 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'
@ -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}>
@ -124,7 +130,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
{owner.address}
</Paragraph>
<CopyBtn content={owner.address} />
<EtherscanBtn value={owner.address} />
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
</Block>
</Block>
</Col>
@ -153,7 +159,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
{ownerAddress}
</Paragraph>
<CopyBtn content={ownerAddress} />
<EtherscanBtn value={ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
</Block>
</Block>
</Col>
@ -178,7 +184,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
{values.ownerAddress}
</Paragraph>
<CopyBtn content={values.ownerAddress} />
<EtherscanBtn value={values.ownerAddress} />
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
</Block>
</Block>
</Col>

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import { useSelector } from 'react-redux'
import EtherscanLink from 'src/components/EtherscanLink'
import { EtherscanLink } from 'src/components/EtherscanLink'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'

View File

@ -1,6 +1,6 @@
import React from 'react'
import { useSelector } from 'react-redux'
import EtherscanLink from 'src/components/EtherscanLink'
import { EtherscanLink } from 'src/components/EtherscanLink'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import Paragraph from 'src/components/layout/Paragraph'

View File

@ -1,6 +1,6 @@
import React from 'react'
import { useSelector } from 'react-redux'
import EtherscanLink from 'src/components/EtherscanLink'
import { EtherscanLink } from 'src/components/EtherscanLink'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'

View File

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

View File

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

View File

@ -101,7 +101,7 @@ const Container = (): React.ReactElement => {
path={`${matchSafeWithAddress?.path}/address-book`}
render={() => wrapInSuspense(<AddressBookTable />, null)}
/>
<Redirect to={`${matchSafeWithAddress?.path}/balances`} />
<Redirect to={`${matchSafeWithAddress?.url}/balances`} />
</Switch>
{modal.isOpen && <GenericModal {...modal} onClose={closeGenericModal} />}
</>

View File

@ -1,8 +1,10 @@
export const APP_ENV = process.env.REACT_APP_ENV
export const NODE_ENV = process.env.NODE_ENV
export const IS_PRODUCTION = process.env.NODE_ENV === 'production'
export const NETWORK = process.env.REACT_APP_NETWORK?.toUpperCase() || 'RINKEBY'
export const INTERCOM_ID = APP_ENV === 'production' ? process.env.REACT_APP_INTERCOM_ID : 'plssl1fl'
export const GOOGLE_ANALYTICS_ID = process.env.REACT_APP_GOOGLE_ANALYTICS || ''
export const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN || ''
export const PORTIS_ID = process.env.REACT_APP_PORTIS_ID ?? '852b763d-f28b-4463-80cb-846d7ec5806b'
export const FORTMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY ?? 'pk_test_CAD437AA29BE0A40'
export const BLOCKNATIVE_KEY = process.env.REACT_APP_BLOCKNATIVE_KEY ?? '7fbb9cee-7e97-4436-8770-8b29a9a8814c'

1892
yarn.lock

File diff suppressed because it is too large Load Diff