WA-238 Implementation and UI adaptation to process ttransactions
This commit is contained in:
parent
0c4708b375
commit
4caacdb184
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// @flow
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
export type Actions = {
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchTransactions,
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue