Merge pull request #2015 from gnosis/release/v3.2.0

This commit is contained in:
Daniel Sanchez 2021-03-12 12:31:31 +01:00 committed by GitHub
commit 99f46043ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
225 changed files with 1993 additions and 8751 deletions

View File

@ -1,10 +1,10 @@
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
'plugin:react/recommended', // React recommended rules plugin
'plugin:@typescript-eslint/recommended', // Plugin to use typescript with eslint
'prettier', // Add prettier rules to eslint
'plugin:prettier/recommended', // Plugin to use prettier rules with eslint
],
plugins: ['react-hooks'],
parserOptions: {

View File

@ -11,7 +11,7 @@ on:
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'ewc'
REACT_APP_NETWORK: 'energy_web_chain'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_EWC_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_EWC }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_EWC }}

View File

@ -1,116 +0,0 @@
# if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
if: (branch = master) OR (tag IS present)
dist: focal
language: node_js
node_js:
- '12'
os:
- linux
matrix:
# include:
# - env:
# - REACT_APP_NETWORK='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}
# if: (branch = development AND NOT type = pull_request) OR (branch = master) OR tag IS present
# - env:
# - REACT_APP_NETWORK='xdai'
# - STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
# - 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'
# - STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME}
# - REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
# - SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
# - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
# if: (branch = master) OR tag IS present
# - env:
# - REACT_APP_NETWORK='energy_web_chain'
# - STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
# - 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:
npm: false
yarn: true
before_script:
- if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi;
- if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi;
before_install:
# Needed to deploy pull request and releases
# - sudo apt-get update
# - sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
# - pip install awscli --upgrade --user
script:
- yarn prettier:check
# - yarn test:coverage
# - yarn build
# - 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
# - ./config/travis/deploy_pull_request.sh
# Releases (tagged commits) - Deploy it to a release environment
# - ./config/travis/deploy_release.sh
# - yarn coveralls
deploy:
# Development environment only on rinkeby
# - provider: s3
# bucket: $DEV_BUCKET_NAME
# access_key_id: $AWS_ACCESS_KEY_ID
# secret_access_key: $AWS_SECRET_ACCESS_KEY
# skip_cleanup: true
# local_dir: build
# upload_dir: app
# region: $AWS_DEFAULT_REGION
# on:
# branch: development
# condition: $REACT_APP_NETWORK = rinkeby
# Staging environment
# - provider: s3
# bucket: $STAGING_BUCKET_NAME
# access_key_id: $AWS_ACCESS_KEY_ID
# secret_access_key: $AWS_SECRET_ACCESS_KEY
# skip_cleanup: true
# local_dir: build
# upload_dir: current/app
# region: $AWS_DEFAULT_REGION
# on:
# branch: master
# Prepare production deployment
# - provider: s3
# bucket: $STAGING_BUCKET_NAME
# secret_access_key: $AWS_SECRET_ACCESS_KEY
# access_key_id: $AWS_ACCESS_KEY_ID
# skip_cleanup: true
# local_dir: build
# upload_dir: releases/$TRAVIS_TAG
# region: $AWS_DEFAULT_REGION
# on:
# tags: true
# - provider: script
# script: ./config/travis/prepare_production_deployment.sh
# on:
# tags: true

View File

@ -1,38 +0,0 @@
#!/bin/bash
function deploy_pull_request {
REVIEW_ENVIRONMENT_DOMAIN='review.gnosisdev.com'
# Pull request name with "pr" prefix
PULL_REQUEST_NAME="pr$TRAVIS_PULL_REQUEST"
# Feature name without all path. Example gnosis/pm-trading-ui -> pm-trading-ui
REPO_NAME=$(basename $TRAVIS_REPO_SLUG)
# Only alphanumeric characters. Example pm-trading-ui -> pmtradingui
REPO_NAME_ALPHANUMERIC=$(echo $REPO_NAME | sed 's/[^a-zA-Z0-9]//g')
# TRAVIS_PULL_REQUEST contains pull request number
REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NAME"
# Deploy safe-team project
aws s3 sync build s3://${REVIEW_BUCKET_NAME}/${REVIEW_FEATURE_FOLDER}/${REACT_APP_NETWORK}/app --delete
}
function publish_pull_request_urls_in_github {
REVIEW_FEATURE_URL="https://$PULL_REQUEST_NAME--$REPO_NAME_ALPHANUMERIC.$REVIEW_ENVIRONMENT_DOMAIN/$REACT_APP_NETWORK/app"
# Using the Issues api instead of the PR api
# Done so because every PR is an issue, and the issues api allows to post general comments,
# while the PR api requires that comments are made to specific files and specific commits
GITHUB_PR_COMMENTS=https://api.github.com/repos/${TRAVIS_REPO_SLUG}/issues/${TRAVIS_PULL_REQUEST}/comments
curl -H "Authorization: token ${GITHUB_API_TOKEN}" --request POST ${GITHUB_PR_COMMENTS} --data '{"body":"Travis automatic deployment:\r\n '${REVIEW_FEATURE_URL}' \r\n"}'
}
# Only:
# - Pull requests
# - Security env variables are available. PR from forks don't have them.
if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ -n "$AWS_ACCESS_KEY_ID" ]
then
deploy_pull_request
publish_pull_request_urls_in_github
fi

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "3.1.2",
"version": "3.2.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -161,21 +161,21 @@
"@gnosis.pm/safe-apps-sdk": "1.0.3",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#fb1a523",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#f610327",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid-singleton": "5.41.0",
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.11.0",
"@material-ui/lab": "4.0.0-alpha.57",
"@openzeppelin/contracts": "3.1.0",
"@sentry/react": "^5.30.0",
"@sentry/tracing": "^5.30.0",
"@sentry/react": "^6.2.1",
"@sentry/tracing": "^6.2.1",
"@truffle/contract": "^4.3.0",
"@unstoppabledomains/resolution": "^1.17.0",
"async-sema": "^3.1.0",
"axios": "0.21.1",
"bignumber.js": "9.0.1",
"bnc-onboard": "1.19.2",
"bnc-onboard": "~1.20.0",
"classnames": "^2.2.6",
"concurrently": "^5.3.0",
"connected-react-router": "6.8.0",
@ -186,13 +186,13 @@
"electron-is-dev": "^1.2.0",
"electron-log": "^4.3.0",
"electron-settings": "^4.0.2",
"electron-updater": "4.3.5",
"electron-updater": "4.3.8",
"eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2",
"ethereumjs-abi": "0.6.8",
"exponential-backoff": "^3.1.0",
"express": "^4.17.1",
"final-form": "^4.20.1",
"final-form": "^4.20.2",
"final-form-calculate": "^1.3.2",
"history": "4.10.1",
"immortal-db": "^1.1.0",
@ -207,7 +207,7 @@
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"object-hash": "^2.1.1",
"qrcode.react": "1.0.1",
"query-string": "6.13.8",
"query-string": "6.14.1",
"react": "16.13.1",
"react-device-detect": "^1.15.0",
"react-dom": "16.13.1",
@ -233,8 +233,8 @@
"web3-utils": "^1.2.11"
},
"devDependencies": {
"@rescripts/cli": "^0.0.15",
"@sentry/cli": "^1.62.0",
"@rescripts/cli": "^0.0.16",
"@sentry/cli": "^1.63.1",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addons": "^5.3.19",
@ -242,41 +242,41 @@
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@typechain/web3-v1": "^2.0.0",
"@typechain/web3-v1": "^2.2.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.16",
"@types/lodash.get": "^4.4.6",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "^14.14.10",
"@types/react": "^16.9.55",
"@types/node": "^14.14.30",
"@types/react": "^16.14.5",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux-actions": "^2.6.1",
"@types/styled-components": "^5.1.4",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"electron": "^9.4.0",
"electron-builder": "22.9.1",
"electron-builder": "22.10.5",
"electron-notarize": "1.0.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-sort-destructure-keys": "^1.3.5",
"husky": "^4.3.0",
"husky": "~4.3.8",
"lint-staged": "^10.5.2",
"patch-package": "^6.2.2",
"patch-package": "^6.4.6",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.2.0",
"sass": "^1.32.0",
"typechain": "^4.0.0",
"typescript": "4.1.3",
"typescript": "4.2.3",
"wait-on": "5.2.1"
}
}

View File

@ -94,7 +94,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement =
<>
<Row align="center" className={classes.heading} grow>
<Paragraph noMargin size="xl" weight="bolder">
Receive funds
Receive assets
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
@ -106,7 +106,7 @@ const ReceiveModal = ({ onClose, safeAddress, safeName }: Props): ReactElement =
</Paragraph>
<Paragraph className={classes.annotation} noMargin size="lg">
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send{' '}
{networkInfo.nativeCoin.name} and ERC-20 tokens to this address!
{networkInfo.nativeCoin.name} and assets to this address (e.g. ETH, ERC20, ERC721)!
</Paragraph>
<Col layout="column" middle="xs">
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">

View File

@ -171,7 +171,7 @@ const SafeHeader = ({
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
<FixedIcon type="arrowSentWhite" />
<Text size="lg" color="white">
New Transaction
New transaction
</Text>
</StyledButton>
</Container>

View File

@ -129,6 +129,10 @@ const CookiesBanner = (): ReactElement => {
}
setLocalAnalytics(acceptedAnalytics)
setLocalNecessary(acceptedNecessary)
if (acceptedAnalytics && !isDesktop) {
loadGoogleAnalytics()
}
}
}
fetchCookiesFromStorage()
@ -162,10 +166,6 @@ const CookiesBanner = (): ReactElement => {
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
}
if (showAnalytics && !isDesktop) {
loadGoogleAnalytics()
}
if (showIntercom) {
loadIntercom()
}

View File

