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

@@ -20,7 +20,8 @@ import { Checkbox, MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
import Form, { Input, InputAddressSelect, Select } from '~/ui/Form';
import Form, { Input, InputAddressSelect, AddressSelect, Select } from '~/ui/Form';
import nullableProptype from '~/util/nullable-proptype';
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
import styles from '../transfer.css';
@@ -132,6 +133,8 @@ export default class Details extends Component {
all: PropTypes.bool,
extras: PropTypes.bool,
images: PropTypes.object.isRequired,
sender: PropTypes.string,
senderError: PropTypes.string,
recipient: PropTypes.string,
recipientError: PropTypes.string,
tag: PropTypes.string,
@@ -139,8 +142,15 @@ export default class Details extends Component {
totalError: PropTypes.string,
value: PropTypes.string,
valueError: PropTypes.string,
onChange: PropTypes.func.isRequired
}
onChange: PropTypes.func.isRequired,
wallet: PropTypes.object,
senders: nullableProptype(PropTypes.object)
};
static defaultProps = {
wallet: null,
senders: null
};
render () {
const { all, extras, tag, total, totalError, value, valueError } = this.props;
@@ -149,6 +159,7 @@ export default class Details extends Component {
return (
<Form>
{ this.renderTokenSelect() }
{ this.renderFromAddress() }
{ this.renderToAddress() }
<div className={ styles.columns }>
<div>
@@ -179,6 +190,7 @@ export default class Details extends Component {
</div>
</Input>
</div>
<div>
<Checkbox
checked={ extras }
@@ -191,6 +203,27 @@ export default class Details extends Component {
);
}
renderFromAddress () {
const { sender, senderError, senders } = this.props;
if (!senders) {
return null;
}
return (
<div className={ styles.address }>
<AddressSelect
accounts={ senders }
error={ senderError }
label='sender address'
hint='the sender address'
value={ sender }
onChange={ this.onEditSender }
/>
</div>
);
}
renderToAddress () {
const { recipient, recipientError } = this.props;
@@ -207,7 +240,11 @@ export default class Details extends Component {
}
renderTokenSelect () {
const { balance, images, tag } = this.props;
const { balance, images, tag, wallet } = this.props;
if (wallet) {
return null;
}
return (
<TokenSelect
@@ -223,6 +260,10 @@ export default class Details extends Component {
this.props.onChange('tag', tag);
}
onEditSender = (event, sender) => {
this.props.onChange('sender', sender);
}
onEditRecipient = (event, recipient) => {
this.props.onChange('recipient', recipient);
}

View File

@@ -33,28 +33,37 @@ const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.com
export default class TransferStore {
@observable stage = 0;
@observable data = '';
@observable dataError = null;
@observable extras = false;
@observable gas = DEFAULT_GAS;
@observable gasEst = '0';
@observable gasError = null;
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@observable valueAll = false;
@observable sending = false;
@observable tag = 'ETH';
@observable total = '0.0';
@observable totalError = null;
@observable value = '0.0';
@observable valueAll = false;
@observable valueError = null;
@observable isEth = true;
@observable busyState = null;
@observable rejected = false;
@observable data = '';
@observable dataError = null;
@observable gas = DEFAULT_GAS;
@observable gasError = null;
@observable gasEst = '0';
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@observable sender = '';
@observable senderError = null;
@observable total = '0.0';
@observable totalError = null;
@observable value = '0.0';
@observable valueError = null;
gasPriceHistogram = {};
account = null;
@@ -62,6 +71,9 @@ export default class TransferStore {
gasLimit = null;
onClose = null;
isWallet = false;
wallet = null;
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@@ -73,7 +85,7 @@ export default class TransferStore {
}
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError;
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError;
const verifyValid = !this.passwordError;
@@ -89,15 +101,28 @@ export default class TransferStore {
}
}
get token () {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, onClose } = props;
const { account, balance, gasLimit, senders, onClose } = props;
this.account = account;
this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose;
this.isWallet = account && account.wallet;
if (this.isWallet) {
this.wallet = props.wallet;
}
if (senders) {
this.senderError = ERRORS.requireSender;
}
}
@action onNext = () => {
@@ -133,6 +158,9 @@ export default class TransferStore {
case 'recipient':
return this._onUpdateRecipient(value);
case 'sender':
return this._onUpdateSender(value);
case 'tag':
return this._onUpdateTag(value);
@@ -165,9 +193,8 @@ export default class TransferStore {
this.onNext();
this.sending = true;
const promise = this.isEth ? this._sendEth() : this._sendToken();
promise
this
.send()
.then((requestId) => {
this.busyState = 'Waiting for authorization in the Parity Signer';
@@ -250,6 +277,23 @@ export default class TransferStore {
});
}
@action _onUpdateSender = (sender) => {
let senderError = null;
if (!sender || !sender.length) {
senderError = ERRORS.requireSender;
} else if (!this.api.util.isAddressValid(sender)) {
senderError = ERRORS.invalidAddress;
}
transaction(() => {
this.sender = sender;
this.senderError = senderError;
this.recalculateGas();
});
}
@action _onUpdateTag = (tag) => {
transaction(() => {
this.tag = tag;
@@ -280,9 +324,8 @@ export default class TransferStore {
return this.recalculate();
}
const promise = this.isEth ? this._estimateGasEth() : this._estimateGasToken();
promise
this
.estimateGas()
.then((gasEst) => {
let gas = gasEst;
let gasLimitError = null;
@@ -361,74 +404,70 @@ export default class TransferStore {
});
}
_sendEth () {
const { account, data, gas, gasPrice, recipient, value } = this;
send () {
const { options, values } = this._getTransferParams();
return this._getTransferMethod().postTransaction(options, values);
}
const options = {
from: account.address,
to: recipient,
gas,
gasPrice,
value: this.api.util.toWei(value || 0)
};
estimateGas () {
const { options, values } = this._getTransferParams(true);
return this._getTransferMethod(true).estimateGas(options, values);
}
if (data && data.length) {
options.data = data;
_getTransferMethod (gas = false) {
const { isEth, isWallet } = this;
if (isEth && !isWallet) {
return gas ? this.api.eth : this.api.parity;
}
return this.api.parity.postTransaction(options);
}
_sendToken () {
const { account, balance } = this;
const { gas, gasPrice, recipient, value, tag } = this;
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
return token.contract.instance.transfer
.postTransaction({
from: account.address,
to: token.address,
gas,
gasPrice
}, [
recipient,
new BigNumber(value).mul(token.format).toFixed(0)
]);
}
_estimateGasToken () {
const { account, balance } = this;
const { recipient, value, tag } = this;
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
return token.contract.instance.transfer
.estimateGas({
gas: MAX_GAS_ESTIMATION,
from: account.address,
to: token.address
}, [
recipient,
new BigNumber(value || 0).mul(token.format).toFixed(0)
]);
}
_estimateGasEth () {
const { account, data, recipient, value } = this;
const options = {
gas: MAX_GAS_ESTIMATION,
from: account.address,
to: recipient,
value: this.api.util.toWei(value || 0)
};
if (data && data.length) {
options.data = data;
if (isWallet) {
return this.wallet.instance.execute;
}
return this.api.eth.estimateGas(options);
return this.token.contract.instance.transfer;
}
_getTransferParams (gas = false) {
const { isEth, isWallet } = this;
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
from: this.sender || this.account.address,
to
};
if (!gas) {
options.gas = this.gas;
options.gasPrice = this.gasPrice;
} else {
options.gas = MAX_GAS_ESTIMATION;
}
if (isEth && !isWallet) {
options.value = this.api.util.toWei(this.value || 0);
if (this.data && this.data.length) {
options.data = this.data;
}
return { options, values: [] };
}
const values = isWallet
? [
this.recipient,
this.api.util.toWei(this.value || 0),
this.data || ''
]
: [
this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0)
];
return { options, values };
}
_validatePositiveNumber (num) {

View File

@@ -26,6 +26,7 @@ import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forwa
import { newError } from '~/ui/Errors/actions';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import nullableProptype from '~/util/nullable-proptype';
import Details from './Details';
import Extras from './Extras';
@@ -45,8 +46,10 @@ class Transfer extends Component {
images: PropTypes.object.isRequired,
account: PropTypes.object,
senders: nullableProptype(PropTypes.object),
balance: PropTypes.object,
balances: PropTypes.object,
wallet: PropTypes.object,
onClose: PropTypes.func
}
@@ -135,9 +138,9 @@ class Transfer extends Component {
}
renderDetailsPage () {
const { account, balance, images } = this.props;
const { valueAll, extras, recipient, recipientError, tag } = this.store;
const { total, totalError, value, valueError } = this.store;
const { account, balance, images, senders } = this.props;
const { valueAll, extras, recipient, recipientError, sender, senderError } = this.store;
const { tag, total, totalError, value, valueError } = this.store;
return (
<Details
@@ -146,14 +149,19 @@ class Transfer extends Component {
balance={ balance }
extras={ extras }
images={ images }
senders={ senders }
recipient={ recipient }
recipientError={ recipientError }
sender={ sender }
senderError={ senderError }
tag={ tag }
total={ total }
totalError={ totalError }
value={ value }
valueError={ valueError }
onChange={ this.store.onUpdateDetails } />
onChange={ this.store.onUpdateDetails }
wallet={ account.wallet && this.props.wallet }
/>
);
}
@@ -249,9 +257,28 @@ class Transfer extends Component {
}
}
function mapStateToProps (state) {
const { gasLimit } = state.nodeStatus;
return { gasLimit };
function mapStateToProps (initState, initProps) {
const { address } = initProps.account;
const isWallet = initProps.account && initProps.account.wallet;
const wallet = isWallet
? initState.wallet.wallets[address]
: null;
const senders = isWallet
? Object
.values(initState.personal.accounts)
.filter((account) => wallet.owners.includes(account.address))
.reduce((accounts, account) => {
accounts[account.address] = account;
return accounts;
}, {})
: null;
return (state) => {
const { gasLimit } = state.nodeStatus;
return { gasLimit, wallet, senders };
};
}
function mapDispatchToProps (dispatch) {