fetch transactions refactoring wip

This commit is contained in:
Mikhail Mikheev 2020-04-30 18:56:27 +04:00
parent ec9e7f9fb8
commit a533f576c3
10 changed files with 136 additions and 69 deletions

View File

@ -6,8 +6,6 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
export const CALL = 0 export const CALL = 0
export const DELEGATE_CALL = 1 export const DELEGATE_CALL = 1
export const TX_TYPE_EXECUTION = 'execution'
export const TX_TYPE_CONFIRMATION = 'confirmation'
type Transaction = { type Transaction = {
safeInstance: any, safeInstance: any,

View File

@ -4,7 +4,6 @@ import axios from 'axios'
import { getTxServiceHost, getTxServiceUriFrom } from '~/config' import { getTxServiceHost, getTxServiceUriFrom } from '~/config'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
export type Operation = 0 | 1 | 2 export type Operation = 0 | 1 | 2
const calculateBodyFrom = async ( const calculateBodyFrom = async (

View File

@ -16,7 +16,6 @@ import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Img from '~/components/layout/Img' import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph/index' import Paragraph from '~/components/layout/Paragraph/index'
import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send'
import { userAccountSelector } from '~/logic/wallets/store/selectors' import { userAccountSelector } from '~/logic/wallets/store/selectors'
import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction' import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction'
import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors'
@ -43,9 +42,7 @@ function getOwnersConfirmations(tx, userAddress) {
currentUserAlreadyConfirmed = true currentUserAlreadyConfirmed = true
} }
if (conf.type === TX_TYPE_CONFIRMATION) {
ownersWhoConfirmed.push(conf.owner) ownersWhoConfirmed.push(conf.owner)
}
}) })
return [ownersWhoConfirmed, currentUserAlreadyConfirmed] return [ownersWhoConfirmed, currentUserAlreadyConfirmed]

View File

@ -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<GlobalState>) => {
dispatch(addTransaction(getMockCreationTx(safeAddress)))
}
export default addMockSafeCreationTx

View File

