Fix wallet view (#6597)

* Add safe fail for empty logs

* Filter transactions

* Add more logging

* Fix Wallet Creation and wallet tx list

* Remove logs

* Prevent selecting twice same wallet owner

* Fix tests

* Remove unused props

* Remove unused props
This commit is contained in:
Nicolas Gotchac 2017-10-09 13:11:18 +02:00 committed by Arkadiy Paronyan
parent 65ca2f9a07
commit 8d1964bc3b
17 changed files with 221 additions and 82 deletions

View File

@ -16,18 +16,21 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui'; import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
import styles from '../createWallet.css'; import styles from '../createWallet.css';
export default class WalletDetails extends Component { class WalletDetails extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired, errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
walletType: PropTypes.string.isRequired walletType: PropTypes.string.isRequired,
knownAddresses: PropTypes.array
}; };
render () { render () {
@ -103,7 +106,10 @@ export default class WalletDetails extends Component {
} }
renderMultisigDetails () { renderMultisigDetails () {
const { accounts, wallet, errors } = this.props; const { accounts, knownAddresses, wallet, errors } = this.props;
const allowedOwners = knownAddresses
// Exclude sender and already owners of the wallet
.filter((address) => !wallet.owners.includes(address) && address !== wallet.account);
return ( return (
<Form> <Form>
@ -163,7 +169,7 @@ export default class WalletDetails extends Component {
/> />
<TypedInput <TypedInput
accounts={ accounts } allowedValues={ allowedOwners }
label={ label={
<FormattedMessage <FormattedMessage
id='createWallet.details.ownersMulti.label' id='createWallet.details.ownersMulti.label'
@ -249,3 +255,21 @@ export default class WalletDetails extends Component {
this.props.onChange({ daylimit }); this.props.onChange({ daylimit });
} }
} }
function mapStateToProps (initState) {
const { accounts, contacts, contracts } = initState.personal;
const knownAddresses = [].concat(
Object.keys(accounts),
Object.keys(contacts),
Object.keys(contracts)
);
return () => ({
knownAddresses
});
}
export default connect(
mapStateToProps,
null
)(WalletDetails);

View File

@ -25,6 +25,22 @@ import { ACCOUNTS } from '../createWallet.test.js';
let component; let component;
let onChange; let onChange;
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: {},
contacts: {},
contracts: {}
}
};
}
};
}
function render (walletType = 'MULTISIG') { function render (walletType = 'MULTISIG') {
onChange = sinon.stub(); onChange = sinon.stub();
component = shallow( component = shallow(
@ -36,7 +52,12 @@ function render (walletType = 'MULTISIG') {
owners: [] owners: []
} } } }
walletType={ walletType } walletType={ walletType }
/> />,
{
context: {
store: createRedux()
}
}
); );
return component; return component;

View File

