Merge pull request #2015 from gnosis/release/v3.2.0
This commit is contained in:
commit
99f46043ae
|
@ -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: {
|
||||
|
|
|
@ -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 }}
|
||||
|
|
116
.travis.yml
116
.travis.yml
|
@ -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
|
|
@ -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
|
42
package.json
42
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -55,7 +55,6 @@ const mainnet: NetworkConfig = {
|
|||
WALLETS.PORTIS,
|
||||
WALLETS.TORUS,
|
||||
WALLETS.TRUST,
|
||||
WALLETS.WALLET_CONNECT,
|
||||
WALLETS.WALLET_LINK,
|
||||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE,
|
||||
|
|
|
@ -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)))
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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}`)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_INCOMING_TRANSACTIONS = 'ADD_INCOMING_TRANSACTIONS'
|
||||
|
||||
export const addIncomingTransactions = createAction(ADD_INCOMING_TRANSACTIONS)
|
|
@ -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)
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) })
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const REMOVE_CANCELLATION_TRANSACTION = 'REMOVE_CANCELLATION_TRANSACTION'
|
||||
|
||||
export const removeCancellationTransaction = createAction(REMOVE_CANCELLATION_TRANSACTION)
|
|
@ -1,5 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const REMOVE_TRANSACTION = 'REMOVE_TRANSACTION'
|
||||
|
||||
export const removeTransaction = createAction(REMOVE_TRANSACTION)
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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' }],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
|
@ -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(),
|
||||
)
|
|
@ -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(),
|
||||
)
|
|
@ -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],
|
||||
}
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
|
@ -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(),
|
||||
)
|
|
@ -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),
|
||||
)
|
|
@ -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,
|
||||
|
|
|
@ -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 {}
|
|
@ -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[],
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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/`
|
||||
}
|
|
@ -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/`
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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/`
|
||||
}
|
|
@ -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 },
|
||||
|
|
|
@ -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[] }>> => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'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}>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -88,6 +88,7 @@ export const EthAddressInput = ({
|
|||
setIsValidAddress={() => {}}
|
||||
fieldMutator={onScannedValue}
|
||||
pristine={pristine}
|
||||
label="Contract address"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
})
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
})
|
|
@ -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
Loading…
Reference in New Issue