refactor: modify how transactions returned by server are processed

This commit is contained in:
fernandomg 2020-05-22 16:49:59 -03:00
parent 89261d0ed3
commit c9a01f6892
10 changed files with 780 additions and 416 deletions

View File

@ -1,5 +1,4 @@
//
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
// SAFE METHODS TO ITS ID
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
@ -54,39 +53,107 @@ const METHOD_TO_ID = {
}
export const decodeParamsFromSafeMethod = (data) => {
const web3 = getWeb3()
const web3 = web3ReadOnly
const [methodId, params] = [data.slice(0, 10), 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) => {
return !!METHOD_TO_ID[methodId]
}
export const decodeMethods = (data: string) => {
const web3 = web3ReadOnly
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
}
}

View File

@ -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)
@ -100,31 +55,38 @@ 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 generateBatchRequests({
abi: ERC20Detailed.abi,
address: tokenAddress,
name: name ? name : tokenSymbol,
methods: ['decimals', 'name', 'symbol'],
})
if (tokenDecimals === null) {
return null
}
const token = makeToken({
address: tokenAddress,
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) => {

View File

@ -1,19 +1,16 @@
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 generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { getStandardTokenContract, getTokenInfos } from 'src/logic/tokens/store/actions/fetchTokens'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { isEmptyData } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
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) => {
return makeToken({
address: ETH_ADDRESS,
name: 'Ether',
symbol: 'ETH',
@ -21,23 +18,6 @@ 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) => {
@ -50,40 +30,64 @@ export const isAddressAToken = async (tokenAddress) => {
// return 'Not a token address'
// }
const web3 = getWeb3()
const web3 = web3ReadOnly
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 = 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<any> => {
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: any, txCode: string, knownTokens: any) => {
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
}

View File

@ -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'
export const useLoadSafe = (safeAddress) => {
const dispatch = useDispatch()
@ -16,12 +16,12 @@ export const useLoadSafe = (safeAddress) => {
if (safeAddress) {
dispatch(fetchLatestMasterContractVersion())
.then(() => dispatch(fetchSafe(safeAddress)))
.then(() => dispatch(fetchSafeTokens(safeAddress)))
.then(() => {
dispatch(fetchSafeTokens(safeAddress))
dispatch(loadAddressBookFromStorage())
return dispatch(fetchTransactions(safeAddress))
dispatch(fetchTransactions(safeAddress))
return dispatch(addViewedSafe(safeAddress))
})
.then(() => dispatch(addViewedSafe(safeAddress)))
}
}
fetchData()

View File

@ -0,0 +1,69 @@
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'
export interface FetchTransactionsInterface {
getServiceUrl(): string
getSafeAddress(): string
setPreviousEtag(eTag: string): void
fetch(): Promise<TxServiceModel[]>
}
class FetchTransactions implements FetchTransactionsInterface {
_fetchConfig: {}
_previousETag?: string | null
_safeAddress?: string | null
_txType = 'outgoing'
_url: string
constructor(safeAddress: string, txType: string) {
this._safeAddress = safeAddress
this._txType = txType
this._url = this.getServiceUrl()
}
getSafeAddress(): string {
return this._safeAddress
}
getServiceUrl(): string {
return {
incoming: buildIncomingTxServiceUrl,
outgoing: buildTxServiceUrl,
}[this._txType](this._safeAddress)
}
setPreviousEtag(eTag: string) {
this._previousETag = eTag ? eTag : this._previousETag
this._fetchConfig = eTag ? { headers: { 'If-None-Match': eTag } } : this._fetchConfig
}
async fetch(): Promise<TxServiceModel[]> {
try {
const response = await axios.get(this._url, this._fetchConfig)
if (response.data.count > 0) {
const { etag } = response.headers
if (this._previousETag !== etag) {
this.setPreviousEtag(etag)
return response.data.results
}
}
} catch (err) {
if (!(err && err.response && err.response.status === 304)) {
console.error(`Requests for outgoing transactions for ${this._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 []
}
}
export default FetchTransactions

View File

@ -1,25 +1,32 @@
import { batch } from 'react-redux'
import { addIncomingTransactions } from '../../addIncomingTransactions'
import { addTransactions } from '../../addTransactions'
import { loadIncomingTransactions } from './loadIncomingTransactions'
import { loadOutgoingTransactions } from './loadOutgoingTransactions'
import { addCancellationTransaction } from '../addCancellationTransaction'
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)
export default (safeAddress: string) => async (dispatch, getState) => {
const transactions: any | typeof undefined = await loadOutgoingTransactions(safeAddress, getState)
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(addCancellationTransaction(cancel))
dispatch(addTransactions(outgoing))
dispatch(updateCancellationTxs)
dispatch(updateOutgoingTxs)
})
}
const incomingTransactions: any | typeof undefined = await loadIncomingTransactions(safeAddress)
const incomingTransactions = await loadIncomingTransactions(safeAddress)
if (incomingTransactions) {
dispatch(addIncomingTransactions(incomingTransactions))

View File

@ -1,195 +1,110 @@
import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json'
import axios from 'axios'
import { List, Map, Record } from 'immutable'
import addMockSafeCreationTx from '../utils/addMockSafeCreationTx'
import { List, Map, fromJS } from 'immutable'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { decodeParamsFromSafeMethod } from 'src/logic/contracts/methodIds'
import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory'
import { getTokenInfos } from 'src/logic/tokens/store/actions/fetchTokens'
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 { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { makeConfirmation } from 'src/routes/safe/store/models/confirmation'
import { makeTransaction } from 'src/routes/safe/store/models/transaction'
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import FetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions/FetchTransactions'
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'
type ConfirmationServiceModel = {
export type ConfirmationServiceModel = {
owner: string
submissionDate: Date
signature: string
transactionHash: string
}
export type DecodedData = {
[key: string]: Array<{ [key: string]: string | number }>
}
export type TxServiceModel = {
origin: null
to: string
value: number
data?: string | null
operation: number
nonce?: number | null
blockNumber?: number | null
safeTxGas: number
baseGas: number
blockNumber?: number | null
confirmations: ConfirmationServiceModel[]
creationTx?: boolean | null
data?: string | null
dataDecoded?: DecodedData | null
executionDate?: string | null
executor: string
gasPrice: number
gasToken: string
refundReceiver: string
safeTxHash: string
submissionDate?: string | null
executor: string
executionDate?: string | null
confirmations: ConfirmationServiceModel[]
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
creationTx?: boolean
value: number
}
export type SafeTransactionsType = {
outgoing: Map<string, List<any>>
cancel: Map<string, List<any>>
cancel: any
outgoing: any
}
export const buildTransactionFrom = async (
safeAddress: string,
tx: TxServiceModel,
knownTokens,
code,
): Promise<any> => {
const confirmations = List(
tx.confirmations.map((conf: ConfirmationServiceModel) =>
makeConfirmation({
owner: conf.owner,
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 =
(code && code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) ||
(isTokenTransfer(tx.data, Number(tx.value)) && !knownTokens.get(tx.to))
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
export type OutgoingTxs = {
cancellationTxs: any
outgoingTxs: any
}
let refundParams = null
if (tx.gasPrice > 0) {
let refundSymbol = 'ETH'
let refundDecimals = 18
if (tx.gasToken !== ZERO_ADDRESS) {
const gasToken = await getTokenInfos(tx.gasToken)
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)
export type BatchProcessTxsProps = OutgoingTxs & {
currentUser?: string
knownTokens: any
safe: any
}
const formattedFee = `${whole}.${fraction}`
refundParams = {
fee: formattedFee,
symbol: refundSymbol,
}
}
let symbol = 'ETH'
let decimals = 18
let decodedParams
if (isSendTokenTx) {
try {
const token = await getTokenInfos(tx.to)
symbol = token.symbol
decimals = token.decimals
} catch (e) {
try {
const [tokenSymbol, tokenDecimals] = await Promise.all(
generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
address: tx.to,
methods: ['symbol', 'decimals'],
}),
)
symbol = tokenSymbol as string
decimals = tokenDecimals as number
} catch (err) {
// 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
/**
* 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]
}
}
const params = web3ReadOnly.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,
})
return acc
},
{
cancellationTxs: {},
outgoingTxs: [],
},
)
}
export const batchTxTokenRequest = (txs: any[]) => {
/**
* 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 = txs.map((tx) => {
const methods = [{ method: 'getCode', type: 'eth', args: [tx.to] }]
const whenTxsValues = transactions.map((tx) => {
return generateBatchRequests({
abi: ERC20Detailed.abi,
abi: [],
address: tx.to,
batch,
context: tx,
methods,
methods: [{ method: 'getCode', type: 'eth', args: [tx.to] }],
})
})
@ -198,51 +113,101 @@ export const batchTxTokenRequest = (txs: any[]) => {
return Promise.all(whenTxsValues)
}
let prevSaveTransactionsEtag = null
export const loadOutgoingTransactions = async (safeAddress: string, getState: any): Promise<any> => {
let transactions: any = addMockSafeCreationTx(safeAddress)
/**
* 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) : []
try {
const config = prevSaveTransactionsEtag
? {
headers: {
'If-None-Match': prevSaveTransactionsEtag,
},
}
: undefined
const url = buildTxServiceUrl(safeAddress)
const response = await axios.get(url, config)
if (response.data.count > 0) {
if (prevSaveTransactionsEtag === response.headers.etag) {
// The txs are the same as we currently have, we don't have to proceed
return
}
transactions = transactions.concat(response.data.results)
prevSaveTransactionsEtag = 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 cancel = {}
for (const [tx, txCode] of cancellationTxsWithData) {
cancel[`${tx.nonce}`] = await buildTx({
cancellationTxs,
currentUser,
knownTokens,
outgoingTxs,
safe,
tx,
txCode,
})
}
const state = getState()
const knownTokens = state[TOKEN_REDUCER_ID]
const txsWithData = await batchTxTokenRequest(transactions)
// In case that the etags don't match, we parse the new transactions and save them to the cache
const txsRecord: Array<Record<any>> = await Promise.all(
txsWithData.map(([tx, code]) => buildTransactionFrom(safeAddress, tx, knownTokens, code)),
)
// outgoing transactions
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))
const outgoing = []
for (const [tx, txCode] of outgoingTxsWithData) {
outgoing.push(
await buildTx({
cancellationTxs,
currentUser,
knownTokens,
outgoingTxs,
safe,
tx,
txCode,
}),
)
}
return { cancel, outgoing }
}
let fetchOutgoingTxs: FetchTransactions | null = null
export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
const defaultResponse = {
cancel: Map(),
outgoing: List(),
}
const state = store.getState()
if (!safeAddress) {
return defaultResponse
}
const 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
}
fetchOutgoingTxs =
!fetchOutgoingTxs || fetchOutgoingTxs.getSafeAddress() !== safeAddress
? new FetchTransactions(safeAddress, 'outgoing')
: fetchOutgoingTxs
const outgoingTransactions: TxServiceModel[] = await fetchOutgoingTxs.fetch()
const { cancellationTxs, outgoingTxs } = extractCancelAndOutgoingTxs(safeAddress, outgoingTransactions)
// this should be only used for the initial load or when paginating
const { cancel, outgoing } = await batchProcessOutgoingTransactions({
cancellationTxs,
currentUser,
knownTokens,
outgoingTxs,
safe,
})
return {
outgoing: Map().set(safeAddress, groupedTxs.get('outgoing')),
cancel: Map().set(safeAddress, groupedTxs.get('cancel')),
cancel: fromJS(cancel),
outgoing: fromJS(outgoing),
}
}

View File

@ -1,58 +0,0 @@
// type TxServiceModel = {
// blockNumber: ?number,
// safeTxHash: string,
// executor: string,
// executionDate: ?string,
// confirmations: ConfirmationServiceModel[],
// isExecuted: boolean,
// isSuccessful: boolean,
// transactionHash: ?string,
// creationTx?: boolean,
// }
// safeInstance,
// to,
// valueInWei,
// data: txData,
// operation,
// nonce,
// safeTxGas,
// baseGas: 0,
// gasPrice: 0,
// gasToken: ZERO_ADDRESS,
// refundReceiver: ZERO_ADDRESS,
// sender: from,
// sigs,
export const mockTransactionCreation = (txArgs) => {
const {
baseGas,
data,
gasPrice,
gasToken,
nonce,
operation,
refundReceiver,
safeTxGas,
safeTxHash,
submissionDate,
to,
valueInWei: value,
} = txArgs
return {
data,
to,
value,
nonce,
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
safeTxHash,
submissionDate,
creationTx: false,
}
}

View File

@ -0,0 +1,288 @@
import { List, Map } from 'immutable'
import { 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 { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { makeConfirmation } from 'src/routes/safe/store/models/confirmation'
import { makeTransaction } from 'src/routes/safe/store/models/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'
export const isEmptyData = (data?: string | null) => {
return !data || data === EMPTY_DATA
}
export const isInnerTransaction = (tx: any, safeAddress: string): boolean => {
return sameAddress(tx.to, safeAddress) && Number(tx.value) === 0
}
export const isCancelTransaction = (tx: any, safeAddress: string): boolean => {
return isInnerTransaction(tx, safeAddress) && isEmptyData(tx.data)
}
export const isPendingTransaction = (tx: any, cancelTx: any): boolean => {
return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending'
}
export const isModifySettingsTransaction = (tx: any, safeAddress: string): boolean => {
return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data)
}
export const isMultiSendTransaction = (tx: any): boolean => {
return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
}
export const isUpgradeTransaction = (tx: any): 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: any, safeAddress: string): boolean => {
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
}
export const isCustomTransaction = async (tx: any, txCode: string, safeAddress: string, knownTokens: any) => {
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: any): any => {
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: any): List<any> => {
return List(
tx.confirmations.map((conf: any) =>
makeConfirmation({
owner: conf.owner,
hash: conf.transactionHash,
signature: conf.signature,
}),
),
)
}
export const isTransactionCancelled = (tx: any, outgoingTxs: Array<any>, cancellationTxs: { number: any }): 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: any, { owners, threshold }: any, currentUser?: string | null): any => {
let txStatus
if (tx.isExecuted && tx.isSuccessful) {
txStatus = 'success'
} else if (tx.cancelled) {
txStatus = 'cancelled'
} else if (tx.confirmations.size === threshold) {
txStatus = 'awaiting_execution'
} else if (tx.creationTx) {
txStatus = 'success'
} else if (!tx.confirmations.size || !!tx.isPending) {
txStatus = '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 ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
}
if (tx.isSuccessful === false) {
txStatus = 'failed'
}
return txStatus
}
export const calculateTransactionType = (tx: any): string => {
let txType = 'outgoing'
if (tx.isTokenTransfer) {
txType = 'token'
} else if (tx.isCollectibleTransfer) {
txType = 'collectible'
} else if (tx.modifySettingsTx) {
txType = 'settings'
} else if (tx.isCancellationTx) {
txType = 'cancellation'
} else if (tx.customTx) {
txType = 'custom'
} else if (tx.creationTx) {
txType = 'creation'
} else if (tx.upgradeTx) {
txType = 'upgrade'
}
return txType
}
export const buildTx = async ({
cancellationTxs,
currentUser,
knownTokens,
outgoingTxs,
safe,
tx,
txCode,
}): Promise<any> => {
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 = null, symbol = null } = isSendERC20Tx ? await getERC20DecimalsAndSymbol(tx.to) : {}
const txToStore = 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: any = {
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,
})
}

View File

@ -2,41 +2,101 @@ import { List, Record } from 'immutable'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
export const OUTGOING_TX_TYPE = 'outgoing'
export type TransactionType =
| 'incoming'
| 'outgoing'
| 'settings'
| 'custom'
| 'creation'
| 'cancellation'
| 'upgrade'
| 'token'
| 'collectible'
export type TransactionStatus =
| 'awaiting_your_confirmation'
| 'awaiting_confirmations'
| 'success'
| 'failed'
| 'cancelled'
| 'awaiting_execution'
| 'pending'
export type TransactionProps = {
baseGas: number
blockNumber?: number | null
cancelled?: boolean
confirmations: List<any>
creationTx: boolean
customTx: boolean
data?: string | null
decimals?: (number | string) | null
decodedParams: any
executionDate?: string | null
executionTxHash?: string | null
executor: string
gasPrice: number
gasToken: string
isCancellationTx: boolean
isCollectibleTransfer: boolean
isExecuted: boolean
isPending?: boolean
isSuccessful: boolean
isTokenTransfer: boolean
modifySettingsTx: boolean
multiSendTx: boolean
nonce?: number | null
operation: number
origin: string | null
recipient: string
refundParams: any
refundReceiver: string
safeTxGas: number
safeTxHash: string
status?: TransactionStatus
submissionDate?: string | null
symbol?: string | null
type: TransactionType
upgradeTx: boolean
value: string
}
export const makeTransaction = Record({
nonce: 0,
blockNumber: 0,
value: '0',
confirmations: List([]),
recipient: '',
data: null,
operation: 0,
safeTxGas: 0,
baseGas: 0,
blockNumber: 0,
cancelled: false,
confirmations: List([]),
creationTx: false,
customTx: false,
data: null,
decimals: 18,
decodedParams: {},
executionDate: '',
executionTxHash: undefined,
executor: '',
gasPrice: 0,
gasToken: ZERO_ADDRESS,
refundReceiver: ZERO_ADDRESS,
isCancellationTx: false,
isCollectibleTransfer: false,
isExecuted: false,
isSuccessful: true,
submissionDate: '',
executor: '',
executionDate: '',
symbol: '',
executionTxHash: undefined,
safeTxHash: '',
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',
modifySettingsTx: false,
multiSendTx: false,
nonce: 0,
operation: 0,
origin: null,
recipient: '',
refundParams: null,
refundReceiver: ZERO_ADDRESS,
safeTxGas: 0,
safeTxHash: '',
status: 'awaiting',
submissionDate: '',
symbol: '',
type: 'outgoing',
upgradeTx: false,
value: 0,
})
export type Transaction = Record<TransactionProps>