Merge branch 'development' of https://github.com/gnosis/safe-react into 957-null-balances
# Conflicts: # package.json
This commit is contained in:
commit
6c857ee02a
|
@ -1,7 +1,6 @@
|
|||
node_modules/
|
||||
build/
|
||||
.DS_Store
|
||||
build/
|
||||
yarn-error.log
|
||||
.env*
|
||||
.idea/
|
||||
|
|
|
@ -21,6 +21,8 @@ matrix:
|
|||
- env:
|
||||
- REACT_APP_NETWORK='rinkeby'
|
||||
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
|
||||
cache:
|
||||
yarn: true
|
||||
before_install:
|
||||
# Needed to deploy pull request and releases
|
||||
- sudo apt-get update
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
class LocalStorageMock {
|
||||
store: Object
|
||||
store
|
||||
|
||||
constructor() {
|
||||
this.store = {}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
import Web3 from 'web3'
|
||||
|
||||
const window = global.window || {}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/tutorial-webpack.html
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
const path = require('path')
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
// @flow
|
||||
jest.setTimeout(60000)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
const Migrations = artifacts.require('./Migrations.sol')
|
||||
|
||||
module.exports = deployer => deployer.deploy(Migrations)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
/* eslint-disable no-console */
|
||||
const TokenOMG = artifacts.require('TokenOMG')
|
||||
const TokenRDN = artifacts.require('TokenRDN')
|
||||
|
|
35
package.json
35
package.json
|
@ -151,25 +151,26 @@
|
|||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "^0.1.2",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.15.0",
|
||||
"@material-ui/core": "4.9.14",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.16.0",
|
||||
"@material-ui/core": "4.10.1",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.39",
|
||||
"@openzeppelin/contracts": "3.0.1",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.9.1",
|
||||
"bnc-onboard": "1.9.4",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.2.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"currency-flags": "2.1.2",
|
||||
"date-fns": "2.13.0",
|
||||
"date-fns": "2.14.0",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"electron-log": "4.1.2",
|
||||
"electron-log": "4.2.1",
|
||||
"electron-updater": "4.3.1",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"express": "^4.17.1",
|
||||
"final-form": "4.19.1",
|
||||
"final-form": "4.20.0",
|
||||
"final-form-calculate": "^1.3.1",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
|
@ -179,12 +180,12 @@
|
|||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"open": "^7.0.3",
|
||||
"polished": "3.6.3",
|
||||
"polished": "3.6.4",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.12.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-final-form": "6.4.0",
|
||||
"react-final-form": "6.5.0",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-hot-loader": "4.12.21",
|
||||
|
@ -208,19 +209,19 @@
|
|||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/node": "14.0.10",
|
||||
"@types/react": "^16.9.32",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||
"@typescript-eslint/parser": "^2.34.0",
|
||||
"autoprefixer": "9.7.6",
|
||||
"@typescript-eslint/eslint-plugin": "3.1.0",
|
||||
"@typescript-eslint/parser": "3.1.0",
|
||||
"autoprefixer": "9.8.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "7.1.8",
|
||||
"electron-builder": "22.2.0",
|
||||
"electron-builder": "22.7.0",
|
||||
"electron-notarize": "^0.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
|
@ -229,12 +230,12 @@
|
|||
"eslint-plugin-sort-destructure-keys": "1.3.4",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.2.2",
|
||||
"lint-staged": "10.2.2",
|
||||
"lint-staged": "10.2.8",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"truffle": "5.1.23",
|
||||
"typescript": "~3.7.2",
|
||||
"truffle": "5.1.28",
|
||||
"typescript": "3.9.3",
|
||||
"wait-on": "5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
import { toDataUrl } from './blockies'
|
||||
|
||||
export default class Identicon extends React.PureComponent<any> {
|
||||
|
@ -45,7 +47,7 @@ export default class Identicon extends React.PureComponent<any> {
|
|||
|
||||
generateBlockieIdenticon = (address, diameter) => {
|
||||
const image = new window.Image()
|
||||
image.src = toDataUrl(address)
|
||||
image.src = toDataUrl(address || ZERO_ADDRESS)
|
||||
image.height = diameter
|
||||
image.width = diameter
|
||||
image.style.borderRadius = `${diameter / 2}px`
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import React from 'react'
|
||||
|
||||
class GnoCheckbox extends React.PureComponent<any> {
|
||||
render() {
|
||||
const {
|
||||
input: { checked, name, onChange, ...restInput },
|
||||
...rest
|
||||
} = this.props
|
||||
|
||||
return <Checkbox {...rest} checked={!!checked} inputProps={restInput} name={name} onChange={onChange} />
|
||||
}
|
||||
}
|
||||
|
||||
export default GnoCheckbox
|
|
@ -1,48 +1,47 @@
|
|||
//
|
||||
import { ensureOnce } from "src/utils/singleton"
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
import { ETHEREUM_NETWORK, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
RELAY_API_URL,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
TX_SERVICE_HOST
|
||||
} from "src/config/names"
|
||||
import devConfig from "./development"
|
||||
import testConfig from "./testing"
|
||||
import stagingConfig from "./staging"
|
||||
import prodConfig from "./production"
|
||||
import mainnetDevConfig from "./development-mainnet"
|
||||
import mainnetProdConfig from "./production-mainnet"
|
||||
import mainnetStagingConfig from "./staging-mainnet"
|
||||
} from 'src/config/names'
|
||||
import devConfig from './development'
|
||||
import testConfig from './testing'
|
||||
import stagingConfig from './staging'
|
||||
import prodConfig from './production'
|
||||
import mainnetDevConfig from './development-mainnet'
|
||||
import mainnetProdConfig from './production-mainnet'
|
||||
import mainnetStagingConfig from './staging-mainnet'
|
||||
|
||||
const configuration = () => {
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return testConfig
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.REACT_APP_NETWORK === "mainnet") {
|
||||
return process.env.REACT_APP_ENV === "production"
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.REACT_APP_NETWORK === 'mainnet') {
|
||||
return process.env.REACT_APP_ENV === 'production'
|
||||
? mainnetProdConfig
|
||||
: mainnetStagingConfig
|
||||
}
|
||||
|
||||
return process.env.REACT_APP_ENV === "production"
|
||||
return process.env.REACT_APP_ENV === 'production'
|
||||
? prodConfig
|
||||
: stagingConfig
|
||||
}
|
||||
|
||||
return process.env.REACT_APP_NETWORK === "mainnet"
|
||||
return process.env.REACT_APP_NETWORK === 'mainnet'
|
||||
? mainnetDevConfig
|
||||
: devConfig
|
||||
}
|
||||
|
||||
export const getNetwork = () =>
|
||||
process.env.REACT_APP_NETWORK === "mainnet"
|
||||
process.env.REACT_APP_NETWORK === 'mainnet'
|
||||
? ETHEREUM_NETWORK.MAINNET
|
||||
: ETHEREUM_NETWORK.RINKEBY
|
||||
|
||||
export const getNetworkId = () =>
|
||||
process.env.REACT_APP_NETWORK === "mainnet" ? 1 : 4
|
||||
process.env.REACT_APP_NETWORK === 'mainnet' ? 1 : 4
|
||||
|
||||
const getConfig = ensureOnce(configuration)
|
||||
|
||||
|
@ -74,9 +73,9 @@ export const getGoogleAnalyticsTrackingID = () =>
|
|||
: process.env.REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY
|
||||
|
||||
export const getIntercomId = () =>
|
||||
process.env.REACT_APP_ENV === "production"
|
||||
process.env.REACT_APP_ENV === 'production'
|
||||
? process.env.REACT_APP_INTERCOM_ID
|
||||
: "plssl1fl"
|
||||
: 'plssl1fl'
|
||||
|
||||
export const getExchangeRatesUrl = () => 'https://api.exchangeratesapi.io/latest'
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
/**
|
||||
* Generates a batch request for grouping RPC calls
|
||||
|
@ -12,7 +11,6 @@ import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
|||
* @returns {Promise<[*]>}
|
||||
*/
|
||||
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => {
|
||||
const web3 = getWeb3()
|
||||
const contractInstance: any = new web3.eth.Contract(abi, address)
|
||||
const localBatch = batch ? null : new web3.BatchRequest()
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
// SAFE METHODS TO ITS ID
|
||||
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
|
||||
|
@ -53,40 +52,124 @@ const METHOD_TO_ID = {
|
|||
'0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD,
|
||||
}
|
||||
|
||||
export const decodeParamsFromSafeMethod = (data) => {
|
||||
const web3 = getWeb3()
|
||||
const [methodId, params] = [data.slice(0, 10), data.slice(10)]
|
||||
type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
|
||||
type DecodedValues = Array<{
|
||||
name: string
|
||||
value: string
|
||||
}>
|
||||
|
||||
type SafeDecodedParams = {
|
||||
[key in SafeMethods]?: DecodedValues
|
||||
}
|
||||
|
||||
type TokenDecodedParams = {
|
||||
[key in TokenMethods]?: DecodedValues
|
||||
}
|
||||
|
||||
export type DecodedMethods = SafeDecodedParams | TokenDecodedParams | null
|
||||
|
||||
export const decodeParamsFromSafeMethod = (data: string): SafeDecodedParams | null => {
|
||||
const [methodId, params] = [data.slice(0, 10) as keyof typeof METHOD_TO_ID | string, data.slice(10)]
|
||||
|
||||
switch (methodId) {
|
||||
// swapOwner
|
||||
case '0xe318b52b':
|
||||
case '0xe318b52b': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['uint', 'address', 'address'], params)
|
||||
return {
|
||||
methodName: METHOD_TO_ID[methodId],
|
||||
args: web3.eth.abi.decodeParameters(['uint', 'address', 'address'], params),
|
||||
[METHOD_TO_ID[methodId]]: [
|
||||
{ name: 'oldOwner', value: decodedParameters[1] },
|
||||
{ name: 'newOwner', value: decodedParameters[2] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// addOwnerWithThreshold
|
||||
case '0x0d582f13':
|
||||
case '0x0d582f13': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['address', 'uint'], params)
|
||||
return {
|
||||
methodName: METHOD_TO_ID[methodId],
|
||||
args: web3.eth.abi.decodeParameters(['address', 'uint'], params),
|
||||
[METHOD_TO_ID[methodId]]: [
|
||||
{ name: 'owner', value: decodedParameters[0] },
|
||||
{ name: '_threshold', value: decodedParameters[1] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// removeOwner
|
||||
case '0xf8dc5dd9':
|
||||
case '0xf8dc5dd9': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params)
|
||||
return {
|
||||
methodName: METHOD_TO_ID[methodId],
|
||||
args: web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params),
|
||||
[METHOD_TO_ID[methodId]]: [
|
||||
{ name: 'oldOwner', value: decodedParameters[1] },
|
||||
{ name: '_threshold', value: decodedParameters[2] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// changeThreshold
|
||||
case '0x694e80c3':
|
||||
case '0x694e80c3': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['uint'], params)
|
||||
return {
|
||||
methodName: METHOD_TO_ID[methodId],
|
||||
args: web3.eth.abi.decodeParameters(['uint'], params),
|
||||
[METHOD_TO_ID[methodId]]: [
|
||||
{ name: '_threshold', value: decodedParameters[0] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return {}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isSafeMethod = (methodId: string): boolean => {
|
||||
return !!METHOD_TO_ID[methodId]
|
||||
}
|
||||
|
||||
export const decodeMethods = (data: string): DecodedMethods => {
|
||||
const [methodId, params] = [data.slice(0, 10), data.slice(10)]
|
||||
|
||||
if (isSafeMethod(methodId)) {
|
||||
return decodeParamsFromSafeMethod(data)
|
||||
}
|
||||
|
||||
switch (methodId) {
|
||||
// a9059cbb - transfer(address,uint256)
|
||||
case '0xa9059cbb': {
|
||||
const decodeParameters = web3.eth.abi.decodeParameters(['address', 'uint'], params)
|
||||
return {
|
||||
transfer: [
|
||||
{ name: 'to', value: decodeParameters[0] },
|
||||
{ name: 'value', value: decodeParameters[1] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 23b872dd - transferFrom(address,address,uint256)
|
||||
case '0x23b872dd': {
|
||||
const decodeParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params)
|
||||
return {
|
||||
transferFrom: [
|
||||
{ name: 'from', value: decodeParameters[0] },
|
||||
{ name: 'to', value: decodeParameters[1] },
|
||||
{ name: 'value', value: decodeParameters[2] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 42842e0e - safeTransferFrom(address,address,uint256)
|
||||
case '0x42842e0e':{
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params)
|
||||
return {
|
||||
safeTransferFrom: [
|
||||
{ name: 'from', value: decodedParameters[0] },
|
||||
{ name: 'to', value: decodedParameters[1] },
|
||||
{ name: 'value', value: decodedParameters[2] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
import { Map } from 'immutable'
|
||||
import { List } from 'immutable'
|
||||
|
||||
export const getAwaitingTransactions = (allTransactions, cancellationTransactionsByNonce, userAccount) => {
|
||||
if (!allTransactions) {
|
||||
return Map({})
|
||||
}
|
||||
import { isPendingTransaction } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
|
||||
const allAwaitingTransactions = allTransactions.map((safeTransactions) => {
|
||||
const nonCancelledTransactions = safeTransactions.filter((transaction) => {
|
||||
// If transactions are not executed, but there's a transaction with the same nonce EXECUTED later
|
||||
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
|
||||
let isTransactionCancelled = false
|
||||
if (!transaction.isExecuted) {
|
||||
if (cancellationTransactionsByNonce.get(transaction.nonce)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
isTransactionCancelled = true
|
||||
}
|
||||
}
|
||||
// The transaction is not executed and is not cancelled, so it's still waiting confirmations
|
||||
if (!transaction.executionTxHash && !isTransactionCancelled) {
|
||||
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this
|
||||
// transaction
|
||||
const transactionWaitingUser = transaction.confirmations.filter(({ owner }) => owner !== userAccount)
|
||||
export const getAwaitingTransactions = (allTransactions = List([]), cancellationTxs, userAccount: string) => {
|
||||
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 && !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
|
||||
})
|
||||
return nonCancelledTransactions
|
||||
})
|
||||
|
||||
return allAwaitingTransactions
|
||||
}
|
||||
|
|
|
@ -46,7 +46,18 @@ export const estimateTxGasCosts = async (safeAddress, to, data, tx?: any, preApp
|
|||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
txData = await safeInstance.methods
|
||||
.execTransaction(to, tx ? tx.value : 0, data, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, signatures)
|
||||
.execTransaction(
|
||||
to,
|
||||
tx ? tx.value : 0,
|
||||
data,
|
||||
CALL,
|
||||
tx ? tx.safeTxGas : 0,
|
||||
0,
|
||||
0,
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS,
|
||||
signatures,
|
||||
)
|
||||
.encodeABI()
|
||||
} else {
|
||||
const txHash = await safeInstance.methods
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ethSigner } from './ethSigner'
|
|||
|
||||
// 1. we try to sign via EIP-712 if user's wallet supports it
|
||||
// 2. If not, try to use eth_sign (Safe version has to be >1.1.1)
|
||||
// If eth_sign, doesn't work continue with the regular flow (on-chain signatures, more in createTransaction.js)
|
||||
// If eth_sign, doesn't work continue with the regular flow (on-chain signatures, more in createTransaction.ts)
|
||||
|
||||
const SIGNERS = {
|
||||
EIP712_V3: getEIP712Signer('v3'),
|
||||
|
|
|
@ -56,7 +56,7 @@ export const buildTxServiceUrl = (safeAddress) => {
|
|||
const host = getTxServiceHost()
|
||||
const address = checksumAddress(safeAddress)
|
||||
const base = getTxServiceUriFrom(address)
|
||||
return `${host}${base}`
|
||||
return `${host}${base}?has_confirmations=True`
|
||||
}
|
||||
|
||||
const SUCCESS_STATUS = 201 // CREATED status
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
||||
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
||||
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721'
|
||||
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
|
||||
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
|
||||
import { List } from 'immutable'
|
||||
import contract from 'truffle-contract'
|
||||
|
||||
import saveTokens from './saveTokens'
|
||||
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { fetchTokenList } from 'src/logic/tokens/api'
|
||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
|
@ -36,53 +38,6 @@ const createERC721TokenContract = async () => {
|
|||
return erc721Token
|
||||
}
|
||||
|
||||
// For the `batchRequest` of balances, we're just using the `balanceOf` method call.
|
||||
// So having a simple ABI only with `balanceOf` prevents errors
|
||||
// when instantiating non-standard ERC-20 Tokens.
|
||||
export const OnlyBalanceToken = {
|
||||
contractName: 'OnlyBalanceToken',
|
||||
abi: [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'owner',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'balanceOf',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'owner',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'balances',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract)
|
||||
|
||||
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
||||
|
@ -96,35 +51,45 @@ export const containsMethodByHash = async (contractAddress, methodHash) => {
|
|||
return byteCode.indexOf(methodHash.replace('0x', '')) !== -1
|
||||
}
|
||||
|
||||
const getTokenValues = (tokenAddress) =>
|
||||
generateBatchRequests({
|
||||
abi: ERC20Detailed.abi,
|
||||
address: tokenAddress,
|
||||
methods: ['decimals', 'name', 'symbol'],
|
||||
})
|
||||
|
||||
export const getTokenInfos = async (tokenAddress) => {
|
||||
if (!tokenAddress) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { tokens } = store.getState()
|
||||
const localToken = tokens.get(tokenAddress)
|
||||
|
||||
// If the token is inside the store we return the store token
|
||||
if (localToken) {
|
||||
return localToken
|
||||
}
|
||||
|
||||
// Otherwise we fetch it, save it to the store and return it
|
||||
const tokenContract = await getHumanFriendlyToken()
|
||||
const tokenInstance = await tokenContract.at(tokenAddress)
|
||||
const [tokenSymbol, tokenDecimals, name] = await Promise.all([
|
||||
tokenInstance.symbol(),
|
||||
tokenInstance.decimals(),
|
||||
tokenInstance.name(),
|
||||
])
|
||||
const savedToken = makeToken({
|
||||
const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
|
||||
|
||||
if (tokenDecimals === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const token = makeToken({
|
||||
address: tokenAddress,
|
||||
name: name ? name : tokenSymbol,
|
||||
name: tokenName ? tokenName : tokenSymbol,
|
||||
symbol: tokenSymbol,
|
||||
decimals: tokenDecimals.toNumber(),
|
||||
decimals: Number(tokenDecimals),
|
||||
logoUri: '',
|
||||
})
|
||||
const newTokens = tokens.set(tokenAddress, savedToken)
|
||||
|
||||
const newTokens = tokens.set(tokenAddress, token)
|
||||
store.dispatch(saveTokens(newTokens))
|
||||
|
||||
return savedToken
|
||||
return token
|
||||
}
|
||||
|
||||
export const fetchTokens = () => async (dispatch, getState) => {
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { Record } from 'immutable'
|
||||
import { Record, RecordOf } from 'immutable'
|
||||
|
||||
export const makeToken = Record({
|
||||
export type TokenProps = {
|
||||
address: string
|
||||
name: string
|
||||
symbol: string
|
||||
decimals: number | string
|
||||
logoUri?: string | null
|
||||
balance?: number | string
|
||||
}
|
||||
|
||||
export const makeToken = Record<TokenProps>({
|
||||
address: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
|
@ -8,5 +17,6 @@ export const makeToken = Record({
|
|||
logoUri: '',
|
||||
balance: undefined,
|
||||
})
|
||||
|
||||
// balance is only set in extendedSafeTokensSelector when we display user's token balances
|
||||
|
||||
export type Token = RecordOf<TokenProps>
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import logo from 'src/assets/icons/icon_etherTokens.svg'
|
||||
import { getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { getStandardTokenContract, getTokenInfos } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
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 { isEmptyData } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
|
||||
export const ETH_ADDRESS = '0x000'
|
||||
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '0x42842e0e'
|
||||
export const DECIMALS_METHOD_HASH = '313ce567'
|
||||
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
||||
|
||||
export const isEther = (symbol) => symbol === 'ETH'
|
||||
|
||||
export const getEthAsToken = (balance) => {
|
||||
const eth = makeToken({
|
||||
export const getEthAsToken = (balance: string): Token => {
|
||||
return makeToken({
|
||||
address: ETH_ADDRESS,
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
|
@ -21,26 +19,9 @@ export const getEthAsToken = (balance) => {
|
|||
logoUri: logo,
|
||||
balance,
|
||||
})
|
||||
|
||||
return eth
|
||||
}
|
||||
|
||||
export const calculateActiveErc20TokensFrom = (tokens) => {
|
||||
const activeTokens = List().withMutations((list) =>
|
||||
tokens.forEach((token) => {
|
||||
const isDeactivated = isEther(token.symbol) || !token.status
|
||||
if (isDeactivated) {
|
||||
return
|
||||
}
|
||||
|
||||
list.push(token)
|
||||
}),
|
||||
)
|
||||
|
||||
return activeTokens
|
||||
}
|
||||
|
||||
export const isAddressAToken = async (tokenAddress) => {
|
||||
export const isAddressAToken = async (tokenAddress): Promise<boolean> => {
|
||||
// SECOND APPROACH:
|
||||
// They both seem to work the same
|
||||
// const tokenContract = await getStandardTokenContract()
|
||||
|
@ -49,41 +30,69 @@ export const isAddressAToken = async (tokenAddress) => {
|
|||
// } catch {
|
||||
// return 'Not a token address'
|
||||
// }
|
||||
|
||||
const web3 = getWeb3()
|
||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
||||
|
||||
return call !== '0x'
|
||||
}
|
||||
|
||||
export const hasDecimalsMethod = async (address) => {
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const token: any = new web3.eth.Contract(ERC20Detailed.abi as any, address)
|
||||
await token.methods.decimals().call()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
export const isTokenTransfer = (tx: any): boolean => {
|
||||
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isTokenTransfer = (data, value) => !!data && data.substring(0, 10) === '0xa9059cbb' && value === 0
|
||||
export const isSendERC721Transaction = (tx: any, txCode: string, knownTokens: any) => {
|
||||
return (
|
||||
(txCode && txCode.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) ||
|
||||
(isTokenTransfer(tx) && !knownTokens.get(tx.to))
|
||||
)
|
||||
}
|
||||
|
||||
export const isMultisendTransaction = (data, value) => !!data && data.substring(0, 10) === '0x8d80ff0a' && value === 0
|
||||
export const getERC20DecimalsAndSymbol = async (
|
||||
tokenAddress: string,
|
||||
): Promise<{ decimals: number; symbol: string }> => {
|
||||
const tokenInfos = await getTokenInfos(tokenAddress)
|
||||
|
||||
// 7de7edef - changeMasterCopy (308, 8)
|
||||
// f08a0323 - setFallbackHandler (550, 8)
|
||||
export const isUpgradeTransaction = (data) =>
|
||||
!!data && data.substr(308, 8) === '7de7edef' && data.substr(550, 8) === 'f08a0323'
|
||||
if (tokenInfos === null) {
|
||||
const [tokenDecimals, tokenSymbol] = await generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
address: tokenAddress,
|
||||
methods: ['decimals', 'symbol'],
|
||||
})
|
||||
|
||||
export const isERC721Contract = async (contractAddress) => {
|
||||
return { decimals: Number(tokenDecimals), symbol: tokenSymbol }
|
||||
}
|
||||
|
||||
return { decimals: Number(tokenInfos.decimals), symbol: tokenInfos.symbol }
|
||||
}
|
||||
|
||||
export const isSendERC20Transaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode: string,
|
||||
knownTokens: any,
|
||||
): Promise<boolean> => {
|
||||
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
|
||||
|
||||
if (isSendTokenTx) {
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.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 const isERC721Contract = async (contractAddress: string): Promise<boolean> => {
|
||||
const ERC721Token = await getStandardTokenContract()
|
||||
let isERC721 = false
|
||||
|
||||
try {
|
||||
isERC721 = true
|
||||
await ERC721Token.at(contractAddress)
|
||||
} catch (error) {
|
||||
console.warn('Asset not found')
|
||||
}
|
||||
|
||||
return isERC721
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@ import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications
|
|||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||
import { updateStoredTransactionsStatus } from 'src/routes/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) => {
|
||||
|
|
|
@ -81,9 +81,9 @@ export const createSafe = (values, userAccount) => {
|
|||
|
||||
const Open = ({ addSafe, network, provider, userAccount }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showProgress, setShowProgress] = useState()
|
||||
const [showProgress, setShowProgress] = useState(false)
|
||||
const [creationTxPromise, setCreationTxPromise] = useState()
|
||||
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState()
|
||||
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<any>()
|
||||
const [safePropsFromUrl, setSafePropsFromUrl] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -182,7 +182,7 @@ const Open = ({ addSafe, network, provider, userAccount }) => {
|
|||
<Page>
|
||||
{showProgress ? (
|
||||
<Opening
|
||||
creationTxHash={safeCreationPendingInfo ? safeCreationPendingInfo.txHash : undefined}
|
||||
creationTxHash={safeCreationPendingInfo?.txHash}
|
||||
onCancel={onCancel}
|
||||
onRetry={onRetry}
|
||||
onSuccess={onSafeCreated as any}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ const BackButton = styled(Button)`
|
|||
|
||||
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stepIndex, setStepIndex] = useState()
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [safeCreationTxHash, setSafeCreationTxHash] = useState()
|
||||
const [createdSafeAddress, setCreatedSafeAddress] = useState()
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @flow
|
||||
import { ContinueFooter, GenericFooter } from './components/Footer'
|
||||
|
||||
export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0
|
||||
|
|
|
@ -69,7 +69,7 @@ const MethodsDropdown = ({ onChange }) => {
|
|||
<>
|
||||
<button className={classes.button} onClick={handleClick} type="button">
|
||||
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
|
||||
{selectedMethod.name}
|
||||
{(selectedMethod as Record<string, string>).name}
|
||||
</span>
|
||||
</button>
|
||||
<Menu
|
||||
|
@ -120,7 +120,7 @@ const MethodsDropdown = ({ onChange }) => {
|
|||
>
|
||||
<ListItemText primary={name} />
|
||||
<ListItemIcon className={classes.iconRight}>
|
||||
{signatureHash === selectedMethod.signatureHash ? (
|
||||
{signatureHash === (selectedMethod as Record<string, string>).signatureHash ? (
|
||||
<img alt="checked" src={CheckIcon} />
|
||||
) : (
|
||||
<span />
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
|
||||
import { composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
|
||||
import { Checkbox } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
type Props = {
|
||||
type: string
|
||||
keyValue: string
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props) => {
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
switch (type) {
|
||||
case 'bool': {
|
||||
const inputProps = {
|
||||
'data-testid': keyValue,
|
||||
}
|
||||
return (
|
||||
<Col>
|
||||
<Field component={Checkbox} name={keyValue} label={placeholder} type="checkbox" inputProps={inputProps} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
case 'address': {
|
||||
return (
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={keyValue}
|
||||
placeholder={placeholder}
|
||||
testId={keyValue}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={composeValidators(required, mustBeEthereumAddress)}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={keyValue}
|
||||
placeholder={placeholder}
|
||||
testId={keyValue}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={required}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default InputComponent
|
|
@ -1,16 +1,14 @@
|
|||
import React from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import { composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
import InputComponent from './InputComponent'
|
||||
|
||||
const RenderInputParams = () => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { valid: true } as any)
|
||||
} = useField('abi', { value: true })
|
||||
const {
|
||||
input: { value: method },
|
||||
}: any = useField('selectedMethod', { value: true })
|
||||
|
@ -21,21 +19,10 @@ const RenderInputParams = () => {
|
|||
: method.inputs.map(({ name, type }, index) => {
|
||||
const placeholder = name ? `${name} (${type})` : type
|
||||
const key = `methodInput-${method.name}_${index}_${type}`
|
||||
const validate = type === 'address' ? composeValidators(required, mustBeEthereumAddress) : required
|
||||
|
||||
return (
|
||||
<Row key={key} margin="sm">
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={key}
|
||||
placeholder={placeholder}
|
||||
testId={key}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={validate}
|
||||
/>
|
||||
</Col>
|
||||
<InputComponent type={type} keyValue={key} placeholder={placeholder} />
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { withSnackbar } from 'notistack'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
|
@ -22,10 +22,33 @@ import Header from 'src/routes/safe/components/Balances/SendModal/screens/Contra
|
|||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||
import { getValueFromTxInputs } from '../utils'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ContractInteractionReview = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: any) => {
|
||||
export type TransactionReviewType = {
|
||||
abi?: string
|
||||
contractAddress?: string
|
||||
data?: string
|
||||
value?: string
|
||||
selectedMethod?: {
|
||||
action: string
|
||||
signature: string
|
||||
signatureHash: string
|
||||
constant: boolean
|
||||
inputs: []
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
onPrev: () => void
|
||||
tx: TransactionReviewType
|
||||
}
|
||||
|
||||
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector)
|
||||
|
@ -118,6 +141,7 @@ const ContractInteractionReview = ({ closeSnackbar, enqueueSnackbar, onClose, on
|
|||
</Row>
|
||||
{tx.selectedMethod.inputs.map(({ name, type }, index) => {
|
||||
const key = `methodInput-${tx.selectedMethod.name}_${index}_${type}`
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
|
@ -128,7 +152,7 @@ const ContractInteractionReview = ({ closeSnackbar, enqueueSnackbar, onClose, on
|
|||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{tx[key]}
|
||||
{value}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
|
@ -173,4 +197,4 @@ const ContractInteractionReview = ({ closeSnackbar, enqueueSnackbar, onClose, on
|
|||
)
|
||||
}
|
||||
|
||||
export default withSnackbar(ContractInteractionReview as any)
|
||||
export default ContractInteractionReview
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { border, lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
|
|
|
@ -36,7 +36,7 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
|||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
||||
if (value || (contractAddress && selectedMethod)) {
|
||||
const data = await createTxObject(selectedMethod, contractAddress, values).encodeABI()
|
||||
onNext({ contractAddress, data, selectedMethod, value, ...values })
|
||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/compon
|
|||
import { getNetwork } from 'src/config'
|
||||
import { getConfiguredSource } from 'src/logic/contractInteraction/sources'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { TransactionReviewType } from '../Review'
|
||||
|
||||
export const NO_CONTRACT = 'no contract'
|
||||
|
||||
|
@ -56,3 +57,11 @@ export const createTxObject = (method, contractAddress, values) => {
|
|||
|
||||
return contract.methods[name](...args)
|
||||
}
|
||||
|
||||
export const getValueFromTxInputs = (key: string, type: string, tx: TransactionReviewType): string => {
|
||||
let value = tx[key]
|
||||
if (type === 'bool') {
|
||||
value = tx[key] ? String(tx[key]) : 'false'
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useSelector } from 'react-redux'
|
|||
import { styles } from './style'
|
||||
import { getSymbolAndDecimalsFromContract } from './utils'
|
||||
|
||||
import Checkbox from 'src/components/forms/Checkbox'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
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'
|
||||
|
@ -140,11 +140,14 @@ const AddCustomAsset = (props) => {
|
|||
text="Token decimals*"
|
||||
type="text"
|
||||
/>
|
||||
<Block justify="left">
|
||||
<Field className={classes.checkbox} component={Checkbox} name="showForAllSafes" type="checkbox" />
|
||||
<Paragraph className={classes.checkboxLabel} size="md" weight="bolder">
|
||||
Activate assets for all Safes
|
||||
</Paragraph>
|
||||
<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}>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { styles } from './style'
|
|||
import { getSymbolAndDecimalsFromContract } from './utils'
|
||||
import { addressIsTokenContract, doesntExistInTokenList } from './validators'
|
||||
|
||||
import Checkbox from 'src/components/forms/Checkbox'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
|
@ -21,6 +20,7 @@ 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'
|
||||
|
||||
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'
|
||||
|
@ -161,11 +161,14 @@ const AddCustomToken = (props) => {
|
|||
text="Token decimals*"
|
||||
type="text"
|
||||
/>
|
||||
<Block justify="left">
|
||||
<Field className={classes.checkbox} component={Checkbox} name="showForAllSafes" type="checkbox" />
|
||||
<Paragraph className={classes.checkboxLabel} size="md" weight="bolder">
|
||||
Activate token for all Safes
|
||||
</Paragraph>
|
||||
<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}>
|
||||
|
|
|
@ -51,7 +51,7 @@ const LayoutHeader = (props) => {
|
|||
className={classes.send}
|
||||
color="primary"
|
||||
disabled={!granted}
|
||||
onClick={() => showSendFunds('Ether')}
|
||||
onClick={() => showSendFunds('')}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { formatDate } from '../../columns'
|
||||
import Bold from '../../../../../../../components/layout/Bold'
|
||||
import Paragraph from '../../../../../../../components/layout/Paragraph'
|
||||
import EtherscanLink from '../../../../../../../components/EtherscanLink'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Block from '../../../../../../../components/layout/Block'
|
||||
import React from 'react'
|
||||
|
||||
import { formatDate } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
address: {
|
||||
|
@ -21,15 +22,16 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
export const CreationTx = (props) => {
|
||||
const { tx } = props
|
||||
export const CreationTx = ({ tx }) => {
|
||||
const classes = useStyles()
|
||||
if (!tx) return null
|
||||
const isCreationTx = tx.type === 'creation'
|
||||
|
||||
console.log('Classes', classes)
|
||||
if (!tx) {
|
||||
return null
|
||||
}
|
||||
|
||||
return !isCreationTx ? null : (
|
||||
const isCreationTx = tx.type === TransactionTypes.CREATION
|
||||
|
||||
return isCreationTx ? (
|
||||
<>
|
||||
<Paragraph noMargin>
|
||||
<Bold>Created: </Bold>
|
||||
|
@ -48,5 +50,5 @@ export const CreationTx = (props) => {
|
|||
{tx.masterCopy ? <EtherscanLink cut={8} type="address" value={tx.masterCopy} /> : 'n/a'}
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react'
|
||||
import { INCOMING_TX_TYPES } from '../../../../../store/models/incomingTransaction'
|
||||
import { formatDate } from '../../columns'
|
||||
import Bold from '../../../../../../../components/layout/Bold'
|
||||
import Paragraph from '../../../../../../../components/layout/Paragraph'
|
||||
import { INCOMING_TX_TYPES } from 'src/routes/safe/store/models/incomingTransaction'
|
||||
import { formatDate } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
|
||||
export const IncomingTx = ({ tx }) => {
|
||||
if (!tx) {
|
||||
return null
|
||||
}
|
||||
|
||||
export const IncomingTx = (props) => {
|
||||
const { tx } = props
|
||||
if (!tx) return null
|
||||
const isIncomingTx = !!INCOMING_TX_TYPES[tx.type]
|
||||
return !isIncomingTx ? null : (
|
||||
<>
|
||||
|
||||
return isIncomingTx ? (
|
||||
<Paragraph noMargin>
|
||||
<Bold>Created: </Bold>
|
||||
{formatDate(tx.executionDate)}
|
||||
</Paragraph>
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { formatDate } from '../../columns'
|
||||
import Bold from '../../../../../../../components/layout/Bold'
|
||||
import Paragraph from '../../../../../../../components/layout/Paragraph'
|
||||
|
||||
export const OutgoingTx = (props) => {
|
||||
const { tx } = props
|
||||
if (!tx || !(tx.type === 'outgoing')) return null
|
||||
return (
|
||||
import { formatDate } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const OutgoingTx = ({ tx }) => {
|
||||
if (!tx) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isOutgoingTx = [
|
||||
TransactionTypes.OUTGOING,
|
||||
TransactionTypes.UPGRADE,
|
||||
TransactionTypes.CUSTOM,
|
||||
TransactionTypes.SETTINGS,
|
||||
TransactionTypes.COLLECTIBLE,
|
||||
TransactionTypes.TOKEN,
|
||||
].includes(tx.type)
|
||||
|
||||
return isOutgoingTx ? (
|
||||
<>
|
||||
<Paragraph noMargin>
|
||||
<Bold>Created: </Bold>
|
||||
|
@ -36,5 +48,5 @@ export const OutgoingTx = (props) => {
|
|||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ConfirmSmallFilledCircle from './assets/confirm-small-filled.svg'
|
|||
import ConfirmSmallGreenCircle from './assets/confirm-small-green.svg'
|
||||
import ConfirmSmallGreyCircle from './assets/confirm-small-grey.svg'
|
||||
import ConfirmSmallRedCircle from './assets/confirm-small-red.svg'
|
||||
import PendingSmallYellowCircle from './assets/confirm-small-yellow.svg'
|
||||
import { styles } from './style'
|
||||
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
|
@ -32,6 +33,8 @@ const OwnerComponent = ({
|
|||
onTxExecute,
|
||||
onTxReject,
|
||||
owner,
|
||||
pendingAcceptAction,
|
||||
pendingRejectAction,
|
||||
showConfirmBtn,
|
||||
showExecuteBtn,
|
||||
showExecuteRejectBtn,
|
||||
|
@ -43,57 +46,39 @@ const OwnerComponent = ({
|
|||
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
|
||||
|
||||
React.useMemo(() => {
|
||||
if (pendingAcceptAction || pendingRejectAction) {
|
||||
setImgCircle(PendingSmallYellowCircle)
|
||||
return
|
||||
}
|
||||
if (confirmed) {
|
||||
setImgCircle(isCancelTx ? CancelSmallFilledCircle : ConfirmSmallFilledCircle)
|
||||
} else if (thresholdReached || executor) {
|
||||
setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
|
||||
return
|
||||
}
|
||||
}, [confirmed, thresholdReached, executor, isCancelTx])
|
||||
if (thresholdReached || executor) {
|
||||
setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
|
||||
return
|
||||
}
|
||||
setImgCircle(ConfirmSmallGreyCircle)
|
||||
}, [confirmed, thresholdReached, executor, isCancelTx, pendingAcceptAction, pendingRejectAction])
|
||||
|
||||
const getTimelineLine = () => (isCancelTx ? classes.verticalLineCancel : classes.verticalLineDone)
|
||||
const getTimelineLine = () => {
|
||||
if (pendingAcceptAction || pendingRejectAction) {
|
||||
return classes.verticalPendingAction
|
||||
}
|
||||
if (isCancelTx) {
|
||||
return classes.verticalLineCancel
|
||||
}
|
||||
return classes.verticalLineDone
|
||||
}
|
||||
|
||||
const confirmButton = () => {
|
||||
if (pendingRejectAction) {
|
||||
return null
|
||||
}
|
||||
if (pendingAcceptAction) {
|
||||
return <Block className={classes.executor}>Pending</Block>
|
||||
}
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<div className={cn(classes.verticalLine, (confirmed || thresholdReached || executor) && getTimelineLine())} />
|
||||
<div className={classes.circleState}>
|
||||
<Img alt="" src={imgCircle} />
|
||||
</div>
|
||||
<Identicon address={owner} className={classes.icon} diameter={32} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{nameInAdbk}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
|
||||
</Block>
|
||||
<Block className={classes.spacer} />
|
||||
{owner === userAddress && (
|
||||
<Block>
|
||||
{isCancelTx ? (
|
||||
<>
|
||||
{showRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
{showExecuteRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={EXECUTE_REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showConfirmBtn && (
|
||||
<Button
|
||||
|
@ -118,9 +103,65 @@ const OwnerComponent = ({
|
|||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const rejectButton = () => {
|
||||
if (pendingRejectAction) {
|
||||
return <Block className={classes.executor}>Pending</Block>
|
||||
}
|
||||
if (pendingAcceptAction) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{showRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
{showExecuteRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={EXECUTE_REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<div
|
||||
className={cn(
|
||||
classes.verticalLine,
|
||||
(confirmed || thresholdReached || executor || pendingAcceptAction || pendingRejectAction) &&
|
||||
getTimelineLine(),
|
||||
)}
|
||||
/>
|
||||
<div className={classes.circleState}>
|
||||
<Img alt="" src={imgCircle} />
|
||||
</div>
|
||||
<Identicon address={owner} className={classes.icon} diameter={32} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{nameInAdbk}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
|
||||
</Block>
|
||||
)}
|
||||
<Block className={classes.spacer} />
|
||||
{owner === userAddress && <Block>{isCancelTx ? rejectButton() : confirmButton()}</Block>}
|
||||
{owner === executor && <Block className={classes.executor}>Executor</Block>}
|
||||
</Block>
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ const OwnersList = ({
|
|||
userAddress={userAddress}
|
||||
/>
|
||||
))}
|
||||
{ownersUnconfirmed.map((owner) => (
|
||||
{ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
|
||||
<OwnerComponent
|
||||
classes={classes}
|
||||
executor={executor}
|
||||
|
@ -48,6 +48,8 @@ const OwnersList = ({
|
|||
onTxExecute={onTxExecute}
|
||||
onTxReject={onTxReject}
|
||||
owner={owner}
|
||||
pendingAcceptAction={hasPendingAcceptActions}
|
||||
pendingRejectAction={hasPendingRejectActions}
|
||||
showConfirmBtn={showConfirmBtn}
|
||||
showExecuteBtn={showExecuteBtn}
|
||||
showExecuteRejectBtn={showExecuteRejectBtn}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||
<circle cx="5" cy="5" r="5" fill="#C97C05" fill-rule="evenodd" />
|
||||
</svg>
|
After Width: | Height: | Size: 161 B |
|
@ -15,10 +15,10 @@ import Block from 'src/components/layout/Block'
|
|||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Paragraph from 'src/components/layout/Paragraph/index'
|
||||
import { TX_TYPE_CONFIRMATION } from 'src/logic/safe/transactions/send'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
|
||||
import { safeOwnersSelector, safeThresholdSelector } from 'src/routes/safe/store/selectors'
|
||||
import { TransactionStatus } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
function getOwnersConfirmations(tx, userAddress) {
|
||||
const ownersWhoConfirmed = []
|
||||
|
@ -29,34 +29,54 @@ function getOwnersConfirmations(tx, userAddress) {
|
|||
currentUserAlreadyConfirmed = true
|
||||
}
|
||||
|
||||
if (conf.type === TX_TYPE_CONFIRMATION) {
|
||||
ownersWhoConfirmed.push(conf.owner)
|
||||
}
|
||||
})
|
||||
|
||||
return [ownersWhoConfirmed, currentUserAlreadyConfirmed]
|
||||
}
|
||||
|
||||
function getPendingOwnersConfirmations(owners, tx, userAddress) {
|
||||
const ownersNotConfirmed = []
|
||||
const ownersWithNoConfirmations = []
|
||||
let currentUserNotConfirmed = true
|
||||
|
||||
owners.forEach((owner) => {
|
||||
const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address)
|
||||
if (!confirmationsEntry) {
|
||||
ownersNotConfirmed.push(owner.address)
|
||||
ownersWithNoConfirmations.push(owner.address)
|
||||
}
|
||||
if (confirmationsEntry && confirmationsEntry.owner === userAddress) {
|
||||
currentUserNotConfirmed = false
|
||||
}
|
||||
})
|
||||
|
||||
return [ownersNotConfirmed, currentUserNotConfirmed]
|
||||
const confirmationPendingActions = tx.ownersWithPendingActions.get('confirm')
|
||||
const confirmationRejectActions = tx.ownersWithPendingActions.get('reject')
|
||||
|
||||
const ownersWithNoConfirmationsSorted = ownersWithNoConfirmations
|
||||
.map((owner) => ({
|
||||
hasPendingAcceptActions: confirmationPendingActions.includes(owner),
|
||||
hasPendingRejectActions: confirmationRejectActions.includes(owner),
|
||||
owner,
|
||||
}))
|
||||
// Reorders the list of unconfirmed owners, owners with pendingActions should be first
|
||||
.sort((ownerA, ownerB) => {
|
||||
// If the first owner has pending actions, A should be before B
|
||||
if (ownerA.hasPendingRejectActions || ownerA.hasPendingAcceptActions) {
|
||||
return -1
|
||||
}
|
||||
// The first owner has not pending actions but the second yes, B should be before A
|
||||
if (ownerB.hasPendingRejectActions || ownerB.hasPendingAcceptActions) {
|
||||
return 1
|
||||
}
|
||||
// Otherwise do not change order
|
||||
return 0
|
||||
})
|
||||
|
||||
return [ownersWithNoConfirmationsSorted, currentUserNotConfirmed]
|
||||
}
|
||||
|
||||
const OwnersColumn = ({
|
||||
tx,
|
||||
cancelTx = makeTransaction(),
|
||||
cancelTx = makeTransaction({ isCancellationTx: true, status: TransactionStatus.AWAITING_YOUR_CONFIRMATION }),
|
||||
classes,
|
||||
thresholdReached,
|
||||
cancelThresholdReached,
|
||||
|
@ -99,6 +119,7 @@ const OwnersColumn = ({
|
|||
const showConfirmBtn =
|
||||
!tx.isExecuted &&
|
||||
tx.status !== 'pending' &&
|
||||
cancelTx.status !== 'pending' &&
|
||||
!tx.cancelled &&
|
||||
userIsUnconfirmedOwner &&
|
||||
!currentUserAlreadyConfirmed &&
|
||||
|
@ -109,6 +130,7 @@ const OwnersColumn = ({
|
|||
const showRejectBtn =
|
||||
!cancelTx.isExecuted &&
|
||||
!tx.isExecuted &&
|
||||
tx.status !== 'pending' &&
|
||||
cancelTx.status !== 'pending' &&
|
||||
userIsUnconfirmedCancelOwner &&
|
||||
!currentUserAlreadyConfirmedCancel &&
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { boldFont, border, error, primary, secondary, secondaryText, sm } from 'src/theme/variables'
|
||||
import { boldFont, border, error, primary, secondary, secondaryText, sm, warning } from 'src/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
ownersList: {
|
||||
|
@ -29,6 +29,9 @@ export const styles = () => ({
|
|||
verticalLineCancel: {
|
||||
backgroundColor: error,
|
||||
},
|
||||
verticalPendingAction: {
|
||||
backgroundColor: warning,
|
||||
},
|
||||
icon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
const getSafeVersion = (data) => {
|
||||
const contractAddress = data.substr(340, 40).toLowerCase()
|
||||
|
@ -12,43 +11,53 @@ const getSafeVersion = (data) => {
|
|||
}
|
||||
|
||||
export const getTxData = (tx) => {
|
||||
const web3 = getWeb3()
|
||||
const { fromWei, toBN } = web3.utils
|
||||
|
||||
const txData: any = {}
|
||||
|
||||
if (tx.isTokenTransfer && tx.decodedParams) {
|
||||
txData.recipient = tx.decodedParams.recipient
|
||||
txData.value = fromWei(toBN(tx.decodedParams.value), 'ether')
|
||||
if (tx.decodedParams) {
|
||||
if (tx.isTokenTransfer) {
|
||||
const { to } = tx.decodedParams.transfer
|
||||
txData.recipient = to
|
||||
txData.isTokenTransfer = true
|
||||
}
|
||||
if (tx.isCollectibleTransfer) {
|
||||
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
|
||||
const { to, value } = safeTransferFrom || transferFrom || transfer
|
||||
txData.recipient = to
|
||||
txData.tokenId = value
|
||||
txData.isCollectibleTransfer = true
|
||||
}
|
||||
if (tx.modifySettingsTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.modifySettingsTx = true
|
||||
|
||||
if (tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.REMOVE_OWNER
|
||||
txData.removedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]) {
|
||||
const { _threshold } = tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.CHANGE_THRESHOLD
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]) {
|
||||
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]
|
||||
txData.action = SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD
|
||||
txData.addedOwner = owner
|
||||
txData.newThreshold = _threshold
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]) {
|
||||
const { newOwner, oldOwner } = tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]
|
||||
txData.action = SAFE_METHODS_NAMES.SWAP_OWNER
|
||||
txData.removedOwner = oldOwner
|
||||
txData.addedOwner = newOwner
|
||||
}
|
||||
}
|
||||
} else if (tx.customTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.value = fromWei(toBN(tx.value), 'ether')
|
||||
txData.data = tx.data
|
||||
txData.customTx = true
|
||||
} else if (Number(tx.value) > 0) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.value = fromWei(toBN(tx.value), 'ether')
|
||||
} else if (tx.modifySettingsTx) {
|
||||
txData.recipient = tx.recipient
|
||||
txData.modifySettingsTx = true
|
||||
|
||||
if (tx.decodedParams) {
|
||||
txData.action = tx.decodedParams.methodName
|
||||
|
||||
if (txData.action === SAFE_METHODS_NAMES.REMOVE_OWNER) {
|
||||
txData.removedOwner = tx.decodedParams.args[1]
|
||||
txData.newThreshold = tx.decodedParams.args[2]
|
||||
} else if (txData.action === SAFE_METHODS_NAMES.CHANGE_THRESHOLD) {
|
||||
txData.newThreshold = tx.decodedParams.args[0]
|
||||
} else if (txData.action === SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD) {
|
||||
txData.addedOwner = tx.decodedParams.args[0]
|
||||
txData.newThreshold = tx.decodedParams.args[1]
|
||||
} else if (txData.action === SAFE_METHODS_NAMES.SWAP_OWNER) {
|
||||
txData.removedOwner = tx.decodedParams.args[1]
|
||||
txData.addedOwner = tx.decodedParams.args[2]
|
||||
}
|
||||
}
|
||||
} else if (tx.cancellationTx) {
|
||||
} else if (tx.isCancellationTx) {
|
||||
txData.cancellationTx = true
|
||||
} else if (tx.creationTx) {
|
||||
txData.creationTx = true
|
||||
|
@ -57,7 +66,6 @@ export const getTxData = (tx) => {
|
|||
txData.data = `The contract of this Safe is upgraded to Version ${getSafeVersion(tx.data)}`
|
||||
} else {
|
||||
txData.recipient = tx.recipient
|
||||
txData.value = 0
|
||||
}
|
||||
|
||||
return txData
|
||||
|
|
|
@ -24,6 +24,7 @@ import { safeNonceSelector, safeThresholdSelector } from 'src/routes/safe/store/
|
|||
import { IncomingTx } from './IncomingTx'
|
||||
import { CreationTx } from './CreationTx'
|
||||
import { OutgoingTx } from './OutgoingTx'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
|
||||
|
@ -35,7 +36,7 @@ const ExpandedTx = ({ cancelTx, tx }) => {
|
|||
const openApproveModal = () => setOpenModal('approveTx')
|
||||
const closeModal = () => setOpenModal(null)
|
||||
const isIncomingTx = !!INCOMING_TX_TYPES[tx.type]
|
||||
const isCreationTx = tx.type === 'creation'
|
||||
const isCreationTx = tx.type === TransactionTypes.CREATION
|
||||
|
||||
const thresholdReached = !isIncomingTx && threshold <= tx.confirmations.size
|
||||
const canExecute = !isIncomingTx && nonce === tx.nonce
|
||||
|
|
|
@ -11,6 +11,8 @@ import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/compone
|
|||
|
||||
const typeToIcon = {
|
||||
outgoing: OutgoingTxIcon,
|
||||
token: OutgoingTxIcon,
|
||||
collectible: OutgoingTxIcon,
|
||||
incoming: IncomingTxIcon,
|
||||
custom: CustomTxIcon,
|
||||
settings: SettingsTxIcon,
|
||||
|
@ -21,6 +23,8 @@ const typeToIcon = {
|
|||
|
||||
const typeToLabel = {
|
||||
outgoing: 'Outgoing transfer',
|
||||
token: 'Outgoing transfer',
|
||||
collectible: 'Outgoing transfer',
|
||||
incoming: 'Incoming transfer',
|
||||
custom: 'Contract Interaction',
|
||||
settings: 'Modify settings',
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BigNumber } from 'bignumber.js'
|
|||
import format from 'date-fns/format'
|
||||
import getTime from 'date-fns/getTime'
|
||||
import parseISO from 'date-fns/parseISO'
|
||||
import { List, Map } from 'immutable'
|
||||
import { List } from 'immutable'
|
||||
import React from 'react'
|
||||
|
||||
import TxType from './TxType'
|
||||
|
@ -43,7 +43,7 @@ export const getIncomingTxAmount = (tx, formatted = true) => {
|
|||
|
||||
export const getTxAmount = (tx, formatted = true) => {
|
||||
const { decimals = 18, decodedParams, isTokenTransfer, symbol } = tx
|
||||
const { value } = isTokenTransfer && decodedParams && decodedParams.value ? decodedParams : tx
|
||||
const { value } = isTokenTransfer && !!decodedParams && !!decodedParams.transfer ? decodedParams.transfer : tx
|
||||
|
||||
if (!isTokenTransfer && !(Number(value) > 0)) {
|
||||
return NOT_AVAILABLE
|
||||
|
@ -65,22 +65,9 @@ const getIncomingTxTableData = (tx) => ({
|
|||
const getTransactionTableData = (tx, cancelTx) => {
|
||||
const txDate = tx.submissionDate
|
||||
|
||||
let txType = 'outgoing'
|
||||
if (tx.modifySettingsTx) {
|
||||
txType = 'settings'
|
||||
} else if (tx.cancellationTx) {
|
||||
txType = 'cancellation'
|
||||
} else if (tx.customTx) {
|
||||
txType = 'custom'
|
||||
} else if (tx.creationTx) {
|
||||
txType = 'creation'
|
||||
} else if (tx.upgradeTx) {
|
||||
txType = 'upgrade'
|
||||
}
|
||||
|
||||
return {
|
||||
[TX_TABLE_ID]: tx.blockNumber,
|
||||
[TX_TABLE_TYPE_ID]: <TxType origin={tx.origin} txType={txType} />,
|
||||
[TX_TABLE_TYPE_ID]: <TxType origin={tx.origin} txType={tx.type} />,
|
||||
[TX_TABLE_DATE_ID]: txDate ? formatDate(txDate) : '',
|
||||
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null,
|
||||
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
||||
|
@ -91,17 +78,12 @@ const getTransactionTableData = (tx, cancelTx) => {
|
|||
}
|
||||
|
||||
export const getTxTableData = (transactions, cancelTxs) => {
|
||||
const cancelTxsByNonce = cancelTxs.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||
|
||||
return transactions.map((tx) => {
|
||||
if (INCOMING_TX_TYPES[tx.type]) {
|
||||
if (INCOMING_TX_TYPES[tx.type] !== undefined) {
|
||||
return getIncomingTxTableData(tx)
|
||||
}
|
||||
|
||||
return getTransactionTableData(
|
||||
tx,
|
||||
Number.isInteger(Number.parseInt(tx.nonce, 10)) ? cancelTxsByNonce.get(tx.nonce) : undefined,
|
||||
)
|
||||
return getTransactionTableData(tx, cancelTxs.get(`${tx.nonce}`))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Collapse from '@material-ui/core/Collapse'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
|
@ -18,9 +19,8 @@ import Table from 'src/components/Table'
|
|||
import { cellWidth } from 'src/components/Table/TableHead'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { extendedTransactionsSelector } from 'src/routes/safe/container/selector'
|
||||
import { safeCancellationTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { Collapse } from '@material-ui/core'
|
||||
import { extendedTransactionsSelector } from 'src/routes/safe/store/selectors/transactions'
|
||||
|
||||
export const TRANSACTION_ROW_TEST_ID = 'transaction-row'
|
||||
|
||||
|
@ -101,14 +101,11 @@ const TxsTable = ({ classes }) => {
|
|||
</Row>
|
||||
</TableCell>
|
||||
<TableCell className={classes.expandCellStyle}>
|
||||
{!row.tx.creationTx && (
|
||||
<IconButton disableRipple>
|
||||
{expandedTx === row.safeTxHash ? <ExpandLess /> : <ExpandMore />}
|
||||
{expandedTx === row.tx.safeTxHash ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{!row.tx.creationTx && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
className={classes.extendedTxContainer}
|
||||
|
@ -125,25 +122,6 @@ const TxsTable = ({ classes }) => {
|
|||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{row.tx.creationTx && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
className={classes.extendedTxContainer}
|
||||
colSpan={6}
|
||||
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||
>
|
||||
<Collapse
|
||||
component={() => (
|
||||
<ExpandedTxComponent cancelTx={row[TX_TABLE_RAW_CANCEL_TX_ID]} tx={row[TX_TABLE_RAW_TX_ID]} />
|
||||
)}
|
||||
in={expandedTx === row.tx.safeTxHash}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollect
|
|||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance'
|
||||
import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/fetchTransactions'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { TIMEOUT } from 'src/utils/constants'
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useMemo } from 'react'
|
||||
import { batch, useDispatch, useSelector } from 'react-redux'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import { fetchCurrencyValues } from 'src/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||
|
@ -8,13 +9,14 @@ import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
|||
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { history } from 'src/store'
|
||||
|
||||
export const useFetchTokens = () => {
|
||||
export const useFetchTokens = (): void => {
|
||||
const dispatch = useDispatch()
|
||||
const address = useSelector(safeParamAddressFromStateSelector)
|
||||
const address: string | null = useSelector(safeParamAddressFromStateSelector)
|
||||
const location = useLocation()
|
||||
|
||||
useMemo(() => {
|
||||
if (COINS_LOCATION_REGEX.test(history.location.pathname)) {
|
||||
if (COINS_LOCATION_REGEX.test(location.pathname)) {
|
||||
batch(() => {
|
||||
// fetch tokens there to get symbols for tokens in TXs list
|
||||
dispatch(fetchTokens())
|
||||
|
@ -23,12 +25,12 @@ export const useFetchTokens = () => {
|
|||
})
|
||||
}
|
||||
|
||||
if (COLLECTIBLES_LOCATION_REGEX.test(history.location.pathname)) {
|
||||
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
|
||||
batch(() => {
|
||||
dispatch(fetchCollectibles()).then(() => {
|
||||
dispatch(activateAssetsByBalance(address))
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [address, dispatch])
|
||||
}, [address, dispatch, location])
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
|||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/fetchTransactions'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import fetchSafeCreationTx from '../../store/actions/fetchSafeCreationTx'
|
||||
|
||||
export const useLoadSafe = (safeAddress) => {
|
||||
|
@ -16,14 +16,16 @@ export const useLoadSafe = (safeAddress) => {
|
|||
const fetchData = () => {
|
||||
if (safeAddress) {
|
||||
dispatch(fetchLatestMasterContractVersion())
|
||||
.then(() => dispatch(fetchSafe(safeAddress)))
|
||||
.then(() => {
|
||||
dispatch(fetchSafeTokens(safeAddress))
|
||||
dispatch(fetchSafe(safeAddress))
|
||||
return dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
dispatch(fetchSafeCreationTx(safeAddress))
|
||||
return dispatch(fetchTransactions(safeAddress))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return dispatch(addViewedSafe(safeAddress))
|
||||
})
|
||||
.then(() => dispatch(addViewedSafe(safeAddress)))
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { Map } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
|
@ -6,39 +6,7 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
|||
import { isUserOwner } from 'src/logic/wallets/ethAddresses'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
import {
|
||||
safeActiveTokensSelector,
|
||||
safeBalancesSelector,
|
||||
safeCancellationTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
safeSelector,
|
||||
safeTransactionsSelector,
|
||||
} from 'src/routes/safe/store/selectors'
|
||||
|
||||
const getTxStatus = (tx, userAddress, safe) => {
|
||||
let txStatus
|
||||
if (tx.executionTxHash) {
|
||||
txStatus = 'success'
|
||||
} else if (tx.cancelled) {
|
||||
txStatus = 'cancelled'
|
||||
} else if (tx.confirmations.size === safe.threshold) {
|
||||
txStatus = 'awaiting_execution'
|
||||
} else if (tx.creationTx) {
|
||||
txStatus = 'success'
|
||||
} else if (!tx.confirmations.size) {
|
||||
txStatus = 'pending'
|
||||
} else {
|
||||
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === userAddress).size === 1
|
||||
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
|
||||
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
|
||||
}
|
||||
|
||||
if (tx.isSuccessful === false) {
|
||||
txStatus = 'failed'
|
||||
}
|
||||
|
||||
return txStatus
|
||||
}
|
||||
import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
export const grantedSelector = createSelector(userAccountSelector, safeSelector, (userAccount, safe) =>
|
||||
isUserOwner(safe, userAccount),
|
||||
|
@ -76,31 +44,3 @@ export const extendedSafeTokensSelector = createSelector(
|
|||
return extendedTokens.toList()
|
||||
},
|
||||
)
|
||||
|
||||
export const extendedTransactionsSelector = createSelector(
|
||||
safeSelector,
|
||||
userAccountSelector,
|
||||
safeTransactionsSelector,
|
||||
safeCancellationTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
(safe, userAddress, transactions, cancellationTransactions, incomingTransactions) => {
|
||||
const cancellationTransactionsByNonce = cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||
const extendedTransactions = transactions.map((tx) => {
|
||||
let extendedTx = tx
|
||||
|
||||
if (!tx.isExecuted) {
|
||||
if (
|
||||
(cancellationTransactionsByNonce.get(tx.nonce) &&
|
||||
cancellationTransactionsByNonce.get(tx.nonce).get('isExecuted')) ||
|
||||
transactions.find((safeTx) => tx.nonce === safeTx.nonce && safeTx.isExecuted)
|
||||
) {
|
||||
extendedTx = tx.set('cancelled', true)
|
||||
}
|
||||
}
|
||||
|
||||
return extendedTx.set('status', getTxStatus(extendedTx, userAddress, safe))
|
||||
})
|
||||
|
||||
return List([...extendedTransactions, ...incomingTransactions])
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_CANCELLATION_TRANSACTIONS = 'ADD_CANCELLATION_TRANSACTIONS'
|
||||
|
||||
export const addCancellationTransactions = createAction(ADD_CANCELLATION_TRANSACTIONS)
|
|
@ -1,5 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||
|
||||
export const addTransactions = createAction(ADD_TRANSACTIONS)
|
|
@ -1,21 +1,82 @@
|
|||
import { push } from 'connected-react-router'
|
||||
import { List, Map } from 'immutable'
|
||||
import { batch } from 'react-redux'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { onboardUser } from 'src/components/ConnectButton'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
|
||||
import { CALL, getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
|
||||
import {
|
||||
CALL,
|
||||
getApprovalTransaction,
|
||||
getExecutionTransaction,
|
||||
SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES,
|
||||
saveTxToHistory,
|
||||
tryOffchainSigning,
|
||||
} from 'src/logic/safe/transactions'
|
||||
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gasNew'
|
||||
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
|
||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/fetchTransactions'
|
||||
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { removeCancellationTransaction } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction'
|
||||
import { removeTransaction } from 'src/routes/safe/store/actions/transactions/removeTransaction'
|
||||
import {
|
||||
generateSafeTxHash,
|
||||
mockTransaction,
|
||||
} from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
|
||||
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { makeConfirmation } from '../models/confirmation'
|
||||
import fetchTransactions from './transactions/fetchTransactions'
|
||||
import { safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { TransactionStatus } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const removeTxFromStore = (tx, safeAddress, dispatch, state) => {
|
||||
if (tx.isCancellationTx) {
|
||||
const newTxStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
|
||||
const transactions = safeTransactionsSelector(state)
|
||||
const txsToUpdate = transactions
|
||||
.filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
|
||||
.withMutations((list) => list.map((tx) => tx.set('status', newTxStatus)))
|
||||
|
||||
batch(() => {
|
||||
dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
|
||||
dispatch(removeCancellationTransaction({ safeAddress, transaction: tx }))
|
||||
})
|
||||
} else {
|
||||
dispatch(removeTransaction({ safeAddress, transaction: tx }))
|
||||
}
|
||||
}
|
||||
|
||||
export const storeTx = async (tx, safeAddress, dispatch, state) => {
|
||||
if (tx.isCancellationTx) {
|
||||
let newTxStatus: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
|
||||
|
||||
if (tx.isExecuted) {
|
||||
newTxStatus = TransactionStatus.CANCELLED
|
||||
} else if (tx.status === TransactionStatus.PENDING) {
|
||||
newTxStatus = tx.status
|
||||
}
|
||||
|
||||
const transactions = safeTransactionsSelector(state)
|
||||
const txsToUpdate = transactions
|
||||
.filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
|
||||
.withMutations((list) =>
|
||||
list.map((tx) => tx.set('status', newTxStatus).set('cancelled', newTxStatus === TransactionStatus.CANCELLED)),
|
||||
)
|
||||
|
||||
batch(() => {
|
||||
dispatch(addOrUpdateCancellationTransactions({ safeAddress, transactions: Map({ [`${tx.nonce}`]: tx }) }))
|
||||
dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
|
||||
})
|
||||
} else {
|
||||
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([tx]) }))
|
||||
}
|
||||
}
|
||||
|
||||
const createTransaction = ({
|
||||
safeAddress,
|
||||
|
@ -42,7 +103,7 @@ const createTransaction = ({
|
|||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
|
||||
const nonce = Number(await getNewTxNonce(txNonce, lastTx, safeInstance))
|
||||
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
const safeTxGas = await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation)
|
||||
|
@ -78,9 +139,6 @@ const createTransaction = ({
|
|||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
// Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library:
|
||||
// https://github.com/LedgerHQ/ledgerjs/issues/378
|
||||
// Couldn't find an issue for trezor but the error is almost the same
|
||||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
|
@ -89,11 +147,7 @@ const createTransaction = ({
|
|||
if (signature) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
signature,
|
||||
origin,
|
||||
} as any)
|
||||
await saveTxToHistory({ ...txArgs, signature, origin })
|
||||
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
@ -110,30 +164,50 @@ const createTransaction = ({
|
|||
sendParams.gas = '7000000'
|
||||
}
|
||||
|
||||
const txToMock = {
|
||||
...txArgs,
|
||||
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
|
||||
value: txArgs.valueInWei,
|
||||
safeTxHash: generateSafeTxHash(safeAddress, txArgs),
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
||||
await tx
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async (hash) => {
|
||||
try {
|
||||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
try {
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
txHash,
|
||||
origin,
|
||||
} as any)
|
||||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash, origin }),
|
||||
storeTx(
|
||||
mockedTx.updateIn(
|
||||
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
|
||||
(previous) => previous.push(from),
|
||||
),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
state,
|
||||
),
|
||||
])
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} catch (e) {
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
})
|
||||
.then((receipt) => {
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
}
|
||||
|
||||
showSnackbar(
|
||||
isExecution
|
||||
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
|
||||
|
@ -142,6 +216,30 @@ const createTransaction = ({
|
|||
closeSnackbar,
|
||||
)
|
||||
|
||||
const toStoreTx = isExecution
|
||||
? mockedTx.withMutations((record) => {
|
||||
record
|
||||
.set('executionTxHash', receipt.transactionHash)
|
||||
.set('executor', from)
|
||||
.set('isExecuted', true)
|
||||
.set('isSuccessful', receipt.status)
|
||||
.set('status', receipt.status ? 'success' : 'failed')
|
||||
})
|
||||
: mockedTx.set('status', 'awaiting_confirmations')
|
||||
|
||||
await storeTx(
|
||||
toStoreTx.withMutations((record) => {
|
||||
record
|
||||
.set('confirmations', List([makeConfirmation({ owner: from })]))
|
||||
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
|
||||
previous.pop(from),
|
||||
)
|
||||
}),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
state,
|
||||
)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
return receipt.transactionHash
|
||||
|
@ -149,7 +247,11 @@ const createTransaction = ({
|
|||
} catch (err) {
|
||||
console.error(err)
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
}
|
||||
|
||||
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
const executeDataUsedSignatures = safeInstance.contract.methods
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// @flow
|
||||
import axios from 'axios'
|
||||
import { List } from 'immutable'
|
||||
import { buildSafeCreationTxUrl } from '../../../../config'
|
||||
|
||||
import { buildSafeCreationTxUrl } from 'src/config'
|
||||
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
|
||||
import { makeTransaction } from '../models/transaction'
|
||||
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
|
||||
import { TransactionTypes, TransactionStatus } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
const getCreationTx = async (safeAddress) => {
|
||||
const url = buildSafeCreationTxUrl(safeAddress)
|
||||
|
@ -29,17 +31,21 @@ const fetchSafeCreationTx = (safeAddress) => async (dispatch) => {
|
|||
transactionHash,
|
||||
type,
|
||||
} = creationTxFetched
|
||||
const txType = type || 'creation'
|
||||
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,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,322 +0,0 @@
|
|||
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
|
||||
import axios from 'axios'
|
||||
import bn from 'bignumber.js'
|
||||
import { List, Map } from 'immutable'
|
||||
import { batch } from 'react-redux'
|
||||
|
||||
import { addIncomingTransactions } from './addIncomingTransactions'
|
||||
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { decodeParamsFromSafeMethod } from 'src/logic/contracts/methodIds'
|
||||
import { buildIncomingTxServiceUrl } from 'src/logic/safe/transactions/incomingTxHistory'
|
||||
import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
||||
import {
|
||||
SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH,
|
||||
isMultisendTransaction,
|
||||
isTokenTransfer,
|
||||
isUpgradeTransaction,
|
||||
} from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { addCancellationTransactions } from 'src/routes/safe/store/actions/addCancellationTransactions'
|
||||
import { makeConfirmation } from 'src/routes/safe/store/models/confirmation'
|
||||
import { makeIncomingTransaction } from 'src/routes/safe/store/models/incomingTransaction'
|
||||
import { addOrUpdateTransactions } from './transactions/addOrUpdateTransactions'
|
||||
import { makeTransaction } from '../models/transaction'
|
||||
|
||||
let web3
|
||||
|
||||
export const buildTransactionFrom = async (
|
||||
safeAddress,
|
||||
knownTokens,
|
||||
tx,
|
||||
txTokenCode,
|
||||
txTokenDecimals,
|
||||
txTokenName,
|
||||
txTokenSymbol,
|
||||
) => {
|
||||
const confirmations = List(
|
||||
tx.confirmations.map((conf) =>
|
||||
makeConfirmation({
|
||||
owner: conf.owner,
|
||||
type: conf.confirmationType.toLowerCase(),
|
||||
hash: conf.transactionHash,
|
||||
signature: conf.signature,
|
||||
}),
|
||||
),
|
||||
)
|
||||
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
||||
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
|
||||
const isERC721Token =
|
||||
(txTokenCode && txTokenCode.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) ||
|
||||
(isTokenTransfer(tx.data, Number(tx.value)) && !knownTokens.get(tx.to) && txTokenDecimals !== null)
|
||||
let isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value))
|
||||
const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
|
||||
const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data)
|
||||
let customTx = !sameAddress(tx.to, safeAddress) && !!tx.data && !isSendTokenTx && !isUpgradeTx && !isERC721Token
|
||||
|
||||
let refundParams = null
|
||||
if (tx.gasPrice > 0) {
|
||||
const refundSymbol = txTokenSymbol || 'ETH'
|
||||
const decimals = txTokenDecimals || 18
|
||||
const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(decimals, '0')
|
||||
const whole = feeString.slice(0, feeString.length - decimals) || '0'
|
||||
const fraction = feeString.slice(feeString.length - decimals)
|
||||
|
||||
const formattedFee = `${whole}.${fraction}`
|
||||
refundParams = {
|
||||
fee: formattedFee,
|
||||
symbol: refundSymbol,
|
||||
}
|
||||
}
|
||||
|
||||
let symbol = txTokenSymbol || 'ETH'
|
||||
let decimals = txTokenDecimals || 18
|
||||
let decodedParams
|
||||
if (isSendTokenTx) {
|
||||
if (txTokenSymbol === null || txTokenDecimals === null) {
|
||||
try {
|
||||
const [tokenSymbol, tokenDecimals] = await Promise.all(
|
||||
generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
address: tx.to,
|
||||
methods: ['symbol', 'decimals'],
|
||||
}),
|
||||
)
|
||||
|
||||
symbol = tokenSymbol
|
||||
decimals = tokenDecimals
|
||||
} catch (e) {
|
||||
// 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 = false
|
||||
customTx = true
|
||||
}
|
||||
}
|
||||
|
||||
const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10))
|
||||
decodedParams = {
|
||||
recipient: params[0],
|
||||
value: params[1],
|
||||
}
|
||||
} else if (modifySettingsTx && tx.data) {
|
||||
decodedParams = decodeParamsFromSafeMethod(tx.data)
|
||||
} else if (customTx && tx.data) {
|
||||
decodedParams = decodeParamsFromSafeMethod(tx.data)
|
||||
}
|
||||
|
||||
return makeTransaction({
|
||||
symbol,
|
||||
nonce: tx.nonce,
|
||||
blockNumber: tx.blockNumber,
|
||||
value: tx.value.toString(),
|
||||
confirmations,
|
||||
decimals,
|
||||
recipient: tx.to,
|
||||
data: tx.data ? tx.data : EMPTY_DATA,
|
||||
operation: tx.operation,
|
||||
safeTxGas: tx.safeTxGas,
|
||||
baseGas: tx.baseGas,
|
||||
gasPrice: tx.gasPrice,
|
||||
gasToken: tx.gasToken || ZERO_ADDRESS,
|
||||
refundReceiver: tx.refundReceiver || ZERO_ADDRESS,
|
||||
refundParams,
|
||||
isExecuted: tx.isExecuted,
|
||||
isSuccessful: tx.isSuccessful,
|
||||
submissionDate: tx.submissionDate,
|
||||
executor: tx.executor,
|
||||
executionDate: tx.executionDate,
|
||||
executionTxHash: tx.transactionHash,
|
||||
safeTxHash: tx.safeTxHash,
|
||||
isTokenTransfer: isSendTokenTx,
|
||||
multiSendTx: isMultiSendTx,
|
||||
upgradeTx: isUpgradeTx,
|
||||
decodedParams,
|
||||
modifySettingsTx,
|
||||
customTx,
|
||||
cancellationTx,
|
||||
creationTx: tx.creationTx,
|
||||
origin: tx.origin,
|
||||
})
|
||||
}
|
||||
|
||||
const batchRequestTxsData = (txs: any[]) => {
|
||||
const web3Batch = new web3.BatchRequest()
|
||||
|
||||
const txsTokenInfo = txs.map((tx) => {
|
||||
const methods = [{ method: 'getCode', type: 'eth', args: [tx.to] }, 'decimals', 'name', 'symbol']
|
||||
return generateBatchRequests({
|
||||
abi: ERC20Detailed.abi,
|
||||
address: tx.to,
|
||||
batch: web3Batch,
|
||||
context: tx,
|
||||
methods,
|
||||
})
|
||||
})
|
||||
|
||||
web3Batch.execute()
|
||||
|
||||
return Promise.all(txsTokenInfo)
|
||||
}
|
||||
|
||||
const batchRequestIncomingTxsData = (txs) => {
|
||||
const web3Batch = new web3.BatchRequest()
|
||||
|
||||
const whenTxsValues = txs.map((tx) => {
|
||||
const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }]
|
||||
|
||||
return generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
address: tx.tokenAddress,
|
||||
batch: web3Batch,
|
||||
context: tx,
|
||||
methods,
|
||||
})
|
||||
})
|
||||
|
||||
web3Batch.execute()
|
||||
|
||||
return Promise.all(whenTxsValues).then((txsValues) =>
|
||||
txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [
|
||||
tx,
|
||||
symbol === null ? 'ETH' : symbol,
|
||||
decimals === null ? '18' : decimals,
|
||||
new bn(gas).div(gasPrice).toFixed(),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
export const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]) => {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
let etagSafeTransactions = null
|
||||
let etagCachedSafeIncomingTransactions = null
|
||||
export const loadSafeTransactions = async (safeAddress, getState) => {
|
||||
let transactions = []
|
||||
|
||||
try {
|
||||
const config = etagSafeTransactions
|
||||
? {
|
||||
headers: {
|
||||
'If-None-Match': etagSafeTransactions,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
const url = buildTxServiceUrl(safeAddress)
|
||||
const response = await axios.get(url, config)
|
||||
if (response.data.count > 0) {
|
||||
if (etagSafeTransactions === response.headers.etag) {
|
||||
// The txs are the same, we can return the cached ones
|
||||
return
|
||||
}
|
||||
transactions = transactions.concat(response.data.results)
|
||||
etagSafeTransactions = response.headers.etag
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.response && err.response.status === 304) {
|
||||
// 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
|
||||
} else {
|
||||
console.error(`Requests for outgoing transactions for ${safeAddress} failed with 404`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const state = getState()
|
||||
const knownTokens = state[TOKEN_REDUCER_ID]
|
||||
const txsWithData = await batchRequestTxsData(transactions)
|
||||
// In case that the etags don't match, we parse the new transactions and save them to the cache
|
||||
const txsRecord = await Promise.all(
|
||||
txsWithData.map(([tx, code, decimals, name, symbol]) => {
|
||||
const knownToken = knownTokens.get(tx.to)
|
||||
|
||||
if (knownToken) {
|
||||
;({ decimals, name, symbol } = knownToken)
|
||||
}
|
||||
|
||||
return buildTransactionFrom(safeAddress, knownTokens, tx, code, decimals, name, symbol)
|
||||
}),
|
||||
)
|
||||
|
||||
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))
|
||||
|
||||
return {
|
||||
outgoing: groupedTxs.get('outgoing') || List([]),
|
||||
cancel: Map().set(safeAddress, groupedTxs.get('cancel')),
|
||||
}
|
||||
}
|
||||
|
||||
export const loadSafeIncomingTransactions = async (safeAddress) => {
|
||||
let incomingTransactions = []
|
||||
try {
|
||||
const config = etagCachedSafeIncomingTransactions
|
||||
? {
|
||||
headers: {
|
||||
'If-None-Match': etagCachedSafeIncomingTransactions,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
const url = buildIncomingTxServiceUrl(safeAddress)
|
||||
const response = await axios.get(url, config)
|
||||
if (response.data.count > 0) {
|
||||
incomingTransactions = response.data.results
|
||||
if (etagCachedSafeIncomingTransactions === response.headers.etag) {
|
||||
// The txs are the same, we can return the cached ones
|
||||
return
|
||||
}
|
||||
etagCachedSafeIncomingTransactions = response.headers.etag
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.response && err.response.status === 304) {
|
||||
// We return cached transactions
|
||||
return
|
||||
} else {
|
||||
console.error(`Requests for incoming transactions for ${safeAddress} failed with 404`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const incomingTxsWithData = await batchRequestIncomingTxsData(incomingTransactions)
|
||||
const incomingTxsRecord = incomingTxsWithData.map(buildIncomingTransactionFrom)
|
||||
return Map().set(safeAddress, List(incomingTxsRecord))
|
||||
}
|
||||
|
||||
export default (safeAddress) => async (dispatch, getState) => {
|
||||
web3 = await getWeb3()
|
||||
|
||||
const transactions = await loadSafeTransactions(safeAddress, getState)
|
||||
if (transactions) {
|
||||
const { cancel, outgoing } = transactions
|
||||
|
||||
batch(() => {
|
||||
dispatch(addCancellationTransactions(cancel))
|
||||
dispatch(addOrUpdateTransactions({ safeAddress, transactions: outgoing }))
|
||||
})
|
||||
}
|
||||
|
||||
const incomingTransactions = await loadSafeIncomingTransactions(safeAddress)
|
||||
|
||||
if (incomingTransactions) {
|
||||
dispatch(addIncomingTransactions(incomingTransactions))
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { fromJS } from 'immutable'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
|
@ -8,10 +9,16 @@ import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/lo
|
|||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/fetchTransactions'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import {
|
||||
isCancelTransaction,
|
||||
mockTransaction,
|
||||
} from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
|
||||
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { makeConfirmation } from '../models/confirmation'
|
||||
import { storeTx } from './createTransaction'
|
||||
|
||||
const processTransaction = ({
|
||||
approveAndExecute,
|
||||
|
@ -47,6 +54,7 @@ const processTransaction = ({
|
|||
let txHash
|
||||
let transaction
|
||||
const txArgs = {
|
||||
...tx.toJS(), // merge the previous tx with new data
|
||||
safeInstance,
|
||||
to: tx.recipient,
|
||||
valueInWei: tx.value,
|
||||
|
@ -76,11 +84,7 @@ const processTransaction = ({
|
|||
if (signature) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
signature,
|
||||
origin,
|
||||
} as any)
|
||||
await saveTxToHistory({ ...txArgs, signature })
|
||||
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
@ -97,6 +101,13 @@ const processTransaction = ({
|
|||
sendParams.gas = '7000000'
|
||||
}
|
||||
|
||||
const txToMock = {
|
||||
...txArgs,
|
||||
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
|
||||
value: txArgs.valueInWei,
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
||||
await transaction
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async (hash) => {
|
||||
|
@ -106,20 +117,34 @@ const processTransaction = ({
|
|||
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
try {
|
||||
await saveTxToHistory({
|
||||
...txArgs,
|
||||
txHash,
|
||||
})
|
||||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash }),
|
||||
storeTx(
|
||||
mockedTx.updateIn(
|
||||
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
|
||||
(previous) => previous.push(from),
|
||||
),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
state,
|
||||
),
|
||||
])
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} catch (e) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
await storeTx(tx, safeAddress, dispatch, state)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
storeTx(tx, safeAddress, dispatch, state)
|
||||
console.error('Processing transaction error: ', error)
|
||||
})
|
||||
.then((receipt) => {
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
}
|
||||
|
||||
showSnackbar(
|
||||
isExecution
|
||||
|
@ -128,6 +153,37 @@ const processTransaction = ({
|
|||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
|
||||
const toStoreTx = isExecution
|
||||
? mockedTx.withMutations((record) => {
|
||||
record
|
||||
.set('executionTxHash', receipt.transactionHash)
|
||||
.set('blockNumber', receipt.blockNumber)
|
||||
.set('executionDate', record.submissionDate)
|
||||
.set('executor', from)
|
||||
.set('isExecuted', true)
|
||||
.set('isSuccessful', receipt.status)
|
||||
.set(
|
||||
'status',
|
||||
receipt.status ? (isCancelTransaction(record, safeAddress) ? 'cancelled' : 'success') : 'failed',
|
||||
)
|
||||
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
|
||||
})
|
||||
: mockedTx.set('status', 'awaiting_confirmations')
|
||||
|
||||
await storeTx(
|
||||
toStoreTx.withMutations((record) => {
|
||||
record
|
||||
.set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })]))
|
||||
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
|
||||
previous.pop(from),
|
||||
)
|
||||
}),
|
||||
safeAddress,
|
||||
dispatch,
|
||||
state,
|
||||
)
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
if (isExecution) {
|
||||
|
@ -138,14 +194,21 @@ const processTransaction = ({
|
|||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
if (txHash !== undefined) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
}
|
||||
|
||||
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
|
||||
|
||||
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, from)
|
||||
console.error(`Error executing the TX: ${errMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
return txHash
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
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,4 +1,3 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_OR_UPDATE_TRANSACTIONS = 'ADD_OR_UPDATE_TRANSACTIONS'
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
|
||||
import { buildIncomingTxServiceUrl } from 'src/logic/safe/transactions/incomingTxHistory'
|
||||
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { IncomingTxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
const getServiceUrl = (txType: string, safeAddress: string): string => {
|
||||
return {
|
||||
[TransactionTypes.INCOMING]: buildIncomingTxServiceUrl,
|
||||
[TransactionTypes.OUTGOING]: buildTxServiceUrl,
|
||||
}[txType](safeAddress)
|
||||
}
|
||||
|
||||
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.INCOMING | TransactionTypes.OUTGOING,
|
||||
safeAddress: string,
|
||||
eTag: string | null,
|
||||
): Promise<{ eTag: string; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
|
||||
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
|
|
@ -0,0 +1,34 @@
|
|||
import { batch } from 'react-redux'
|
||||
|
||||
import { addIncomingTransactions } from '../../addIncomingTransactions'
|
||||
|
||||
import { loadIncomingTransactions } from './loadIncomingTransactions'
|
||||
import { loadOutgoingTransactions } from './loadOutgoingTransactions'
|
||||
|
||||
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
|
||||
const noFunc = () => {}
|
||||
|
||||
export default (safeAddress: string) => async (dispatch) => {
|
||||
const transactions = await loadOutgoingTransactions(safeAddress)
|
||||
|
||||
if (transactions) {
|
||||
const { cancel, outgoing } = transactions
|
||||
const updateCancellationTxs = cancel.size
|
||||
? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel })
|
||||
: noFunc
|
||||
const updateOutgoingTxs = outgoing.size ? addOrUpdateTransactions({ safeAddress, transactions: outgoing }) : noFunc
|
||||
|
||||
batch(() => {
|
||||
dispatch(updateCancellationTxs)
|
||||
dispatch(updateOutgoingTxs)
|
||||
})
|
||||
}
|
||||
|
||||
const incomingTransactions = await loadIncomingTransactions(safeAddress)
|
||||
|
||||
if (incomingTransactions.get(safeAddress).size) {
|
||||
dispatch(addIncomingTransactions(incomingTransactions))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import bn from 'bignumber.js'
|
||||
import { List, Map } from 'immutable'
|
||||
|
||||
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/routes/safe/store/models/incomingTransaction'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
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 whenTxsValues = txs.map((tx) => {
|
||||
const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }]
|
||||
|
||||
return generateBatchRequests({
|
||||
abi: ALTERNATIVE_TOKEN_ABI,
|
||||
address: tx.tokenAddress,
|
||||
batch,
|
||||
context: tx,
|
||||
methods,
|
||||
})
|
||||
})
|
||||
|
||||
batch.execute()
|
||||
|
||||
return Promise.all(whenTxsValues).then((txsValues) =>
|
||||
txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [
|
||||
tx,
|
||||
symbol === null ? 'ETH' : symbol,
|
||||
decimals === null ? '18' : decimals,
|
||||
new bn(gas).div(gasPrice).toFixed(),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
let previousETag = null
|
||||
export const loadIncomingTransactions = async (safeAddress: string) => {
|
||||
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) })
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import { fromJS, List, Map } from 'immutable'
|
||||
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { buildTx, isCancelTransaction } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { store } from 'src/store'
|
||||
import { DecodedMethods } from 'src/logic/contracts/methodIds'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions/fetchTransactions'
|
||||
import { TransactionTypes } from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export type ConfirmationServiceModel = {
|
||||
owner: string
|
||||
submissionDate: Date
|
||||
signature: string
|
||||
transactionHash: string
|
||||
}
|
||||
|
||||
export type TxServiceModel = {
|
||||
baseGas: number
|
||||
blockNumber?: number | null
|
||||
confirmations: ConfirmationServiceModel[]
|
||||
creationTx?: boolean | null
|
||||
data?: string | null
|
||||
dataDecoded?: DecodedMethods
|
||||
executionDate?: string | null
|
||||
executor: string
|
||||
gasPrice: number
|
||||
gasToken: string
|
||||
isExecuted: boolean
|
||||
isSuccessful: boolean
|
||||
nonce?: number | null
|
||||
operation: number
|
||||
origin?: string | null
|
||||
refundReceiver: string
|
||||
safeTxGas: number
|
||||
safeTxHash: string
|
||||
submissionDate?: string | null
|
||||
to: string
|
||||
transactionHash?: string | null
|
||||
value: number
|
||||
}
|
||||
|
||||
export type SafeTransactionsType = {
|
||||
cancel: any
|
||||
outgoing: any
|
||||
}
|
||||
|
||||
export type OutgoingTxs = {
|
||||
cancellationTxs: any
|
||||
outgoingTxs: any
|
||||
}
|
||||
|
||||
export type BatchProcessTxsProps = OutgoingTxs & {
|
||||
currentUser?: string
|
||||
knownTokens: any
|
||||
safe: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, transaction) => {
|
||||
if (isCancelTransaction(transaction, safeAddress)) {
|
||||
if (!isNaN(Number(transaction.nonce))) {
|
||||
acc.cancellationTxs[transaction.nonce] = transaction
|
||||
}
|
||||
} else {
|
||||
acc.outgoingTxs = [...acc.outgoingTxs, transaction]
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{
|
||||
cancellationTxs: {},
|
||||
outgoingTxs: [],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: any[]): Promise<any[]> => {
|
||||
if (!transactions || !Array.isArray(transactions)) {
|
||||
throw new Error('`transactions` must be provided in order to lookup information')
|
||||
}
|
||||
|
||||
const batch = new web3ReadOnly.BatchRequest()
|
||||
|
||||
const whenTxsValues = transactions.map((tx) => {
|
||||
return generateBatchRequests({
|
||||
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,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
}: BatchProcessTxsProps): Promise<{
|
||||
cancel: any
|
||||
outgoing: any
|
||||
}> => {
|
||||
// cancellation transactions
|
||||
const cancelTxsValues = Object.values(cancellationTxs)
|
||||
const cancellationTxsWithData = cancelTxsValues.length ? await batchRequestContractCode(cancelTxsValues) : []
|
||||
|
||||
const cancel = {}
|
||||
for (const [tx, txCode] of cancellationTxsWithData) {
|
||||
cancel[`${tx.nonce}`] = await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
})
|
||||
}
|
||||
|
||||
// outgoing transactions
|
||||
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
|
||||
|
||||
const outgoing = []
|
||||
for (const [tx, txCode] of outgoingTxsWithData) {
|
||||
outgoing.push(
|
||||
await buildTx({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return { cancel, outgoing }
|
||||
}
|
||||
|
||||
let previousETag = null
|
||||
export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
|
||||
const defaultResponse = {
|
||||
cancel: Map(),
|
||||
outgoing: List(),
|
||||
}
|
||||
const state = store.getState()
|
||||
|
||||
if (!safeAddress) {
|
||||
return defaultResponse
|
||||
}
|
||||
|
||||
const knownTokens = state[TOKEN_REDUCER_ID]
|
||||
const currentUser = state[PROVIDER_REDUCER_ID].get('account')
|
||||
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, 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,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
})
|
||||
|
||||
return {
|
||||
cancel: fromJS(cancel),
|
||||
outgoing: fromJS(outgoing),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const REMOVE_CANCELLATION_TRANSACTION = 'REMOVE_CANCELLATION_TRANSACTION'
|
||||
|
||||
export const removeCancellationTransaction = createAction(REMOVE_CANCELLATION_TRANSACTION)
|
|
@ -0,0 +1,5 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const REMOVE_TRANSACTION = 'REMOVE_TRANSACTION'
|
||||
|
||||
export const removeTransaction = createAction(REMOVE_TRANSACTION)
|
|
@ -0,0 +1,27 @@
|
|||
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
|
|
@ -0,0 +1,377 @@
|
|||
import { List, Map } from 'immutable'
|
||||
|
||||
import { DecodedMethods, decodeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import {
|
||||
getERC20DecimalsAndSymbol,
|
||||
isSendERC20Transaction,
|
||||
isSendERC721Transaction,
|
||||
} from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { makeConfirmation } from 'src/routes/safe/store/models/confirmation'
|
||||
import { Confirmation } from 'src/routes/safe/store/models/types/confirmation'
|
||||
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
|
||||
import {
|
||||
Transaction,
|
||||
TransactionStatus,
|
||||
TransactionStatusValues,
|
||||
TransactionTypes,
|
||||
TransactionTypeValues,
|
||||
TxArgs,
|
||||
} from 'src/routes/safe/store/models/types/transaction'
|
||||
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
|
||||
import { store } from 'src/store'
|
||||
import { safeSelector, safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { TypedDataUtils } from 'eth-sig-util'
|
||||
|
||||
export const isEmptyData = (data?: string | null): boolean => {
|
||||
return !data || data === EMPTY_DATA
|
||||
}
|
||||
|
||||
export const isInnerTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
return sameAddress(tx.to, safeAddress) && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isCancelTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
return isInnerTransaction(tx, safeAddress) && isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isPendingTransaction = (tx: Transaction, cancelTx: Transaction): boolean => {
|
||||
return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending'
|
||||
}
|
||||
|
||||
export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isMultiSendTransaction = (tx: TxServiceModel): boolean => {
|
||||
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
|
||||
return (
|
||||
!isEmptyData(tx.data) &&
|
||||
isMultiSendTransaction(tx) &&
|
||||
tx.data.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8)
|
||||
tx.data.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8)
|
||||
)
|
||||
}
|
||||
|
||||
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isCustomTransaction = async (
|
||||
tx: TxServiceModel,
|
||||
txCode: string,
|
||||
safeAddress: string,
|
||||
knownTokens: any,
|
||||
): Promise<boolean> => {
|
||||
return (
|
||||
isOutgoingTransaction(tx, safeAddress) &&
|
||||
!(await isSendERC20Transaction(tx, txCode, knownTokens)) &&
|
||||
!isUpgradeTransaction(tx) &&
|
||||
!isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
)
|
||||
}
|
||||
|
||||
export const getRefundParams = async (
|
||||
tx: any,
|
||||
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
|
||||
): Promise<any> => {
|
||||
let refundParams = null
|
||||
|
||||
if (tx.gasPrice > 0) {
|
||||
let refundSymbol = 'ETH'
|
||||
let refundDecimals = 18
|
||||
|
||||
if (tx.gasToken !== ZERO_ADDRESS) {
|
||||
const gasToken = await tokenInfo(tx.gasToken)
|
||||
|
||||
if (gasToken !== null) {
|
||||
refundSymbol = gasToken.symbol
|
||||
refundDecimals = gasToken.decimals
|
||||
}
|
||||
}
|
||||
|
||||
const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(refundDecimals, '0')
|
||||
const whole = feeString.slice(0, feeString.length - refundDecimals) || '0'
|
||||
const fraction = feeString.slice(feeString.length - refundDecimals)
|
||||
|
||||
refundParams = {
|
||||
fee: `${whole}.${fraction}`,
|
||||
symbol: refundSymbol,
|
||||
}
|
||||
}
|
||||
|
||||
return refundParams
|
||||
}
|
||||
|
||||
export const getDecodedParams = (tx: TxServiceModel): DecodedMethods => {
|
||||
if (tx.dataDecoded) {
|
||||
return Object.keys(tx.dataDecoded).reduce((acc, key) => {
|
||||
acc[key] = {
|
||||
...tx.dataDecoded[key].reduce(
|
||||
(acc, param) => ({
|
||||
...acc,
|
||||
[param.name]: param.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const getConfirmations = (tx: TxServiceModel): List<Confirmation> => {
|
||||
return List(
|
||||
tx.confirmations.map((conf) =>
|
||||
makeConfirmation({
|
||||
owner: conf.owner,
|
||||
hash: conf.transactionHash,
|
||||
signature: conf.signature,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const isTransactionCancelled = (
|
||||
tx: TxServiceModel,
|
||||
outgoingTxs: Array<TxServiceModel>,
|
||||
cancellationTxs: { number: TxServiceModel },
|
||||
): 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 }: any,
|
||||
currentUser?: string | null,
|
||||
): TransactionStatusValues => {
|
||||
let txStatus
|
||||
|
||||
if (tx.isExecuted && tx.isSuccessful) {
|
||||
txStatus = TransactionStatus.SUCCESS
|
||||
} else if (tx.cancelled) {
|
||||
txStatus = TransactionStatus.CANCELLED
|
||||
} else if (tx.confirmations.size === threshold) {
|
||||
txStatus = TransactionStatus.AWAITING_EXECUTION
|
||||
} else if (tx.creationTx) {
|
||||
txStatus = TransactionStatus.SUCCESS
|
||||
} else if (!tx.confirmations.size || !!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 const buildTx = async ({
|
||||
cancellationTxs,
|
||||
currentUser,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx,
|
||||
txCode,
|
||||
}): Promise<Transaction> => {
|
||||
const safeAddress = safe.address
|
||||
const isModifySettingsTx = isModifySettingsTransaction(tx, safeAddress)
|
||||
const isTxCancelled = isTransactionCancelled(tx, outgoingTxs, cancellationTxs)
|
||||
const isSendERC721Tx = isSendERC721Transaction(tx, txCode, knownTokens)
|
||||
const isSendERC20Tx = await isSendERC20Transaction(tx, txCode, knownTokens)
|
||||
const isMultiSendTx = isMultiSendTransaction(tx)
|
||||
const isUpgradeTx = isUpgradeTransaction(tx)
|
||||
const isCustomTx = await isCustomTransaction(tx, txCode, safeAddress, knownTokens)
|
||||
const isCancellationTx = isCancelTransaction(tx, safeAddress)
|
||||
const refundParams = await getRefundParams(tx, getERC20DecimalsAndSymbol)
|
||||
const decodedParams = getDecodedParams(tx)
|
||||
const confirmations = getConfirmations(tx)
|
||||
const { decimals = 18, symbol = 'ETH' } = isSendERC20Tx ? await getERC20DecimalsAndSymbol(tx.to) : {}
|
||||
|
||||
const txToStore: Transaction = makeTransaction({
|
||||
baseGas: tx.baseGas,
|
||||
blockNumber: tx.blockNumber,
|
||||
cancelled: isTxCancelled,
|
||||
confirmations,
|
||||
creationTx: tx.creationTx,
|
||||
customTx: isCustomTx,
|
||||
data: tx.data ? tx.data : EMPTY_DATA,
|
||||
decimals,
|
||||
decodedParams,
|
||||
executionDate: tx.executionDate,
|
||||
executionTxHash: tx.transactionHash,
|
||||
executor: tx.executor,
|
||||
gasPrice: tx.gasPrice,
|
||||
gasToken: tx.gasToken || ZERO_ADDRESS,
|
||||
isCancellationTx,
|
||||
isCollectibleTransfer: isSendERC721Tx,
|
||||
isExecuted: tx.isExecuted,
|
||||
isSuccessful: tx.isSuccessful,
|
||||
isTokenTransfer: isSendERC20Tx,
|
||||
modifySettingsTx: isModifySettingsTx,
|
||||
multiSendTx: isMultiSendTx,
|
||||
nonce: tx.nonce,
|
||||
operation: tx.operation,
|
||||
origin: tx.origin,
|
||||
recipient: tx.to,
|
||||
refundParams,
|
||||
refundReceiver: tx.refundReceiver || ZERO_ADDRESS,
|
||||
safeTxGas: tx.safeTxGas,
|
||||
safeTxHash: tx.safeTxHash,
|
||||
submissionDate: tx.submissionDate,
|
||||
symbol,
|
||||
upgradeTx: isUpgradeTx,
|
||||
value: tx.value.toString(),
|
||||
})
|
||||
|
||||
return txToStore
|
||||
.set('status', calculateTransactionStatus(txToStore, safe, currentUser))
|
||||
.set('type', calculateTransactionType(txToStore))
|
||||
}
|
||||
|
||||
export const mockTransaction = (tx, safeAddress: string, state): Promise<any> => {
|
||||
const submissionDate = new Date().toISOString()
|
||||
|
||||
const transactionStructure: TxServiceModel = {
|
||||
blockNumber: null,
|
||||
confirmationsRequired: null,
|
||||
dataDecoded: decodeMethods(tx.data),
|
||||
ethGasPrice: null,
|
||||
executionDate: null,
|
||||
executor: null,
|
||||
fee: null,
|
||||
gasUsed: null,
|
||||
isExecuted: false,
|
||||
isSuccessful: null,
|
||||
modified: submissionDate,
|
||||
origin: null,
|
||||
safe: safeAddress,
|
||||
safeTxHash: null,
|
||||
signatures: null,
|
||||
submissionDate,
|
||||
transactionHash: null,
|
||||
confirmations: [],
|
||||
...tx,
|
||||
}
|
||||
|
||||
const knownTokens = state[TOKEN_REDUCER_ID]
|
||||
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
|
||||
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
|
||||
const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List()
|
||||
|
||||
return buildTx({
|
||||
cancellationTxs,
|
||||
currentUser: null,
|
||||
knownTokens,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx: transactionStructure,
|
||||
txCode: EMPTY_DATA,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateStoredTransactionsStatus = (dispatch, walletRecord): 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' }],
|
||||
SafeTx: [
|
||||
{ type: 'address', name: 'to' },
|
||||
{ type: 'uint256', name: 'value' },
|
||||
{ type: 'bytes', name: 'data' },
|
||||
{ type: 'uint8', name: 'operation' },
|
||||
{ type: 'uint256', name: 'safeTxGas' },
|
||||
{ type: 'uint256', name: 'baseGas' },
|
||||
{ type: 'uint256', name: 'gasPrice' },
|
||||
{ type: 'address', name: 'gasToken' },
|
||||
{ type: 'address', name: 'refundReceiver' },
|
||||
{ type: 'uint256', name: 'nonce' },
|
||||
],
|
||||
}
|
||||
const primaryType: 'SafeTx' = 'SafeTx'
|
||||
|
||||
const typedData = {
|
||||
types: messageTypes,
|
||||
domain: {
|
||||
verifyingContract: safeAddress,
|
||||
},
|
||||
primaryType,
|
||||
message: {
|
||||
to: txArgs.to,
|
||||
value: txArgs.valueInWei,
|
||||
data: txArgs.data,
|
||||
operation: txArgs.operation,
|
||||
safeTxGas: txArgs.safeTxGas,
|
||||
baseGas: txArgs.baseGas,
|
||||
gasPrice: txArgs.gasPrice,
|
||||
gasToken: txArgs.gasToken,
|
||||
refundReceiver: txArgs.refundReceiver,
|
||||
nonce: txArgs.nonce,
|
||||
},
|
||||
}
|
||||
|
||||
return `0x${TypedDataUtils.sign<typeof messageTypes>(typedData).toString('hex')}`
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { push } from 'connected-react-router'
|
||||
import { List, Map } from 'immutable'
|
||||
|
||||
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
|
@ -12,11 +11,15 @@ import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/Txs
|
|||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { ADD_INCOMING_TRANSACTIONS } from 'src/routes/safe/store/actions/addIncomingTransactions'
|
||||
import { ADD_SAFE } from 'src/routes/safe/store/actions/addSafe'
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
|
||||
import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/routes/safe/store/selectors'
|
||||
import {
|
||||
safeParamAddressFromStateSelector,
|
||||
safesMapSelector,
|
||||
safeCancellationTransactionsSelector,
|
||||
} from 'src/routes/safe/store/selectors'
|
||||
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from '../actions/transactions/addOrUpdateTransactions'
|
||||
|
||||
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_SAFE]
|
||||
|
||||
|
@ -59,48 +62,41 @@ const sendAwaitingTransactionNotification = async (
|
|||
await saveToStorage(LAST_TIME_USED_LOGGED_IN_ID, lastTimeUserLoggedInForSafes)
|
||||
}
|
||||
|
||||
const onNotificationClicked = (dispatch, notificationKey, safeAddress) => () => {
|
||||
dispatch(closeSnackbarAction({ key: notificationKey }))
|
||||
dispatch(push(`/safes/${safeAddress}/transactions`))
|
||||
}
|
||||
|
||||
const notificationsMiddleware = (store) => (next) => async (action) => {
|
||||
const handledAction = next(action)
|
||||
const { dispatch } = store
|
||||
|
||||
if (watchedActions.includes(action.type)) {
|
||||
const state = store.getState()
|
||||
|
||||
switch (action.type) {
|
||||
case ADD_OR_UPDATE_TRANSACTIONS: {
|
||||
const { safeAddress, transactions } = action.payload
|
||||
const userAddress: string = userAccountSelector(state)
|
||||
const cancellationTransactions = state.cancellationTransactions.get(safeAddress)
|
||||
const cancellationTransactionsByNonce = cancellationTransactions
|
||||
? cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
|
||||
: Map()
|
||||
|
||||
const awaitingTransactions = getAwaitingTransactions(
|
||||
Map().set(safeAddress, transactions),
|
||||
cancellationTransactionsByNonce,
|
||||
userAddress,
|
||||
)
|
||||
const awaitingTxsSubmissionDateList = awaitingTransactions
|
||||
.get(safeAddress, List([]))
|
||||
.map((tx) => tx.submissionDate)
|
||||
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 (!isUserOwner(currentSafe, userAddress) || awaitingTxsSubmissionDateList.size === 0) {
|
||||
if (!isUserOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const notificationKey = `${safeAddress}-awaiting`
|
||||
const onNotificationClicked = () => {
|
||||
dispatch(closeSnackbarAction({ key: notificationKey }))
|
||||
dispatch(push(`/safes/${safeAddress}/transactions`))
|
||||
}
|
||||
|
||||
await sendAwaitingTransactionNotification(
|
||||
dispatch,
|
||||
safeAddress,
|
||||
awaitingTxsSubmissionDateList,
|
||||
notificationKey,
|
||||
onNotificationClicked,
|
||||
onNotificationClicked(dispatch, notificationKey, safeAddress),
|
||||
)
|
||||
|
||||
break
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Record } from 'immutable'
|
||||
import { ConfirmationProps } from './types/confirmation'
|
||||
|
||||
export const makeConfirmation = Record({
|
||||
export const makeConfirmation = Record<ConfirmationProps>({
|
||||
owner: '',
|
||||
type: 'initialised',
|
||||
hash: '',
|
||||
|
|
|
@ -1,48 +1,54 @@
|
|||
import { List, Record } from 'immutable'
|
||||
import { List, Map, Record } from 'immutable'
|
||||
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import {
|
||||
TransactionProps,
|
||||
PendingActionType,
|
||||
TransactionStatus,
|
||||
TransactionTypes,
|
||||
} from 'src/routes/safe/store/models/types/transaction'
|
||||
|
||||
export const OUTGOING_TX_TYPE = 'outgoing'
|
||||
|
||||
export const makeTransaction = Record({
|
||||
nonce: 0,
|
||||
blockNumber: 0,
|
||||
value: 0,
|
||||
confirmations: List([]),
|
||||
recipient: '',
|
||||
data: null,
|
||||
operation: 0,
|
||||
safeTxGas: 0,
|
||||
export const makeTransaction = Record<TransactionProps>({
|
||||
baseGas: 0,
|
||||
gasPrice: 0,
|
||||
gasToken: ZERO_ADDRESS,
|
||||
refundReceiver: ZERO_ADDRESS,
|
||||
isExecuted: false,
|
||||
isSuccessful: true,
|
||||
submissionDate: '',
|
||||
executor: '',
|
||||
executionDate: '',
|
||||
symbol: '',
|
||||
executionTxHash: undefined,
|
||||
safeTxHash: '',
|
||||
blockNumber: 0,
|
||||
cancelled: false,
|
||||
modifySettingsTx: false,
|
||||
cancellationTx: false,
|
||||
customTx: false,
|
||||
creationTx: false,
|
||||
multiSendTx: false,
|
||||
upgradeTx: false,
|
||||
status: 'awaiting',
|
||||
decimals: 18,
|
||||
isTokenTransfer: false,
|
||||
decodedParams: {},
|
||||
refundParams: null,
|
||||
type: 'outgoing',
|
||||
origin: null,
|
||||
confirmations: List([]),
|
||||
created: false,
|
||||
creator: '',
|
||||
creationTx: false,
|
||||
customTx: false,
|
||||
data: null,
|
||||
decimals: 18,
|
||||
decodedParams: {},
|
||||
executionDate: '',
|
||||
executionTxHash: undefined,
|
||||
executor: '',
|
||||
factoryAddress: '',
|
||||
gasPrice: 0,
|
||||
gasToken: ZERO_ADDRESS,
|
||||
isCancellationTx: false,
|
||||
isCollectibleTransfer: false,
|
||||
isExecuted: 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',
|
||||
})
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { RecordOf } from 'immutable'
|
||||
|
||||
export type ConfirmationProps = {
|
||||
owner: string
|
||||
type: string
|
||||
hash: string
|
||||
signature: string | null
|
||||
}
|
||||
|
||||
export type Confirmation = RecordOf<ConfirmationProps>
|
|
@ -0,0 +1,93 @@
|
|||
export enum TransactionTypes {
|
||||
INCOMING = 'incoming',
|
||||
OUTGOING = 'outgoing',
|
||||
SETTINGS = 'settings',
|
||||
CUSTOM = 'custom',
|
||||
CREATION = 'creation',
|
||||
CANCELLATION = 'cancellation',
|
||||
UPGRADE = 'upgrade',
|
||||
TOKEN = 'token',
|
||||
COLLECTIBLE = 'collectible',
|
||||
}
|
||||
export type TransactionTypeValues = typeof TransactionTypes[keyof typeof TransactionTypes]
|
||||
|
||||
export enum TransactionStatus {
|
||||
AWAITING_YOUR_CONFIRMATION = 'awaiting_your_confirmation',
|
||||
AWAITING_CONFIRMATIONS = 'awaiting_confirmations',
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed',
|
||||
CANCELLED = 'cancelled',
|
||||
AWAITING_EXECUTION = 'awaiting_execution',
|
||||
PENDING = 'pending',
|
||||
}
|
||||
export type TransactionStatusValues = typeof TransactionStatus[keyof typeof TransactionStatus]
|
||||
|
||||
export enum PendingActionType {
|
||||
CONFIRM = 'confirm',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
export type PendingActionValues = PendingActionType[keyof PendingActionType]
|
||||
|
||||
export type TransactionProps = {
|
||||
baseGas: number
|
||||
blockNumber?: number | null
|
||||
cancelled?: boolean
|
||||
confirmations: import('immutable').List<any>
|
||||
created: boolean
|
||||
creator: string
|
||||
creationTx: boolean
|
||||
customTx: boolean
|
||||
data?: string | null
|
||||
decimals?: (number | string) | null
|
||||
decodedParams: import('src/logic/contracts/methodIds').DecodedMethods
|
||||
executionDate?: string | null
|
||||
executionTxHash?: string | null
|
||||
executor: string
|
||||
factoryAddress: string
|
||||
gasPrice: number
|
||||
gasToken: string
|
||||
isCancellationTx: boolean
|
||||
isCollectibleTransfer: boolean
|
||||
isExecuted: boolean
|
||||
isPending?: boolean
|
||||
isSuccessful: boolean
|
||||
isTokenTransfer: boolean
|
||||
masterCopy: string
|
||||
modifySettingsTx: boolean
|
||||
multiSendTx: boolean
|
||||
nonce?: number | null
|
||||
operation: number
|
||||
origin: string | null
|
||||
ownersWithPendingActions: import('immutable').Map<PendingActionValues, import('immutable').List<any>>
|
||||
recipient: string
|
||||
refundParams: any
|
||||
refundReceiver: string
|
||||
safeTxGas: number
|
||||
safeTxHash: string
|
||||
setupData: string
|
||||
status?: TransactionStatus
|
||||
submissionDate?: string | null
|
||||
symbol?: string | null
|
||||
transactionHash: string
|
||||
type: TransactionTypes
|
||||
upgradeTx: boolean
|
||||
value: string
|
||||
}
|
||||
|
||||
export type Transaction = import('immutable').RecordOf<TransactionProps>
|
||||
|
||||
export type TxArgs = {
|
||||
data: any
|
||||
baseGas: number
|
||||
gasToken: string
|
||||
safeInstance: any
|
||||
nonce: number
|
||||
valueInWei: any
|
||||
safeTxGas: number
|
||||
refundReceiver: string
|
||||
sender: any
|
||||
sigs: string
|
||||
to: any
|
||||
operation: any
|
||||
gasPrice: number
|
||||
}
|
|
@ -1,13 +1,57 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_CANCELLATION_TRANSACTIONS } from 'src/routes/safe/store/actions/addCancellationTransactions'
|
||||
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction'
|
||||
|
||||
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ADD_CANCELLATION_TRANSACTIONS]: (state, action) => action.payload,
|
||||
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => {
|
||||
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) => {
|
||||
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,42 +1,69 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_TRANSACTIONS } from 'src/routes/safe/store/actions/addTransactions'
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from '../actions/transactions/addOrUpdateTransactions'
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { REMOVE_TRANSACTION } from 'src/routes/safe/store/actions/transactions/removeTransaction'
|
||||
|
||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ADD_TRANSACTIONS]: (state, action) => action.payload,
|
||||
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action) => {
|
||||
const { safeAddress, transactions } = action.payload
|
||||
|
||||
if (!safeAddress || !transactions) {
|
||||
if (!safeAddress || !transactions || !transactions.size) {
|
||||
return state
|
||||
}
|
||||
|
||||
const newState = state.withMutations((map) => {
|
||||
return state.withMutations((map) => {
|
||||
const stateTransactionsList = map.get(safeAddress)
|
||||
|
||||
if (stateTransactionsList) {
|
||||
let newTxList
|
||||
const txsToStore = stateTransactionsList.withMutations((txsList) => {
|
||||
transactions.forEach((updateTx) => {
|
||||
const txIndex = stateTransactionsList.findIndex((txIterator) => txIterator.nonce === updateTx.nonce)
|
||||
if (txIndex !== -1) {
|
||||
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.nonce === updateTx.nonce)
|
||||
|
||||
if (storedTxIndex !== -1) {
|
||||
// Update
|
||||
newTxList = stateTransactionsList.update(txIndex, (oldTx) => oldTx.merge(updateTx))
|
||||
map.set(safeAddress, newTxList)
|
||||
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
|
||||
map.update(safeAddress, (oldTxList) => oldTxList.merge(List([updateTx])))
|
||||
txsList.unshift(updateTx)
|
||||
}
|
||||
})
|
||||
})
|
||||
map.set(safeAddress, txsToStore)
|
||||
} else {
|
||||
map.set(safeAddress, transactions)
|
||||
}
|
||||
})
|
||||
},
|
||||
[REMOVE_TRANSACTION]: (state, action) => {
|
||||
const { safeAddress, transaction } = action.payload
|
||||
|
||||
return newState
|
||||
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,4 +1,4 @@
|
|||
import { List, Set } from 'immutable'
|
||||
import { List, Map, Set } from 'immutable'
|
||||
import { matchPath } from 'react-router-dom'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
|
@ -32,7 +32,7 @@ const cancellationTransactionsSelector = (state) => state[CANCELLATION_TRANSACTI
|
|||
|
||||
const incomingTransactionsSelector = (state) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
export const safeParamAddressFromStateSelector = (state) => {
|
||||
export const safeParamAddressFromStateSelector = (state): string | null => {
|
||||
const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||
|
||||
if (match) {
|
||||
|
@ -60,7 +60,7 @@ export const safeTransactionsSelector = createSelector(
|
|||
return List([])
|
||||
}
|
||||
|
||||
return transactions.get(address) || List([])
|
||||
return transactions.get(address, List([]))
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -79,14 +79,14 @@ export const safeCancellationTransactionsSelector = createSelector(
|
|||
safeParamAddressFromStateSelector,
|
||||
(cancellationTransactions, address) => {
|
||||
if (!cancellationTransactions) {
|
||||
return List([])
|
||||
return Map()
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return List([])
|
||||
return Map()
|
||||
}
|
||||
|
||||
return cancellationTransactions.get(address) || List([])
|
||||
return cancellationTransactions.get(address, Map({}))
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -102,7 +102,7 @@ export const safeIncomingTransactionsSelector = createSelector(
|
|||
return List([])
|
||||
}
|
||||
|
||||
return incomingTransactions.get(address) || List([])
|
||||
return incomingTransactions.get(address, List([]))
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -167,37 +167,23 @@ export const safeBalancesSelector = createSelector(safeSelector, (safe) => {
|
|||
return safe.balances
|
||||
})
|
||||
|
||||
export const safeNameSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.name : undefined
|
||||
})
|
||||
export const safeFieldSelector = (field) => (safe) => safe?.[field]
|
||||
|
||||
export const safeEthBalanceSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.ethBalance : undefined
|
||||
})
|
||||
export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))
|
||||
|
||||
export const safeNeedsUpdateSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.needsUpdate : undefined
|
||||
})
|
||||
export const safeEthBalanceSelector = createSelector(safeSelector, safeFieldSelector('ethBalance'))
|
||||
|
||||
export const safeCurrentVersionSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.currentVersion : undefined
|
||||
})
|
||||
export const safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate'))
|
||||
|
||||
export const safeThresholdSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.threshold : undefined
|
||||
})
|
||||
export const safeCurrentVersionSelector = createSelector(safeSelector, safeFieldSelector('currentVersion'))
|
||||
|
||||
export const safeNonceSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.nonce : undefined
|
||||
})
|
||||
export const safeThresholdSelector = createSelector(safeSelector, safeFieldSelector('threshold'))
|
||||
|
||||
export const safeOwnersSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.owners : undefined
|
||||
})
|
||||
export const safeNonceSelector = createSelector(safeSelector, safeFieldSelector('nonce'))
|
||||
|
||||
export const safeFeaturesEnabledSelector = createSelector(safeSelector, (safe) => {
|
||||
return safe ? safe.featuresEnabled : undefined
|
||||
})
|
||||
export const safeOwnersSelector = createSelector(safeSelector, safeFieldSelector('owners'))
|
||||
|
||||
export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFieldSelector('featuresEnabled'))
|
||||
|
||||
export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {
|
||||
const addresses = Set().withMutations((set) => {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { List } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
export const extendedTransactionsSelector = createSelector(
|
||||
safeTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
(transactions, incomingTransactions) => List([...transactions, ...incomingTransactions]),
|
||||
)
|
|
@ -38,9 +38,9 @@ const finalCreateStore = composeEnhancers(
|
|||
applyMiddleware(
|
||||
thunk,
|
||||
routerMiddleware(history),
|
||||
notificationsMiddleware,
|
||||
safeStorage,
|
||||
providerWatcher,
|
||||
notificationsMiddleware,
|
||||
addressBookMiddleware,
|
||||
currencyValuesStorageMiddleware,
|
||||
),
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
//
|
||||
import * as React from 'react'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { } from 'redux'
|
||||
import { Provider } from 'react-redux'
|
||||
import { render } from '@testing-library/react'
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import PageFrame from 'src/components/layout/PageFrame'
|
||||
import ListItemText from 'src/components/List/ListItemText/index'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/fetchTransactions'
|
||||
// import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import { sleep } from 'src/utils/timer'
|
||||
import { history, } from 'src/store'
|
||||
import AppRoutes from 'src/routes'
|
||||
|
@ -15,13 +13,13 @@ import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
|||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
||||
|
||||
export const EXPAND_BALANCE_INDEX = 0
|
||||
export const EXPAND_OWNERS_INDEX = 1
|
||||
export const ADD_OWNERS_INDEX = 2
|
||||
export const EDIT_THRESHOLD_INDEX = 3
|
||||
export const EDIT_INDEX = 4
|
||||
export const WITHDRAW_INDEX = 5
|
||||
export const LIST_TXS_INDEX = 6
|
||||
// export const EXPAND_BALANCE_INDEX = 0
|
||||
// export const EXPAND_OWNERS_INDEX = 1
|
||||
// export const ADD_OWNERS_INDEX = 2
|
||||
// export const EDIT_THRESHOLD_INDEX = 3
|
||||
// export const EDIT_INDEX = 4
|
||||
// export const WITHDRAW_INDEX = 5
|
||||
// export const LIST_TXS_INDEX = 6
|
||||
|
||||
export const checkMinedTx = (Transaction, name) => {
|
||||
const paragraphs = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
|
||||
|
@ -80,10 +78,10 @@ export const checkPendingTx = async (
|
|||
}
|
||||
}
|
||||
|
||||
export const refreshTransactions = async (store, safeAddress) => {
|
||||
await store.dispatch(fetchTransactions(safeAddress))
|
||||
await sleep(1500)
|
||||
}
|
||||
// export const refreshTransactions = async (store, safeAddress) => {
|
||||
// await store.dispatch(fetchTransactions(safeAddress))
|
||||
// await sleep(1500)
|
||||
// }
|
||||
|
||||
const renderApp = (store) => ({
|
||||
...render(
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
//
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
import { aNewStore } from 'src/store'
|
||||
import { aMinedSafe } from 'src/test/builder/safe.redux.builder'
|
||||
import { renderSafeView } from 'src/test/builder/safe.dom.utils'
|
||||
import { sleep } from 'src/utils/timer'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from 'src/routes/safe/components/Layout/index'
|
||||
import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from 'src/routes/safe/components/Layout'
|
||||
import { SAFE_NAME_INPUT_TEST_ID, SAFE_NAME_SUBMIT_BTN_TEST_ID } from 'src/routes/safe/components/Settings/SafeDetails'
|
||||
|
||||
describe('DOM > Feature > Settings - Name', () => {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @flow
|
||||
|
||||
module.exports = {
|
||||
migrations_directory: './migrations',
|
||||
networks: {
|
||||
|
|
Loading…
Reference in New Issue