@ -11,6 +11,7 @@ import Controls from './Controls'
import GnoForm from 'src/components/forms/GnoForm'
import Hairline from 'src/components/layout/Hairline'
import { history } from 'src/store'
import { LoadFormValues } from 'src/routes/load/container/Load'
const transitionProps = {
timeout: {
@ -20,7 +21,7 @@ const transitionProps = {
}
export interface StepperPageFormProps {
values: Record<string, string>
values: LoadFormValues
errors: Record<string, string>
form: FormApi
}

View File

@ -55,7 +55,6 @@ const mainnet: NetworkConfig = {
WALLETS.PORTIS,
WALLETS.TORUS,
WALLETS.TRUST,
WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM,
WALLETS.LATTICE,

View File

@ -16,10 +16,6 @@ export const nftAssetsListSelector = createSelector(nftAssets, (assets): NFTAsse
return assets ? Object.values(assets) : []
})
export const nftAssetsListAddressesSelector = createSelector(nftAssetsListSelector, (assets): string[] => {
return Array.from(new Set(assets.map((nftAsset) => nftAsset.address)))
})
export const availableNftAssetsAddresses = createSelector(nftTokensSelector, (userNftTokens): string[] => {
return Array.from(new Set(userNftTokens.map((nftToken) => nftToken.assetAddress)))
})

View File

@ -1,13 +1,8 @@
import { getNetworkId, getNetworkInfo } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
import { BuildTx, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { CollectibleTx } from 'src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible'
import { store } from 'src/store'
import { sameString } from 'src/utils/strings'
// CryptoKitties Contract Addresses by network
// This is an exception made for a popular NFT that's not ERC721 standard-compatible,
@ -29,24 +24,6 @@ const ENS_CONTRACT_ADDRESS = {
// safeTransferFrom(address,address,uint256)
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
/**
* Verifies that a tx received by the transaction service is an ERC721 token-related transaction
* @param {BuildTx['tx']} tx
* @returns boolean
*/
export const isSendERC721Transaction = (tx: BuildTx['tx']): boolean => {
let hasERC721Transfer = false
if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) {
hasERC721Transfer = tx.dataDecoded.parameters.findIndex((param) => sameString(param.name, 'tokenId')) !== -1
}
// Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this
const state = store.getState()
const knownAssets = nftAssetsListAddressesSelector(state)
return knownAssets.includes((tx as ServiceTx).to) || hasERC721Transfer
}
/**
* Returns the symbol of the provided ERC721 contract
* @param {string} contractAddress

View File

@ -5,7 +5,7 @@ import Web3 from 'web3'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'
@ -99,7 +99,7 @@ export const getSafeDeploymentTransaction = (
safeAccounts,
numConfirmations,
ZERO_ADDRESS,
'0x',
EMPTY_DATA,
DEFAULT_FALLBACK_HANDLER_ADDRESS,
ZERO_ADDRESS,
0,
@ -120,7 +120,7 @@ export const estimateGasForDeployingSafe = async (
safeAccounts,
numConfirmations,
ZERO_ADDRESS,
'0x',
EMPTY_DATA,
DEFAULT_FALLBACK_HANDLER_ADDRESS,
ZERO_ADDRESS,
0,
@ -130,14 +130,11 @@ export const estimateGasForDeployingSafe = async (
const proxyFactoryData = proxyFactoryMaster.methods
.createProxyWithNonce(safeMaster.options.address, gnosisSafeData, safeCreationSalt)
.encodeABI()
const gas = await calculateGasOf({
return calculateGasOf({
data: proxyFactoryData,
from: userAccount,
to: proxyFactoryMaster.options.address,
})
const gasPrice = await calculateGasPrice()
return gas * parseInt(gasPrice, 10)
}
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {

View File

@ -1,6 +1,6 @@
import axios from 'axios'
import { getSafeServiceBaseUrl } from 'src/config'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import {
fetchTokenCurrenciesBalances,
BalanceEndpoint,
@ -46,7 +46,7 @@ describe('fetchTokenCurrenciesBalances', () => {
},
]
const apiUrl = getSafeServiceBaseUrl(safeAddress)
const apiUrl = getSafeClientGatewayBaseUrl(safeAddress)
// @ts-ignore
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
@ -57,6 +57,6 @@ describe('fetchTokenCurrenciesBalances', () => {
// then
expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?exclude_spam=${excludeSpamTokens}`)
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?trusted=false&exclude_spam=${excludeSpamTokens}`)
})
})

View File

@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js'
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
import { sameString } from 'src/utils/strings'
import { AVAILABLE_CURRENCIES } from '../store/model/currencyValues'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
const fetchCurrenciesRates = async (
baseCurrency: string,
@ -15,8 +15,8 @@ const fetchCurrenciesRates = async (
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
try {
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress)
if (tokenCurrenciesBalances?.length) {
rate = new BigNumber(1).div(tokenCurrenciesBalances[0].fiatConversion).toNumber()
if (tokenCurrenciesBalances.items.length) {
rate = new BigNumber(1).div(tokenCurrenciesBalances.items[0].fiatConversion).toNumber()
}
} catch (error) {
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)

View File

@ -1,22 +1,29 @@
import axios from 'axios'
import { getSafeServiceBaseUrl } from 'src/config'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import { TokenProps } from 'src/logic/tokens/store/model/token'
import { checksumAddress } from 'src/utils/checksumAddress'
export type BalanceEndpoint = {
tokenAddress: string
token?: TokenProps
export type TokenBalance = {
tokenInfo: TokenProps
balance: string
fiatBalance: string
fiatConversion: string
fiatCode: string
}
export type BalanceEndpoint = {
fiatTotal: string
items: TokenBalance[]
}
export const fetchTokenCurrenciesBalances = (
safeAddress: string,
excludeSpamTokens = true,
): Promise<BalanceEndpoint[]> => {
const url = `${getSafeServiceBaseUrl(safeAddress)}/balances/usd/?exclude_spam=${excludeSpamTokens}`
trustedTokens = false,
): Promise<BalanceEndpoint> => {
const url = `${getSafeClientGatewayBaseUrl(
checksumAddress(safeAddress),
)}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
return axios.get(url).then(({ data }) => data)
}

View File

@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { userAccountSelector } from '../wallets/store/selectors'
import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { getNetworkInfo } from 'src/config'
import { calculateGasPrice } from 'src/logic/wallets/ethTransactions'
type EstimateSafeCreationGasProps = {
addresses: string[]
numOwners: number
safeCreationSalt: number
}
type SafeCreationEstimationResult = {
gasEstimation: number // Amount of gas needed for execute or approve the transaction
gasCostFormatted: string // Cost of gas in format '< | > 100'
gasLimit: number // Minimum gas requited to execute the Tx
}
const { nativeCoin } = getNetworkInfo()
export const useEstimateSafeCreationGas = ({
addresses,
numOwners,
safeCreationSalt,
}: EstimateSafeCreationGasProps): SafeCreationEstimationResult => {
const [gasEstimation, setGasEstimation] = useState<SafeCreationEstimationResult>({
gasEstimation: 0,
gasCostFormatted: '< 0.001',
gasLimit: 0,
})
const userAccount = useSelector(userAccountSelector)
useEffect(() => {
const estimateGas = async () => {
if (!addresses.length || !numOwners || !userAccount) {
return
}
const gasEstimation = await estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt)
const gasPrice = await calculateGasPrice()
const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10)
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const gasCostFormatted = formatAmount(gasCost)
setGasEstimation({
gasEstimation,
gasCostFormatted,
gasLimit: gasEstimation,
})
}
estimateGas()
}, [numOwners, userAccount, safeCreationSalt, addresses])
return gasEstimation
}

View File

@ -237,12 +237,13 @@ export const useEstimateTransactionGas = ({
approvalAndExecution,
})
const totalGasEstimation = (gasEstimation + fixedGasCosts) * 2
const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice()
const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei')
const estimatedGasCosts = (gasEstimation + fixedGasCosts) * parseInt(gasPrice, 10)
const estimatedGasCosts = totalGasEstimation * parseInt(gasPrice, 10)
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const gasCostFormatted = formatAmount(gasCost)
const gasLimit = ((gasEstimation + fixedGasCosts) * 2).toString()
const gasLimit = totalGasEstimation.toString()
let txEstimationExecutionStatus = EstimationStatus.SUCCESS

View File

@ -7,7 +7,6 @@ import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
export const useLoadSafe = (safeAddress?: string): boolean => {
@ -21,7 +20,6 @@ export const useLoadSafe = (safeAddress?: string): boolean => {
await dispatch(fetchSafe(safeAddress))
setIsSafeLoaded(true)
await dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchSafeCreationTx(safeAddress))
dispatch(fetchTransactions(safeAddress))
dispatch(addViewedSafe(safeAddress))
}

View File

@ -1,275 +1,15 @@
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
import { TransactionStatus, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
import makeSafe from 'src/logic/safe/store/models/safe'
import { List, Map, Record } from 'immutable'
import { makeToken, TokenProps } from 'src/logic/tokens/store/model/token'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import {
buildTx,
calculateTransactionStatus,
calculateTransactionType,
generateSafeTxHash,
getRefundParams,
isCancelTransaction,
isCustomTransaction,
isInnerTransaction,
isModifySettingsTransaction,
isMultiSendTransaction,
isOutgoingTransaction,
isPendingTransaction,
isUpgradeTransaction,
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getERC20DecimalsAndSymbol } from 'src/logic/tokens/utils/tokenHelpers'
import { DELEGATE_CALL } from 'src/logic/safe/transactions'
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
describe('isInnerTransaction', () => {
it('It should return true if the transaction recipient is our given safeAddress and the txValue is 0', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
it('It should return false if the transaction recipient is our given safeAddress and the txValue is >0', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '100' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction recipient is not our given safeAddress', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return true if the transaction recipient is the given safeAddress and the txValue is 0', () => {
// given
const transaction = makeTransaction({ recipient: safeAddress, value: '0' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
it('It should return false if the transaction recipient is the given safeAddress and the txValue is >0', () => {
// given
const transaction = makeTransaction({ recipient: safeAddress, value: '100' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction recipient is not the given safeAddress', () => {
// given
const transaction = makeTransaction({ recipient: safeAddress2, value: '100' })
// when
const result = isInnerTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
})
describe.skip('isCancelTransaction', () => {
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const mockedETHAccount = '0xd76e0B566e218a80F4c96458FE09a322EBAa9aF2'
it('It should return false if given a inner transaction with empty data', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
it('It should return false if given a inner transaction without empty data', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction recipient is not the safeAddress', () => {
// given
const transaction = getMockedTxServiceModel({ to: mockedETHAccount, value: '0', data: null })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction value is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '100', data: null })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction data is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: mockedETHAccount })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction operation is not call', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null, operation: DELEGATE_CALL })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction baseGas is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null, baseGas: 10 })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction gasPrice is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null, gasPrice: '10' })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction gasToken is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null, gasToken: mockedETHAccount })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the refundReceiver is not empty', () => {
// given
const transaction = getMockedTxServiceModel({
to: safeAddress,
value: '0',
data: null,
refundReceiver: mockedETHAccount,
})
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return true for a transaction with everything empty except for to parameter equals to the safeAddress,', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress })
// when
const result = isCancelTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
})
describe('isPendingTransaction', () => {
it('It should return true if the transaction is on pending status', () => {
// given
const transaction = makeTransaction({ status: TransactionStatus.PENDING })
const cancelTx = makeTransaction({ data: null })
// when
const result = isPendingTransaction(transaction, cancelTx)
// then
expect(result).toBe(true)
})
it('It should return true If the transaction is not pending status but the cancellation transaction is', () => {
// given
const transaction = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS })
const cancelTx = makeTransaction({ status: TransactionStatus.PENDING })
// when
const result = isPendingTransaction(transaction, cancelTx)
// then
expect(result).toBe(true)
})
it('It should return true If the transaction and a cancellation transaction are not pending', () => {
// given
const transaction = makeTransaction({ status: TransactionStatus.CANCELLED })
const cancelTx = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS })
// when
const result = isPendingTransaction(transaction, cancelTx)
// then
expect(result).toBe(false)
})
})
describe('isModifySettingsTransaction', () => {
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
it('It should return true if given an inner transaction without empty data', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
// when
const result = isModifySettingsTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
it('It should return false if given an inner transaction with empty data', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
// when
const result = isModifySettingsTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
})
describe('isMultiSendTransaction', () => {
it('It should return true if given a transaction without value, the data has multisend data', () => {
// given
@ -337,160 +77,6 @@ describe('isUpgradeTransaction', () => {
})
})
describe('isOutgoingTransaction', () => {
it('It should return true if the transaction recipient is not a safe address and data is not empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
// when
const result = isOutgoingTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
})
it('It should return true if the transaction has an address equal to the safe address', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' })
// when
const result = isOutgoingTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
it('It should return false if the transaction recipient is not a safe address and data is empty', () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null })
// when
const result = isOutgoingTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
})
})
jest.mock('src/logic/collectibles/utils')
jest.mock('src/logic/tokens/utils/tokenHelpers')
describe('isCustomTransaction', () => {
afterAll(() => {
jest.unmock('src/logic/collectibles/utils')
jest.unmock('src/logic/tokens/utils/tokenHelpers')
})
it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
// when
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
// when
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(true)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
it('It should return false if is outgoing transaction, not SendERC20Transaction, isUpgradeTransaction and not isSendERC721Transaction', async () => {
// given
const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000`
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData })
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => true)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => false)
// when
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
})
it('It should return false if is outgoing transaction, is not SendERC20Transaction, not isUpgradeTransaction and isSendERC721Transaction', async () => {
// given
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' })
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const collectiblesHelpers = require('src/logic/collectibles/utils')
const txHelpers = require('src/logic/tokens/utils/tokenHelpers')
txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false)
collectiblesHelpers.isSendERC721Transaction.mockImplementationOnce(() => true)
// when
const result = await isCustomTransaction(transaction, safeAddress)
// then
expect(result).toBe(false)
expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled()
expect(collectiblesHelpers.isSendERC721Transaction).toHaveBeenCalled()
})
})
describe('getRefundParams', () => {
it('It should return null if given a transaction with the gasPrice == 0', async () => {
// given
@ -588,374 +174,6 @@ describe('getRefundParams', () => {
})
})
describe('getDecodedParams', () => {
it('', () => {
// given
// when
// then
})
})
describe('isTransactionCancelled', () => {
it('', () => {
// given
// when
// then
})
})
describe('calculateTransactionStatus', () => {
it('It should return SUCCESS if the tx is executed and successful', () => {
// given
const transaction = makeTransaction({ isExecuted: true, isSuccessful: true })
const safe = makeSafe()
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.SUCCESS)
})
it('It should return CANCELLED if the tx is cancelled and successful', () => {
// given
const transaction = makeTransaction({ cancelled: true })
const safe = makeSafe()
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.CANCELLED)
})
it('It should return AWAITING_EXECUTION if the tx has an amount of confirmations equal to the safe threshold', () => {
// given
const makeUser = Record({
owner: '',
type: '',
hash: '',
signature: '',
})
const transaction = makeTransaction({ cancelled: true, confirmations: List([makeUser(), makeUser(), makeUser()]) })
const safe = makeSafe({ threshold: 3 })
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.CANCELLED)
})
it('It should return SUCCESS if the tx is the creation transaction', () => {
// given
const transaction = makeTransaction({ creationTx: true, confirmations: List() })
const safe = makeSafe({ threshold: 3 })
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.SUCCESS)
})
it('It should return PENDING if the tx is pending', () => {
// given
const transaction = makeTransaction({ confirmations: List(), isPending: true })
const safe = makeSafe({ threshold: 3 })
const currentUser = safeAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.PENDING)
})
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const makeUser = Record({
owner: '',
type: '',
hash: '',
signature: '',
})
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
const safe = makeSafe({
threshold: 3,
owners: List([
{ name: '', address: userAddress },
{ name: '', address: userAddress2 },
]),
})
const currentUser = userAddress
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS)
})
it('It should return AWAITING_YOUR_CONFIRMATION if the tx has confirmations bellow the threshold, the user is owner and not signed', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const makeUser = Record({
owner: '',
type: '',
hash: '',
signature: '',
})
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
const safe = makeSafe({
threshold: 3,
owners: List([
{ name: '', address: userAddress },
{ name: '', address: userAddress2 },
]),
})
const currentUser = userAddress2
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.AWAITING_YOUR_CONFIRMATION)
})
it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is not owner', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const makeUser = Record({
owner: '',
type: '',
hash: '',
signature: '',
})
const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) })
const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) })
const currentUser = userAddress2
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS)
})
it('It should return FAILED if the tx is not successful', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const makeUser = Record({
owner: '',
type: '',
hash: '',
signature: '',
})
const transaction = makeTransaction({
confirmations: List([makeUser({ owner: userAddress })]),
isSuccessful: false,
})
const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) })
const currentUser = userAddress2
// when
const result = calculateTransactionStatus(transaction, safe, currentUser)
// then
expect(result).toBe(TransactionStatus.FAILED)
})
})
describe('calculateTransactionType', () => {
it('It should return TOKEN If the tx is a token transfer transaction', () => {
// given
const transaction = makeTransaction({ isTokenTransfer: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.TOKEN)
})
it('It should return COLLECTIBLE If the tx is a collectible transfer transaction', () => {
// given
const transaction = makeTransaction({ isCollectibleTransfer: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.COLLECTIBLE)
})
it('It should return SETTINGS If the tx is a modifySettings transaction', () => {
// given
const transaction = makeTransaction({ modifySettingsTx: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.SETTINGS)
})
it('It should return CANCELLATION If the tx is a cancellation transaction', () => {
// given
const transaction = makeTransaction({ isCancellationTx: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.CANCELLATION)
})
it('It should return CUSTOM If the tx is a custom transaction', () => {
// given
const transaction = makeTransaction({ customTx: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.CUSTOM)
})
it('It should return CUSTOM If the tx is a creation transaction', () => {
// given
const transaction = makeTransaction({ creationTx: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.CREATION)
})
it('It should return UPGRADE If the tx is an upgrade transaction', () => {
// given
const transaction = makeTransaction({ upgradeTx: true })
// when
const result = calculateTransactionType(transaction)
// then
expect(result).toBe(TransactionTypes.UPGRADE)
})
})
describe('buildTx', () => {
it('Returns a valid transaction', async () => {
// given
const cancelTx1 = {
baseGas: 0,
blockNumber: 0,
confirmations: [],
confirmationsRequired: 2,
data: null,
dataDecoded: undefined,
ethGasPrice: '0',
executionDate: null,
executor: '',
fee: '',
gasPrice: '',
gasToken: '',
gasUsed: 0,
isExecuted: false,
isSuccessful: true,
modified: '',
nonce: 0,
operation: 0,
origin: null,
refundReceiver: '',
safe: '',
safeTxGas: 0,
safeTxHash: '',
signatures: '',
submissionDate: null,
to: '',
transactionHash: null,
value: '',
}
const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' })
const userAddress = 'address1'
const cancellationTxs = List([cancelTx1])
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
const knownTokens = Map<string, Record<TokenProps> & Readonly<TokenProps>>()
knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token)
const outgoingTxs = [cancelTx1]
const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress })
const expectedTx = makeTransaction({
baseGas: 0,
blockNumber: 0,
cancelled: false,
confirmations: List([]),
creationTx: false,
customTx: false,
data: EMPTY_DATA,
dataDecoded: null,
decimals: 18,
decodedParams: null,
executionDate: '',
executionTxHash: '',
executor: '',
gasPrice: '',
gasToken: ZERO_ADDRESS,
isCancellationTx: false,
isCollectibleTransfer: false,
isExecuted: false,
isSuccessful: false,
isTokenTransfer: false,
modifySettingsTx: false,
multiSendTx: false,
nonce: 0,
operation: 0,
origin: '',
recipient: safeAddress2,
refundParams: null,
refundReceiver: ZERO_ADDRESS,
safeTxGas: 0,
safeTxHash: '',
setupData: '',
status: TransactionStatus.FAILED,
submissionDate: '',
symbol: 'ETH',
upgradeTx: false,
value: '0',
fee: '',
})
// when
const txResult = await buildTx({
cancellationTxs,
currentUser: userAddress,
outgoingTxs,
safe: safeInstance,
tx: transaction,
})
// then
expect(txResult).toStrictEqual(expectedTx)
})
})
describe('updateStoredTransactionsStatus', () => {
it('', () => {
// given
// when
// then
})
})
describe('generateSafeTxHash', () => {
it('It should return a safe transaction hash', () => {
// given

View File

@ -1,99 +1,158 @@
import { getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { getMockedSafeInstance } from 'src/test/utils/safeHelper'
import { NonPayableTransactionObject } from 'src/types/contracts/types'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
import axios from 'axios'
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
describe('Store actions utils > getNewTxNonce', () => {
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
// Given
const lastTx = { nonce: 44 } as TxServiceModel
const safeInstance = {
methods: {
nonce: () => ({
call: () => Promise.resolve('45'),
}),
},
}
describe('shouldExecuteTransaction', () => {
it('It should return false if given a safe with a threshold > 1', async () => {
// given
const nonce = '0'
const threshold = '2'
const safeInstance = getMockedSafeInstance({ threshold })
const lastTx = getMockedTxServiceModel({})
// When
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
// when
const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
// Then
expect(nonce).toBe('45')
// then
expect(result).toBe(false)
})
it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => {
// given
const nonce = '1'
const threshold = '1'
const safeInstance = getMockedSafeInstance({ threshold, nonce })
const lastTx = getMockedTxServiceModel({})
it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => {
// Given
// when
const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
// then
expect(result).toBe(true)
})
it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => {
// given
const nonce = '10'
const threshold = '1'
const safeInstance = getMockedSafeInstance({ threshold, nonce })
const lastTx = getMockedTxServiceModel({ isExecuted: true })
// when
const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
// then
expect(result).toBe(true)
})
it('It should return false if given a safe with a threshold === 1 and the previous transaction is not yet executed', async () => {
// given
const nonce = '10'
const threshold = '1'
const safeInstance = getMockedSafeInstance({ threshold })
const lastTx = getMockedTxServiceModel({ isExecuted: false })
// when
const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
// then
expect(result).toBe(false)
})
})
describe('getNewTxNonce', () => {
it('It should return 2 if given the last transaction with nonce 1', async () => {
// given
const safeInstance = getMockedSafeInstance({})
const lastTx = getMockedTxServiceModel({ nonce: 1 })
const expectedResult = '2'
// when
const result = await getNewTxNonce(lastTx, safeInstance)
// then
expect(result).toBe(expectedResult)
})
it('It should return 0 if given a safe with nonce 0 and no transactions should use safe contract instance for retrieving nonce', async () => {
// given
const safeNonce = '0'
const safeInstance = getMockedSafeInstance({ nonce: safeNonce })
const expectedResult = '0'
const mockFnCall = jest.fn().mockImplementation(() => safeNonce)
const mockFnNonce = jest.fn().mockImplementation(() => ({ call: mockFnCall }))
safeInstance.methods.nonce = mockFnNonce
// when
const result = await getNewTxNonce(null, safeInstance)
// then
expect(result).toBe(expectedResult)
expect(mockFnNonce).toHaveBeenCalled()
expect(mockFnCall).toHaveBeenCalled()
mockFnNonce.mockRestore()
mockFnCall.mockRestore()
})
it('Given a Safe and the last transaction, should return nonce of the last transaction + 1', async () => {
// given
const safeInstance = getMockedSafeInstance({})
const expectedResult = '11'
const lastTx = getMockedTxServiceModel({ nonce: 10 })
// when
const result = await getNewTxNonce(lastTx, safeInstance)
// then
expect(result).toBe(expectedResult)
})
})
jest.mock('axios')
jest.mock('console')
describe('getLastTx', () => {
afterAll(() => {
jest.unmock('axios')
jest.unmock('console')
})
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
it('It should return the last transaction for a given a safe address', async () => {
// given
const lastTx = getMockedTxServiceModel({ nonce: 1 })
const url = buildTxServiceUrl(safeAddress)
// when
// @ts-ignore
axios.get.mockImplementationOnce(() => {
return {
data: {
results: [lastTx],
},
}
})
const result = await getLastTx(safeAddress)
// then
expect(result).toStrictEqual(lastTx)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } })
})
it('If should return null If catches an error getting last transaction', async () => {
// given
const lastTx = null
const safeInstance = {
methods: {
nonce: () => ({
call: () => Promise.resolve('45'),
}),
},
}
const url = buildTxServiceUrl(safeAddress)
// When
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
// when
// @ts-ignore
axios.get.mockImplementationOnce(() => {
throw new Error()
})
console.error = jest.fn()
const result = await getLastTx(safeAddress)
const spyConsole = jest.spyOn(console, 'error').mockImplementation()
// Then
expect(nonce).toBe('45')
})
})
describe('Store actions utils > shouldExecuteTransaction', () => {
it(`should return false if there's a previous tx pending to be executed`, async () => {
// Given
const safeInstance = getMockedSafeInstance({})
safeInstance.methods.getThreshold = () =>
({
call: () => Promise.resolve('1'),
} as NonPayableTransactionObject<string>)
const nonce = '1'
const lastTx = { isExecuted: false } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeFalsy()
})
it(`should return false if threshold is greater than 1`, async () => {
// Given
const safeInstance = getMockedSafeInstance({})
safeInstance.methods.getThreshold = () =>
({
call: () => Promise.resolve('2'),
} as NonPayableTransactionObject<string>)
const nonce = '1'
const lastTx = { isExecuted: true } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeFalsy()
})
it(`should return true is threshold is 1 and previous tx is executed`, async () => {
// Given
const safeInstance = getMockedSafeInstance({ nonce: '1' })
safeInstance.methods.getThreshold = () =>
({
call: () => Promise.resolve('1'),
} as NonPayableTransactionObject<string>)
const nonce = '1'
const lastTx = { isExecuted: true } as TxServiceModel
// When
const isExecution = await shouldExecuteTransaction(safeInstance as GnosisSafe, nonce, lastTx)
// Then
expect(isExecution).toBeTruthy()
// then
expect(result).toStrictEqual(lastTx)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } })
expect(spyConsole).toHaveBeenCalled()
})
})

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_INCOMING_TRANSACTIONS = 'ADD_INCOMING_TRANSACTIONS'
export const addIncomingTransactions = createAction(ADD_INCOMING_TRANSACTIONS)

View File

@ -1,13 +0,0 @@
import { createAction } from 'redux-actions'
import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions'
export const ADD_MODULE_TRANSACTIONS = 'ADD_MODULE_TRANSACTIONS'
export type AddModuleTransactionsAction = {
payload: {
safeAddress: string
modules: ModuleTxServiceModel[]
}
}
export const addModuleTransactions = createAction(ADD_MODULE_TRANSACTIONS)

View File

@ -1,86 +0,0 @@
import axios, { AxiosResponse } from 'axios'
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
export type ServiceUriParams = {
safeAddress: string
limit: number
offset: number
orderBy?: string // todo: maybe this should be key of MultiSigTransaction | keyof EthereumTransaction
queued?: boolean
trusted?: boolean
}
type TransactionDTO = {
count: number
next?: string
previous?: string
results: Transaction[]
}
const getAllTransactionsUri = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
return `${getSafeServiceBaseUrl(address)}/all-transactions/`
}
const fetchAllTransactions = async (
urlParams: ServiceUriParams,
eTag?: string,
): Promise<{ responseEtag?: string; results: Transaction[]; count?: number }> => {
const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams
try {
const url = getAllTransactionsUri(safeAddress)
const config = {
params: {
limit,
offset,
orderBy,
queued,
trusted,
},
headers: eTag ? { 'If-None-Match': eTag } : undefined,
}
const response: AxiosResponse<TransactionDTO> = await axios.get(url, config)
if (response.data.count > 0) {
const { etag } = response.headers
if (eTag !== etag) {
return {
responseEtag: etag,
results: response.data.results,
count: response.data.count,
}
}
}
} catch (err) {
if (!(err && err.response && err.response.status === 304)) {
console.error(`Requests for outgoing transactions for ${safeAddress || 'unknown'} failed with 404`, err)
} else {
// NOTE: this is the expected implementation, currently the backend is not returning 304.
// So I check if the returned etag is the same instead (see above)
}
}
return { responseEtag: eTag, results: [] }
}
const etagsByPage = {}
export const loadAllTransactions = async (
uriParams: ServiceUriParams,
): Promise<{
transactions: Transaction[]
totalTransactionsAmount?: number
}> => {
const previousEtag = etagsByPage && etagsByPage[uriParams.offset]
const { responseEtag, results, count } = await fetchAllTransactions(uriParams, previousEtag)
etagsByPage[uriParams.offset] = responseEtag
return {
transactions: results,
totalTransactionsAmount: count,
}
}

View File

@ -1,14 +0,0 @@
import { createAction } from 'redux-actions'
import { Transaction } from '../../models/types/transactions.d'
export const LOAD_MORE_TRANSACTIONS = 'LOAD_MORE_TRANSACTIONS'
export type LoadMoreTransactionsAction = {
payload: {
safeAddress: string
transactions: Transaction[]
totalTransactionsAmount: number
}
}
export const loadMore = createAction(LOAD_MORE_TRANSACTIONS)

View File

@ -1,55 +0,0 @@
import axios from 'axios'
import { List } from 'immutable'
import { buildSafeCreationTxUrl } from 'src/logic/safe/utils/buildSafeCreationTxUrl'
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
import { TransactionTypes, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
const getCreationTx = async (safeAddress) => {
const url = buildSafeCreationTxUrl(safeAddress)
const response = await axios.get(url)
return {
...response.data,
creationTx: true,
nonce: null,
}
}
const fetchSafeCreationTx = (safeAddress) => async (dispatch) => {
if (!safeAddress) return
const creationTxFetched = await getCreationTx(safeAddress)
const {
created,
creationTx,
creator,
factoryAddress,
masterCopy,
setupData,
transactionHash,
type,
} = creationTxFetched
const txType = type || TransactionTypes.CREATION
const safeTxHash = web3ReadOnly.utils.toHex('this is the creation transaction')
const creationTxAsRecord = makeTransaction({
created,
creator,
factoryAddress,
masterCopy,
nonce: -1,
setupData,
creationTx,
executionTxHash: transactionHash,
type: txType,
safeTxHash,
status: TransactionStatus.SUCCESS,
submissionDate: created,
})
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([creationTxAsRecord]) }))
}
export default fetchSafeCreationTx

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS = 'ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS'
export const addOrUpdateCancellationTransactions = createAction(ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS)

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_OR_UPDATE_TRANSACTIONS = 'ADD_OR_UPDATE_TRANSACTIONS'
export const addOrUpdateTransactions = createAction(ADD_OR_UPDATE_TRANSACTIONS)

View File

@ -1,69 +0,0 @@
import axios from 'axios'
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
import { buildIncomingTxServiceUrl } from 'src/logic/safe/transactions/incomingTxHistory'
import { buildModuleTxServiceUrl } from 'src/logic/safe/transactions/moduleTxHistory'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { IncomingTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions'
import { TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
import { ModuleTxServiceModel } from './loadModuleTransactions'
const getServiceUrl = (txType: string, safeAddress: string): string => {
return {
[TransactionTypes.INCOMING]: buildIncomingTxServiceUrl,
[TransactionTypes.OUTGOING]: buildTxServiceUrl,
[TransactionTypes.MODULE]: buildModuleTxServiceUrl,
}[txType](safeAddress)
}
// TODO: Remove this magic
/* eslint-disable */
async function fetchTransactions(
txType: TransactionTypes.MODULE,
safeAddress: string,
eTag: string | null,
): Promise<{ eTag: string | null; results: ModuleTxServiceModel[] }>
async function fetchTransactions(
txType: TransactionTypes.INCOMING,
safeAddress: string,
eTag: string | null,
): Promise<{ eTag: string | null; results: IncomingTxServiceModel[] }>
async function fetchTransactions(
txType: TransactionTypes.OUTGOING,
safeAddress: string,
eTag: string | null,
): Promise<{ eTag: string | null; results: TxServiceModel[] }>
async function fetchTransactions(
txType: TransactionTypes.MODULE | TransactionTypes.INCOMING | TransactionTypes.OUTGOING,
safeAddress: string,
eTag: string | null,
): Promise<{ eTag: string | null; results: ModuleTxServiceModel[] | TxServiceModel[] | IncomingTxServiceModel[] }> {
/* eslint-enable */
try {
const url = getServiceUrl(txType, safeAddress)
const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined)
if (response.data.count > 0) {
const { etag } = response.headers
if (eTag !== etag) {
return {
eTag: etag,
results: response.data.results,
}
}
}
} catch (err) {
if (!(err && err.response && err.response.status === 304)) {
console.error(`Requests for outgoing transactions for ${safeAddress || 'unknown'} failed with 404`, err)
} else {
// NOTE: this is the expected implementation, currently the backend is not returning 304.
// So I check if the returned etag is the same instead (see above)
}
}
// defaults to an empty array to avoid type errors
return { eTag, results: [] }
}
export default fetchTransactions

View File

@ -42,15 +42,25 @@ export const loadPagedHistoryTransactions = async (
export const loadHistoryTransactions = async (safeAddress: string): Promise<HistoryGatewayResponse['results']> => {
const historyTransactionsUrl = getHistoryTransactionsUrl(safeAddress)
const {
data: { results, ...pointers },
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(historyTransactionsUrl)
try {
const {
data: { results, ...pointers },
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(historyTransactionsUrl)
if (!historyPointers[safeAddress]) {
historyPointers[safeAddress] = pointers
if (!historyPointers[safeAddress]) {
historyPointers[safeAddress] = pointers
}
return results
} catch (error) {
// When the safe is just created there is a delay until the gateway recognize the
// safe address, when that happens it returns 404.
if (error.response.status === 404) {
return []
}
throw Error(`There was an error trying to fetch history txs from safeAddress ${safeAddress}`)
}
return results
}
/************/
@ -90,14 +100,23 @@ export const loadPagedQueuedTransactions = async (
export const loadQueuedTransactions = async (safeAddress: string): Promise<QueuedGatewayResponse['results']> => {
const queuedTransactionsUrl = getQueuedTransactionsUrl(safeAddress)
try {
const {
data: { results, ...pointers },
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(queuedTransactionsUrl)
const {
data: { results, ...pointers },
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(queuedTransactionsUrl)
if (!queuedPointers[safeAddress] || queuedPointers[safeAddress].next === null) {
queuedPointers[safeAddress] = pointers
}
if (!queuedPointers[safeAddress] || queuedPointers[safeAddress].next === null) {
queuedPointers[safeAddress] = pointers
return results
} catch (error) {
// When the safe is just created there is a delay until the gateway recognize the
// safe address, when that happens it returns 404.
if (error.response.status === 404) {
return []
}
throw Error(`There was an error trying to fetch queued txs from safeAddress ${safeAddress}`)
}
return results
}

View File

@ -1,103 +0,0 @@
import bn from 'bignumber.js'
import { List, Map } from 'immutable'
import { Transaction, TransactionReceipt } from 'web3-core'
import { AbiItem } from 'web3-utils'
import { getNetworkInfo } from 'src/config'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { makeIncomingTransaction } from 'src/logic/safe/store/models/incomingTransaction'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
import { TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
import { isENSContract } from 'src/logic/collectibles/utils'
export type IncomingTxServiceModel = {
blockNumber: number
transactionHash: string
to: string
value: number
tokenAddress: string
from: string
}
const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [
IncomingTxServiceModel,
string,
number,
string,
]) => {
// this is a particular treatment for the DCD token, as it seems to lack of symbol and decimal methods
if (tx.tokenAddress && tx.tokenAddress.toLowerCase() === '0xe0b7927c4af23765cb51314a0e0521a9645f0e2a') {
symbol = 'DCD'
decimals = 9
}
const { transactionHash, ...incomingTx } = tx
return makeIncomingTransaction({
...incomingTx,
symbol,
decimals,
fee,
executionTxHash: transactionHash,
safeTxHash: transactionHash,
})
}
const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
const batch = new web3ReadOnly.BatchRequest()
const { nativeCoin } = getNetworkInfo()
const whenTxsValues = txs.map((tx) => {
const methods = [
'symbol',
'decimals',
{ method: 'getTransaction', args: [tx.transactionHash], type: 'eth' },
{ method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' },
]
return generateBatchRequests<
[
IncomingTxServiceModel,
string | undefined,
string | undefined,
Transaction | undefined,
TransactionReceipt | undefined,
]
>({
abi: ALTERNATIVE_TOKEN_ABI as AbiItem[],
address: tx.tokenAddress,
batch,
context: tx,
methods,
})
})
batch.execute()
return Promise.all(whenTxsValues).then((txsValues) =>
txsValues.map(([tx, symbolFetched, decimals, ethTx, ethTxReceipt]) => {
let symbol = symbolFetched
if (!symbolFetched) {
symbol = isENSContract(tx.tokenAddress) ? 'ENS' : nativeCoin.symbol
}
return [
tx,
symbol,
decimals ? decimals : nativeCoin.decimals,
new bn(ethTx?.gasPrice ?? 0).times(ethTxReceipt?.gasUsed ?? 0),
]
}),
)
}
let previousETag: string | null = null
export const loadIncomingTransactions = async (safeAddress: string): Promise<Map<string, List<any>>> => {
const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag)
previousETag = eTag
const incomingTxsWithData = await batchIncomingTxsTokenDataRequest(results)
const incomingTxsRecord = incomingTxsWithData.map(buildIncomingTransactionFrom)
return Map({ [safeAddress]: List(incomingTxsRecord) })
}

View File

@ -1,35 +0,0 @@
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
import { TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
import { DataDecoded, Operation } from 'src/logic/safe/store/models/types/transactions.d'
export type ModuleTxServiceModel = {
created: string
executionDate: string
blockNumber: number
transactionHash: string
safe: string
module: string
to: string
value: string
data: string
operation: Operation
dataDecoded: DataDecoded
}
type ETag = string | null
let previousETag: ETag = null
export const loadModuleTransactions = async (safeAddress: string): Promise<ModuleTxServiceModel[]> => {
if (!safeAddress) {
return []
}
const { eTag, results }: { eTag: ETag; results: ModuleTxServiceModel[] } = await fetchTransactions(
TransactionTypes.MODULE,
safeAddress,
previousETag,
)
previousETag = eTag
return results
}

View File

@ -1,14 +1,6 @@
import { fromJS, List, Map } from 'immutable'
import { List } from 'immutable'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import { store } from 'src/store'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
import { DataDecoded } from 'src/logic/safe/store/models/types/transactions.d'
@ -52,13 +44,8 @@ export type TxServiceModel = {
value: string
}
export type SafeTransactionsType = {
cancel: any
outgoing: any
}
export type OutgoingTxs = {
cancellationTxs: Record<number, TxServiceModel> | CancellationTransactions
cancellationTxs: Record<number, TxServiceModel>
outgoingTxs: TxServiceModel[] | List<Transaction>
}
@ -66,161 +53,3 @@ export type BatchProcessTxsProps = OutgoingTxs & {
currentUser?: string
safe: SafeRecord
}
/**
* Differentiates outgoing transactions from its cancel ones and returns a split map
* @param {string} safeAddress - safe's Ethereum Address
* @param {TxServiceModel[]} outgoingTxs - collection of transactions (usually, returned by the /transactions service)
* @returns {any|{cancellationTxs: {}, outgoingTxs: []}}
*/
const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => {
return outgoingTxs.reduce(
(acc: { cancellationTxs: Record<number, TxServiceModel>; outgoingTxs: TxServiceModel[] }, transaction) => {
if (
isCancelTransaction(transaction, safeAddress) &&
outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress))
) {
if (!isNaN(Number(transaction.nonce))) {
acc.cancellationTxs[transaction.nonce] = transaction
}
} else {
acc.outgoingTxs = [...acc.outgoingTxs, transaction]
}
return acc
},
{
cancellationTxs: {},
outgoingTxs: [],
},
)
}
type BatchRequestReturnValues = [TxServiceModel | Transaction, string | undefined]
/**
* Requests Contract's code for all the Contracts the Safe has interacted with
* @param transactions
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
*/
const batchRequestContractCode = (
transactions: (TxServiceModel | Transaction)[],
): Promise<BatchRequestReturnValues[]> => {
if (!transactions || !Array.isArray(transactions)) {
throw new Error('`transactions` must be provided in order to lookup information')
}
const batch = new web3ReadOnly.BatchRequest()
// this will no longer be used when txs-list-v2 feature is finished
// that's why I'm doing this to move forward
const whenTxsValues = (transactions as any[]).map((tx) => {
return generateBatchRequests<BatchRequestReturnValues>({
abi: [],
address: tx.to,
batch,
context: tx,
methods: [{ method: 'getCode', type: 'eth', args: [tx.to] }],
})
})
batch.execute()
return Promise.all(whenTxsValues)
}
/**
* Receives a list of outgoing and its cancellation transactions and builds the tx object that will be store
* @param cancellationTxs
* @param currentUser
* @param knownTokens
* @param outgoingTxs
* @param safe
* @returns {Promise<{cancel: {}, outgoing: []}>}
*/
const batchProcessOutgoingTransactions = async ({
cancellationTxs,
currentUser,
outgoingTxs,
safe,
}: BatchProcessTxsProps): Promise<{
cancel: Record<string, Transaction>
outgoing: Transaction[]
}> => {
// cancellation transactions
const cancelTxsValues = List(Object.values(cancellationTxs))
const cancellationTxsWithData = cancelTxsValues.size ? await batchRequestContractCode(cancelTxsValues.toArray()) : []
const cancel = {}
for (const [tx] of cancellationTxsWithData) {
cancel[`${tx.nonce}`] = await buildTx({
cancellationTxs,
currentUser,
outgoingTxs,
safe,
tx,
})
}
// outgoing transactions
const outgoingTxsList: List<Transaction | TxServiceModel> =
(outgoingTxs as TxServiceModel[]).length !== undefined
? List(outgoingTxs as TxServiceModel[])
: (outgoingTxs as List<Transaction>)
const outgoingTxsWithData = outgoingTxsList.size ? await batchRequestContractCode(outgoingTxsList.toArray()) : []
const outgoing: Transaction[] = []
for (const [tx] of outgoingTxsWithData) {
outgoing.push(
await buildTx({
cancellationTxs,
currentUser,
outgoingTxs,
safe,
tx,
}),
)
}
return { cancel, outgoing }
}
let previousETag: string | null = null
export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
const defaultResponse = {
cancel: Map(),
outgoing: List(),
}
const state = store.getState()
if (!safeAddress) {
return defaultResponse
}
const currentUser: string = state[PROVIDER_REDUCER_ID].get('account')
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
if (!safe) {
return defaultResponse
}
const { eTag, results }: { eTag: string | null; results: TxServiceModel[] } = await fetchTransactions(
TransactionTypes.OUTGOING,
safeAddress,
previousETag,
)
previousETag = eTag
const { cancellationTxs, outgoingTxs } = extractCancelAndOutgoingTxs(safeAddress, results)
// this should be only used for the initial load or when paginating
const { cancel, outgoing } = await batchProcessOutgoingTransactions({
cancellationTxs,
currentUser,
outgoingTxs,
safe,
})
return {
cancel: fromJS(cancel),
outgoing: fromJS(outgoing),
}
}

View File

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

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const REMOVE_CANCELLATION_TRANSACTION = 'REMOVE_CANCELLATION_TRANSACTION'
export const removeCancellationTransaction = createAction(REMOVE_CANCELLATION_TRANSACTION)

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const REMOVE_TRANSACTION = 'REMOVE_TRANSACTION'
export const removeTransaction = createAction(REMOVE_TRANSACTION)

View File

@ -1,27 +0,0 @@
const addMockSafeCreationTx = (safeAddress: string) => [
{
blockNumber: null,
baseGas: 0,
confirmations: [],
data: null,
executionDate: null,
gasPrice: 0,
gasToken: '0x0000000000000000000000000000000000000000',
isExecuted: true,
nonce: null,
operation: 0,
refundReceiver: '0x0000000000000000000000000000000000000000',
safe: safeAddress,
safeTxGas: 0,
safeTxHash: '',
signatures: null,
submissionDate: null,
executor: '',
to: '',
transactionHash: null,
value: 0,
creationTx: true,
},
]
export default addMockSafeCreationTx

View File

@ -1,11 +0,0 @@
import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions.d'
export const isMultiSigTx = (tx: Transaction): boolean => {
return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION
}
export const isModuleTx = (tx: Transaction): boolean => {
return TxType[tx.txType] === TxType.MODULE_TRANSACTION
}
export const isEthereumTx = (tx: Transaction): boolean => {
return TxType[tx.txType] === TxType.ETHEREUM_TRANSACTION
}

View File

@ -1,105 +1,17 @@
import { List } from 'immutable'
import { getNetworkInfo } from 'src/config'
import { getERC20DecimalsAndSymbol, isSendERC20Transaction } from 'src/logic/tokens/utils/tokenHelpers'
import { getERC721Symbol, isSendERC721Transaction } from 'src/logic/collectibles/utils'
import { isEmptyAddress, sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { makeTransaction } from 'src/logic/safe/store/models/transaction'
import {
Transaction,
TransactionStatus,
TransactionStatusValues,
TransactionTypes,
TransactionTypeValues,
TxArgs,
RefundParams,
isStoredTransaction,
} from 'src/logic/safe/store/models/types/transaction'
import { AppReduxState, store } from 'src/store'
import {
safeSelector,
safeTransactionsSelector,
safeCancellationTransactionsSelector,
} from 'src/logic/safe/store/selectors'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { Transaction, TxArgs, RefundParams } from 'src/logic/safe/store/models/types/transaction'
import {
BatchProcessTxsProps,
TxServiceModel,
} from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { TypedDataUtils } from 'eth-sig-util'
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
import { CALL } from 'src/logic/safe/transactions'
export const isEmptyData = (data?: string | null): boolean => {
return !data || data === EMPTY_DATA
}
export const isInnerTransaction = (tx: BuildTx['tx'] | Transaction, safeAddress: string): boolean => {
let isSameAddress = false
if ((tx as TxServiceModel).to !== undefined) {
isSameAddress = sameAddress((tx as TxServiceModel).to, safeAddress)
} else if ((tx as Transaction).recipient !== undefined) {
isSameAddress = sameAddress((tx as Transaction).recipient, safeAddress)
}
return isSameAddress && Number(tx.value) === 0
}
export const isCancelTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
if (isStoredTransaction(tx)) {
if (!sameAddress(tx.recipient, safeAddress)) {
return false
}
} else {
if (!sameAddress(tx.to, safeAddress)) {
return false
}
}
if (Number(tx.value)) {
return false
}
if (tx.data && !isEmptyData(tx.data)) {
return false
}
if (tx.operation !== CALL) {
return false
}
if (tx.baseGas) {
return false
}
if (Number(tx.gasPrice)) {
return false
}
if (tx.gasToken && !isEmptyAddress(tx.gasToken)) {
return false
}
if (tx.refundReceiver && !isEmptyAddress(tx.refundReceiver)) {
return false
}
return true
}
export const isPendingTransaction = (tx: Transaction, cancelTx: Transaction): boolean => {
return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending'
}
export const isModifySettingsTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data)
}
export const isMultiSendTransaction = (tx: BuildTx['tx']): boolean => {
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
}
@ -113,19 +25,6 @@ export const isUpgradeTransaction = (tx: BuildTx['tx']): boolean => {
)
}
export const isOutgoingTransaction = (tx: BuildTx['tx'], safeAddress?: string): boolean => {
return !sameAddress((tx as ServiceTx).to, safeAddress) && !isEmptyData(tx.data)
}
export const isCustomTransaction = async (tx: BuildTx['tx'], safeAddress?: string): Promise<boolean> => {
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
const isErc20 = await isSendERC20Transaction(tx)
const isUpgrade = isUpgradeTransaction(tx)
const isErc721 = isSendERC721Transaction(tx)
return isOutgoing && !isErc20 && !isUpgrade && !isErc721
}
export const getRefundParams = async (
tx: BuildTx['tx'],
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
@ -162,224 +61,14 @@ export const getRefundParams = async (
return refundParams
}
export const getDecodedParams = (tx: BuildTx['tx']): DecodedParams | null => {
if (tx.dataDecoded) {
return {
[tx.dataDecoded.method]: tx.dataDecoded.parameters.reduce(
(acc, param) => ({
...acc,
[param.name]: param.value,
}),
{},
),
}
}
return null
}
export const getConfirmations = (tx: BuildTx['tx']): List<Confirmation> => {
return List(
(tx.confirmations as ServiceTx['confirmations'])?.map((conf) =>
makeConfirmation({
owner: conf.owner,
hash: conf.transactionHash,
signature: conf.signature,
}),
) ?? [],
)
}
export const isTransactionCancelled = (
tx: BuildTx['tx'],
outgoingTxs: BuildTx['outgoingTxs'],
cancellationTxs: BuildTx['cancellationTxs'],
): boolean => {
return (
// not executed
!tx.isExecuted &&
// there's an executed cancel tx, with same nonce
((tx.nonce && !!cancellationTxs[tx.nonce] && cancellationTxs[tx.nonce].isExecuted) ||
// there's an executed tx, with same nonce
outgoingTxs.some((outgoingTx) => tx.nonce === outgoingTx.nonce && outgoingTx.isExecuted))
)
}
export const calculateTransactionStatus = (
tx: Transaction,
{ owners, threshold, nonce }: SafeRecord,
currentUser?: string | null,
): TransactionStatusValues => {
let txStatus
if (tx.isExecuted && tx.isSuccessful) {
txStatus = TransactionStatus.SUCCESS
} else if (tx.creationTx) {
txStatus = TransactionStatus.SUCCESS
} else if (tx.cancelled || nonce > tx.nonce) {
txStatus = TransactionStatus.CANCELLED
} else if (tx.confirmations.size === threshold) {
txStatus = TransactionStatus.AWAITING_EXECUTION
} else if (!!tx.isPending) {
txStatus = TransactionStatus.PENDING
} else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === currentUser).size === 1
const userIsSafeOwner = owners.filter((owner) => owner.address === currentUser).size === 1
txStatus =
!userConfirmed && userIsSafeOwner
? TransactionStatus.AWAITING_YOUR_CONFIRMATION
: TransactionStatus.AWAITING_CONFIRMATIONS
}
if (tx.isSuccessful === false) {
txStatus = TransactionStatus.FAILED
}
return txStatus
}
export const calculateTransactionType = (tx: Transaction): TransactionTypeValues => {
let txType = TransactionTypes.OUTGOING
if (tx.isTokenTransfer) {
txType = TransactionTypes.TOKEN
} else if (tx.isCollectibleTransfer) {
txType = TransactionTypes.COLLECTIBLE
} else if (tx.modifySettingsTx) {
txType = TransactionTypes.SETTINGS
} else if (tx.isCancellationTx) {
txType = TransactionTypes.CANCELLATION
} else if (tx.customTx) {
txType = TransactionTypes.CUSTOM
} else if (tx.creationTx) {
txType = TransactionTypes.CREATION
} else if (tx.upgradeTx) {
txType = TransactionTypes.UPGRADE
}
return txType
}
export type ServiceTx = TxServiceModel | TxToMock
export type BuildTx = BatchProcessTxsProps & {
tx: ServiceTx | Transaction
}
export const buildTx = async ({
cancellationTxs,
currentUser,
outgoingTxs,
safe,
tx,
}: BuildTx): Promise<Transaction> => {
const safeAddress = safe.address
const { nativeCoin } = getNetworkInfo()
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
const isSendERC721Tx = isSendERC721Transaction(tx)
const isSendERC20Tx = await isSendERC20Transaction(tx)
const isMultiSendTx = isMultiSendTransaction(tx)
const isUpgradeTx = isUpgradeTransaction(tx)
const isCustomTx = await isCustomTransaction(tx, safeAddress)
const isCancellationTx = isCancelTransaction(tx, safeAddress)
const refundParams = await getRefundParams(tx, getERC20DecimalsAndSymbol)
const decodedParams = getDecodedParams(tx)
const confirmations = getConfirmations(tx)
let tokenDecimals = nativeCoin.decimals
let tokenSymbol = nativeCoin.symbol
try {
if (isSendERC20Tx) {
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
tokenDecimals = decimals
tokenSymbol = symbol
} else if (isSendERC721Tx) {
tokenSymbol = await getERC721Symbol((tx as ServiceTx).to)
}
} catch (err) {
console.log(`Failed to retrieve token data from ${(tx as ServiceTx).to}`)
}
const txToStore = makeTransaction({
baseGas: tx.baseGas,
blockNumber: (tx as ServiceTx).blockNumber,
cancelled: isTxCancelled,
confirmations,
customTx: isCustomTx,
data: tx.data ? tx.data : EMPTY_DATA,
dataDecoded: tx.dataDecoded,
decimals: tokenDecimals,
decodedParams,
executionDate: (tx as ServiceTx).executionDate,
executionTxHash: (tx as ServiceTx).transactionHash,
executor: (tx as ServiceTx).executor,
fee: (tx as ServiceTx).fee,
gasPrice: tx.gasPrice,
gasToken: tx.gasToken || ZERO_ADDRESS,
isCancellationTx,
isCollectibleTransfer: isSendERC721Tx,
isExecuted: tx.isExecuted,
isSuccessful: (tx as ServiceTx).isSuccessful,
isTokenTransfer: isSendERC20Tx,
modifySettingsTx: isModifySettingsTx,
multiSendTx: isMultiSendTx,
nonce: tx.nonce,
operation: tx.operation,
origin: (tx as ServiceTx).origin,
recipient: (tx as ServiceTx).to,
refundParams,
refundReceiver: tx.refundReceiver || ZERO_ADDRESS,
safeTxGas: tx.safeTxGas,
safeTxHash: tx.safeTxHash,
submissionDate: tx.submissionDate,
symbol: tokenSymbol,
upgradeTx: isUpgradeTx,
value: tx.value?.toString(),
})
return txToStore
.set('status', calculateTransactionStatus(txToStore, safe, currentUser))
.set('type', calculateTransactionType(txToStore))
}
export type TxToMock = TxArgs & Partial<TxServiceModel>
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
const safe = safeSelector(state)
const cancellationTxs = safeCancellationTransactionsSelector(state)
const outgoingTxs = safeTransactionsSelector(state)
if (!safe) {
throw new Error('Failed to recover Safe from the store')
}
return buildTx({
cancellationTxs,
currentUser: undefined,
outgoingTxs,
safe,
tx,
})
}
export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRecord: ProviderRecord): void => {
const state = store.getState()
const safe = safeSelector(state)
if (safe) {
const safeAddress = safe.address
const transactions = safeTransactionsSelector(state)
dispatch(
addOrUpdateTransactions({
safeAddress,
transactions: transactions.withMutations((list) =>
list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))),
),
}),
)
}
}
export function generateSafeTxHash(safeAddress: string, txArgs: TxArgs): string {
const messageTypes = {
EIP712Domain: [{ type: 'address', name: 'verifyingContract' }],

View File

@ -3,34 +3,19 @@ import { push } from 'connected-react-router'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import {
getAwaitingTransactions,
getAwaitingGatewayTransactions,
} from 'src/logic/safe/transactions/awaitingTransactions'
import { getAwaitingGatewayTransactions } from 'src/logic/safe/transactions/awaitingTransactions'
import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import {
safeParamAddressFromStateSelector,
safesMapSelector,
safeCancellationTransactionsSelector,
} from 'src/logic/safe/store/selectors'
import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/logic/safe/store/selectors'
import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
const watchedActions = [
ADD_OR_UPDATE_TRANSACTIONS,
ADD_INCOMING_TRANSACTIONS,
ADD_OR_UPDATE_SAFE,
ADD_QUEUED_TRANSACTIONS,
]
const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS]
const LAST_TIME_USED_LOGGED_IN_ID = 'LAST_TIME_USED_LOGGED_IN_ID'
@ -85,32 +70,6 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
const state = store.getState()
switch (action.type) {
case ADD_OR_UPDATE_TRANSACTIONS: {
const { safeAddress, transactions } = action.payload
const userAddress: string = userAccountSelector(state)
const cancellationTransactions = safeCancellationTransactionsSelector(state)
const awaitingTransactions = getAwaitingTransactions(transactions, cancellationTransactions, userAddress)
const awaitingTxsSubmissionDateList = awaitingTransactions.map((tx) => tx.submissionDate)
const safes = safesMapSelector(state)
const currentSafe = safes.get(safeAddress)
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
break
}
const notificationKey = `${safeAddress}-awaiting`
await sendAwaitingTransactionNotification(
dispatch,
safeAddress,
awaitingTxsSubmissionDateList,
notificationKey,
onNotificationClicked(dispatch, notificationKey, safeAddress),
)
break
}
case ADD_QUEUED_TRANSACTIONS: {
const { safeAddress, values } = action.payload
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
@ -138,23 +97,6 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
break
}
case ADD_INCOMING_TRANSACTIONS: {
action.payload.forEach((incomingTransactions, safeAddress) => {
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {})
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
dispatch(
updateSafe({
address: safeAddress,
latestIncomingTxBlock: newIncomingTransactions.size
? newIncomingTransactions.first().blockNumber
: latestIncomingTxBlock,
}),
)
})
break
}
case ADD_OR_UPDATE_SAFE: {
const state = store.getState()
const { safe } = action.payload

View File

@ -1,45 +0,0 @@
import { Record } from 'immutable'
export const INCOMING_TX_TYPES = {
INCOMING: 'INCOMING',
ERC721_TRANSFER: 'ERC721_TRANSFER',
ERC20_TRANSFER: 'ERC20_TRANSFER',
ETHER_TRANSFER: 'ETHER_TRANSFER',
}
export const makeIncomingTransaction = Record({
blockNumber: 0,
executionTxHash: '',
safeTxHash: '',
to: '',
value: 0,
tokenAddress: '',
from: '',
symbol: '',
decimals: 18,
fee: '',
executionDate: '',
type: 'INCOMING',
status: 'success',
nonce: null,
confirmations: null,
recipient: null,
data: null,
operation: null,
safeTxGas: null,
baseGas: null,
gasPrice: null,
gasToken: null,
refundReceiver: null,
isExecuted: null,
submissionDate: null,
executor: null,
cancelled: null,
modifySettingsTx: null,
cancellationTx: null,
customTx: null,
creationTx: null,
isTokenTransfer: null,
decodedParams: null,
refundParams: null,
})

View File

@ -1,57 +0,0 @@
import { List, Map, Record } from 'immutable'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import {
PendingActionType,
TransactionProps,
TransactionStatus,
TransactionTypes,
} from 'src/logic/safe/store/models/types/transaction'
export const makeTransaction = Record<TransactionProps>({
baseGas: 0,
blockNumber: 0,
cancelled: false,
confirmations: List([]),
created: '',
creator: '',
creationTx: false,
customTx: false,
data: null,
dataDecoded: null,
decimals: 18,
decodedParams: {},
executionDate: '',
executionTxHash: undefined,
executor: '',
factoryAddress: '',
fee: null,
gasPrice: '0',
gasToken: ZERO_ADDRESS,
isCancellationTx: false,
isCollectibleTransfer: false,
isExecuted: false,
isPending: false,
isSuccessful: true,
isTokenTransfer: false,
masterCopy: '',
modifySettingsTx: false,
multiSendTx: false,
nonce: 0,
operation: 0,
origin: null,
ownersWithPendingActions: Map({ [PendingActionType.CONFIRM]: List([]), [PendingActionType.REJECT]: List([]) }),
recipient: '',
refundParams: null,
refundReceiver: ZERO_ADDRESS,
safeTxGas: 0,
safeTxHash: '',
setupData: '',
status: TransactionStatus.PENDING,
submissionDate: '',
symbol: '',
transactionHash: '',
type: TransactionTypes.OUTGOING,
upgradeTx: false,
value: '0',
})

View File

@ -1,15 +1,10 @@
import { List, Map, RecordOf } from 'immutable'
import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions'
import { Token } from 'src/logic/tokens/store/model/token'
import { Confirmation } from './confirmation'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { DataDecoded, Transfer } from './transactions'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
import { BuildTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { DataDecoded, DecodedParams, Transfer } from './transactions.d'
export enum TransactionTypes {
INCOMING = 'incoming',
OUTGOING = 'outgoing',
SETTINGS = 'settings',
CUSTOM = 'custom',
@ -92,10 +87,6 @@ export type TransactionProps = {
export type Transaction = RecordOf<TransactionProps> & Readonly<TransactionProps>
export const isStoredTransaction = (tx: BuildTx['tx']): tx is Transaction => {
return typeof (tx as Transaction).recipient !== 'undefined'
}
export type TxArgs = {
baseGas: number
data: string
@ -111,17 +102,3 @@ export type TxArgs = {
to: string
valueInWei: string
}
type SafeModuleCompatibilityTypes = {
nonce?: string // not required for this tx: added for compatibility
fee?: number // not required for this tx: added for compatibility
executionTxHash?: string // not required for this tx: added for compatibility
safeTxHash: string // table uses this key as a unique row identifier, added for compatibility
}
export type SafeModuleTransaction = ModuleTxServiceModel &
SafeModuleCompatibilityTypes & {
status: TransactionStatus
type: TransactionTypes
tokenInfo?: Token
}

View File

@ -116,73 +116,6 @@ export interface Transfer {
from: string
}
export enum TxType {
MULTISIG_TRANSACTION = 'MULTISIG_TRANSACTION',
ETHEREUM_TRANSACTION = 'ETHEREUM_TRANSACTION',
MODULE_TRANSACTION = 'MODULE_TRANSACTION',
}
export interface MultiSigTransaction {
safe: string
to: string
value: string
data: string | null
operation: number
gasToken: string
safeTxGas: number
baseGas: number
gasPrice: string
refundReceiver: string
nonce: number
executionDate: string | null
submissionDate: string
modified: string
blockNumber: number | null
transactionHash: string | null
safeTxHash: string
executor: string | null
isExecuted: boolean
isSuccessful: boolean | null
ethGasPrice: string | null
gasUsed: number | null
fee: string | null
origin: string | null
dataDecoded: DataDecoded | null
confirmationsRequired: number | null
confirmations: Confirmation[]
signatures: string | null
transfers: Transfer[]
txType: TxType.MULTISIG_TRANSACTION
}
export interface ModuleTransaction {
created: string
executionDate: string
blockNumber: number
transactionHash: string
safe: string
module: string
to: string
value: string
data: string
operation: Operation
transfers: Transfer[]
txType: TxType.MODULE_TRANSACTION
}
export interface EthereumTransaction {
executionDate: string
to: string
data: string | null
txHash: string
blockNumber: number
transfers: Transfer[]
txType: TxType.ETHEREUM_TRANSACTION
from: string
}
export type Transaction = MultiSigTransaction | ModuleTransaction | EthereumTransaction
// SAFE METHODS TO ITS ID
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
// https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol

View File

@ -1,37 +0,0 @@
import { handleActions } from 'redux-actions'
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
import {
LOAD_MORE_TRANSACTIONS,
LoadMoreTransactionsAction,
} from 'src/logic/safe/store/actions/allTransactions/pagination'
export const TRANSACTIONS = 'allTransactions'
export interface TransactionsState {
[safeAddress: string]: {
totalTransactionsCount: number
transactions: Transaction[]
}
}
export default handleActions(
{
// todo: because we are thinking in remove immutableJS, I will implement this without it so it can be easier removed in future
[LOAD_MORE_TRANSACTIONS]: (state: TransactionsState, action: LoadMoreTransactionsAction) => {
const { safeAddress, transactions, totalTransactionsAmount } = action.payload
const oldState = state[safeAddress]
return {
...state,
[safeAddress]: {
...oldState,
transactions: [...(oldState?.transactions || []), ...transactions],
totalTransactionsCount:
totalTransactionsAmount > 0 ? totalTransactionsAmount : state[safeAddress].totalTransactionsCount,
},
}
},
},
{},
)

View File

@ -1,68 +0,0 @@
import { Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { AppReduxState } from 'src/store'
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
export type CancellationTransactions = Map<string, Transaction>
export type CancellationTxState = Map<string, CancellationTransactions>
type CancellationTransactionsPayload = { safeAddress: string; transactions: CancellationTransactions }
type CancellationTransactionPayload = { safeAddress: string; transaction: Transaction }
export default handleActions<
AppReduxState['cancellationTransactions'],
CancellationTransactionsPayload | CancellationTransactionPayload
>(
{
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action: Action<CancellationTransactionsPayload>) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
return state
}
return state.withMutations((map) => {
const stateTransactionsMap = map.get(safeAddress)
if (stateTransactionsMap) {
transactions.forEach((updateTx) => {
const keyPath = [safeAddress, `${updateTx.nonce}`]
if (updateTx.confirmations.size) {
// if there are confirmations then we replace what's stored with the new tx
// as we assume that this is the newest tx returned by the server
map.setIn(keyPath, updateTx)
} else {
// if there are no confirmations, we assume this is a mocked tx
// as txs without confirmation are not being returned by the server (?has_confirmations=true)
map.mergeDeepIn(keyPath, updateTx)
}
})
} else {
map.set(safeAddress, transactions)
}
})
},
[REMOVE_CANCELLATION_TRANSACTION]: (state, action: Action<CancellationTransactionPayload>) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {
return state
}
return state.withMutations((map) => {
const stateTransactionsMap = map.get(safeAddress)
if (stateTransactionsMap) {
map.deleteIn([safeAddress, `${transaction.nonce}`])
}
})
},
},
Map(),
)

View File

@ -1,14 +0,0 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
import { AppReduxState } from 'src/store'
export const INCOMING_TRANSACTIONS_REDUCER_ID = 'incomingTransactions'
export default handleActions<AppReduxState['incomingTransactions']>(
{
[ADD_INCOMING_TRANSACTIONS]: (state, action) => action.payload,
},
Map(),
)

View File

@ -1,32 +0,0 @@
import { handleActions } from 'redux-actions'
import {
ADD_MODULE_TRANSACTIONS,
AddModuleTransactionsAction,
} from 'src/logic/safe/store/actions/addModuleTransactions'
import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions'
export const MODULE_TRANSACTIONS_REDUCER_ID = 'moduleTransactions'
export interface ModuleTransactionsState {
[safeAddress: string]: ModuleTxServiceModel[]
}
export default handleActions(
{
[ADD_MODULE_TRANSACTIONS]: (state: ModuleTransactionsState, action: AddModuleTransactionsAction) => {
const { modules, safeAddress } = action.payload
const oldModuleTxs = state[safeAddress] ?? []
const oldModuleTxsHashes = oldModuleTxs.map(({ transactionHash }) => transactionHash)
// As backend is returning the whole list of txs on every request,
// to avoid duplicates, filtering happens in this level.
const newModuleTxs = modules.filter((moduleTx) => !oldModuleTxsHashes.includes(moduleTx.transactionHash))
return {
...state,
[safeAddress]: [...oldModuleTxs, ...newModuleTxs],
}
},
},
{},
)

View File

@ -1,78 +0,0 @@
import { List, Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { REMOVE_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { AppReduxState } from 'src/store'
export const TRANSACTIONS_REDUCER_ID = 'transactions'
type TransactionBasePayload = { safeAddress: string }
type TransactionsPayload = TransactionBasePayload & { transactions: List<Transaction> }
type TransactionPayload = TransactionBasePayload & { transaction: Transaction }
type Payload = TransactionsPayload | TransactionPayload
export default handleActions<AppReduxState['transactions'], Payload>(
{
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action: Action<TransactionsPayload>) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
return state
}
return state.withMutations((map) => {
const stateTransactionsList = map.get(safeAddress)
if (stateTransactionsList) {
const txsToStore = stateTransactionsList.withMutations((txsList) => {
transactions.forEach((updateTx) => {
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.safeTxHash === updateTx.safeTxHash)
if (storedTxIndex !== -1) {
// Update
if (updateTx.confirmations.size) {
// if there are confirmations then we replace what's stored with the new tx
// as we assume that this is the newest tx returned by the server
txsList.update(storedTxIndex, () => updateTx)
} else {
// if there are no confirmations, we assume this is a mocked tx
// as txs without confirmation are not being returned by the server (?has_confirmations=true)
txsList.update(storedTxIndex, (storedTx) => storedTx.mergeDeep(updateTx))
}
} else {
// Add new
txsList.unshift(updateTx)
}
})
})
map.set(safeAddress, txsToStore)
} else {
map.set(safeAddress, transactions)
}
})
},
[REMOVE_TRANSACTION]: (state, action: Action<TransactionPayload>) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {
return state
}
return state.withMutations((map) => {
const stateTransactionsList = map.get(safeAddress)
if (stateTransactionsList) {
const storedTxIndex = stateTransactionsList.findIndex((storedTx) => storedTx.equals(transaction))
if (storedTxIndex !== -1) {
map.deleteIn([safeAddress, storedTxIndex])
}
}
})
},
},
Map(),
)

