refactor: modify how transactions returned by server are processed
This commit is contained in:
parent
89261d0ed3
commit
c9a01f6892
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue