merge with develop
This commit is contained in:
commit
f9a345d200
|
@ -8,3 +8,5 @@ dist
|
||||||
electron-builder.yml
|
electron-builder.yml
|
||||||
.yalc/
|
.yalc/
|
||||||
yalc.lock
|
yalc.lock
|
||||||
|
# testing
|
||||||
|
/coverage/
|
||||||
|
|
|
@ -38,6 +38,7 @@ after_success:
|
||||||
- ./config/travis/deploy_pull_request.sh
|
- ./config/travis/deploy_pull_request.sh
|
||||||
# Releases (tagged commits) - Deploy it to a release environment
|
# Releases (tagged commits) - Deploy it to a release environment
|
||||||
- ./config/travis/deploy_release.sh
|
- ./config/travis/deploy_release.sh
|
||||||
|
- yarn coveralls
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
# Development environment
|
# Development environment
|
||||||
|
|
|
@ -6,4 +6,5 @@ if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi
|
||||||
|
|
||||||
yarn lint:check
|
yarn lint:check
|
||||||
yarn prettier:check
|
yarn prettier:check
|
||||||
|
yarn test:coverage
|
||||||
yarn build
|
yarn build
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "2.5.2",
|
"version": "2.6.1",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -36,7 +36,9 @@
|
||||||
"release": "electron-builder --mac --linux --windows -p always",
|
"release": "electron-builder --mac --linux --windows -p always",
|
||||||
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
|
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom"
|
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
|
||||||
|
"test:coverage": "yarn test --coverage --watchAll=false",
|
||||||
|
"coveralls": "cat ./coverage/lcov.info | coveralls"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
@ -49,6 +51,15 @@
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"!src/**/*.{.test.*}",
|
||||||
|
"!src/**/test/**/*",
|
||||||
|
"!src/**/assets/**",
|
||||||
|
"!src/config/**/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"productName": "Safe Multisig",
|
"productName": "Safe Multisig",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "io.gnosis.safe.macos",
|
"appId": "io.gnosis.safe.macos",
|
||||||
|
@ -163,6 +174,7 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"connected-react-router": "6.8.0",
|
"connected-react-router": "6.8.0",
|
||||||
|
"coveralls": "^3.1.0",
|
||||||
"currency-flags": "2.1.2",
|
"currency-flags": "2.1.2",
|
||||||
"date-fns": "2.14.0",
|
"date-fns": "2.14.0",
|
||||||
"electron-is-dev": "^1.1.0",
|
"electron-is-dev": "^1.1.0",
|
||||||
|
@ -241,6 +253,7 @@
|
||||||
"truffle": "5.1.33",
|
"truffle": "5.1.33",
|
||||||
"typescript": "3.9.6",
|
"typescript": "3.9.6",
|
||||||
"wait-on": "5.0.1",
|
"wait-on": "5.0.1",
|
||||||
|
"web3-core": "^1.2.9",
|
||||||
"web3-eth-contract": "^1.2.9",
|
"web3-eth-contract": "^1.2.9",
|
||||||
"web3-utils": "^1.2.8"
|
"web3-utils": "^1.2.8"
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props) => {
|
||||||
{ethBalance && (
|
{ethBalance && (
|
||||||
<StyledBlock>
|
<StyledBlock>
|
||||||
<Paragraph noMargin>
|
<Paragraph noMargin>
|
||||||
Balance: <Bold>{`${ethBalance} ETH`}</Bold>
|
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ETH`}</Bold>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</StyledBlock>
|
</StyledBlock>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,7 +3,10 @@ import Web3 from 'web3'
|
||||||
import { sameAddress } from './ethAddresses'
|
import { sameAddress } from './ethAddresses'
|
||||||
import { EMPTY_DATA } from './ethTransactions'
|
import { EMPTY_DATA } from './ethTransactions'
|
||||||
|
|
||||||
import { getNetwork } from 'src/config/index'
|
import { getNetwork } from '../../config'
|
||||||
|
import { ContentHash } from 'web3-eth-ens'
|
||||||
|
import { provider as Provider } from 'web3-core'
|
||||||
|
import { ProviderProps } from './store/model/provider'
|
||||||
|
|
||||||
export const ETHEREUM_NETWORK = {
|
export const ETHEREUM_NETWORK = {
|
||||||
MAINNET: 'MAINNET',
|
MAINNET: 'MAINNET',
|
||||||
|
@ -48,14 +51,14 @@ export const ETHEREUM_NETWORK_IDS = {
|
||||||
42: ETHEREUM_NETWORK.KOVAN,
|
42: ETHEREUM_NETWORK.KOVAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEtherScanLink = (type, value) => {
|
export const getEtherScanLink = (type: string, value: string): string => {
|
||||||
const network = getNetwork()
|
const network = getNetwork()
|
||||||
return `https://${
|
return `https://${
|
||||||
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
|
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
|
||||||
}etherscan.io/${type}/${value}`
|
}etherscan.io/${type}/${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getInfuraUrl = () => {
|
export const getInfuraUrl = (): string => {
|
||||||
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
||||||
|
|
||||||
return `https://${isMainnet ? 'mainnet' : 'rinkeby'}.infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
|
return `https://${isMainnet ? 'mainnet' : 'rinkeby'}.infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
|
||||||
|
@ -66,39 +69,41 @@ export const getInfuraUrl = () => {
|
||||||
export const web3ReadOnly =
|
export const web3ReadOnly =
|
||||||
process.env.NODE_ENV !== 'test'
|
process.env.NODE_ENV !== 'test'
|
||||||
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl()))
|
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl()))
|
||||||
: new Web3((window as any).web3.currentProvider)
|
: new Web3(window.web3?.currentProvider || 'ws://localhost:8545')
|
||||||
|
|
||||||
let web3 = web3ReadOnly
|
let web3 = web3ReadOnly
|
||||||
export const getWeb3 = () => web3
|
export const getWeb3 = (): Web3 => web3
|
||||||
|
|
||||||
export const resetWeb3 = () => {
|
export const resetWeb3 = (): void => {
|
||||||
web3 = web3ReadOnly
|
web3 = web3ReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAccountFrom = async (web3Provider) => {
|
export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => {
|
||||||
const accounts = await web3Provider.eth.getAccounts()
|
const accounts = await web3Provider.eth.getAccounts()
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test' && (window as any).testAccountIndex) {
|
if (process.env.NODE_ENV === 'test' && window.testAccountIndex) {
|
||||||
return accounts[(window as any).testAccountIndex]
|
return accounts[window.testAccountIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts && accounts.length > 0 ? accounts[0] : null
|
return accounts && accounts.length > 0 ? accounts[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNetworkIdFrom = (web3Provider) => web3Provider.eth.net.getId()
|
export const getNetworkIdFrom = (web3Provider: Web3): Promise<number> => web3Provider.eth.net.getId()
|
||||||
|
|
||||||
const isHardwareWallet = (walletName) =>
|
const isHardwareWallet = (walletName: string) =>
|
||||||
sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName)
|
sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName)
|
||||||
|
|
||||||
const isSmartContractWallet = async (web3Provider, account) => {
|
const isSmartContractWallet = async (web3Provider: Web3, account: string): Promise<boolean> => {
|
||||||
const contractCode = await web3Provider.eth.getCode(account)
|
const contractCode = await web3Provider.eth.getCode(account)
|
||||||
|
|
||||||
return contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== ''
|
return contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProviderInfo = async (web3Provider, providerName = 'Wallet') => {
|
export const getProviderInfo = async (
|
||||||
|
web3Provider: string | Provider,
|
||||||
|
providerName = 'Wallet',
|
||||||
|
): Promise<ProviderProps> => {
|
||||||
web3 = new Web3(web3Provider)
|
web3 = new Web3(web3Provider)
|
||||||
|
|
||||||
const account = await getAccountFrom(web3)
|
const account = await getAccountFrom(web3)
|
||||||
const network = await getNetworkIdFrom(web3)
|
const network = await getNetworkIdFrom(web3)
|
||||||
const smartContractWallet = await isSmartContractWallet(web3, account)
|
const smartContractWallet = await isSmartContractWallet(web3, account)
|
||||||
|
@ -117,15 +122,15 @@ export const getProviderInfo = async (web3Provider, providerName = 'Wallet') =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAddressFromENS = (name: string) => web3.eth.ens.getAddress(name)
|
export const getAddressFromENS = (name: string): Promise<string> => web3.eth.ens.getAddress(name)
|
||||||
|
|
||||||
export const getContentFromENS = (name: string) => web3.eth.ens.getContenthash(name)
|
export const getContentFromENS = (name: string): Promise<ContentHash> => web3.eth.ens.getContenthash(name)
|
||||||
|
|
||||||
export const setWeb3 = (provider) => {
|
export const setWeb3 = (provider: Provider): void => {
|
||||||
web3 = new Web3(provider)
|
web3 = new Web3(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBalanceInEtherOf = async (safeAddress) => {
|
export const getBalanceInEtherOf = async (safeAddress: string): Promise<string> => {
|
||||||
if (!web3) {
|
if (!web3) {
|
||||||
return '0'
|
return '0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
|
||||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||||
import { updateStoredTransactionsStatus } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
import { updateStoredTransactionsStatus } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||||
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
export const processProviderResponse = (dispatch, provider) => {
|
export const processProviderResponse = (dispatch, provider) => {
|
||||||
const walletRecord = makeProvider(provider)
|
const walletRecord = makeProvider(provider)
|
||||||
|
@ -48,9 +49,9 @@ const handleProviderNotification = (provider, dispatch) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (providerName) => async (dispatch) => {
|
export default (providerName: string) => async (dispatch: Dispatch): Promise<void> => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const providerInfo = await getProviderInfo(web3, providerName)
|
const providerInfo = await getProviderInfo(web3.currentProvider, providerName)
|
||||||
await handleProviderNotification(providerInfo, dispatch)
|
await handleProviderNotification(providerInfo, dispatch)
|
||||||
processProviderResponse(dispatch, providerInfo)
|
processProviderResponse(dispatch, providerInfo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ const providerWatcherMware = (store) => (next) => async (action) => {
|
||||||
|
|
||||||
watcherInterval = setInterval(async () => {
|
watcherInterval = setInterval(async () => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const providerInfo = await getProviderInfo(web3)
|
const providerInfo = await getProviderInfo(web3.currentProvider)
|
||||||
|
|
||||||
const networkChanged = currentProviderProps.network !== providerInfo.network
|
const networkChanged = currentProviderProps.network !== providerInfo.network
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri
|
||||||
href={getEtherScanLink('tx', safeCreationTxHash)}
|
href={getEtherScanLink('tx', safeCreationTxHash)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
data-testid="safe-create-etherscan-link"
|
||||||
>
|
>
|
||||||
Etherscan.io
|
Etherscan.io
|
||||||
</EtherScanLink>
|
</EtherScanLink>
|
||||||
|
|
|
@ -63,7 +63,7 @@ const isTxValid = (t: SafeAppTx): boolean => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof t.value === 'string' && !/^\d+$/.test(t.value)) {
|
if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ const removeLastTrailingSlash = (url) => {
|
||||||
|
|
||||||
const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl())
|
const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl())
|
||||||
export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
||||||
|
// 1inch
|
||||||
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUDTSghr154kCCGguyA3cbG5HRVd2tQgNR7yD69bcsjm5`, disabled: false },
|
||||||
// Aave
|
// Aave
|
||||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmY1MUZo44UkT8EokYHs7xDvWEziYSn7n3c4ojVB6qo3SM`, disabled: false },
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmY1MUZo44UkT8EokYHs7xDvWEziYSn7n3c4ojVB6qo3SM`, disabled: false },
|
||||||
// Compound
|
// Compound
|
||||||
|
@ -20,7 +22,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
||||||
// Idle
|
// Idle
|
||||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUoqmq8jw98VwTSf7aTQeBCfPKicQgcJL5k2Bch9QT8BJ`, disabled: false },
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUoqmq8jw98VwTSf7aTQeBCfPKicQgcJL5k2Bch9QT8BJ`, disabled: false },
|
||||||
// request
|
// request
|
||||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQapdJP6zERqpDKKPECNeMDDgwmGUqbKk1PjHpYj8gfDJ`, disabled: false },
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTBBaiDQyGa17DJ7DdviyHbc51fTVgf6Z5PW5w2YUTkgR`, disabled: false },
|
||||||
// Sablier
|
// Sablier
|
||||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
|
||||||
// TX-Builder
|
// TX-Builder
|
||||||
|
|
|
@ -72,7 +72,7 @@ const Coins = (props) => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case BALANCE_TABLE_BALANCE_ID: {
|
case BALANCE_TABLE_BALANCE_ID: {
|
||||||
cellItem = <div>{row[id]}</div>
|
cellItem = <div data-testid={`balance-${row[BALANCE_TABLE_ASSET_ID].symbol}`}>{row[id]}</div>
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case BALANCE_TABLE_VALUE_ID: {
|
case BALANCE_TABLE_VALUE_ID: {
|
||||||
|
|
|
@ -110,6 +110,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||||
minWidth={260}
|
minWidth={260}
|
||||||
onClick={() => setActiveScreen('sendFunds')}
|
onClick={() => setActiveScreen('sendFunds')}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
testId="modal-send-funds-btn"
|
||||||
>
|
>
|
||||||
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
||||||
Send funds
|
Send funds
|
||||||
|
@ -122,6 +123,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||||
minWidth={260}
|
minWidth={260}
|
||||||
onClick={() => setActiveScreen('sendCollectible')}
|
onClick={() => setActiveScreen('sendCollectible')}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
testId="modal-send-collectible-btn"
|
||||||
>
|
>
|
||||||
<Img
|
<Img
|
||||||
alt="Send collectible"
|
alt="Send collectible"
|
||||||
|
@ -138,6 +140,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||||
minWidth={260}
|
minWidth={260}
|
||||||
onClick={() => setActiveScreen('contractInteraction')}
|
onClick={() => setActiveScreen('contractInteraction')}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
testId="modal-contract-interaction-btn"
|
||||||
>
|
>
|
||||||
<Img
|
<Img
|
||||||
alt="Contract Interaction"
|
alt="Contract Interaction"
|
||||||
|
|
|
@ -105,7 +105,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row align="center" className={classes.heading} grow>
|
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
|
||||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||||
Send Funds
|
Send Funds
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
@ -136,7 +136,12 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col layout="column" xs={11}>
|
<Col layout="column" xs={11}>
|
||||||
<Block justify="left">
|
<Block justify="left">
|
||||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
<Paragraph
|
||||||
|
className={classes.address}
|
||||||
|
noMargin
|
||||||
|
weight="bolder"
|
||||||
|
data-testid="recipient-address-review-step"
|
||||||
|
>
|
||||||
{tx.recipientAddress}
|
{tx.recipientAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={tx.recipientAddress} />
|
<CopyBtn content={tx.recipientAddress} />
|
||||||
|
@ -151,12 +156,12 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||||
</Row>
|
</Row>
|
||||||
<Row align="center" margin="md">
|
<Row align="center" margin="md">
|
||||||
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.logoUri} />
|
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.logoUri} />
|
||||||
<Paragraph className={classes.amount} noMargin size="md">
|
<Paragraph className={classes.amount} noMargin size="md" data-testid={`amount-${txToken.symbol}-review-step`}>
|
||||||
{tx.amount} {txToken.symbol}
|
{tx.amount} {txToken.symbol}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<Paragraph data-testid="fee-meg-review-step">
|
||||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -57,7 +57,11 @@ const TokenSelectField = ({ classes, initialValue, isValid, tokens }) => (
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />
|
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={token.name} secondary={`${formatAmount(token.balance)} ${token.symbol}`} />
|
<ListItemText
|
||||||
|
primary={token.name}
|
||||||
|
secondary={`${formatAmount(token.balance)} ${token.symbol}`}
|
||||||
|
data-testid={`select-token-${token.name}`}
|
||||||
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -84,7 +84,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row align="center" className={classes.heading} grow>
|
<Row align="center" className={classes.heading} grow data-testid="modal-title-send-funds">
|
||||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||||
Send Funds
|
Send Funds
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
@ -221,7 +221,11 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
Amount
|
Amount
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<ButtonLink onClick={() => mutators.setMax(selectedTokenRecord.balance)} weight="bold">
|
<ButtonLink
|
||||||
|
onClick={() => mutators.setMax(selectedTokenRecord.balance)}
|
||||||
|
weight="bold"
|
||||||
|
testId="send-max-btn"
|
||||||
|
>
|
||||||
Send max
|
Send max
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -239,6 +243,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
placeholder="Amount*"
|
placeholder="Amount*"
|
||||||
text="Amount*"
|
text="Amount*"
|
||||||
type="text"
|
type="text"
|
||||||
|
testId="amount-input"
|
||||||
validate={composeValidators(
|
validate={composeValidators(
|
||||||
required,
|
required,
|
||||||
mustBeFloat,
|
mustBeFloat,
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const getBalanceData = (activeTokens, currencySelected, currencyValues, c
|
||||||
name: token.name,
|
name: token.name,
|
||||||
logoUri: token.logoUri,
|
logoUri: token.logoUri,
|
||||||
address: token.address,
|
address: token.address,
|
||||||
|
symbol: token.symbol,
|
||||||
},
|
},
|
||||||
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name,
|
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name,
|
||||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`,
|
[BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`,
|
||||||
|
|
|
@ -147,6 +147,7 @@ const ApproveTxModal = ({
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />}
|
control={<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />}
|
||||||
label="Execute transaction"
|
label="Execute transaction"
|
||||||
|
data-testid="execute-checkbox"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -144,9 +144,16 @@ const OwnersColumn = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={classes.rightCol} layout="block" xs={6}>
|
<Col className={classes.rightCol} layout="block" xs={6}>
|
||||||
<Block className={cn(classes.ownerListTitle, (thresholdReached || tx.isExecuted) && classes.ownerListTitleDone)}>
|
<Block
|
||||||
|
className={cn(classes.ownerListTitle, (thresholdReached || tx.isExecuted) && classes.ownerListTitleDone)}
|
||||||
|
data-testid={`confirmed-${tx.confirmations.size}-out-of-${txThreshold}`}
|
||||||
|
>
|
||||||
<div className={classes.circleState}>
|
<div className={classes.circleState}>
|
||||||
<Img alt="" src={thresholdReached || tx.isExecuted ? CheckLargeFilledGreenCircle : ConfirmLargeGreenCircle} />
|
<Img
|
||||||
|
alt=""
|
||||||
|
src={thresholdReached || tx.isExecuted ? CheckLargeFilledGreenCircle : ConfirmLargeGreenCircle}
|
||||||
|
data-testid={thresholdReached || tx.isExecuted ? 'confirmed-tx-check' : 'not-confirmed-tx-check'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tx.isExecuted
|
{tx.isExecuted
|
||||||
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
||||||
|
@ -169,6 +176,7 @@ const OwnersColumn = ({
|
||||||
classes.ownerListTitle,
|
classes.ownerListTitle,
|
||||||
(cancelThresholdReached || cancelTx.isExecuted) && classes.ownerListTitleCancelDone,
|
(cancelThresholdReached || cancelTx.isExecuted) && classes.ownerListTitleCancelDone,
|
||||||
)}
|
)}
|
||||||
|
data-testid={`rejected-${cancelTx.confirmations.size}-out-of-${cancelThreshold}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(classes.verticalLine, tx.isExecuted ? classes.verticalLineDone : classes.verticalLinePending)}
|
className={cn(classes.verticalLine, tx.isExecuted ? classes.verticalLineDone : classes.verticalLinePending)}
|
||||||
|
|
|
@ -42,7 +42,7 @@ const Status = ({ classes, status }) => {
|
||||||
return (
|
return (
|
||||||
<Block className={`${classes.container} ${classes[status]}`}>
|
<Block className={`${classes.container} ${classes[status]}`}>
|
||||||
{typeof Icon === 'object' ? Icon : <Img alt={statusToLabel[status]} src={Icon} style={statusIconStyle} />}
|
{typeof Icon === 'object' ? Icon : <Img alt={statusToLabel[status]} src={Icon} style={statusIconStyle} />}
|
||||||
<Paragraph className={classes.statusText} noMargin>
|
<Paragraph className={classes.statusText} noMargin data-testid={`tx-status-${statusToLabel[status]}`}>
|
||||||
{statusToLabel[status]}
|
{statusToLabel[status]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -14,7 +14,8 @@ describe('TxsTable Columns > getTxTableData', () => {
|
||||||
const txRow = txTableData.first()
|
const txRow = txTableData.first()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
|
// expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
|
||||||
|
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined()
|
||||||
})
|
})
|
||||||
it('should not include CancelTx object inside TxTableData', () => {
|
it('should not include CancelTx object inside TxTableData', () => {
|
||||||
// Given
|
// Given
|
|
@ -72,7 +72,7 @@ export const aMinedSafe = async (
|
||||||
threshold = 1,
|
threshold = 1,
|
||||||
name = 'Safe Name',
|
name = 'Safe Name',
|
||||||
) => {
|
) => {
|
||||||
const provider = await getProviderInfo((window as any).web3.currentProvider)
|
const provider = await getProviderInfo(window.web3?.currentProvider || 'ws://localhost:8545')
|
||||||
const walletRecord = makeProvider(provider)
|
const walletRecord = makeProvider(provider)
|
||||||
store.dispatch(addProvider(walletRecord))
|
store.dispatch(addProvider(walletRecord))
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ afterAll(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderOpenSafeForm = async (localStore) => {
|
const renderOpenSafeForm = async (localStore) => {
|
||||||
const provider = await getProviderInfo((window as any).web3.currentProvider)
|
const provider = await getProviderInfo(window.web3.currentProvider)
|
||||||
const walletRecord = makeProvider(provider)
|
const walletRecord = makeProvider(provider)
|
||||||
localStore.dispatch(addProvider(walletRecord))
|
localStore.dispatch(addProvider(walletRecord))
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
//
|
|
||||||
import { fireEvent } from '@testing-library/react'
|
|
||||||
import { Map, Set, List } from 'immutable'
|
|
||||||
import { aNewStore } from 'src/store'
|
|
||||||
import { aMinedSafe } from 'src/test/builder/safe.redux.builder'
|
|
||||||
import { sendTokenTo, sendEtherTo, get6DecimalsTokenContract } from 'src/test/utils/tokenMovements'
|
|
||||||
import { renderSafeView } from 'src/test/builder/safe.dom.utils'
|
|
||||||
import { getWeb3, getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
|
|
||||||
import { dispatchAddTokenToList } from 'src/test/utils/transactions/moveTokens.helper'
|
|
||||||
import { sleep } from 'src/utils/timer'
|
|
||||||
import saveTokens from 'src/logic/tokens/store/actions/saveTokens'
|
|
||||||
// import { calculateBalanceOf } from 'src/routes/safe/store/actions/fetchTokenBalances'
|
|
||||||
import updateActiveTokens from 'src/routes/safe/store/actions/updateActiveTokens'
|
|
||||||
import '@testing-library/jest-dom/extend-expect'
|
|
||||||
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
|
|
||||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
|
||||||
import { checkRegisteredTxSend, fillAndSubmitSendFundsForm } from './utils/transactions'
|
|
||||||
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
|
|
||||||
|
|
||||||
describe('DOM > Feature > Sending Funds', () => {
|
|
||||||
let store
|
|
||||||
let safeAddress
|
|
||||||
let accounts
|
|
||||||
beforeEach(async () => {
|
|
||||||
store = aNewStore()
|
|
||||||
safeAddress = await aMinedSafe(store)
|
|
||||||
accounts = await getWeb3().eth.getAccounts()
|
|
||||||
})
|
|
||||||
|
|
||||||
// it('Sends ETH with threshold = 1', async () => {
|
|
||||||
// // GIVEN
|
|
||||||
// const ethAmount = '5'
|
|
||||||
|
|
||||||
// // the tests are run in parallel, lets use account 9 because it's not used anywhere else
|
|
||||||
// // (in other tests we trigger transactions and pay gas for it, so we can't really make reliable
|
|
||||||
// // assumptions on account's ETH balance)
|
|
||||||
// await sendEtherTo(safeAddress, ethAmount, 9)
|
|
||||||
|
|
||||||
// // WHEN
|
|
||||||
// const SafeDom = renderSafeView(store, safeAddress)
|
|
||||||
// await sleep(1300)
|
|
||||||
|
|
||||||
// // Open send funds modal
|
|
||||||
// const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
|
|
||||||
// expect(balanceRows[0]).toHaveTextContent(`${ethAmount} ETH`)
|
|
||||||
// const sendButton = SafeDom.getByTestId('balance-send-btn')
|
|
||||||
// fireEvent.click(sendButton)
|
|
||||||
|
|
||||||
// const receiverBalanceBeforeTx = await getBalanceInEtherOf(accounts[9])
|
|
||||||
// await fillAndSubmitSendFundsForm(SafeDom, sendButton, ethAmount, accounts[9])
|
|
||||||
|
|
||||||
// // THEN
|
|
||||||
// const safeFunds = await getBalanceInEtherOf(safeAddress)
|
|
||||||
// expect(Number(safeFunds)).toBe(0)
|
|
||||||
// const receiverBalanceAfterTx = await getBalanceInEtherOf(accounts[9])
|
|
||||||
|
|
||||||
// const ESTIMATED_GASCOSTS = 0.3
|
|
||||||
// expect(Number(parseInt(receiverBalanceAfterTx, 10) - parseInt(receiverBalanceBeforeTx, 10))).toBeGreaterThan(
|
|
||||||
// parseInt(ethAmount, 10) - ESTIMATED_GASCOSTS,
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // Check that the transaction was registered
|
|
||||||
// await checkRegisteredTxSend(SafeDom, ethAmount, 'ETH', accounts[9])
|
|
||||||
// })
|
|
||||||
|
|
||||||
// it('Sends Tokens with 18 decimals with threshold = 1', async () => {
|
|
||||||
// // GIVEN
|
|
||||||
// const tokensAmount = '100'
|
|
||||||
// const tokenReceiver = accounts[1]
|
|
||||||
// const tokenAddress = await sendTokenTo(safeAddress, tokensAmount)
|
|
||||||
// await dispatchAddTokenToList(store, tokenAddress)
|
|
||||||
|
|
||||||
// // WHEN
|
|
||||||
// const SafeDom = await renderSafeView(store, safeAddress)
|
|
||||||
// await sleep(1300)
|
|
||||||
|
|
||||||
// // Activate token
|
|
||||||
// // const safeTokenBalance = await calculateBalanceOf(tokenAddress, safeAddress, 18)
|
|
||||||
// // expect(safeTokenBalance).toBe(tokensAmount)
|
|
||||||
|
|
||||||
// // const balances = Map({
|
|
||||||
// // [tokenAddress]: safeTokenBalance,
|
|
||||||
// // })
|
|
||||||
|
|
||||||
// store.dispatch(updateActiveTokens(safeAddress, Set([tokenAddress])))
|
|
||||||
// store.dispatch(updateSafe({ address: safeAddress, balances }))
|
|
||||||
// await sleep(1000)
|
|
||||||
|
|
||||||
// // Open send funds modal
|
|
||||||
// const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
|
|
||||||
// expect(balanceRows.length).toBe(2)
|
|
||||||
// const sendButtons = SafeDom.getAllByTestId('balance-send-btn')
|
|
||||||
// expect(sendButtons.length).toBe(2)
|
|
||||||
|
|
||||||
// await fillAndSubmitSendFundsForm(SafeDom, sendButtons[1], tokensAmount, tokenReceiver)
|
|
||||||
|
|
||||||
// // THEN
|
|
||||||
// // const safeFunds = await calculateBalanceOf(tokenAddress, safeAddress, 18)
|
|
||||||
// // expect(Number(safeFunds)).toBe(0)
|
|
||||||
// // const receiverFunds = await calculateBalanceOf(tokenAddress, tokenReceiver, 18)
|
|
||||||
// // expect(receiverFunds).toBe(tokensAmount)
|
|
||||||
|
|
||||||
// // Check that the transaction was registered
|
|
||||||
// await checkRegisteredTxSend(SafeDom, tokensAmount, 'OMG', tokenReceiver)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// it('Sends Tokens with decimals other than 18 with threshold = 1', async () => {
|
|
||||||
// // GIVEN
|
|
||||||
// const tokensAmount = '1000000'
|
|
||||||
// const tokenReceiver = accounts[1]
|
|
||||||
// const web3 = await getWeb3()
|
|
||||||
// const SixDecimalsToken = await get6DecimalsTokenContract(web3, accounts[0])
|
|
||||||
// const tokenList = List([
|
|
||||||
// makeToken({
|
|
||||||
// address: SixDecimalsToken.address,
|
|
||||||
// name: '6 Decimals',
|
|
||||||
// symbol: '6DEC',
|
|
||||||
// decimals: 6,
|
|
||||||
// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
|
|
||||||
// }),
|
|
||||||
// ])
|
|
||||||
// await store.dispatch(saveTokens(tokenList))
|
|
||||||
|
|
||||||
// await SixDecimalsToken.contract.methods.transfer(safeAddress, tokensAmount).send({ from: accounts[0] })
|
|
||||||
|
|
||||||
// // WHEN
|
|
||||||
// const SafeDom = await renderSafeView(store, safeAddress)
|
|
||||||
// await sleep(1300)
|
|
||||||
|
|
||||||
// // Activate token
|
|
||||||
// const safeTokenBalance = await calculateBalanceOf(SixDecimalsToken.address, safeAddress, 6)
|
|
||||||
// expect(safeTokenBalance).toBe('1')
|
|
||||||
|
|
||||||
// const balances = Map({
|
|
||||||
// [SixDecimalsToken.address]: safeTokenBalance,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// store.dispatch(updateActiveTokens(safeAddress, Set([SixDecimalsToken.address])))
|
|
||||||
// store.dispatch(updateSafe({ address: safeAddress, balances }))
|
|
||||||
// await sleep(1000)
|
|
||||||
|
|
||||||
// // Open send funds modal
|
|
||||||
// const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
|
|
||||||
// expect(balanceRows.length).toBe(2)
|
|
||||||
// const sendButtons = SafeDom.getAllByTestId('balance-send-btn')
|
|
||||||
// expect(sendButtons.length).toBe(2)
|
|
||||||
|
|
||||||
// await fillAndSubmitSendFundsForm(SafeDom, sendButtons[1], '1', tokenReceiver)
|
|
||||||
|
|
||||||
// // THEN
|
|
||||||
// const safeFunds = await calculateBalanceOf(SixDecimalsToken.address, safeAddress, 6)
|
|
||||||
// expect(Number(safeFunds)).toBe(0)
|
|
||||||
// const receiverFunds = await calculateBalanceOf(SixDecimalsToken.address, tokenReceiver, 6)
|
|
||||||
// expect(receiverFunds).toBe('1')
|
|
||||||
// })
|
|
||||||
})
|
|
|
@ -7,7 +7,7 @@ import { ConnectedRouter } from 'connected-react-router'
|
||||||
import Load from 'src/routes/load/container/Load'
|
import Load from 'src/routes/load/container/Load'
|
||||||
import { aNewStore, history, } from 'src/store'
|
import { aNewStore, history, } from 'src/store'
|
||||||
import { sleep } from 'src/utils/timer'
|
import { sleep } from 'src/utils/timer'
|
||||||
import { getProviderInfo } from 'src/logic/wallets/getWeb3'
|
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import addProvider from 'src/logic/wallets/store/actions/addProvider'
|
import addProvider from 'src/logic/wallets/store/actions/addProvider'
|
||||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||||
import { aMinedSafe } from './builder/safe.redux.builder'
|
import { aMinedSafe } from './builder/safe.redux.builder'
|
||||||
|
@ -29,7 +29,7 @@ afterAll(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderLoadSafe = async (localStore) => {
|
const renderLoadSafe = async (localStore) => {
|
||||||
const provider = await getProviderInfo((window as any).web3.currentProvider)
|
const provider = await getProviderInfo(window.web3.currentProvider || 'ws://localhost:8545')
|
||||||
const walletRecord = makeProvider(provider)
|
const walletRecord = makeProvider(provider)
|
||||||
localStore.dispatch(addProvider(walletRecord))
|
localStore.dispatch(addProvider(walletRecord))
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { aMinedSafe } from 'src/test/builder/safe.redux.builder'
|
||||||
import { renderSafeView } from 'src/test/builder/safe.dom.utils'
|
import { renderSafeView } from 'src/test/builder/safe.dom.utils'
|
||||||
import { sleep } from 'src/utils/timer'
|
import { sleep } from 'src/utils/timer'
|
||||||
import saveTokens from 'src/logic/tokens/store/actions/saveTokens'
|
import saveTokens from 'src/logic/tokens/store/actions/saveTokens'
|
||||||
import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './utils/DOMNavigation'
|
import { clickOnManageTokens, closeManageTokensModal, toggleToken } from './utils/DOMNavigation'
|
||||||
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
|
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
|
||||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||||
import '@testing-library/jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
|
@ -1,2 +1,9 @@
|
||||||
|
import Web3 from 'web3'
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
web3?: Web3
|
||||||
|
testAccountIndex?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
||||||
declare module 'currency-flags/dist/currency-flags.min.css'
|
declare module 'currency-flags/dist/currency-flags.min.css'
|
||||||
|
|
Loading…
Reference in New Issue