View File

@ -1,22 +0,0 @@
import { TransactionsState, TRANSACTIONS } from '../reducer/allTransactions'
import { createSelector } from 'reselect'
import { safeParamAddressFromStateSelector } from './index'
import { AppReduxState } from 'src/store'
export const getTransactionsStateSelector = (state: AppReduxState): TransactionsState => state[TRANSACTIONS]
export const allTransactionsSelector = createSelector(getTransactionsStateSelector, (transactionsState) => {
return transactionsState
})
export const safeAllTransactionsSelector = createSelector(
safeParamAddressFromStateSelector,
allTransactionsSelector,
(safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.transactions : []),
)
export const safeTotalTransactionsAmountSelector = createSelector(
safeParamAddressFromStateSelector,
allTransactionsSelector,
(safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.totalTransactionsCount : 0),
)

View File

@ -1,15 +1,9 @@
import { List, Map, Set } from 'immutable'
import { List, Set } from 'immutable'
import { matchPath, RouteComponentProps } from 'react-router-dom'
import { createSelector } from 'reselect'
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes'
import {
CANCELLATION_TRANSACTIONS_REDUCER_ID,
CancellationTransactions,
} from 'src/logic/safe/store/reducer/cancellationTransactions'
import { INCOMING_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/incomingTransactions'
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -30,12 +24,6 @@ export const latestMasterContractVersionSelector = createSelector(safesStateSele
safeState.get('latestMasterContractVersion'),
)
const transactionsSelector = (state: AppReduxState) => state[TRANSACTIONS_REDUCER_ID]
const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELLATION_TRANSACTIONS_REDUCER_ID]
const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
export const safeParamAddressFromStateSelector = (state: AppReduxState): string => {
const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, {
path: `${SAFELIST_ADDRESS}/:safeAddress`,
@ -56,22 +44,6 @@ export const safeParamAddressSelector = (
return urlAdd ? checksumAddress(urlAdd) : ''
}
export const safeTransactionsSelector = createSelector(
transactionsSelector,
safeParamAddressFromStateSelector,
(transactions, address) => {
if (!transactions) {
return List([])
}
if (!address) {
return List([])
}
return transactions.get(address, List([]))
},
)
export const addressBookQueryParamsSelector = (state: AppReduxState): string | undefined => {
const { location } = state.router
@ -81,38 +53,6 @@ export const addressBookQueryParamsSelector = (state: AppReduxState): string | u
}
}
export const safeCancellationTransactionsSelector = createSelector(
cancellationTransactionsSelector,
safeParamAddressFromStateSelector,
(cancellationTransactions, address): CancellationTransactions => {
if (!cancellationTransactions) {
return Map()
}
if (!address) {
return Map()
}
return cancellationTransactions.get(address, Map())
},
)
export const safeIncomingTransactionsSelector = createSelector(
incomingTransactionsSelector,
safeParamAddressFromStateSelector,
(incomingTransactions, address) => {
if (!incomingTransactions) {
return List([])
}
if (!address) {
return List([])
}
return incomingTransactions.get(address, List())
},
)
export const safeSelector = createSelector(
safesMapSelector,
safeParamAddressFromStateSelector,

View File

@ -1,18 +0,0 @@
// import { List } from 'immutable'
// import { createSelector } from 'reselect'
//
// import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
// import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction'
// import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector'
// export const extendedTransactionsSelector = createSelector(
// safeTransactionsSelector,
// safeIncomingTransactionsSelector,
// safeModuleTransactionsSelector,
// (transactions, incomingTransactions, moduleTransactions): List<Transaction | SafeModuleTransaction> =>
// List().withMutations((list) => {
// list.concat(transactions).concat(incomingTransactions).concat(moduleTransactions)
// }),
// )
export {}

View File

@ -1,30 +1,6 @@
import { List } from 'immutable'
import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { isStatusAwaitingConfirmation } from 'src/logic/safe/store/models/types/gateway.d'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
import { Transaction as GatewayTransaction } from 'src/logic/safe/store/models/types/gateway'
import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils'
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
export const getAwaitingTransactions = (
allTransactions: List<Transaction>,
cancellationTxs: CancellationTransactions,
userAccount: string,
): List<Transaction> => {
return allTransactions.filter((tx) => {
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null
// The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations
if (!tx.executionTxHash && !tx.cancelled && cancelTx && !isPendingTransaction(tx, cancelTx)) {
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
const transactionWaitingUser = tx.confirmations.filter(({ owner }) => owner !== userAccount)
return transactionWaitingUser.size > 0
}
return false
})
}
import { addressInList } from 'src/routes/safe/components/Transactions/TxList/utils'
export const getAwaitingGatewayTransactions = (
allTransactions: GatewayTransaction[],

View File

@ -296,10 +296,12 @@ export const estimateGasForTransactionExecution = async ({
}: TransactionExecutionEstimationProps): Promise<number> => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
try {
if (approvalAndExecution) {
console.info(`Estimating transaction success for execution & approval...`)
let gasEstimation
// If safeTxGas === 0 we still have to estimate the gas limit to execute the transaction so we need to get an estimation
if (approvalAndExecution || safeTxGas === 0) {
console.info(`Estimating transaction necessary gas...`)
// @todo (agustin) once we solve the problem with the preApprovingOwner, we need to use the method bellow (execTransaction) with sigs = generateSignaturesFromTxConfirmations(txConfirmations,from)
const gasEstimation = await estimateGasForTransactionCreation(
gasEstimation = await estimateGasForTransactionCreation(
safeAddress,
txData,
txRecipient,
@ -307,17 +309,21 @@ export const estimateGasForTransactionExecution = async ({
operation,
safeTxGas,
)
console.info(`Gas estimation successfully finished with gas amount: ${gasEstimation}`)
return gasEstimation
if (approvalAndExecution) {
// If it's approve and execute we don't have all the signatures to do a complete simulation, we return the gas estimation
console.info(`Gas estimation successfully finished with gas amount: ${gasEstimation}`)
return gasEstimation
}
}
// If we have all signatures we can do a call to ensure the transaction will be successful or fail
const sigs = generateSignaturesFromTxConfirmations(txConfirmations)
console.info(`Estimating transaction success for with gas amount: ${safeTxGas}...`)
console.info(`Check transaction success with gas amount: ${safeTxGas}...`)
await safeInstance.methods
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
.call()
console.info(`Gas estimation successfully finished with gas amount: ${safeTxGas}`)
return safeTxGas
return safeTxGas || gasEstimation
} catch (error) {
throw new Error(`Gas estimation failed with gas amount: ${safeTxGas}`)
}

View File

@ -1,7 +0,0 @@
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildIncomingTxServiceUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
return `${getSafeServiceBaseUrl(address)}/incoming-transfers/`
}

View File

@ -1,9 +0,0 @@
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildModuleTxServiceUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
const url = getSafeServiceBaseUrl(address)
return `${url}/module-transactions/`
}

View File

@ -17,7 +17,7 @@ const SIGNERS = {
const getSignersByWallet = (isHW) =>
isHW ? [SIGNERS.ETH_SIGN] : [SIGNERS.EIP712_V3, SIGNERS.EIP712_V4, SIGNERS.EIP712, SIGNERS.ETH_SIGN]
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.0.0'
export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boolean): Promise<string> => {
let signature

View File

@ -1,7 +0,0 @@
import { getSafeServiceBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export const buildSafeCreationTxUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress)
return `${getSafeServiceBaseUrl(address)}/creation/`
}

View File

@ -14,7 +14,7 @@ type FeatureConfigByVersion = {
}
const FEATURES_BY_VERSION: FeatureConfigByVersion[] = [
{ name: FEATURES.ERC721, validVersion: '>=1.1.1' },
{ name: FEATURES.ERC721 },
{ name: FEATURES.ERC1155, validVersion: '>=1.1.1' },
{ name: FEATURES.SAFE_APPS },
{ name: FEATURES.CONTRACT_INTERACTION },

View File

@ -1,6 +1,7 @@
import axios, { AxiosResponse } from 'axios'
import { getTokensServiceBaseUrl } from 'src/config'
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
export type TokenResult = {
address: string
@ -8,7 +9,7 @@ export type TokenResult = {
logoUri: string
name: string
symbol: string
type: string
type: TokenType
}
export const fetchErc20AndErc721AssetsList = (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {

View File

@ -1,6 +1,6 @@
import axios, { AxiosResponse } from 'axios'
import { getSafeServiceBaseUrl } from 'src/config'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
export type CollectibleResult = {
@ -17,8 +17,6 @@ export type CollectibleResult = {
}
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
const address = checksumAddress(safeAddress)
const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
const url = `${getSafeClientGatewayBaseUrl(checksumAddress(safeAddress))}/collectibles/`
return axios.get<CollectibleResult[], AxiosResponse<CollectibleResult[]>>(url)
}

View File

@ -2,12 +2,13 @@ import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable'
import { Dispatch } from 'redux'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import {
fetchTokenCurrenciesBalances,
BalanceEndpoint,
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { CurrencyRateValueRecord, makeBalanceCurrency } from 'src/logic/currencyValues/store/model/currencyValues'
AVAILABLE_CURRENCIES,
CurrencyRateValueRecord,
makeBalanceCurrency,
} from 'src/logic/currencyValues/store/model/currencyValues'
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
@ -16,6 +17,10 @@ import { AppReduxState } from 'src/store'
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { getNetworkInfo } from 'src/config'
interface ExtractedData {
balances: Map<string, string>
@ -24,17 +29,20 @@ interface ExtractedData {
tokens: List<Token>
}
const extractDataFromResult = (currentTokens: TokenState) => (
const { nativeCoin } = getNetworkInfo()
const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => (
acc: ExtractedData,
{ balance, fiatBalance, fiatCode, token, tokenAddress }: BalanceEndpoint,
{ balance, fiatBalance, tokenInfo }: TokenBalance,
): ExtractedData => {
if (tokenAddress === null) {
const { address: tokenAddress, decimals } = tokenInfo
if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) {
acc.ethBalance = humanReadableValue(balance, 18)
} else {
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token?.decimals)) })
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(decimals)) })
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token }))
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
}
@ -58,6 +66,7 @@ const fetchSafeTokens = (safeAddress: string) => async (
const state = getState()
const safe = safeSelector(state)
const currentTokens = tokensSelector(state)
const currencySelected = currentCurrencySelector(state)
if (!safe) {
return
@ -67,8 +76,8 @@ const fetchSafeTokens = (safeAddress: string) => async (
const alreadyActiveTokens = safeActiveTokensSelector(state)
const blacklistedTokens = safeBlacklistedTokensSelector(state)
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.reduce<ExtractedData>(
extractDataFromResult(currentTokens),
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD),
{
balances: Map(),
currencyList: List(),

View File

@ -1,4 +1,5 @@
import { Record, RecordOf } from 'immutable'
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
export type TokenProps = {
address: string
@ -7,6 +8,7 @@ export type TokenProps = {
decimals: number | string
logoUri: string
balance: number | string
type?: TokenType
}
export const makeToken = Record<TokenProps>({

View File

@ -4,11 +4,10 @@ import { AbiItem } from 'web3-utils'
import { getNetworkInfo } from 'src/config'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { getTokenInfos } from 'src/logic/tokens/store/actions/fetchTokens'
import { isSendERC721Transaction } from 'src/logic/collectibles/utils'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
import { BuildTx, isEmptyData, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { BuildTx, isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { CALL } from 'src/logic/safe/transactions'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
@ -69,21 +68,6 @@ export const getERC20DecimalsAndSymbol = async (
return tokenInfo
}
export const isSendERC20Transaction = async (tx: BuildTx['tx']): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx) && isTokenTransfer(tx)
if (isSendTokenTx) {
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction
isSendTokenTx = decimals !== null && symbol !== null
}
return isSendTokenTx
}
export type GetTokenByAddress = {
tokenAddress: string
tokens: List<Token>

View File

@ -8,12 +8,10 @@ import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
import { makeProvider } from 'src/logic/wallets/store/model/provider'
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
export const processProviderResponse = (dispatch, provider) => {
const walletRecord = makeProvider(provider)
dispatch(addProvider(walletRecord))
updateStoredTransactionsStatus(dispatch, walletRecord)
}
const handleProviderNotification = (provider, dispatch) => {

View File

@ -17,8 +17,9 @@ 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'
import { LoadFormValues } from 'src/routes/load/container/Load'
const checkIfUserAddressIsAnOwner = (values: Record<string, string>, userAddress: string): boolean => {
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
let isOwner = false
for (let i = 0; i < getNumOwnersFrom(values); i += 1) {
@ -33,7 +34,7 @@ const checkIfUserAddressIsAnOwner = (values: Record<string, string>, userAddress
interface Props {
userAddress: string
values: Record<string, string>
values: LoadFormValues
}
const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => {
@ -138,7 +139,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement =>
}
const Review = ({ userAddress }: { userAddress: string }) =>
function ReviewPage(controls: React.ReactNode, { values }: { values: Record<string, string> }): React.ReactElement {
function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement {
return (
<>
<OpenPaper controls={controls} padding={false}>

View File

@ -35,12 +35,24 @@ export const loadSafe = async (
await addSafe(safeProps)
}
export interface LoadFormValues {
interface ReviewSafeCreationValues {
confirmations: string
name: string
owner0Address: string
owner0Name: string
safeCreationSalt: number
}
interface LoadForm {
name: string
address: string
threshold: string
owner0Address: string
owner0Name: string
}
export type LoadFormValues = ReviewSafeCreationValues | LoadForm
const Load = (): React.ReactElement => {
const dispatch = useDispatch()
const provider = useSelector(providerNameSelector)

View File

@ -20,7 +20,7 @@ import {
import { WelcomeLayout } from 'src/routes/welcome/components/index'
import { history } from 'src/store'
import { secondary, sm } from 'src/theme/variables'
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
import { providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
import { useSelector } from 'react-redux'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
@ -94,7 +94,6 @@ export const Layout = (props: LayoutProps): React.ReactElement => {
const { onCallSafeContractSubmit, safeProps } = props
const provider = useSelector(providerNameSelector)
const network = useSelector(networkSelector)
const userAccount = useSelector(userAccountSelector)
useEffect(() => {
@ -107,33 +106,31 @@ export const Layout = (props: LayoutProps): React.ReactElement => {
const initialValues = useInitialValuesFrom(userAccount, safeProps)
if (!provider) {
return <WelcomeLayout isOldMultisigMigration />
}
return (
<>
{provider ? (
<Block>
<Row align="center">
<IconButton disableRipple onClick={back} style={iconStyle}>
<ChevronLeft />
</IconButton>
<Heading tag="h2" testId="create-safe-form-title">
Create New Safe
</Heading>
</Row>
<Stepper
initialValues={initialValues}
mutators={formMutators}
onSubmit={onCallSafeContractSubmit}
steps={steps}
testId="create-safe-form"
>
<StepperPage component={SafeNameField} />
<StepperPage component={SafeOwnersPage} validate={validateOwnersForm} />
<StepperPage network={network} userAccount={userAccount} component={Review} />
</Stepper>
</Block>
) : (
<WelcomeLayout isOldMultisigMigration />
)}
</>
<Block>
<Row align="center">
<IconButton disableRipple onClick={back} style={iconStyle}>
<ChevronLeft />
</IconButton>
<Heading tag="h2" testId="create-safe-form-title">
Create New Safe
</Heading>
</Row>
<Stepper
initialValues={initialValues}
mutators={formMutators}
onSubmit={onCallSafeContractSubmit}
steps={steps}
testId="create-safe-form"
>
<StepperPage component={SafeNameField} />
<StepperPage component={SafeOwnersPage} validate={validateOwnersForm} />
<StepperPage component={Review} />
</Stepper>
</Block>
)
}

View File

@ -1,7 +1,6 @@
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 React, { ReactElement, useEffect, useMemo } from 'react'
import { getExplorerInfo, getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn'
import Identicon from 'src/components/Identicon'
@ -11,45 +10,41 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { getAccountsFrom, getNamesFrom, getSafeCreationSaltFrom } from 'src/routes/open/utils/safeDataExtractor'
import {
CreateSafeValues,
getAccountsFrom,
getNamesFrom,
getSafeCreationSaltFrom,
} from 'src/routes/open/utils/safeDataExtractor'
import { FIELD_CONFIRMATIONS, FIELD_NAME, getNumOwnersFrom } from '../fields'
import { useStyles } from './styles'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { useEstimateSafeCreationGas } from 'src/logic/hooks/useEstimateSafeCreationGas'
import { FormApi } from 'final-form'
import { StepperPageFormProps } from 'src/components/Stepper'
import { LoadFormValues } from 'src/routes/load/container/Load'
type ReviewComponentProps = {
userAccount: string
values: any
values: LoadFormValues
form: FormApi
}
const { nativeCoin } = getNetworkInfo()
const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => {
const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement => {
const classes = useStyles()
const [gasCosts, setGasCosts] = useState('< 0.001')
const names = getNamesFrom(values)
const addresses = getAccountsFrom(values)
const addresses = useMemo(() => getAccountsFrom(values), [values])
const numOwners = getNumOwnersFrom(values)
const safeCreationSalt = getSafeCreationSaltFrom(values)
const safeCreationSalt = getSafeCreationSaltFrom(values as CreateSafeValues)
const { gasCostFormatted, gasLimit } = useEstimateSafeCreationGas({ addresses, numOwners, safeCreationSalt })
useEffect(() => {
const estimateGas = async () => {
if (!addresses.length || !numOwners || !userAccount) {
return
}
const estimatedGasCosts = (
await estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt)
).toString()
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const formattedGasCosts = formatAmount(gasCosts)
setGasCosts(formattedGasCosts)
}
estimateGas()
}, [addresses, numOwners, safeCreationSalt, userAccount])
form.mutators.setValue('gasLimit', gasLimit)
}, [gasLimit, form.mutators])
return (
<>
@ -135,8 +130,8 @@ const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => {
<Row align="center" className={classes.info}>
<Paragraph color="primary" noMargin size="md">
You&apos;re about to create a new Safe and will have to confirm a transaction with your currently connected
wallet. The creation will cost approximately {gasCosts} {nativeCoin.name}. The exact amount will be determined
by your wallet.
wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be
determined by your wallet.
</Paragraph>
</Row>
</>
@ -144,7 +139,7 @@ const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => {
}
export const Review = () =>
function ReviewPage(controls, props): React.ReactElement {
function ReviewPage(controls: React.ReactNode, props: StepperPageFormProps): React.ReactElement {
return (
<>
<OpenPaper controls={controls} padding={false}>

View File

@ -4,13 +4,17 @@ export const FIELD_OWNERS = 'owners'
export const FIELD_SAFE_NAME = 'safeName'
export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt'
export const getOwnerNameBy = (index) => `owner${index}Name`
export const getOwnerAddressBy = (index) => `owner${index}Address`
export const getOwnerNameBy = (index: number): string => `owner${index}Name`
export const getOwnerAddressBy = (index: number): string => `owner${index}Address`
export const getNumOwnersFrom = (values) => {
const accounts = Object.keys(values)
.sort()
.filter((key) => /^owner\d+Address$/.test(key) && !!values[key])
.filter((key) => {
const res = /^owner\d+Address$/.test(key)
return res && !!values[key]
})
return accounts.length
}

View File

@ -6,11 +6,12 @@ import { useLocation } from 'react-router-dom'
import { PromiEvent, TransactionReceipt } from 'web3-core'
import { SafeDeployment } from 'src/routes/opening'
import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout'
import { Layout } from 'src/routes/open/components/Layout'
import Page from 'src/components/layout/Page'
import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts'
import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions'
import {
CreateSafeValues,
getAccountsFrom,
getNamesFrom,
getOwnersFrom,
@ -29,6 +30,8 @@ import { useAnalytics } from 'src/utils/googleAnalytics'
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
type LoadedSafeType = CreateSafeValues & { txHash: string }
interface SafeCreationQueryParams {
ownerAddresses: string | string[] | null
ownerNames: string | string[] | null
@ -85,7 +88,7 @@ export const getSafeProps = async (
return safeProps
}
export const createSafe = (values: InitialValuesForm, userAccount: string): PromiEvent<TransactionReceipt> => {
export const createSafe = (values: CreateSafeValues, userAccount: string): PromiEvent<TransactionReceipt> => {
const confirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values)
const ownersNames = getNamesFrom(values)
@ -93,7 +96,10 @@ export const createSafe = (values: InitialValuesForm, userAccount: string): Prom
const safeCreationSalt = getSafeCreationSaltFrom(values)
const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt)
const promiEvent = deploymentTx.send({ from: userAccount })
const promiEvent = deploymentTx.send({
from: userAccount,
gas: values?.gasLimit,
})
promiEvent
.once('transactionHash', (txHash) => {
@ -155,28 +161,28 @@ const Open = (): React.ReactElement => {
load()
}, [])
const createSafeProxy = async (formValues?: InitialValuesForm) => {
const createSafeProxy = async (formValues?: CreateSafeValues) => {
let values = formValues
// save form values, used when the user rejects the TX and wants to retry
if (formValues) {
const copy = { ...formValues }
if (values) {
const copy = { ...values }
saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, copy)
} else {
values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
values = (await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)) as CreateSafeValues
}
const promiEvent = createSafe(values as InitialValuesForm, userAccount)
const promiEvent = createSafe(values, userAccount)
setCreationTxPromise(promiEvent)
setShowProgress(true)
}
const onSafeCreated = async (safeAddress): Promise<void> => {
const pendingCreation = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY)
const pendingCreation = await loadFromStorage<LoadedSafeType>(SAFE_PENDING_CREATION_STORAGE_KEY)
const name = getSafeNameFrom(pendingCreation)
const ownersNames = getNamesFrom(pendingCreation)
const ownerAddresses = getAccountsFrom(pendingCreation)
const name = pendingCreation ? getSafeNameFrom(pendingCreation) : ''
const ownersNames = getNamesFrom(pendingCreation as CreateSafeValues)
const ownerAddresses = pendingCreation ? getAccountsFrom(pendingCreation) : []
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
await dispatch(addOrUpdateSafe(safeProps))

View File

@ -8,6 +8,9 @@ describe('Test JS', () => {
owner1Address: 'bar',
owner2Address: 'baz',
owners: 3,
confirmations: '0',
name: '',
safeCreationSalt: 0,
}
expect(getAccountsFrom(safe)).toEqual(['foo', 'bar', 'baz'])
@ -15,9 +18,15 @@ describe('Test JS', () => {
it('return the names of owners', () => {
const safe = {
owner0Name: 'foo',
owner0Address: '0x',
owner1Name: 'bar',
owner1Address: '0x',
owner2Name: 'baz',
owner2Address: '0x',
owners: 3,
confirmations: '0',
name: '',
safeCreationSalt: 0,
}
expect(getNamesFrom(safe)).toEqual(['foo', 'bar', 'baz'])
@ -31,12 +40,15 @@ describe('Test JS', () => {
owner2Name: 'bazName',
owner2Address: 'bazAddress',
owners: 1,
confirmations: '0',
name: '',
safeCreationSalt: 0,
}
expect(getNamesFrom(safe)).toEqual(['fooName'])
expect(getAccountsFrom(safe)).toEqual(['fooAddress'])
expect(getNamesFrom(safe)).toEqual(['fooName', 'barName', 'bazName'])
expect(getAccountsFrom(safe)).toEqual(['fooAddress', 'barAddress', 'bazAddress'])
})
it('return name and address ordered alphabetically', () => {
it('return name and address keys ordered alphabetically', () => {
const safe = {
owner1Name: 'barName',
owner1Address: 'barAddress',
@ -45,14 +57,19 @@ describe('Test JS', () => {
owner2Address: 'bazAddress',
owner0Address: 'fooAddress',
owners: 1,
confirmations: '0',
name: '',
safeCreationSalt: 0,
}
expect(getNamesFrom(safe)).toEqual(['fooName'])
expect(getAccountsFrom(safe)).toEqual(['fooAddress'])
expect(getNamesFrom(safe)).toEqual(['fooName', 'barName', 'bazName'])
expect(getAccountsFrom(safe)).toEqual(['fooAddress', 'barAddress', 'bazAddress'])
})
it('return the number of required confirmations', () => {
const safe = {
confirmations: '1',
name: '',
safeCreationSalt: 0,
}
expect(getThresholdFrom(safe)).toEqual(1)

View File

@ -2,31 +2,45 @@ import { List } from 'immutable'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { LoadFormValues } from 'src/routes/load/container/Load'
import { getNumOwnersFrom } from 'src/routes/open/components/fields'
export const getAccountsFrom = (values) => {
export type CreateSafeValues = {
confirmations: string
name: string
owner0Address?: string
owner0Name?: string
safeCreationSalt: number
gasLimit?: number
owners?: number | string
}
export const getAccountsFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
const accounts = Object.keys(values)
.sort()
.filter((key) => /^owner\d+Address$/.test(key))
return accounts.map((account) => values[account]).slice(0, values.owners)
const numOwners = getNumOwnersFrom(values)
return accounts.map((account) => values[account]).slice(0, numOwners)
}
export const getNamesFrom = (values) => {
export const getNamesFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
const accounts = Object.keys(values)
.sort()
.filter((key) => /^owner\d+Name$/.test(key))
return accounts.map((account) => values[account]).slice(0, values.owners)
const numOwners = getNumOwnersFrom(values)
return accounts.map((account) => values[account]).slice(0, numOwners)
}
export const getOwnersFrom = (names, addresses): List<SafeOwner> => {
export const getOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
return List(owners)
}
export const getThresholdFrom = (values) => Number(values.confirmations)
export const getThresholdFrom = (values: CreateSafeValues): number => Number(values.confirmations)
export const getSafeNameFrom = (values) => values.name
export const getSafeNameFrom = (values: CreateSafeValues): string => values.name
export const getSafeCreationSaltFrom = (values) => values.safeCreationSalt
export const getSafeCreationSaltFrom = (values: CreateSafeValues): number => values.safeCreationSalt

View File

@ -0,0 +1,48 @@
import { isAppManifestValid } from '../utils'
import { SafeApp, SAFE_APP_FETCH_STATUS } from '../types.d'
describe('SafeApp manifest', () => {
it('It should return true given a manifest with mandatory values supplied', async () => {
const manifest = {
name: 'test',
description: 'a test',
error: false,
}
const result = isAppManifestValid(manifest as SafeApp)
expect(result).toBe(true)
})
it('It should return false given a manifest without name', async () => {
const manifest = {
name: '',
description: 'a test',
error: false,
}
const result = isAppManifestValid(manifest as SafeApp)
expect(result).toBe(false)
})
it('It should return false given a manifest without description', async () => {
const manifest = {
name: 'test',
description: '',
error: false,
}
const result = isAppManifestValid(manifest as SafeApp)
expect(result).toBe(false)
})
it('It should return false given a manifest with error', async () => {
const manifest = {
name: 'test',
description: 'a test',
error: true,
}
const result = isAppManifestValid(manifest as SafeApp)
expect(result).toBe(false)
})
})

View File

@ -79,7 +79,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQs6CUbMUyKe3Sa3tU3HcnWWzsuCk8oJEk8CZKhRcJfEh`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
networks: [ETHEREUM_NETWORK.MAINNET],
},
// Pooltogether
{
@ -119,7 +119,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
},
// TX-Builder
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmYES1Se6i6679z3PfQ62bydgVVEoSRUabvjB35DfUGPGA`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZBgEvjqi9Jg8xATr9uUgNUVmnfYiECNxZv9Taux7mzgV`,
disabled: false,
networks: [
ETHEREUM_NETWORK.MAINNET,
@ -165,8 +165,6 @@ export const isAppManifestValid = (appInfo: SafeApp): boolean =>
appInfo.name !== 'unknown' &&
// `description` exists
!!appInfo.description &&
// `url` exists
!!appInfo.url &&
// no `error` (or `error` undefined)
!appInfo.error
@ -201,7 +199,7 @@ export const getAppInfoFromUrl = memoize(
const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`, { timeout: 5_000 })
// verify imported app fulfil safe requirements
if (!appInfo?.data || isAppManifestValid(appInfo.data)) {
if (!appInfo?.data || !isAppManifestValid(appInfo.data)) {
throw Error('The app does not fulfil the structure required.')
}
@ -210,9 +208,16 @@ export const getAppInfoFromUrl = memoize(
const jsonDataLength = 20
const remainingSpace = originFieldSize - res.url.length - jsonDataLength
const appInfoData = {
name: appInfo.data.name,
iconPath: appInfo.data.iconPath,
description: appInfo.data.description,
providedBy: appInfo.data.providedBy,
}
res = {
...res,
...appInfo.data,
...appInfoData,
id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }),
error: false,
loadingStatus: SAFE_APP_FETCH_STATUS.SUCCESS,

View File

@ -53,7 +53,7 @@ const CurrencyTooltip = (props: CurrencyTooltipProps): React.ReactElement | null
const value = valueWithCurrency.replace(/[^\d.-]/g, '')
if (!Number(value) && Number(balance)) {
return (
<Tooltip placement="top" title="Balance may be zero due to missing token price information">
<Tooltip placement="top" title="Value may be zero due to missing token price information">
<span>
<Img className={classes.tooltipInfo} alt="Info Tooltip" height={16} src={InfoIcon} />
</span>
@ -91,7 +91,7 @@ const Coins = (props: Props): React.ReactElement => {
data={filteredData}
defaultFixed
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
defaultRowsPerPage={10}
defaultRowsPerPage={100}
label="Balances"
size={filteredData.size}
>

View File

@ -13,7 +13,7 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
import { useStyles } from 'src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style'
import ContractInteractionIcon from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/custom.svg'
import ContractInteractionIcon from 'src/routes/safe/components/Transactions/TxList/assets/custom.svg'
import Collectible from '../assets/collectibles.svg'
import Token from '../assets/token.svg'
@ -116,7 +116,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: ChooseTxTy
className={classNames(classes.leftIcon, classes.iconSmall)}
src={ContractInteractionIcon}
/>
Contract Interaction
Contract interaction
</Button>
)}
</Col>

View File

@ -30,7 +30,7 @@ const Buttons = ({ onClose }: ButtonProps) => {
return (
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose} color="secondary">
Cancel
</Button>
<Button

View File

@ -88,6 +88,7 @@ export const EthAddressInput = ({
setIsValidAddress={() => {}}
fieldMutator={onScannedValue}
pristine={pristine}
label="Contract address"
/>
)}
</Col>

View File

@ -17,9 +17,15 @@ import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
import { DropdownListTheme } from 'src/theme/mui'
import { extractUsefulMethods, AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
import { Text } from '@gnosis.pm/safe-react-components'
import styled from 'styled-components'
const MENU_WIDTH = '452px'
const StyledText = styled(Text)`
padding: 4px 0 0 8px;
`
interface MethodsDropdownProps {
onChange: (method: AbiItem) => void
}
@ -78,9 +84,13 @@ export const MethodsDropdown = ({ onChange }: MethodsDropdownProps): ReactElemen
<MuiThemeProvider theme={DropdownListTheme}>
<>
<button className={classes.button} onClick={handleClick} type="button">
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
{(selectedMethod as Record<string, string>).name}
</span>
<StyledText
size="md"
color="placeHolder"
className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}
>
{(selectedMethod as Record<string, string>).name || 'Method'}
</StyledText>
</button>
<Menu
anchorEl={anchorEl}

View File

@ -133,7 +133,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
>
{(txParameters, toggleEditMode) => (
<>
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
<Header onClose={onClose} subTitle="2 of 2" title="Contract interaction" />
<Hairline />
<Block className={classes.formContainer}>
<Row margin="xs">
@ -223,7 +223,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
</div>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onPrev}>
<Button minWidth={140} onClick={onPrev} color="secondary">
Back
</Button>
<Button

View File

@ -99,7 +99,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.headingText} noMargin weight="bolder">
Send Custom Tx
Contract interaction
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -182,7 +182,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
</Block>
)}
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onPrev}>
<Button minWidth={140} onClick={onPrev} color="secondary">
Back
</Button>
<Button

View File

@ -100,7 +100,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin weight="bolder">
Send custom transactions
Contract interaction
</Paragraph>
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -161,7 +161,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
>
<Row margin="xs">
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
Recipient
Contract address
</Paragraph>
</Row>
<Row align="center" margin="md">
@ -203,6 +203,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
pristine={pristine}
setIsValidAddress={setIsValidAddress}
setSelectedEntry={setSelectedEntry}
label="Contract address"
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
@ -255,14 +256,14 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
/>
</Col>
</Row>
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
Use custom data (hex encoded)
<Paragraph color="disabled" noMargin size="lg" style={{ letterSpacing: '-0.5px' }}>
<Switch onChange={() => saveForm(args[2].values)} checked={!isABI} />
Use custom data (hex encoded)
</Paragraph>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose} color="secondary">
Cancel
</Button>
<Button

View File

@ -93,7 +93,7 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
return (
<>
<Header onClose={onClose} subTitle="1 of 2" title="Contract Interaction" />
<Header onClose={onClose} subTitle="1 of 2" title="Contract interaction" />
<Hairline />
<GnoForm
decorators={[ensResolver]}
@ -112,7 +112,7 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
<EthAddressInput
name="contractAddress"
onScannedValue={mutators.setContractAddress}
text="Contract Address*"
text="Contract address*"
/>
<ContractABI />
<MethodsDropdown onChange={mutators.setSelectedMethod} />
@ -120,9 +120,9 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
<RenderInputParams />
<RenderOutputParams />
<FormErrorMessage />
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
Use custom data (hex encoded)
<Paragraph color="disabled" noMargin size="lg" style={{ letterSpacing: '-0.5px' }}>
<Switch checked={!isABI} onChange={() => saveForm(rest.values)} />
Use custom data (hex encoded)
</Paragraph>
</Block>
<Hairline />

View File

@ -82,13 +82,13 @@ export const getParsedJSONOrArrayFromString = (parameter: string): (string | num
export const handleSubmitError = (error: SubmissionErrors, values: Record<string, string>): Record<string, string> => {
for (const key in values) {
if (values.hasOwnProperty(key) && values[key] === error.value) {
if (values.hasOwnProperty(key) && error !== undefined && values[key] === error.value) {
return { [key]: error.reason }
}
}
// .call() failed and we're logging a generic error
return { [FORM_ERROR]: error.message }
return { [FORM_ERROR]: error ? error.message : undefined }
}
export const generateFormFieldKey = (type: string, signatureHash: string, index: number): string => {

View File

@ -149,7 +149,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.headingText} noMargin weight="bolder">
Send Collectible
Send collectible
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -218,7 +218,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
/>
</div>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onPrev}>
<Button minWidth={140} onClick={onPrev} color="secondary">
Back
</Button>
<Button

View File

@ -188,7 +188,7 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
{/* Header */}
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
<Paragraph className={classes.headingText} noMargin weight="bolder">
Send Funds
Send funds
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>

View File

@ -120,7 +120,7 @@ const SendCollectible = ({
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin weight="bolder">
Send Collectible
Send collectible
</Paragraph>
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -267,7 +267,7 @@ const SendCollectible = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose} color="secondary">
Cancel
</Button>
<Button

View File

@ -160,7 +160,7 @@ const SendFunds = ({
<>
<Row align="center" className={classes.heading} grow data-testid="modal-title-send-funds">
<Paragraph className={classes.manage} noMargin weight="bolder">
Send Funds
Send funds
</Paragraph>
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -322,7 +322,7 @@ const SendFunds = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose} color="secondary">
Cancel
</Button>
<Button

View File

@ -2,7 +2,7 @@ import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React, { useState } from 'react'
import React from 'react'
import { useSelector } from 'react-redux'
import { styles } from './style'
@ -12,9 +12,7 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors'
import AddCustomAssetComponent from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset'
import AddCustomToken from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
import AssetsList from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList'
import { AssetsList } from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList'
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors'
@ -30,13 +28,12 @@ type Props = {
onClose: () => void
}
const Tokens = (props: Props): React.ReactElement => {
export const Tokens = (props: Props): React.ReactElement => {
const { modalScreen, onClose, safeAddress } = props
const tokens = useSelector(orderedTokenListSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
const classes = useStyles()
const [activeScreen, setActiveScreen] = useState(modalScreen)
return (
<>
@ -49,31 +46,15 @@ const Tokens = (props: Props): React.ReactElement => {
</IconButton>
</Row>
<Hairline />
{activeScreen === 'tokenList' && (
{modalScreen === 'tokenList' && (
<TokenList
activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens}
safeAddress={safeAddress}
setActiveScreen={setActiveScreen}
tokens={tokens}
/>
)}
{activeScreen === 'assetsList' && <AssetsList setActiveScreen={setActiveScreen} />}
{activeScreen === 'addCustomToken' && (
<AddCustomToken
activeTokens={activeTokens}
onClose={onClose}
parentList={'tokenList'}
safeAddress={safeAddress}
setActiveScreen={setActiveScreen}
tokens={tokens}
/>
)}
{activeScreen === 'addCustomAsset' && (
<AddCustomAssetComponent onClose={onClose} parentList={'assetsList'} setActiveScreen={setActiveScreen} />
)}
{modalScreen === 'assetsList' && <AssetsList />}
</>
)
}
export default Tokens

View File

@ -1,187 +0,0 @@
import { withStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import { FormSpy } from 'react-final-form'
import { useSelector } from 'react-redux'
import { styles } from './style'
import { getSymbolAndDecimalsFromContract } from './utils'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import AddressInput from 'src/components/forms/AddressInput'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors'
import {
addressIsAssetContract,
doesntExistInAssetsList,
} from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators'
import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg'
import { Checkbox } from '@gnosis.pm/safe-react-components'
export const ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID = 'add-custom-asset-address-input'
export const ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID = 'add-custom-asset-symbols-input'
export const ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID = 'add-custom-asset-decimals-input'
export const ADD_CUSTOM_ASSET_FORM = 'add-custom-asset-form'
const INITIAL_FORM_STATE = {
address: '',
decimals: '',
symbol: '',
logoUri: '',
}
const AddCustomAsset = (props) => {
const { classes, onClose, parentList, setActiveScreen } = props
const nftAssetsList = useSelector(nftAssetsListSelector)
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const handleSubmit = () => {
onClose()
}
const populateFormValuesFromAddress = async (tokenAddress) => {
const tokenData = await getSymbolAndDecimalsFromContract(tokenAddress)
if (tokenData.length) {
const [symbol, decimals] = tokenData
setFormValues({
address: tokenAddress,
symbol,
decimals,
name: symbol,
} as any)
}
}
const formSpyOnChangeHandler = async (state) => {
const { dirty, errors, submitSucceeded, validating, values } = state
// for some reason this is called after submitting, we don't need to update the values
// after submit
if (submitSucceeded) {
return
}
if (dirty && !validating && errors.address) {
setFormValues(INITIAL_FORM_STATE)
}
if (!errors.address && !validating && dirty) {
await populateFormValuesFromAddress(values.address)
}
}
const formMutators = {
setAssetAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
const goBack = () => {
setActiveScreen(parentList)
}
return (
<>
<GnoForm
initialValues={formValues}
onSubmit={handleSubmit}
formMutators={formMutators}
testId={ADD_CUSTOM_ASSET_FORM}
>
{(...args) => {
const mutators = args[3]
return (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom asset
</Paragraph>
<AddressInput
fieldMutator={mutators.setAssetAddress}
className={classes.addressInput}
name="address"
placeholder="Asset contract address*"
testId={ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID}
text="Asset contract address*"
validators={[doesntExistInAssetsList(nftAssetsList), addressIsAssetContract]}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate assets for all Safes"
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</>
)
}}
</GnoForm>
</>
)
}
const AddCustomAssetComponent = withStyles(styles as any)(AddCustomAsset)
export default AddCustomAssetComponent

View File

@ -1,33 +0,0 @@
import { lg, md } from 'src/theme/variables'
export const styles = () => ({
title: {
padding: `${lg} 0 20px`,
fontSize: md,
},
formContainer: {
padding: '0 20px',
minHeight: '369px',
},
addressInput: {
marginBottom: '15px',
display: 'flex',
flexGrow: 1,
backgroundColor: 'red',
},
tokenImageHeading: {
margin: '0 0 15px',
},
checkbox: {
padding: '0 7px 0 0',
width: '18px',
height: '18px',
},
checkboxLabel: {
letterSpacing: '-0.5px',
},
buttonRow: {
height: '84px',
justifyContent: 'center',
},
})

View File

@ -1,16 +0,0 @@
import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens'
export const getSymbolAndDecimalsFromContract = async (tokenAddress) => {
const tokenContract = await getHumanFriendlyToken()
const token = await tokenContract.at(tokenAddress)
let values
try {
const [symbol, decimals] = await Promise.all([token.symbol(), token.decimals()])
values = [symbol, decimals.toString()]
} catch {
values = []
}
return values
}

View File

@ -1,21 +0,0 @@
import memoize from 'lodash.memoize'
import { isERC721Contract } from 'src/logic/collectibles/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
// eslint-disable-next-line
export const addressIsAssetContract = memoize(async (tokenAddress) => {
const isAsset = await isERC721Contract(tokenAddress)
if (!isAsset) {
return 'Not a asset address'
}
})
// eslint-disable-next-line
export const doesntExistInAssetsList = (assetsList) =>
memoize((tokenAddress) => {
const tokenIndex = assetsList.findIndex(({ address }) => sameAddress(address, tokenAddress))
if (tokenIndex !== -1) {
return 'Token already exists in your token list'
}
})

View File

@ -1,214 +0,0 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import { FormSpy } from 'react-final-form'
import { styles } from './style'
import { getSymbolAndDecimalsFromContract } from './utils'
import { addressIsTokenContract, doesntExistInTokenList } from './validators'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import AddressInput from 'src/components/forms/AddressInput'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Checkbox } from '@gnosis.pm/safe-react-components'
import { useDispatch } from 'react-redux'
import { addToken } from 'src/logic/tokens/store/actions/addToken'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import activateTokenForAllSafes from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { Token } from 'src/logic/tokens/store/model/token'
import { List, Set } from 'immutable'
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input'
export const ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID = 'add-custom-token-decimals-input'
export const ADD_CUSTOM_TOKEN_FORM = 'add-custom-token-form'
const INITIAL_FORM_STATE = {
address: '',
decimals: '',
symbol: '',
logoUri: '',
}
const useStyles = makeStyles(styles)
type Props = {
activeTokens: List<Token>
onClose: () => void
parentList: string
safeAddress: string
setActiveScreen: (screen: string) => void
tokens: List<Token>
}
const AddCustomToken = (props: Props): React.ReactElement => {
const { activeTokens, onClose, parentList, safeAddress, setActiveScreen, tokens } = props
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const classes = useStyles()
const dispatch = useDispatch()
const handleSubmit = (values) => {
const address = checksumAddress(values.address)
const token = {
address,
decimals: values.decimals,
symbol: values.symbol,
name: values.symbol,
}
dispatch(addToken(token))
if (values.showForAllSafes) {
dispatch(activateTokenForAllSafes(token.address))
} else {
const activeTokensAddresses = Set(activeTokens.map(({ address }) => address))
dispatch(updateActiveTokens(safeAddress, activeTokensAddresses.add(token.address)))
}
onClose()
}
const populateFormValuesFromAddress = async (tokenAddress) => {
const tokenData = await getSymbolAndDecimalsFromContract(tokenAddress)
if (tokenData.length) {
const [symbol, decimals] = tokenData
setFormValues({
address: tokenAddress,
symbol,
decimals,
name: symbol,
} as any)
}
}
const formSpyOnChangeHandler = async (state) => {
const { dirty, errors, submitSucceeded, validating, values } = state
// for some reason this is called after submitting, we don't need to update the values
// after submit
if (submitSucceeded) {
return
}
if (dirty && !validating && errors.address) {
setFormValues(INITIAL_FORM_STATE)
}
if (!errors.address && !validating && dirty) {
await populateFormValuesFromAddress(values.address)
}
}
const formMutators = {
setTokenAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
const goBack = () => {
setActiveScreen(parentList)
}
return (
<>
<GnoForm
initialValues={formValues}
onSubmit={handleSubmit}
formMutators={formMutators}
testId={ADD_CUSTOM_TOKEN_FORM}
>
{(...args) => {
const mutators = args[3]
return (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom token
</Paragraph>
<AddressInput
fieldMutator={mutators.setTokenAddress}
className={classes.addressInput}
name="address"
placeholder="Token contract address*"
testId={ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID}
text="Token contract address*"
validators={[doesntExistInTokenList(tokens), addressIsTokenContract]}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate token for all Safes"
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</>
)
}}
</GnoForm>
</>
)
}
export default AddCustomToken

View File

@ -1,34 +0,0 @@
import { lg, md } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
title: {
padding: `${lg} 0 20px`,
fontSize: md,
},
formContainer: {
padding: '0 20px',
minHeight: '369px',
},
addressInput: {
marginBottom: '15px',
display: 'flex',
flexGrow: 1,
backgroundColor: 'red',
},
tokenImageHeading: {
margin: '0 0 15px',
},
checkbox: {
padding: '0 7px 0 0',
width: '18px',
height: '18px',
},
checkboxLabel: {
letterSpacing: '-0.5px',
},
buttonRow: {
height: '84px',
justifyContent: 'center',
},
})

View File

@ -1,16 +0,0 @@
import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens'
export const getSymbolAndDecimalsFromContract = async (tokenAddress) => {
const tokenContract = await getHumanFriendlyToken()
const token = await tokenContract.at(tokenAddress)
let values
try {
const [symbol, decimals] = await Promise.all([token.symbol(), token.decimals()])
values = [symbol, decimals.toString()]
} catch {
values = []
}
return values
}

View File

@ -1,31 +0,0 @@
import memoize from 'lodash.memoize'
import { isAddressAToken } from 'src/logic/tokens/utils/tokenHelpers'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
export const addressIsTokenContract = memoize(async (tokenAddress) => {
// SECOND APPROACH:
// They both seem to work the same
// const tokenContract = await getStandardTokenContract()
// try {
// await tokenContract.at(tokenAddress)
// } catch {
// return 'Not a token address'
// }
const isToken = await isAddressAToken(tokenAddress)
if (!isToken) {
return 'Not a token address'
}
})
// eslint-disable-next-line
export const doesntExistInTokenList = (tokenList) =>
memoize((tokenAddress: string) => {
const tokenIndex = tokenList.findIndex(({ address }) => sameAddress(address, tokenAddress))
if (tokenIndex !== -1) {
return 'Token already exists in your token list'
}
})

View File

@ -10,10 +10,7 @@ import Paragraph from 'src/components/layout/Paragraph'
import { useStyles } from './style'
import Spacer from 'src/components/Spacer'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Divider from 'src/components/layout/Divider'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors'
@ -26,12 +23,6 @@ import {
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
export const ADD_CUSTOM_ASSET_BUTTON_TEST_ID = 'add-custom-asset-btn'
type Props = {
setActiveScreen: (newScreen: string) => void
}
const filterBy = (filter, nfts) =>
nfts.filter(
(asset) =>
@ -41,7 +32,7 @@ const filterBy = (filter, nfts) =>
asset.symbol.toLowerCase().includes(filter.toLowerCase()),
)
const AssetsList = (props: Props): React.ReactElement => {
export const AssetsList = (): React.ReactElement => {
const classes = useStyles()
const searchClasses = {
input: classes.searchInput,
@ -99,7 +90,7 @@ const AssetsList = (props: Props): React.ReactElement => {
const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList)
const itemData = createItemData(nftAssetsFilteredList)
const switchToAddCustomAssetScreen = () => props.setActiveScreen('addCustomAsset')
return (
<>
<Block className={classes.root}>
@ -113,21 +104,6 @@ const AssetsList = (props: Props): React.ReactElement => {
searchIcon={<div />}
value={filterValue}
/>
<Spacer />
<Divider />
<Spacer />
<Button
classes={{ label: classes.addBtnLabel }}
className={classes.add}
color="primary"
disabled
onClick={switchToAddCustomAssetScreen}
size="small"
testId={ADD_CUSTOM_ASSET_BUTTON_TEST_ID}
variant="contained"
>
+ Add custom asset
</Button>
</Row>
<Hairline />
</Block>
@ -154,5 +130,3 @@ const AssetsList = (props: Props): React.ReactElement => {
</>
)
}
export default AssetsList

Some files were not shown because too many files have changed in this diff Show More