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:
committed by
Jaco Greeff
parent
ad36743122
commit
bec3539651
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user