@ -6,12 +6,12 @@ import type { Dispatch as ReduxDispatch } from 'redux'
import { addIncomingTransactions } from '../addIncomingTransactions' import { addIncomingTransactions } from '../addIncomingTransactions'
import { addTransactions } from '../addTransactions' import { addTransactions } from '../addTransactions'
import { loadIncomingTransactions } from './loadIncomingTransactions'
import { type SafeTransactionsType, loadOutgoingTransactions } from './loadOutgoingTransactions' import { type SafeTransactionsType, loadOutgoingTransactions } from './loadOutgoingTransactions'
import { addCancellationTransactions } from '~/routes/safe/store/actions/transactions/addCancellationTransactions' import { addCancellationTransactions } from '~/routes/safe/store/actions/transactions/addCancellationTransactions'
import { type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' import { type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
import { type GlobalState } from '~/store' import { type GlobalState } from '~/store'
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState) => { export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState) => {
const transactions: SafeTransactionsType | typeof undefined = await loadOutgoingTransactions(safeAddress, getState) const transactions: SafeTransactionsType | typeof undefined = await loadOutgoingTransactions(safeAddress, getState)
if (transactions) { if (transactions) {
@ -25,7 +25,7 @@ export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalSta
const incomingTransactions: const incomingTransactions:
| Map<string, List<IncomingTransaction>> | Map<string, List<IncomingTransaction>>
| typeof undefined = await loadSafeIncomingTransactions(safeAddress) | typeof undefined = await loadIncomingTransactions(safeAddress)
if (incomingTransactions) { if (incomingTransactions) {
dispatch(addIncomingTransactions(incomingTransactions)) dispatch(addIncomingTransactions(incomingTransactions))

View File

@ -71,7 +71,7 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
} }
let prevIncomingTxsEtag = null let prevIncomingTxsEtag = null
export const loadSafeIncomingTransactions = async (safeAddress: string) => { export const loadIncomingTransactions = async (safeAddress: string) => {
let incomingTransactions: IncomingTxServiceModel[] = [] let incomingTransactions: IncomingTxServiceModel[] = []
try { try {
const config = prevIncomingTxsEtag const config = prevIncomingTxsEtag

View File

@ -3,9 +3,12 @@ import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed
import axios from 'axios' import axios from 'axios'
import { List, Map, type RecordInstance } from 'immutable' import { List, Map, type RecordInstance } from 'immutable'
import addMockSafeCreationTx from '../utils/addMockSafeCreationTx'
import generateBatchRequests from '~/logic/contracts/generateBatchRequests' import generateBatchRequests from '~/logic/contracts/generateBatchRequests'
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' 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 { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
import { import {
SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH, 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 { makeConfirmation } from '~/routes/safe/store/models/confirmation'
import type { TransactionProps } from '~/routes/safe/store/models/transaction' import type { TransactionProps } from '~/routes/safe/store/models/transaction'
import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction' import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction'
type ConfirmationServiceModel = { type ConfirmationServiceModel = {
owner: string, owner: string,
submissionDate: Date, submissionDate: Date,
confirmationType: string, signature: string,
transactionHash: string, transactionHash: string,
} }
@ -58,16 +62,12 @@ export const buildTransactionFrom = async (
safeAddress: string, safeAddress: string,
tx: TxServiceModel, tx: TxServiceModel,
knownTokens, knownTokens,
txTokenDecimals,
txTokenSymbol,
txTokenName,
code, code,
): Promise<Transaction> => { ): Promise<Transaction> => {
const confirmations = List( const confirmations = List(
tx.confirmations.map((conf: ConfirmationServiceModel) => tx.confirmations.map((conf: ConfirmationServiceModel) =>
makeConfirmation({ makeConfirmation({
owner: conf.owner, owner: conf.owner,
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
hash: conf.transactionHash, hash: conf.transactionHash,
signature: conf.signature, signature: conf.signature,
}), }),
@ -77,7 +77,7 @@ export const buildTransactionFrom = async (
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
const isERC721Token = const isERC721Token =
(code && code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) || (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)) let isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value))
const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value)) const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data) const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data)
@ -85,11 +85,16 @@ export const buildTransactionFrom = async (
let refundParams = null let refundParams = null
if (tx.gasPrice > 0) { if (tx.gasPrice > 0) {
const refundSymbol = txTokenSymbol || 'ETH' let refundSymbol = 'ETH'
const decimals = txTokenName || 18 let refundDecimals = 18
const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(decimals, 0) if (tx.gasToken !== ZERO_ADDRESS) {
const whole = feeString.slice(0, feeString.length - decimals) || '0' const gasToken = await getTokenInfos(tx.gasToken)
const fraction = feeString.slice(feeString.length - decimals) 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}` const formattedFee = `${whole}.${fraction}`
refundParams = { refundParams = {
@ -98,11 +103,16 @@ export const buildTransactionFrom = async (
} }
} }
let symbol = txTokenSymbol || 'ETH' let symbol = 'ETH'
let decimals = txTokenDecimals || 18 let decimals = 18
let decodedParams let decodedParams
if (isSendTokenTx) { if (isSendTokenTx) {
if (txTokenSymbol === null || txTokenDecimals === null) { try {
const token = await getTokenInfos(tx.to)
symbol = token.symbol
decimals = token.decimals
} catch (e) {
try { try {
const [tokenSymbol, tokenDecimals] = await Promise.all( const [tokenSymbol, tokenDecimals] = await Promise.all(
generateBatchRequests({ generateBatchRequests({
@ -114,7 +124,7 @@ export const buildTransactionFrom = async (
symbol = tokenSymbol symbol = tokenSymbol
decimals = tokenDecimals decimals = tokenDecimals
} catch (e) { } catch (err) {
// some contracts may implement the same methods as in ERC20 standard // 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 may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction // 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 = { decodedParams = {
recipient: params[0], recipient: params[0],
value: params[1], value: params[1],
@ -173,7 +183,7 @@ const batchTxTokenRequest = (txs: any[]) => {
const batch = new web3ReadOnly.BatchRequest() const batch = new web3ReadOnly.BatchRequest()
const whenTxsValues = txs.map((tx) => { 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({ return generateBatchRequests({
abi: ERC20Detailed.abi, abi: ERC20Detailed.abi,
address: tx.to, address: tx.to,
@ -229,9 +239,7 @@ export const loadOutgoingTransactions = async (
const txsWithData = await batchTxTokenRequest(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 // In case that the etags don't match, we parse the new transactions and save them to the cache
const txsRecord: Array<RecordInstance<TransactionProps>> = await Promise.all( const txsRecord: Array<RecordInstance<TransactionProps>> = await Promise.all(
txsWithData.map(([tx: TxServiceModel, decimals, code, symbol, name]) => txsWithData.map(([tx: TxServiceModel, code]) => buildTransactionFrom(safeAddress, tx, knownTokens, code)),
buildTransactionFrom(safeAddress, tx, knownTokens, decimals, symbol, name, code),
),
) )
const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing')) const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing'))

View File

@ -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

View File

@ -2,18 +2,14 @@
import { Record } from 'immutable' import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable' import type { RecordFactory, RecordOf } from 'immutable'
import { type TxServiceType } from '~/logic/safe/transactions/txHistory'
export type ConfirmationProps = { export type ConfirmationProps = {
owner: string, owner: string,
type: TxServiceType,
hash: string, hash: string,
signature?: string, signature?: string,
} }
export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({ export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({
owner: '', owner: '',
type: 'initialised',
hash: '', hash: '',
signature: null, signature: null,
}) })

View File

@ -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<Transaction | IncomingTransaction>,
> = 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])
},
)