diff --git a/src/routes/safe/component/AddTransaction/MultisigForm/index.jsx b/src/routes/safe/component/AddTransaction/MultisigForm/index.jsx index 66771bf8..f0393c33 100644 --- a/src/routes/safe/component/AddTransaction/MultisigForm/index.jsx +++ b/src/routes/safe/component/AddTransaction/MultisigForm/index.jsx @@ -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' diff --git a/src/routes/safe/component/AddTransaction/ReviewTx/index.jsx b/src/routes/safe/component/AddTransaction/ReviewTx/index.jsx index e8e004d6..86524b60 100644 --- a/src/routes/safe/component/AddTransaction/ReviewTx/index.jsx +++ b/src/routes/safe/component/AddTransaction/ReviewTx/index.jsx @@ -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, diff --git a/src/routes/safe/component/AddTransaction/transactions.js b/src/routes/safe/component/AddTransaction/createTransactions.js similarity index 100% rename from src/routes/safe/component/AddTransaction/transactions.js rename to src/routes/safe/component/AddTransaction/createTransactions.js diff --git a/src/routes/safe/component/AddTransaction/index.jsx b/src/routes/safe/component/AddTransaction/index.jsx index 1fe11a57..8acf7df8 100644 --- a/src/routes/safe/component/AddTransaction/index.jsx +++ b/src/routes/safe/component/AddTransaction/index.jsx @@ -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' diff --git a/src/routes/safe/component/AddTransaction/test/transactions.test.js b/src/routes/safe/component/AddTransaction/test/transactions.test.js index c3a2894d..367403bf 100644 --- a/src/routes/safe/component/AddTransaction/test/transactions.test.js +++ b/src/routes/safe/component/AddTransaction/test/transactions.test.js @@ -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' diff --git a/src/routes/safe/component/Safe.txs.test.js b/src/routes/safe/component/Safe.txs.test.js index 5cbc1a16..1567f58c 100644 --- a/src/routes/safe/component/Safe.txs.test.js +++ b/src/routes/safe/component/Safe.txs.test.js @@ -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) diff --git a/src/routes/safe/component/Transactions/Collapsed/index.jsx b/src/routes/safe/component/Transactions/Collapsed/index.jsx index f81ede1d..21761f47 100644 --- a/src/routes/safe/component/Transactions/Collapsed/index.jsx +++ b/src/routes/safe/component/Transactions/Collapsed/index.jsx @@ -15,7 +15,6 @@ type Props = { safeName: string, confirmations: ImmutableList, destination: string, - tx: string, } const listStyle = { @@ -25,7 +24,7 @@ const listStyle = { class Collapsed extends React.PureComponent { render() { const { - confirmations, destination, safeName, tx, + confirmations, destination, safeName, } = this.props return ( @@ -41,12 +40,6 @@ class Collapsed extends React.PureComponent { - { tx && - - - - - } diff --git a/src/routes/safe/component/Transactions/Transaction/index.jsx b/src/routes/safe/component/Transactions/Transaction/index.jsx index f36012e8..c1956410 100644 --- a/src/routes/safe/component/Transactions/Transaction/index.jsx +++ b/src/routes/safe/component/Transactions/Transaction/index.jsx @@ -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 { 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 { + + + { txHash && + + + + + } + { !txHash && + + } + + { open && } diff --git a/src/routes/safe/component/Transactions/actions.js b/src/routes/safe/component/Transactions/actions.js new file mode 100644 index 00000000..681ad469 --- /dev/null +++ b/src/routes/safe/component/Transactions/actions.js @@ -0,0 +1,10 @@ +// @flow +import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' + +export type Actions = { + fetchTransactions: typeof fetchTransactions, +} + +export default { + fetchTransactions, +} diff --git a/src/routes/safe/component/Transactions/index.jsx b/src/routes/safe/component/Transactions/index.jsx index 11c595f0..5dac8846 100644 --- a/src/routes/safe/component/Transactions/index.jsx +++ b/src/routes/safe/component/Transactions/index.jsx @@ -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 { - 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 { return ( { hasTransactions - ? transactions.map((tx: Transaction) => ) + ? transactions.map((tx: Transaction) => ) : } @@ -37,4 +38,4 @@ class Transactions extends React.Component { } } -export default connect(selector)(Transactions) +export default connect(selector, actions)(Transactions) diff --git a/src/routes/safe/component/Transactions/processTransactions.js b/src/routes/safe/component/Transactions/processTransactions.js new file mode 100644 index 00000000..b39756f7 --- /dev/null +++ b/src/routes/safe/component/Transactions/processTransactions.js @@ -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, + 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, 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 = updateConfirmations(tx.get('confirmations'), userAddress, confirmationHash) + + return updateTransaction( + txName, + nonce, + txDestination, + txValue, + userAddress, + executedConfirmations, + txReceipt.tx, + safeAddress, + threshold + 1, + ) +} diff --git a/src/routes/safe/component/Transactions/selector.js b/src/routes/safe/component/Transactions/selector.js index 1941f7c5..4d3c1a6f 100644 --- a/src/routes/safe/component/Transactions/selector.js +++ b/src/routes/safe/component/Transactions/selector.js @@ -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, + userAddress: userAccountSelector, } export default createStructuredSelector({ transactions: safeTransactionsSelector, + userAddress: userAccountSelector, })