From a533f576c3aa537a6f4f77d269e5a387363beb32 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Thu, 30 Apr 2020 18:56:27 +0400 Subject: [PATCH] fetch transactions refactoring wip --- src/logic/safe/transactions/send.js | 2 - src/logic/safe/transactions/txHistory.js | 1 - .../ExpandedTx/OwnersColumn/index.jsx | 5 +- .../transactions/addMockSafeCreationTx.js | 34 --------- .../transactions/fetchTransactions/index.js | 4 +- .../loadIncomingTransactions.js | 2 +- .../loadOutgoingTransactions.js | 50 +++++++------ .../utils/addMockSafeCreationTx.js | 29 ++++++++ src/routes/safe/store/models/confirmation.js | 4 - .../safe/store/selectors/transactions.js | 74 +++++++++++++++++++ 10 files changed, 136 insertions(+), 69 deletions(-) delete mode 100644 src/routes/safe/store/actions/transactions/addMockSafeCreationTx.js create mode 100644 src/routes/safe/store/actions/transactions/utils/addMockSafeCreationTx.js create mode 100644 src/routes/safe/store/selectors/transactions.js diff --git a/src/logic/safe/transactions/send.js b/src/logic/safe/transactions/send.js index d6ce555b..9e7dd14c 100644 --- a/src/logic/safe/transactions/send.js +++ b/src/logic/safe/transactions/send.js @@ -6,8 +6,6 @@ import { getWeb3 } from '~/logic/wallets/getWeb3' export const CALL = 0 export const DELEGATE_CALL = 1 -export const TX_TYPE_EXECUTION = 'execution' -export const TX_TYPE_CONFIRMATION = 'confirmation' type Transaction = { safeInstance: any, diff --git a/src/logic/safe/transactions/txHistory.js b/src/logic/safe/transactions/txHistory.js index cc8f4588..0ec7605a 100644 --- a/src/logic/safe/transactions/txHistory.js +++ b/src/logic/safe/transactions/txHistory.js @@ -4,7 +4,6 @@ import axios from 'axios' import { getTxServiceHost, getTxServiceUriFrom } from '~/config' import { getWeb3 } from '~/logic/wallets/getWeb3' -export type TxServiceType = 'confirmation' | 'execution' | 'initialised' export type Operation = 0 | 1 | 2 const calculateBodyFrom = async ( diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx index f0429d89..77ec0a9b 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx @@ -16,7 +16,6 @@ import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' import Img from '~/components/layout/Img' import Paragraph from '~/components/layout/Paragraph/index' -import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send' import { userAccountSelector } from '~/logic/wallets/store/selectors' import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction' import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' @@ -43,9 +42,7 @@ function getOwnersConfirmations(tx, userAddress) { currentUserAlreadyConfirmed = true } - if (conf.type === TX_TYPE_CONFIRMATION) { - ownersWhoConfirmed.push(conf.owner) - } + ownersWhoConfirmed.push(conf.owner) }) return [ownersWhoConfirmed, currentUserAlreadyConfirmed] diff --git a/src/routes/safe/store/actions/transactions/addMockSafeCreationTx.js b/src/routes/safe/store/actions/transactions/addMockSafeCreationTx.js deleted file mode 100644 index 422ba881..00000000 --- a/src/routes/safe/store/actions/transactions/addMockSafeCreationTx.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import type { Dispatch as ReduxDispatch } from 'redux' - -import { type GlobalState } from '~/store' - -const getMockCreationTx = (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, -}) - -const addMockSafeCreationTx = (safeAddress: string) => (dispatch: ReduxDispatch) => { - dispatch(addTransaction(getMockCreationTx(safeAddress))) -} - -export default addMockSafeCreationTx diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/index.js b/src/routes/safe/store/actions/transactions/fetchTransactions/index.js index c16a191d..943623d8 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions/index.js +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/index.js @@ -6,12 +6,12 @@ import type { Dispatch as ReduxDispatch } from 'redux' import { addIncomingTransactions } from '../addIncomingTransactions' import { addTransactions } from '../addTransactions' +import { loadIncomingTransactions } from './loadIncomingTransactions' import { type SafeTransactionsType, loadOutgoingTransactions } from './loadOutgoingTransactions' import { addCancellationTransactions } from '~/routes/safe/store/actions/transactions/addCancellationTransactions' import { type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' import { type GlobalState } from '~/store' - export default (safeAddress: string) => async (dispatch: ReduxDispatch, getState: GetState) => { const transactions: SafeTransactionsType | typeof undefined = await loadOutgoingTransactions(safeAddress, getState) if (transactions) { @@ -25,7 +25,7 @@ export default (safeAddress: string) => async (dispatch: ReduxDispatch> - | typeof undefined = await loadSafeIncomingTransactions(safeAddress) + | typeof undefined = await loadIncomingTransactions(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 index d0da0d45..5739220f 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.js +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.js @@ -71,7 +71,7 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => { } let prevIncomingTxsEtag = null -export const loadSafeIncomingTransactions = async (safeAddress: string) => { +export const loadIncomingTransactions = async (safeAddress: string) => { let incomingTransactions: IncomingTxServiceModel[] = [] try { const config = prevIncomingTxsEtag diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js index 85c302cd..487badf9 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.js @@ -3,9 +3,12 @@ import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed import axios from 'axios' import { List, Map, type RecordInstance } from 'immutable' +import addMockSafeCreationTx from '../utils/addMockSafeCreationTx' + import generateBatchRequests from '~/logic/contracts/generateBatchRequests' import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' -import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions/txHistory' +import { buildTxServiceUrl } from '~/logic/safe/transactions/txHistory' +import { getTokenInfos } from '~/logic/tokens/store/actions/fetchTokens' import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens' import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH, @@ -19,10 +22,11 @@ import { web3ReadOnly } from '~/logic/wallets/getWeb3' import { makeConfirmation } from '~/routes/safe/store/models/confirmation' import type { TransactionProps } from '~/routes/safe/store/models/transaction' import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction' + type ConfirmationServiceModel = { owner: string, submissionDate: Date, - confirmationType: string, + signature: string, transactionHash: string, } @@ -58,16 +62,12 @@ export const buildTransactionFrom = async ( safeAddress: string, tx: TxServiceModel, knownTokens, - txTokenDecimals, - txTokenSymbol, - txTokenName, code, ): Promise => { const confirmations = List( tx.confirmations.map((conf: ConfirmationServiceModel) => makeConfirmation({ owner: conf.owner, - type: ((conf.confirmationType.toLowerCase(): any): TxServiceType), hash: conf.transactionHash, signature: conf.signature, }), @@ -77,7 +77,7 @@ export const buildTransactionFrom = async ( 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) && txTokenDecimals !== null) + (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) @@ -85,11 +85,16 @@ export const buildTransactionFrom = async ( let refundParams = null if (tx.gasPrice > 0) { - const refundSymbol = txTokenSymbol || 'ETH' - const decimals = txTokenName || 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) + 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) const formattedFee = `${whole}.${fraction}` refundParams = { @@ -98,11 +103,16 @@ export const buildTransactionFrom = async ( } } - let symbol = txTokenSymbol || 'ETH' - let decimals = txTokenDecimals || 18 + let symbol = 'ETH' + let decimals = 18 let decodedParams if (isSendTokenTx) { - if (txTokenSymbol === null || txTokenDecimals === null) { + try { + const token = await getTokenInfos(tx.to) + + symbol = token.symbol + decimals = token.decimals + } catch (e) { try { const [tokenSymbol, tokenDecimals] = await Promise.all( generateBatchRequests({ @@ -114,7 +124,7 @@ export const buildTransactionFrom = async ( symbol = tokenSymbol decimals = tokenDecimals - } catch (e) { + } 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 @@ -123,7 +133,7 @@ export const buildTransactionFrom = async ( } } - const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10)) + const params = web3ReadOnly.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10)) decodedParams = { recipient: params[0], value: params[1], @@ -173,7 +183,7 @@ const batchTxTokenRequest = (txs: any[]) => { const batch = new web3ReadOnly.BatchRequest() const whenTxsValues = txs.map((tx) => { - const methods = ['decimals', { method: 'getCode', type: 'eth', args: [tx.to] }, 'symbol', 'name'] + const methods = [{ method: 'getCode', type: 'eth', args: [tx.to] }] return generateBatchRequests({ abi: ERC20Detailed.abi, address: tx.to, @@ -229,9 +239,7 @@ export const loadOutgoingTransactions = async ( 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), - ), + txsWithData.map(([tx: TxServiceModel, code]) => buildTransactionFrom(safeAddress, tx, knownTokens, code)), ) const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing')) diff --git a/src/routes/safe/store/actions/transactions/utils/addMockSafeCreationTx.js b/src/routes/safe/store/actions/transactions/utils/addMockSafeCreationTx.js new file mode 100644 index 00000000..32c53099 --- /dev/null +++ b/src/routes/safe/store/actions/transactions/utils/addMockSafeCreationTx.js @@ -0,0 +1,29 @@ +// @flow + +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 diff --git a/src/routes/safe/store/models/confirmation.js b/src/routes/safe/store/models/confirmation.js index 90d47a89..5b26abd2 100644 --- a/src/routes/safe/store/models/confirmation.js +++ b/src/routes/safe/store/models/confirmation.js @@ -2,18 +2,14 @@ import { Record } from 'immutable' import type { RecordFactory, RecordOf } from 'immutable' -import { type TxServiceType } from '~/logic/safe/transactions/txHistory' - export type ConfirmationProps = { owner: string, - type: TxServiceType, hash: string, signature?: string, } export const makeConfirmation: RecordFactory = Record({ owner: '', - type: 'initialised', hash: '', signature: null, }) diff --git a/src/routes/safe/store/selectors/transactions.js b/src/routes/safe/store/selectors/transactions.js new file mode 100644 index 00000000..38732879 --- /dev/null +++ b/src/routes/safe/store/selectors/transactions.js @@ -0,0 +1,74 @@ +// @flow +import { List, Map } from 'immutable' +import { type Selector, createSelector } from 'reselect' + +import { userAccountSelector } from '~/logic/wallets/store/selectors' +import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' +import { type Safe } from '~/routes/safe/store/models/safe' +import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction' +import { + type RouterProps, + safeCancellationTransactionsSelector, + safeIncomingTransactionsSelector, + safeSelector, + safeTransactionsSelector, +} from '~/routes/safe/store/selectors' +import { type GlobalState } from '~/store' + +const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): TransactionStatus => { + 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 +} + +export const extendedTransactionsSelector: Selector< + GlobalState, + RouterProps, + List, +> = 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: Transaction) => + tx.withMutations((transaction) => { + if (!transaction.isExecuted) { + if ( + (cancellationTransactionsByNonce.get(tx.nonce) && + cancellationTransactionsByNonce.get(tx.nonce).get('isExecuted')) || + transactions.find((safeTx) => tx.nonce === safeTx.nonce && safeTx.isExecuted) + ) { + transaction.set('cancelled', true) + } + } + transaction.set('status', getTxStatus(transaction, userAddress, safe)) + + return transaction + }), + ) + + return List([...extendedTransactions, ...incomingTransactions]) + }, +)