WA-238 Implementation and UI adaptation to process ttransactions

This commit is contained in:
apanizo 2018-05-27 12:07:58 +02:00
parent 0c4708b375
commit 4caacdb184
12 changed files with 185 additions and 27 deletions

View File

@ -5,7 +5,7 @@ import TextField from '~/components/forms/TextField'
import { composeValidators, inLimit, mustBeNumber, required, greaterThan, mustBeEthereumAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading'
import { TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from '~/routes/safe/component/AddTransaction/transactions'
import { TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from '~/routes/safe/component/AddTransaction/createTransactions'
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'

View File

@ -5,7 +5,7 @@ import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import Heading from '~/components/layout/Heading'
import Paragraph from '~/components/layout/Paragraph'
import { TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from '~/routes/safe/component/AddTransaction/transactions'
import { TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from '~/routes/safe/component/AddTransaction/createTransactions'
type FormProps = {
values: Object,

View File

@ -6,7 +6,7 @@ import { sleep } from '~/utils/timer'
import { type Safe } from '~/routes/safe/store/model/safe'
import actions, { type Actions } from './actions'
import selector, { type SelectorProps } from './selector'
import { createTransaction, TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from './transactions'
import { createTransaction, TX_NAME_PARAM, TX_DESTINATION_PARAM, TX_VALUE_PARAM } from './createTransactions'
import MultisigForm from './MultisigForm'
import ReviewTx from './ReviewTx'

View File

@ -1,6 +1,6 @@
// @flow
import { List, Map } from 'immutable'
import { storeTransaction, buildConfirmationsFrom, EXECUTED_CONFIRMATION_HASH, buildExecutedConfirmationFrom } from '~/routes/safe/component/AddTransaction/transactions'
import { storeTransaction, buildConfirmationsFrom, EXECUTED_CONFIRMATION_HASH, buildExecutedConfirmationFrom } from '~/routes/safe/component/AddTransaction/createTransactions'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { type Safe } from '~/routes/safe/store/model/safe'

View File

@ -90,7 +90,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
TestUtils.Simulate.click(paragraphs[2]) // expanded
await sleep(1000) // Time to expand
const paragraphsExpanded = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
const txHashParagraph = paragraphsExpanded[paragraphsExpanded.length - 1]
const txHashParagraph = paragraphsExpanded[3]
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
const batteryTx = transactions.get(0)

View File

@ -15,7 +15,6 @@ type Props = {
safeName: string,
confirmations: ImmutableList<Confirmation>,
destination: string,
tx: string,
}
const listStyle = {
@ -25,7 +24,7 @@ const listStyle = {
class Collapsed extends React.PureComponent<Props, {}> {
render() {
const {
confirmations, destination, safeName, tx,
confirmations, destination, safeName,
} = this.props
return (
@ -41,12 +40,6 @@ class Collapsed extends React.PureComponent<Props, {}> {
<Avatar><MailOutline /></Avatar>
<ListItemText primary="Destination" secondary={destination} />
</ListItem>
{ tx &&
<ListItem>
<Avatar><MailOutline /></Avatar>
<ListItemText cut primary="Transaction Hash" secondary={tx} />
</ListItem>
}
</List>
</Col>
</Row>

View File

@ -11,20 +11,25 @@ import Avatar from 'material-ui/Avatar'
import AttachMoney from 'material-ui-icons/AttachMoney'
import Atm from 'material-ui-icons/LocalAtm'
import DoneAll from 'material-ui-icons/DoneAll'
import CompareArrows from 'material-ui-icons/CompareArrows'
import Collapsed from '~/routes/safe/component/Transactions/Collapsed'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import Hairline from '~/components/layout/Hairline/index'
import Button from '~/components/layout/Button'
import selector, { type SelectorProps } from './selector'
type Props = Open & SelectorProps & {
transaction: Transaction,
safeName: string,
onProcessTx: (tx: Transaction, alreadyConfirmed: number) => void,
}
export const PROCESS_TXS = 'PROCESS TRANSACTION'
class GnoTransaction extends React.PureComponent<Props, {}> {
render() {
const {
open, toggle, transaction, confirmed, safeName,
open, toggle, transaction, confirmed, safeName, onProcessTx,
} = this.props
const txHash = transaction.get('tx')
@ -51,12 +56,30 @@ class GnoTransaction extends React.PureComponent<Props, {}> {
</ListItemIcon>
</ListItem>
</Row>
<Row>
<ListItem>
{ txHash &&
<React.Fragment>
<Avatar><CompareArrows /></Avatar>
<ListItemText cut primary="Transaction Hash" secondary={txHash} />
</React.Fragment>
}
{ !txHash &&
<Button
variant="raised"
color="primary"
onClick={onProcessTx(transaction, confirmed)}
>
{PROCESS_TXS}
</Button>
}
</ListItem>
</Row>
{ open &&
<Collapsed
safeName={safeName}
confirmations={transaction.get('confirmations')}
destination={transaction.get('destination')}
tx={transaction.get('tx')}
/> }
<Hairline />
</React.Fragment>

View File

@ -0,0 +1,10 @@
// @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
export type Actions = {
fetchTransactions: typeof fetchTransactions,
}
export default {
fetchTransactions,
}

View File

@ -4,22 +4,23 @@ import { connect } from 'react-redux'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
import { sleep } from '~/utils/timer'
import { processTransaction } from './processTransactions'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
type Props = SelectorProps & {
type Props = SelectorProps & Actions & {
onAddTx: () => void,
safeName: string,
safeAddress: string,
}
class Transactions extends React.Component<Props, {}> {
onConfirm = () => {
// eslint-disable-next-line
console.log("Confirming tx")
}
onExecute = () => {
// eslint-disable-next-line
console.log("Confirming tx")
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
const { fetchTransactions, safeAddress, userAddress } = this.props
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
await sleep(1200)
fetchTransactions()
}
render() {
@ -29,7 +30,7 @@ class Transactions extends React.Component<Props, {}> {
return (
<React.Fragment>
{ hasTransactions
? transactions.map((tx: Transaction) => <GnoTransaction key={tx.get('nonce')} safeName={safeName} transaction={tx} />)
? transactions.map((tx: Transaction) => <GnoTransaction key={tx.get('nonce')} safeName={safeName} onProcessTx={this.onProcessTx} transaction={tx} />)
: <NoTransactions onAddTx={onAddTx} />
}
</React.Fragment>
@ -37,4 +38,4 @@ class Transactions extends React.Component<Props, {}> {
}
}
export default connect(selector)(Transactions)
export default connect(selector, actions)(Transactions)

View File

@ -0,0 +1,128 @@
// @flow
import { List } from 'immutable'
import { type Owner } from '~/routes/safe/store/model/owner'
import { load, TX_KEY } from '~/utils/localStorage'
import { type Confirmation, makeConfirmation } from '~/routes/safe/store/model/confirmation'
import { makeTransaction, type Transaction } from '~/routes/safe/store/model/transaction'
import { getGnosisSafeContract } from '~/wallets/safeContracts'
import { getWeb3 } from '~/wallets/getWeb3'
import { EXECUTED_CONFIRMATION_HASH } from '~/routes/safe/component/AddTransaction/createTransactions'
export const updateTransaction = (
name: string,
nonce: number,
destination: string,
value: number,
creator: string,
confirmations: List<Confirmation>,
tx: string,
safeAddress: string,
safeThreshold: number,
) => {
const transaction: Transaction = makeTransaction({
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx,
})
const safeTransactions = load(TX_KEY) || {}
const transactions = safeTransactions[safeAddress]
const txsRecord = transactions ? List(transactions) : List([])
const index = txsRecord.findIndex((trans: Transaction) => trans.get('nonce') === nonce)
safeTransactions[safeAddress] = txsRecord.update(index, transaction)
localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions))
}
const getData = () => '0x'
const getOperation = () => 0
const execTransaction = async (
gnosisSafe: any,
destination: string,
txValue: number,
nonce: number,
executor: string,
) => {
const data = getData()
const CALL = getOperation()
const web3 = getWeb3()
const valueInWei = web3.toWei(txValue, 'ether')
const txReceipt = await gnosisSafe.execTransactionIfApproved(destination, valueInWei, data, CALL, nonce, { from: executor, gas: '5000000' })
return txReceipt
}
const execConfirmation = async (
gnosisSafe: any,
txDestination: string,
txValue: number,
nonce: number,
executor: string,
) => {
const data = getData()
const CALL = getOperation()
const web3 = getWeb3()
const valueInWei = web3.toWei(txValue, 'ether')
const txConfirmationReceipt = await gnosisSafe.approveTransactionWithParameters(txDestination, valueInWei, data, CALL, nonce, { from: executor, gas: '5000000' })
return txConfirmationReceipt
}
const updateConfirmations = (confirmations: List<Confirmation>, userAddress: string, txHash: string) =>
confirmations.map((confirmation: Confirmation) => {
const owner: Owner = confirmation.get('owner')
const status: boolean = owner.get('address') === userAddress ? true : confirmation.get('status')
const hash: string = owner.get('address') === userAddress ? txHash : confirmation.get('hash')
return makeConfirmation({ owner, status, hash })
})
export const processTransaction = async (
safeAddress: string,
tx: Transaction,
alreadyConfirmed: number,
userAddress: string,
) => {
const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)
const gnosisSafe = GnosisSafe.at(safeAddress)
const confirmations = tx.get('confirmations')
const userHasAlreadyConfirmed = confirmations.filter((confirmation: Confirmation) => {
const ownerAddress = confirmation.get('owner').get('address')
const samePerson = ownerAddress === userAddress
return samePerson && confirmation.get('status')
}).count() > 0
if (userHasAlreadyConfirmed) {
throw new Error('Owner has already confirmed this transaction')
}
const threshold = tx.get('threshold')
const thresholdReached = threshold >= alreadyConfirmed + 1
const nonce = tx.get('nonce')
const txName = tx.get('name')
const txValue = tx.get('value')
const txDestination = tx.get('destination')
const txReceipt = thresholdReached
? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress)
: await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress)
const confirmationHash = thresholdReached ? EXECUTED_CONFIRMATION_HASH : txReceipt.tx
const executedConfirmations: List<Confirmation> = updateConfirmations(tx.get('confirmations'), userAddress, confirmationHash)
return updateTransaction(
txName,
nonce,
txDestination,
txValue,
userAddress,
executedConfirmations,
txReceipt.tx,
safeAddress,
threshold + 1,
)
}

View File

@ -3,11 +3,14 @@ import { List } from 'immutable'
import { createStructuredSelector } from 'reselect'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { userAccountSelector } from '~/wallets/store/selectors/index'
export type SelectorProps = {
transactions: List<Transaction>,
userAddress: userAccountSelector,
}
export default createStructuredSelector({
transactions: safeTransactionsSelector,
userAddress: userAccountSelector,
})