@ -283,7 +283,8 @@ export default class CreateWalletStore {
const owners = _wallet.owners.filter((owner) => !/^(0x)?0*$/.test(owner)); const owners = _wallet.owners.filter((owner) => !/^(0x)?0*$/.test(owner));
if (_wallet.required > owners.length) { // Real number of owners is owners + creator
if (_wallet.required > owners.length + 1) {
requiredValidation.valueError = 'the number of required validators should be lower or equal the number of owners'; requiredValidation.valueError = 'the number of required validators should be lower or equal the number of owners';
} }

View File

@ -43,7 +43,6 @@ export default class ParametersStep extends Component {
}; };
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired,
onParamsChange: PropTypes.func.isRequired, onParamsChange: PropTypes.func.isRequired,
inputs: PropTypes.array, inputs: PropTypes.array,
@ -60,7 +59,7 @@ export default class ParametersStep extends Component {
} }
renderConstructorInputs () { renderConstructorInputs () {
const { accounts, params, paramsError } = this.props; const { params, paramsError } = this.props;
const { inputs } = this.props; const { inputs } = this.props;
if (!inputs || !inputs.length) { if (!inputs || !inputs.length) {
@ -78,7 +77,6 @@ export default class ParametersStep extends Component {
return ( return (
<div key={ index } className={ styles.funcparams }> <div key={ index } className={ styles.funcparams }>
<TypedInput <TypedInput
accounts={ accounts }
error={ error } error={ error }
isEth={ false } isEth={ false }
label={ label } label={ label }

View File

@ -314,7 +314,6 @@ class DeployContract extends Component {
return ( return (
<ParametersStep <ParametersStep
{ ...this.state } { ...this.state }
accounts={ accounts }
onParamsChange={ this.onParamsChange } onParamsChange={ this.onParamsChange }
readOnly={ readOnly } readOnly={ readOnly }
/> />

View File

@ -177,7 +177,7 @@ export default class DetailsStep extends Component {
} }
renderParameters () { renderParameters () {
const { accounts, func, values, valuesError, onValueChange } = this.props; const { func, values, valuesError, onValueChange } = this.props;
if (!func) { if (!func) {
return null; return null;
@ -197,7 +197,6 @@ export default class DetailsStep extends Component {
value={ values[index] } value={ values[index] }
error={ valuesError[index] } error={ valuesError[index] }
onChange={ onChange } onChange={ onChange }
accounts={ accounts }
param={ input.type } param={ input.type }
isEth={ false } isEth={ false }
/> />

View File

@ -34,7 +34,6 @@ class WalletSettings extends Component {
}; };
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
senders: PropTypes.object.isRequired senders: PropTypes.object.isRequired
@ -74,7 +73,7 @@ class WalletSettings extends Component {
default: default:
case 'EDIT': case 'EDIT':
const { errors, fromString, wallet } = this.store; const { errors, fromString, wallet } = this.store;
const { accountsInfo, senders } = this.props; const { senders } = this.props;
return ( return (
<Form> <Form>
@ -143,7 +142,6 @@ class WalletSettings extends Component {
} }
value={ wallet.owners.slice() } value={ wallet.owners.slice() }
onChange={ this.store.onOwnersChange } onChange={ this.store.onOwnersChange }
accounts={ accountsInfo }
param='address[]' param='address[]'
/> />
@ -443,13 +441,13 @@ class WalletSettings extends Component {
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {
const { accountsInfo, accounts } = initState.personal; const { accounts } = initState.personal;
const { owners } = initProps.wallet; const { owners } = initProps.wallet;
const senders = pick(accounts, owners); const senders = pick(accounts, owners);
return () => { return () => {
return { accountsInfo, senders }; return { senders };
}; };
} }

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { eq } from 'lodash';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -93,6 +94,18 @@ class AddressSelect extends Component {
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!eq(Object.keys(this.props.accounts), Object.keys(nextProps.accounts))) {
return this.setValues(nextProps);
}
if (!eq(Object.keys(this.props.contacts), Object.keys(nextProps.contacts))) {
return this.setValues(nextProps);
}
if (!eq(Object.keys(this.props.contracts), Object.keys(nextProps.contracts))) {
return this.setValues(nextProps);
}
if (this.store.values && this.store.values.length > 0) { if (this.store.values && this.store.values.length > 0) {
return; return;
} }

View File

@ -165,7 +165,8 @@ export default class AddressSelectStore {
const contactsN = Object.keys(contacts).length; const contactsN = Object.keys(contacts).length;
if (accountsN + contractsN + contactsN === 0) { if (accountsN + contractsN + contactsN === 0) {
return; this.initValues = [];
return this.handleChange();
} }
this.initValues = [ this.initValues = [

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -28,6 +29,7 @@ class InputAddressSelect extends Component {
contracts: PropTypes.object.isRequired, contracts: PropTypes.object.isRequired,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
allowedValues: PropTypes.array,
className: PropTypes.string, className: PropTypes.string,
error: nodeOrStringProptype(), error: nodeOrStringProptype(),
hint: nodeOrStringProptype(), hint: nodeOrStringProptype(),
@ -38,16 +40,33 @@ class InputAddressSelect extends Component {
}; };
render () { render () {
const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props; const { accounts, allowCopy, allowedValues, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
// Add the currently selected value to the list
// of allowed values, if any given
const nextAllowedValues = allowedValues
? [].concat(allowedValues, value || [])
: null;
const filteredAccounts = nextAllowedValues
? pick(accounts, nextAllowedValues)
: accounts;
const filteredContacts = nextAllowedValues
? pick(contacts, nextAllowedValues)
: accounts;
const filteredContracts = nextAllowedValues
? pick(contracts, nextAllowedValues)
: accounts;
return ( return (
<AddressSelect <AddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
allowInput allowInput
accounts={ accounts } accounts={ filteredAccounts }
className={ className } className={ className }
contacts={ contacts } contacts={ filteredContacts }
contracts={ contracts } contracts={ filteredContracts }
error={ error } error={ error }
hint={ hint } hint={ hint }
label={ label } label={ label }

View File

@ -40,8 +40,8 @@ export default class TypedInput extends Component {
PropTypes.string PropTypes.string
]).isRequired, ]).isRequired,
accounts: PropTypes.object,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
allowedValues: PropTypes.array,
className: PropTypes.string, className: PropTypes.string,
error: PropTypes.any, error: PropTypes.any,
hint: nodeOrStringProptype(), hint: nodeOrStringProptype(),
@ -97,7 +97,7 @@ export default class TypedInput extends Component {
const { type } = param; const { type } = param;
if (type === ABI_TYPES.ARRAY) { if (type === ABI_TYPES.ARRAY) {
const { accounts, className, label } = this.props; const { allowedValues, className, label } = this.props;
const { subtype, length } = param; const { subtype, length } = param;
const value = this.getValue() || param.default; const value = this.getValue() || param.default;
@ -113,8 +113,8 @@ export default class TypedInput extends Component {
return ( return (
<TypedInput <TypedInput
accounts={ accounts }
allowCopy={ allowCopy } allowCopy={ allowCopy }
allowedValues={ allowedValues }
className={ className } className={ className }
key={ `${subtype.type}_${index}` } key={ `${subtype.type}_${index}` }
onChange={ onChange } onChange={ onChange }
@ -340,13 +340,13 @@ export default class TypedInput extends Component {
} }
renderAddress () { renderAddress () {
const { accounts, allowCopy, className, label, error, hint, readOnly } = this.props; const { allowCopy, allowedValues, className, label, error, hint, readOnly } = this.props;
const value = this.getValue(); const value = this.getValue();
return ( return (
<InputAddressSelect <InputAddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
accounts={ accounts } allowedValues={ allowedValues }
className={ className } className={ className }
error={ error } error={ error }
hint={ hint } hint={ hint }

View File

@ -78,13 +78,39 @@ export default class WalletsUtils {
.delegateCall(api, walletContract.address, 'fetchTransactions', [ walletContract ]) .delegateCall(api, walletContract.address, 'fetchTransactions', [ walletContract ])
.then((transactions) => { .then((transactions) => {
return transactions.sort((txA, txB) => { return transactions.sort((txA, txB) => {
const comp = txB.blockNumber.comparedTo(txA.blockNumber); const bnA = txA.blockNumber;
const bnB = txB.blockNumber;
if (!bnA) {
console.warn('could not find block number in transaction', txA);
return 1;
}
if (!bnB) {
console.warn('could not find block number in transaction', txB);
return -1;
}
const comp = bnA.comparedTo(bnB);
if (comp !== 0) { if (comp !== 0) {
return comp; return comp;
} }
return txB.transactionIndex.comparedTo(txA.transactionIndex); const txIdxA = txA.transactionIndex;
const txIdxB = txB.transactionIndex;
if (!txIdxA) {
console.warn('could not find transaction index in transaction', txA);
return 1;
}
if (!txIdxB) {
console.warn('could not find transaction index in transaction', txB);
return -1;
}
return txIdxA.comparedTo(txIdxB);
}); });
}); });
} }

View File

@ -212,6 +212,7 @@ export default class ConsensysWalletUtils {
const transaction = { const transaction = {
transactionHash: log.transactionHash, transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockNumber: log.blockNumber blockNumber: log.blockNumber
}; };

View File

@ -130,27 +130,67 @@ export default class FoundationWalletUtils {
.ConfirmationNeeded .ConfirmationNeeded
.getAllLogs() .getAllLogs()
.then((logs) => { .then((logs) => {
return logs.map((log) => ({ return logs
initiator: log.params.initiator.value, .filter((log) => {
to: log.params.to.value, if (!log.blockNumber) {
data: log.params.data.value, console.warn('got a log without blockNumber', log);
value: log.params.value.value, return false;
operation: bytesToHex(log.params.operation.value), }
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash, if (!log.transactionIndex) {
blockNumber: log.blockNumber, console.warn('got a log without transactionIndex', log);
confirmedBy: [] return false;
})); }
return true;
})
.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
}) })
.then((logs) => { .then((logs) => {
return logs.sort((logA, logB) => { return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber); const bnA = logA.blockNumber;
const bnB = logA.blockNumber;
if (!bnA) {
console.warn('could not find block number in log', logA);
return 1;
}
if (!bnB) {
console.warn('could not find block number in log', logB);
return -1;
}
const comp = bnA.comparedTo(bnB);
if (comp !== 0) { if (comp !== 0) {
return comp; return comp;
} }
return logA.transactionIndex.comparedTo(logB.transactionIndex); const txIdxA = logA.transactionIndex;
const txIdxB = logB.transactionIndex;
if (!txIdxA) {
console.warn('could not find transaction index in log', logA);
return 1;
}
if (!txIdxB) {
console.warn('could not find transaction index in log', logB);
return -1;
}
return txIdxA.comparedTo(txIdxB);
}); });
}) })
.then((pendingTxs) => { .then((pendingTxs) => {
@ -205,40 +245,48 @@ export default class FoundationWalletUtils {
] ] ] ]
}) })
.then((logs) => { .then((logs) => {
const transactions = logs.map((log) => { const transactions = logs
const signature = toHex(log.topics[0]); .map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value; const value = log.params.value.value;
const from = signature === WalletSignatures.Deposit const from = signature === WalletSignatures.Deposit
? log.params['_from'].value ? log.params['_from'].value
: walletContract.address; : walletContract.address;
const to = signature === WalletSignatures.Deposit const to = signature === WalletSignatures.Deposit
? walletContract.address ? walletContract.address
: log.params.to.value; : log.params.to.value;
const transaction = { const transaction = {
transactionHash: log.transactionHash, transactionHash: log.transactionHash,
blockNumber: log.blockNumber, transactionIndex: log.transactionIndex,
from, to, value blockNumber: log.blockNumber,
}; from, to, value
};
if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) { if (!transaction.blockNumber) {
transaction.creates = log.params.created.value; console.warn('log without block number', log);
delete transaction.to; return null;
} }
if (log.params.operation) { if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) {
transaction.operation = bytesToHex(log.params.operation.value); transaction.creates = log.params.created.value;
checkPendingOperation(api, log, transaction.operation); delete transaction.to;
} }
if (log.params.data) { if (log.params.operation) {
transaction.data = log.params.data.value; transaction.operation = bytesToHex(log.params.operation.value);
} checkPendingOperation(api, log, transaction.operation);
}
return transaction; if (log.params.data) {
}); transaction.data = log.params.data.value;
}
return transaction;
})
.filter((tx) => tx);
return transactions; return transactions;
}); });

View File

@ -35,7 +35,6 @@ class InputQuery extends Component {
}; };
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired, contract: PropTypes.object.isRequired,
inputs: arrayOrObjectProptype().isRequired, inputs: arrayOrObjectProptype().isRequired,
outputs: arrayOrObjectProptype().isRequired, outputs: arrayOrObjectProptype().isRequired,
@ -122,7 +121,7 @@ class InputQuery extends Component {
renderResults () { renderResults () {
const { results, isLoading } = this.state; const { results, isLoading } = this.state;
const { accountsInfo, outputs } = this.props; const { outputs } = this.props;
if (isLoading) { if (isLoading) {
return ( return (
@ -143,7 +142,6 @@ class InputQuery extends Component {
.map((out, index) => { .map((out, index) => {
const input = ( const input = (
<TypedInput <TypedInput
accounts={ accountsInfo }
allowCopy allowCopy
isEth={ false } isEth={ false }
param={ out.type } param={ out.type }

View File

@ -29,7 +29,6 @@ export default class Queries extends Component {
} }
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object.isRequired,
contract: PropTypes.object, contract: PropTypes.object,
values: PropTypes.object values: PropTypes.object
} }
@ -94,12 +93,11 @@ export default class Queries extends Component {
renderInputQuery (fn) { renderInputQuery (fn) {
const { abi, name, signature } = fn; const { abi, name, signature } = fn;
const { accountsInfo, contract } = this.props; const { contract } = this.props;
return ( return (
<div className={ styles.container } key={ fn.signature }> <div className={ styles.container } key={ fn.signature }>
<InputQuery <InputQuery
accountsInfo={ accountsInfo }
className={ styles.method } className={ styles.method }
inputs={ abi.inputs } inputs={ abi.inputs }
outputs={ abi.outputs } outputs={ abi.outputs }
@ -144,13 +142,11 @@ export default class Queries extends Component {
return null; return null;
} }
const { accountsInfo } = this.props;
const { name, type } = output; const { name, type } = output;
const label = `${name ? `${name}: ` : ''}${type}`; const label = `${name ? `${name}: ` : ''}${type}`;
return ( return (
<TypedInput <TypedInput
accounts={ accountsInfo }
allowCopy allowCopy
key={ key } key={ key }
isEth={ false } isEth={ false }

View File

@ -45,7 +45,6 @@ class Contract extends Component {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object, accounts: PropTypes.object,
accountsInfo: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
params: PropTypes.object params: PropTypes.object
@ -128,7 +127,7 @@ class Contract extends Component {
} }
render () { render () {
const { accountsInfo, contracts, netVersion, params } = this.props; const { contracts, netVersion, params } = this.props;
const { allEvents, contract, queryValues, loadingEvents } = this.state; const { allEvents, contract, queryValues, loadingEvents } = this.state;
const account = contracts[params.address]; const account = contracts[params.address];
@ -150,7 +149,6 @@ class Contract extends Component {
{ this.renderBlockNumber(account.meta) } { this.renderBlockNumber(account.meta) }
</Header> </Header>
<Queries <Queries
accountsInfo={ accountsInfo }
contract={ contract } contract={ contract }
values={ queryValues } values={ queryValues }
/> />
@ -530,12 +528,11 @@ class Contract extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, accountsInfo, contracts } = state.personal; const { accounts, contracts } = state.personal;
const { netVersion } = state.nodeStatus; const { netVersion } = state.nodeStatus;
return { return {
accounts, accounts,
accountsInfo,
contracts, contracts,
netVersion netVersion
}; };