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 { composeValidators, inLimit, mustBeNumber, required, greaterThan, mustBeEthereumAddress } from '~/components/forms/validator'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Heading from '~/components/layout/Heading'
|
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'
|
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 Bold from '~/components/layout/Bold'
|
||||||
import Heading from '~/components/layout/Heading'
|
import Heading from '~/components/layout/Heading'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
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 = {
|
type FormProps = {
|
||||||
values: Object,
|
values: Object,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { sleep } from '~/utils/timer'
|
||||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
import selector, { type SelectorProps } from './selector'
|
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 MultisigForm from './MultisigForm'
|
||||||
import ReviewTx from './ReviewTx'
|
import ReviewTx from './ReviewTx'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
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 { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
|
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
|
||||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
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
|
TestUtils.Simulate.click(paragraphs[2]) // expanded
|
||||||
await sleep(1000) // Time to expand
|
await sleep(1000) // Time to expand
|
||||||
const paragraphsExpanded = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
|
const paragraphsExpanded = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
|
||||||
const txHashParagraph = paragraphsExpanded[paragraphsExpanded.length - 1]
|
const txHashParagraph = paragraphsExpanded[3]
|
||||||
|
|
||||||
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
const batteryTx = transactions.get(0)
|
const batteryTx = transactions.get(0)
|
||||||
|
|
|
@ -15,7 +15,6 @@ type Props = {
|
||||||
safeName: string,
|
safeName: string,
|
||||||
confirmations: ImmutableList<Confirmation>,
|
confirmations: ImmutableList<Confirmation>,
|
||||||
destination: string,
|
destination: string,
|
||||||
tx: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const listStyle = {
|
const listStyle = {
|
||||||
|
@ -25,7 +24,7 @@ const listStyle = {
|
||||||
class Collapsed extends React.PureComponent<Props, {}> {
|
class Collapsed extends React.PureComponent<Props, {}> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
confirmations, destination, safeName, tx,
|
confirmations, destination, safeName,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,12 +40,6 @@ class Collapsed extends React.PureComponent<Props, {}> {
|
||||||
<Avatar><MailOutline /></Avatar>
|
<Avatar><MailOutline /></Avatar>
|
||||||
<ListItemText primary="Destination" secondary={destination} />
|
<ListItemText primary="Destination" secondary={destination} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{ tx &&
|
|
||||||
<ListItem>
|
|
||||||
<Avatar><MailOutline /></Avatar>
|
|
||||||
<ListItemText cut primary="Transaction Hash" secondary={tx} />
|
|
||||||
</ListItem>
|
|
||||||
}
|
|
||||||
</List>
|
</List>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -11,20 +11,25 @@ import Avatar from 'material-ui/Avatar'
|
||||||
import AttachMoney from 'material-ui-icons/AttachMoney'
|
import AttachMoney from 'material-ui-icons/AttachMoney'
|
||||||
import Atm from 'material-ui-icons/LocalAtm'
|
import Atm from 'material-ui-icons/LocalAtm'
|
||||||
import DoneAll from 'material-ui-icons/DoneAll'
|
import DoneAll from 'material-ui-icons/DoneAll'
|
||||||
|
import CompareArrows from 'material-ui-icons/CompareArrows'
|
||||||
import Collapsed from '~/routes/safe/component/Transactions/Collapsed'
|
import Collapsed from '~/routes/safe/component/Transactions/Collapsed'
|
||||||
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
import Hairline from '~/components/layout/Hairline/index'
|
import Hairline from '~/components/layout/Hairline/index'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
|
|
||||||
type Props = Open & SelectorProps & {
|
type Props = Open & SelectorProps & {
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
|
onProcessTx: (tx: Transaction, alreadyConfirmed: number) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PROCESS_TXS = 'PROCESS TRANSACTION'
|
||||||
|
|
||||||
class GnoTransaction extends React.PureComponent<Props, {}> {
|
class GnoTransaction extends React.PureComponent<Props, {}> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
open, toggle, transaction, confirmed, safeName,
|
open, toggle, transaction, confirmed, safeName, onProcessTx,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const txHash = transaction.get('tx')
|
const txHash = transaction.get('tx')
|
||||||
|
@ -51,12 +56,30 @@ class GnoTransaction extends React.PureComponent<Props, {}> {
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Row>
|
</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 &&
|
{ open &&
|
||||||
<Collapsed
|
<Collapsed
|
||||||
safeName={safeName}
|
safeName={safeName}
|
||||||
confirmations={transaction.get('confirmations')}
|
confirmations={transaction.get('confirmations')}
|
||||||
destination={transaction.get('destination')}
|
destination={transaction.get('destination')}
|
||||||
tx={transaction.get('tx')}
|
|
||||||
/> }
|
/> }
|
||||||
<Hairline />
|
<Hairline />
|
||||||
</React.Fragment>
|
</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 { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
|
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
|
||||||
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
|
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import { processTransaction } from './processTransactions'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
|
import actions, { type Actions } from './actions'
|
||||||
|
|
||||||
type Props = SelectorProps & {
|
type Props = SelectorProps & Actions & {
|
||||||
onAddTx: () => void,
|
onAddTx: () => void,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
|
safeAddress: string,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Transactions extends React.Component<Props, {}> {
|
class Transactions extends React.Component<Props, {}> {
|
||||||
onConfirm = () => {
|
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
|
||||||
// eslint-disable-next-line
|
const { fetchTransactions, safeAddress, userAddress } = this.props
|
||||||
console.log("Confirming tx")
|
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
|
||||||
}
|
await sleep(1200)
|
||||||
|
fetchTransactions()
|
||||||
onExecute = () => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log("Confirming tx")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -29,7 +30,7 @@ class Transactions extends React.Component<Props, {}> {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ hasTransactions
|
{ 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} />
|
: <NoTransactions onAddTx={onAddTx} />
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</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 { createStructuredSelector } from 'reselect'
|
||||||
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
|
import { userAccountSelector } from '~/wallets/store/selectors/index'
|
||||||
|
|
||||||
export type SelectorProps = {
|
export type SelectorProps = {
|
||||||
transactions: List<Transaction>,
|
transactions: List<Transaction>,
|
||||||
|
userAddress: userAccountSelector,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStructuredSelector({
|
export default createStructuredSelector({
|
||||||
transactions: safeTransactionsSelector,
|
transactions: safeTransactionsSelector,
|
||||||
|
userAddress: userAccountSelector,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue