First draft of the MultiSig Wallet (#3700)

* Wallet Creation Modal #3282

* Name and description to Wallet #3282

* Add Wallet to the Account Page and Wallet Page #3282

* Fix Linting

* Crete MobX store for Transfer modal

* WIP Wallet Redux Store

* Basic Details for Wallet #3282

* Fixing linting

* Refactoring Transfer store for Wallet

* Working wallet init transfer #3282

* Optional gas in MethodDecoding + better input

* Show confirmations for Wallet #3282

* Order confirmations

* Method Decoding selections

* MultiSig txs and confirm pending #3282

* MultiSig Wallet Revoke #3282

* Confirmations and Txs Update #3282

* Feedback for Confirmations #3282

* Merging master fixes...

* Remove unused CSS
This commit is contained in:
Nicolas Gotchac
2016-12-06 09:37:59 +01:00
committed by Jaco Greeff
parent ad36743122
commit bec3539651
47 changed files with 3202 additions and 160 deletions

View File

@@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletDetails';

View File

@@ -0,0 +1,111 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Form, TypedInput, Input, AddressSelect } from '../../../ui';
import { parseAbiType } from '../../../util/abi';
export default class WalletDetails extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
};
render () {
const { accounts, wallet, errors } = this.props;
return (
<Form>
<AddressSelect
label='from account (contract owner)'
hint='the owner account for this contract'
value={ wallet.account }
error={ errors.account }
onChange={ this.onAccoutChange }
accounts={ accounts }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
<TypedInput
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.onOwnersChange }
accounts={ accounts }
param={ parseAbiType('address[]') }
/>
<TypedInput
label='required owners'
hint='number of required owners to accept a transaction'
value={ wallet.required }
error={ errors.required }
onChange={ this.onRequiredChange }
param={ parseAbiType('uint') }
/>
<TypedInput
label='wallet day limit'
hint='number of days to wait for other owners confirmation'
value={ wallet.daylimit }
error={ errors.daylimit }
onChange={ this.onDaylimitChange }
param={ parseAbiType('uint') }
/>
</Form>
);
}
onAccoutChange = (_, account) => {
this.props.onChange({ account });
}
onNameChange = (_, name) => {
this.props.onChange({ name });
}
onDescriptionChange = (_, description) => {
this.props.onChange({ description });
}
onOwnersChange = (owners) => {
this.props.onChange({ owners });
}
onRequiredChange = (required) => {
this.props.onChange({ required });
}
onDaylimitChange = (daylimit) => {
this.props.onChange({ daylimit });
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletInfo';

View File

@@ -0,0 +1,85 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '../../../ui';
import styles from '../createWallet.css';
export default class WalletInfo extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
account: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
owners: PropTypes.array.isRequired,
required: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
daylimit: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired
};
render () {
const { address, required, daylimit, name } = this.props;
return (
<CompletedStep>
<div><code>{ name }</code> has been deployed at</div>
<div>
<CopyToClipboard data={ address } label='copy address to clipboard' />
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ address }</div>
</div>
<div>with the following owners</div>
<div>
{ this.renderOwners() }
</div>
<p>
<code>{ required }</code> owners are required to confirm a transaction.
</p>
<p>
The daily limit is set to <code>{ daylimit }</code>.
</p>
</CompletedStep>
);
}
renderOwners () {
const { account, owners } = this.props;
return [].concat(account, owners).map((address, id) => (
<div key={ id } className={ styles.owner }>
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ this.addressToString(address) }</div>
</div>
));
}
addressToString (address) {
const { accounts } = this.props;
if (accounts[address]) {
return accounts[address].name || address;
}
return address;
}
}

View File

@@ -0,0 +1,39 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.address {
vertical-align: top;
display: inline-block;
}
.identityicon {
margin: -8px 0.5em;
}
.owner {
height: 40px;
color: lightgrey;
display: flex;
align-items: center;
justify-content: center;
.identityicon {
width: 24px;
height: 24px;
}
}

View File

@@ -0,0 +1,182 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { Button, Modal, TxHash, BusyStep } from '../../ui';
import WalletDetails from './WalletDetails';
import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore';
// import styles from './createWallet.css';
@observer
export default class CreateWallet extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired
};
store = new CreateWalletStore(this.context.api, this.props.accounts);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Modal
visible
title='rejected'
actions={ this.renderDialogActions() }
>
<BusyStep
title='The deployment has been rejected'
state='The wallet will not be created. You can safely close this window.'
/>
</Modal>
);
}
return (
<Modal
visible
actions={ this.renderDialogActions() }
current={ stage }
steps={ steps }
waiting={ waiting }
>
{ this.renderPage() }
</Modal>
);
}
renderPage () {
const { step } = this.store;
const { accounts } = this.props;
switch (step) {
case 'DEPLOYMENT':
return (
<BusyStep
title='The deployment is currently in progress'
state={ this.store.deployState }
>
{ this.store.txhash ? (<TxHash hash={ this.store.txhash } />) : null }
</BusyStep>
);
case 'INFO':
return (
<WalletInfo
accounts={ accounts }
account={ this.store.wallet.account }
address={ this.store.wallet.address }
owners={ this.store.wallet.owners.slice() }
required={ this.store.wallet.required }
daylimit={ this.store.wallet.daylimit }
name={ this.store.wallet.name }
/>
);
default:
case 'DETAILS':
return (
<WalletDetails
accounts={ accounts }
wallet={ this.store.wallet }
errors={ this.store.errors }
onChange={ this.store.onChange }
/>
);
}
}
renderDialogActions () {
const { step, hasErrors, rejected, onCreate } = this.store;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ this.onClose }
/>
);
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ this.onClose }
/>
);
const doneBtn = (
<Button
icon={ <ActionDone /> }
label='Done'
onClick={ this.onClose }
/>
);
const sendingBtn = (
<Button
icon={ <ActionDone /> }
label='Sending...'
disabled
/>
);
const createBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Create'
disabled={ hasErrors }
onClick={ onCreate }
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'DEPLOYMENT':
return [ closeBtn, sendingBtn ];
case 'INFO':
return [ doneBtn ];
default:
case 'DETAILS':
return [ cancelBtn, createBtn ];
}
}
onClose = () => {
this.props.onClose();
}
}

View File

@@ -0,0 +1,199 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observable, computed, action, transaction } from 'mobx';
import { ERRORS, validateUint, validateAddress, validateName } from '../../util/validation';
import { ERROR_CODES } from '../../api/transport/error';
import { wallet as walletAbi } from '../../contracts/abi';
import { wallet as walletCode } from '../../contracts/code';
const STEPS = {
DETAILS: { title: 'wallet details' },
DEPLOYMENT: { title: 'wallet deployment', waiting: true },
INFO: { title: 'wallet informaton' }
};
const STEPS_KEYS = Object.keys(STEPS);
export default class CreateWalletStore {
@observable step = null;
@observable rejected = false;
@observable deployState = null;
@observable deployError = null;
@observable txhash = null;
@observable wallet = {
account: '',
address: '',
owners: [],
required: 1,
daylimit: 0,
name: '',
description: ''
};
@observable errors = {
account: null,
owners: null,
required: null,
daylimit: null,
name: ERRORS.invalidName
};
@computed get stage () {
return STEPS_KEYS.findIndex((k) => k === this.step);
}
@computed get hasErrors () {
return !!Object.values(this.errors).find((e) => !!e);
}
steps = Object.values(STEPS).map((s) => s.title);
waiting = Object.values(STEPS)
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
constructor (api, accounts) {
this.api = api;
this.step = STEPS_KEYS[0];
this.wallet.account = Object.values(accounts)[0].address;
}
@action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet);
const { errors, wallet } = this.validateWallet(newWallet);
transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
}
@action onCreate = () => {
if (this.hasErrors) {
return;
}
this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit, name, description } = this.wallet;
const options = {
data: walletCode,
from: account
};
this.api
.newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState)
.then((address) => {
return Promise
.all([
this.api.parity.setAccountName(address, name),
this.api.parity.setAccountMeta(address, {
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
description,
name
})
])
.then(() => {
transaction(() => {
this.wallet.address = address;
this.step = 'INFO';
});
});
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return;
}
console.error('error deploying contract', error);
this.deployError = error;
});
}
onDeploymentState = (error, data) => {
if (error) {
return console.error('createWallet::onDeploymentState', error);
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'getTransactionReceipt':
this.deployState = 'Waiting for the contract deployment transaction receipt';
this.txhash = data.txhash;
return;
case 'hasReceipt':
case 'getCode':
this.deployState = 'Validating the deployed contract code';
return;
case 'completed':
this.deployState = 'The contract deployment has been completed';
return;
default:
console.error('createWallet::onDeploymentState', 'unknow contract deployment state', data);
return;
}
}
validateWallet = (_wallet) => {
const accountValidation = validateAddress(_wallet.account);
const requiredValidation = validateUint(_wallet.required);
const daylimitValidation = validateUint(_wallet.daylimit);
const nameValidation = validateName(_wallet.name);
const errors = {
account: accountValidation.addressError,
required: requiredValidation.valueError,
daylimit: daylimitValidation.valueError,
name: nameValidation.nameError
};
const wallet = {
..._wallet,
account: accountValidation.address,
required: requiredValidation.value,
daylimit: daylimitValidation.value,
name: nameValidation.name
};
return { errors, wallet };
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './createWallet';