diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions.js b/src/routes/safe/store/actions/transactions/fetchTransactions.js index 67bb3358..dd18d1ca 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions.js +++ b/src/routes/safe/store/actions/transactions/fetchTransactions.js @@ -1,7 +1,6 @@ // @flow import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json' import axios from 'axios' -import bn from 'bignumber.js' import { List, Map, type RecordInstance } from 'immutable' import { batch } from 'react-redux' import type { Dispatch as ReduxDispatch } from 'redux' @@ -11,10 +10,8 @@ import { addTransactions } from './addTransactions' import generateBatchRequests from '~/logic/contracts/generateBatchRequests' import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' -import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory' import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions/txHistory' import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens' -import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi' import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH, isMultisendTransaction, @@ -63,15 +60,6 @@ type TxServiceModel = { creationTx?: boolean, } -type IncomingTxServiceModel = { - blockNumber: number, - transactionHash: string, - to: string, - value: number, - tokenAddress: string, - from: string, -} - export const buildTransactionFrom = async ( safeAddress: string, tx: TxServiceModel, @@ -213,83 +201,31 @@ const addMockSafeCreationTx = (safeAddress): Array => [ }, ] -const batchRequestTxsData = (txs: any[]) => { - const web3Batch = new web3.BatchRequest() +const batchTxTokenRequest = (txs: any[]) => { + const batch = new web3.BatchRequest() const whenTxsValues = txs.map((tx) => { const methods = ['decimals', { method: 'getCode', type: 'eth', args: [tx.to] }, 'symbol', 'name'] return generateBatchRequests({ abi: ERC20Detailed.abi, address: tx.to, - batch: web3Batch, + batch, context: tx, methods, }) }) - web3Batch.execute() + batch.execute() return Promise.all(whenTxsValues) } -const batchRequestIncomingTxsData = (txs: IncomingTxServiceModel[]) => { - 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, - bn(gas).div(gasPrice).toFixed(), - ]), - ) -} - -export const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [ - IncomingTxServiceModel, - string, - string, - 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, - }) -} - export type SafeTransactionsType = { outgoing: Map>, cancel: Map>, } let etagSafeTransactions = null -let etagCachedSafeIncommingTransactions = null export const loadSafeTransactions = async (safeAddress: string, getState: GetState): Promise => { let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress) @@ -306,7 +242,7 @@ export const loadSafeTransactions = async (safeAddress: string, getState: GetSta 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 + // The txs are the same as we currently have, we don't have to proceed return } transactions = transactions.concat(response.data.results) @@ -324,7 +260,7 @@ export const loadSafeTransactions = async (safeAddress: string, getState: GetSta const state = getState() const knownTokens = state[TOKEN_REDUCER_ID] - const txsWithData = await batchRequestTxsData(transactions) + 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> = await Promise.all( txsWithData.map(([tx: TxServiceModel, decimals, code, symbol, name]) => @@ -340,44 +276,10 @@ export const loadSafeTransactions = async (safeAddress: string, getState: GetSta } } -export const loadSafeIncomingTransactions = async (safeAddress: string) => { - let incomingTransactions: IncomingTxServiceModel[] = [] - try { - const config = etagCachedSafeIncommingTransactions - ? { - headers: { - 'If-None-Match': etagCachedSafeIncommingTransactions, - }, - } - : undefined - const url = buildIncomingTxServiceUrl(safeAddress) - const response = await axios.get(url, config) - if (response.data.count > 0) { - incomingTransactions = response.data.results - if (etagCachedSafeIncommingTransactions === response.headers.etag) { - // The txs are the same, we can return the cached ones - return - } - etagCachedSafeIncommingTransactions = 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: string) => async (dispatch: ReduxDispatch, getState: GetState) => { web3 = await getWeb3() - const transactions: SafeTransactionsType | undefined = await loadSafeTransactions(safeAddress, getState) + const transactions: SafeTransactionsType | typeof undefined = await loadSafeTransactions(safeAddress, getState) if (transactions) { const { cancel, outgoing } = transactions @@ -387,9 +289,9 @@ export default (safeAddress: string) => async (dispatch: ReduxDispatch> | undefined = await loadSafeIncomingTransactions( - safeAddress, - ) + const incomingTransactions: + | Map> + | typeof undefined = await loadSafeIncomingTransactions(safeAddress) if (incomingTransactions) { dispatch(addIncomingTransactions(incomingTransactions)) diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.js b/src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.js new file mode 100644 index 00000000..d0da0d45 --- /dev/null +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.js @@ -0,0 +1,106 @@ +// @flow + +import axios from 'axios' +import bn from 'bignumber.js' +import { List, Map, type RecordInstance } from 'immutable' + +import generateBatchRequests from '~/logic/contracts/generateBatchRequests' +import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory' +import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi' +import { web3ReadOnly } from '~/logic/wallets/getWeb3' +import { makeIncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' + +type IncomingTxServiceModel = { + blockNumber: number, + transactionHash: string, + to: string, + value: number, + tokenAddress: string, + from: string, +} + +const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [ + IncomingTxServiceModel, + string, + string, + 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, + bn(gas).div(gasPrice).toFixed(), + ]), + ) +} + +let prevIncomingTxsEtag = null +export const loadSafeIncomingTransactions = async (safeAddress: string) => { + let incomingTransactions: IncomingTxServiceModel[] = [] + try { + const config = prevIncomingTxsEtag + ? { + headers: { + 'If-None-Match': prevIncomingTxsEtag, + }, + } + : undefined + const url = buildIncomingTxServiceUrl(safeAddress) + const response = await axios.get(url, config) + if (response.data.count > 0) { + incomingTransactions = response.data.results + if (prevIncomingTxsEtag === response.headers.etag) { + // The txs are the same as we currently have, we don't have to proceed + return + } + prevIncomingTxsEtag = 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 batchIncomingTxsTokenDataRequest(incomingTransactions) + const incomingTxsRecord = incomingTxsWithData.map(buildIncomingTransactionFrom) + return Map({ safeAddress: List(incomingTxsRecord) }) +} diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js new file mode 100644 index 00000000..fbc1b8ea --- /dev/null +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js @@ -0,0 +1,54 @@ +// @flow +let prevSaveTransactionsEtag = null +export const loadOutgoingTransactions = async ( + safeAddress: string, + getState: GetState, +): Promise => { + let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress) + + 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 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> = await Promise.all( + txsWithData.map(([tx: TxServiceModel, decimals, code, symbol, name]) => + buildTransactionFrom(safeAddress, tx, knownTokens, decimals, symbol, name, code), + ), + ) + + const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing')) + + return { + outgoing: Map().set(safeAddress, groupedTxs.get('outgoing')), + cancel: Map().set(safeAddress, groupedTxs.get('cancel')), + } +} diff --git a/src/routes/safe/store/actions/transactions/utils/mockTransaction.js b/src/routes/safe/store/actions/transactions/utils/mockTransaction.js index 0a4959f4..a00a8a14 100644 --- a/src/routes/safe/store/actions/transactions/utils/mockTransaction.js +++ b/src/routes/safe/store/actions/transactions/utils/mockTransaction.js @@ -1,4 +1,7 @@ // @flow + +import { buildTransactionFrom } from '../fetchTransactions' + // type TxServiceModel = { // blockNumber: ?number, // safeTxHash: string,