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

@ -209,8 +209,10 @@ export default class Contract {
_bindFunction = (func) => {
func.call = (options, values = []) => {
const callData = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth
.call(this._encodeOptions(func, this._addOptionsTo(options), values))
.call(callData)
.then((encoded) => func.decodeOutput(encoded))
.then((tokens) => tokens.map((token) => token.value))
.then((returns) => returns.length === 1 ? returns[0] : returns);

View File

@ -0,0 +1,21 @@
// 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 wallet from './wallet';
export {
wallet
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,388 @@
//sol Wallet
// Multi-sig, daily-limited account proxy/wallet.
// @authors:
// Gav Wood <g@ethdev.com>
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a
// single, or, crucially, each of a number of, designated owners.
// usage:
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed.
pragma solidity ^0.4.6;
contract multiowned {
// TYPES
// struct for the status of a pending operation.
struct PendingState {
uint yetNeeded;
uint ownersDone;
uint index;
}
// EVENTS
// this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function multiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) external constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
function isOwner(address _addr) returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
delete m_pendingIndex;
}
// FIELDS
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
}
// inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable)
// on a particular resource per calendar day. is multiowned to allow the limit to be altered. resource that method
// uses is specified in the modifier.
contract daylimit is multiowned {
// MODIFIERS
// simple modifier for daily limit.
modifier limitedDaily(uint _value) {
if (underLimit(_value))
_;
}
// METHODS
// constructor - stores initial daily limit and records the present day's index.
function daylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) external {
m_spentToday = 0;
}
// INTERNAL METHODS
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
// FIELDS
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
}
// interface contract for multisig proxy contracts; see below for docs.
contract multisig {
// EVENTS
// logged events:
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
// FUNCTIONS
// TODO: document
function changeOwner(address _from, address _to) external;
function execute(address _to, uint _value, bytes _data) external returns (bytes32);
function confirm(bytes32 _h) returns (bool);
}
// usage:
// bytes32 h = Wallet(w).from(oneOwner).execute(to, value, data);
// Wallet(w).from(anotherOwner).confirm(h);
contract Wallet is multisig, multiowned, daylimit {
// TYPES
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
// METHODS
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function Wallet(address[] _owners, uint _required, uint _daylimit)
multiowned(_owners, _required) daylimit(_daylimit) {
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) {
// first, take the opportunity to check that we're under the daily limit.
if (underLimit(_value)) {
SingleTransact(msg.sender, _value, _to, _data);
// yes - just execute the call.
_to.call.value(_value)(_data);
return 0;
}
// determine our operation hash.
_r = sha3(msg.data, block.number);
if (!confirm(_r) && m_txs[_r].to == 0) {
m_txs[_r].to = _to;
m_txs[_r].value = _value;
m_txs[_r].data = _data;
ConfirmationNeeded(_r, msg.sender, _value, _to, _data);
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) {
if (m_txs[_h].to != 0) {
m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data);
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
delete m_txs[m_pendingIndex[i]];
super.clearPending();
}
// FIELDS
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
}

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { Redirect, Router, Route } from 'react-router';
import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from '~/views';
import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Wallet, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from '~/views';
import styles from './reset.css';
@ -37,6 +37,7 @@ export default class MainApplication extends Component {
<Route path='/' component={ Application }>
<Route path='accounts' component={ Accounts } />
<Route path='account/:address' component={ Account } />
<Route path='wallet/:address' component={ Wallet } />
<Route path='addresses' component={ Addresses } />
<Route path='address/:address' component={ Address } />
<Route path='apps' component={ Dapps } />

View File

@ -192,8 +192,6 @@ export default class CreateAccount extends Component {
};
});
console.log(accounts);
this.setState({
selectedAddress: addresses[0],
accounts: accounts
@ -201,8 +199,7 @@ export default class CreateAccount extends Component {
});
})
.catch((error) => {
console.log('createIdentities', error);
console.error('createIdentities', error);
setTimeout(this.createIdentities, 1000);
this.newError(error);
});

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';

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) {

View File

@ -17,6 +17,7 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@ -33,6 +34,7 @@ export {
AddAddress,
AddContract,
CreateAccount,
CreateWallet,
DeleteAccount,
DeployContract,
EditMeta,

View File

@ -28,3 +28,4 @@ export statusReducer from './statusReducer';
export blockchainReducer from './blockchainReducer';
export compilerReducer from './compilerReducer';
export snackbarReducer from './snackbarReducer';
export walletReducer from './walletReducer';

View File

@ -17,11 +17,45 @@
import { isEqual } from 'lodash';
import { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions';
export function personalAccountsInfo (accountsInfo) {
const accounts = {};
const contacts = {};
const contracts = {};
const wallets = {};
Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
accounts[account.address] = account;
} else if (account.meta.wallet) {
account.wallet = true;
wallets[account.address] = account;
} else if (account.meta.contract) {
contracts[account.address] = account;
} else {
contacts[account.address] = account;
}
});
return (dispatch) => {
const data = {
accountsInfo,
accounts, contacts, contracts, wallets
};
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
};
}
function _personalAccountsInfo (data) {
return {
type: 'personalAccountsInfo',
accountsInfo
...data
};
}

View File

@ -25,28 +25,14 @@ const initialState = {
hasContacts: false,
contracts: {},
hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: []
};
export default handleActions({
personalAccountsInfo (state, action) {
const { accountsInfo } = action;
const accounts = {};
const contacts = {};
const contracts = {};
Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
accounts[account.address] = account;
} else if (account.meta.contract) {
contracts[account.address] = account;
} else {
contacts[account.address] = account;
}
});
const { accountsInfo, accounts, contacts, contracts, wallets } = action;
return Object.assign({}, state, {
accountsInfo,
@ -55,7 +41,9 @@ export default handleActions({
contacts,
hasContacts: Object.keys(contacts).length !== 0,
contracts,
hasContracts: Object.keys(contracts).length !== 0
hasContracts: Object.keys(contracts).length !== 0,
wallets,
hasWallets: Object.keys(wallets).length !== 0
});
},

View File

@ -0,0 +1,567 @@
// 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 { isEqual, uniq, range } from 'lodash';
import Contract from '../../api/contract';
import { wallet as WALLET_ABI } from '../../contracts/abi';
import { bytesToHex, toHex } from '../../api/util/format';
import { ERROR_CODES } from '../../api/transport/error';
import { MAX_GAS_ESTIMATION } from '../../util/constants';
import { newError } from '../../ui/Errors/actions';
const UPDATE_OWNERS = 'owners';
const UPDATE_REQUIRE = 'require';
const UPDATE_DAILYLIMIT = 'dailylimit';
const UPDATE_TRANSACTIONS = 'transactions';
const UPDATE_CONFIRMATIONS = 'confirmations';
export function confirmOperation (address, owner, operation) {
return modifyOperation('confirm', address, owner, operation);
}
export function revokeOperation (address, owner, operation) {
return modifyOperation('revoke', address, owner, operation);
}
function modifyOperation (method, address, owner, operation) {
return (dispatch, getState) => {
const { api } = getState();
const contract = new Contract(api, WALLET_ABI).at(address);
const options = {
from: owner,
gas: MAX_GAS_ESTIMATION
};
const values = [ operation ];
dispatch(setOperationPendingState(address, operation, true));
contract.instance[method]
.estimateGas(options, values)
.then((gas) => {
options.gas = gas;
return contract.instance[method].postTransaction(options, values);
})
.then((requestId) => {
return api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
dispatch(setOperationPendingState(address, operation, false));
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
return;
}
throw e;
});
})
.catch((error) => {
dispatch(setOperationPendingState(address, operation, false));
dispatch(newError(error));
});
};
}
export function attachWallets (_wallets) {
return (dispatch, getState) => {
const { wallet, api } = getState();
const prevAddresses = wallet.walletsAddresses;
const nextAddresses = Object.keys(_wallets).map((a) => a.toLowerCase()).sort();
if (isEqual(prevAddresses, nextAddresses)) {
return;
}
if (wallet.filterSubId) {
api.eth.uninstallFilter(wallet.filterSubId);
}
if (nextAddresses.length === 0) {
return dispatch(updateWallets({ wallets: {}, walletsAddresses: [], filterSubId: null }));
}
const filterOptions = {
fromBlock: 0,
toBlock: 'latest',
address: nextAddresses
};
api.eth
.newFilter(filterOptions)
.then((filterId) => {
dispatch(updateWallets({ wallets: _wallets, walletsAddresses: nextAddresses, filterSubId: filterId }));
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
console.error('walletActions::attachWallets', error);
} else {
throw error;
}
});
fetchWalletsInfo(Object.keys(_wallets))(dispatch, getState);
};
}
export function load (api) {
return (dispatch, getState) => {
const contract = new Contract(api, WALLET_ABI);
dispatch(setWalletContract(contract));
api.subscribe('eth_blockNumber', (error) => {
if (error) {
if (process.env.NODE_ENV === 'production') {
return console.error('[eth_blockNumber] walletActions::load', error);
} else {
throw error;
}
}
const { filterSubId } = getState().wallet;
if (!filterSubId) {
return;
}
api.eth
.getFilterChanges(filterSubId)
.then((logs) => contract.parseEventLogs(logs))
.then((logs) => {
parseLogs(logs)(dispatch, getState);
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
return console.error('[getFilterChanges] walletActions::load', error);
} else {
throw error;
}
});
});
};
}
function fetchWalletsInfo (updates) {
return (dispatch, getState) => {
if (Array.isArray(updates)) {
const _updates = updates.reduce((updates, address) => {
updates[address] = {
[ UPDATE_OWNERS ]: true,
[ UPDATE_REQUIRE ]: true,
[ UPDATE_DAILYLIMIT ]: true,
[ UPDATE_CONFIRMATIONS ]: true,
[ UPDATE_TRANSACTIONS ]: true,
address
};
return updates;
}, {});
return fetchWalletsInfo(_updates)(dispatch, getState);
}
const { api } = getState();
const _updates = Object.values(updates);
Promise
.all(_updates.map((update) => {
const contract = new Contract(api, WALLET_ABI).at(update.address);
return fetchWalletInfo(contract, update, getState);
}))
.then((updates) => {
dispatch(updateWalletsDetails(updates));
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
return console.error('walletAction::fetchWalletsInfo', error);
} else {
throw error;
}
});
};
}
function fetchWalletInfo (contract, update, getState) {
const promises = [];
if (update[UPDATE_OWNERS]) {
promises.push(fetchWalletOwners(contract));
}
if (update[UPDATE_REQUIRE]) {
promises.push(fetchWalletRequire(contract));
}
if (update[UPDATE_DAILYLIMIT]) {
promises.push(fetchWalletDailylimit(contract));
}
if (update[UPDATE_TRANSACTIONS]) {
promises.push(fetchWalletTransactions(contract));
}
return Promise
.all(promises)
.then((updates) => {
if (update[UPDATE_CONFIRMATIONS]) {
const ownersUpdate = updates.find((u) => u.key === UPDATE_OWNERS);
const transactionsUpdate = updates.find((u) => u.key === UPDATE_TRANSACTIONS);
const owners = ownersUpdate && ownersUpdate.value || null;
const transactions = transactionsUpdate && transactionsUpdate.value || null;
return fetchWalletConfirmations(contract, owners, transactions, getState)
.then((update) => {
updates.push(update);
return updates;
});
}
return updates;
})
.then((updates) => {
const wallet = { address: update.address };
updates.forEach((update) => {
wallet[update.key] = update.value;
});
return wallet;
});
}
function fetchWalletTransactions (contract) {
const walletInstance = contract.instance;
const signatures = {
single: toHex(walletInstance.SingleTransact.signature),
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return contract
.getAllLogs({
topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ]
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logB.blockNumber.comparedTo(logA.blockNumber);
if (comp !== 0) {
return comp;
}
return logB.transactionIndex.comparedTo(logA.transactionIndex);
});
})
.then((logs) => {
const transactions = logs.map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value;
const from = signature === signatures.deposit
? log.params['_from'].value
: contract.address;
const to = signature === signatures.deposit
? contract.address
: log.params.to.value;
const transaction = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
from, to, value
};
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
}
if (log.params.data) {
transaction.data = log.params.data.value;
}
return transaction;
});
return {
key: UPDATE_TRANSACTIONS,
value: transactions
};
});
}
function fetchWalletOwners (contract) {
const walletInstance = contract.instance;
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
})
.then((value) => {
return {
key: UPDATE_OWNERS,
value
};
});
}
function fetchWalletRequire (contract) {
const walletInstance = contract.instance;
return walletInstance
.m_required.call()
.then((value) => {
return {
key: UPDATE_REQUIRE,
value
};
});
}
function fetchWalletDailylimit (contract) {
const walletInstance = contract.instance;
return Promise
.all([
walletInstance.m_dailyLimit.call(),
walletInstance.m_spentToday.call(),
walletInstance.m_lastDay.call()
])
.then((values) => {
return {
key: UPDATE_DAILYLIMIT,
value: {
limit: values[0],
spent: values[1],
last: values[2]
}
};
});
}
function fetchWalletConfirmations (contract, _owners = null, _transactions = null, getState) {
const walletInstance = contract.instance;
const wallet = getState().wallet.wallets[contract.address];
const owners = _owners || (wallet && wallet.owners) || null;
const transactions = _transactions || (wallet && wallet.transactions) || null;
return walletInstance
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
if (comp !== 0) {
return comp;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
});
})
.then((logs) => {
return logs.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),
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
if (transactions) {
const operations = transactions
.filter((t) => t.operation)
.map((t) => t.operation);
return confirmations.filter((confirmation) => {
return !operations.includes(confirmation.operation);
});
}
return confirmations;
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
const operations = confirmations.map((conf) => conf.operation);
return Promise
.all(operations.map((op) => fetchOperationConfirmations(contract, op, owners)))
.then((confirmedBys) => {
confirmations.forEach((_, index) => {
confirmations[index].confirmedBy = confirmedBys[index];
});
return confirmations;
});
})
.then((confirmations) => {
return {
key: UPDATE_CONFIRMATIONS,
value: confirmations
};
});
}
function fetchOperationConfirmations (contract, operation, owners = null) {
if (!owners) {
console.warn('[fetchOperationConfirmations] try to provide the owners for the Wallet', contract.address);
}
const walletInstance = contract.instance;
const promise = owners
? Promise.resolve({ value: owners })
: fetchWalletOwners(contract);
return promise
.then((result) => {
const owners = result.value;
return Promise
.all(owners.map((owner) => walletInstance.hasConfirmed.call({}, [ operation, owner ])))
.then((data) => {
return owners.filter((owner, index) => data[index]);
});
});
}
function parseLogs (logs) {
return (dispatch, getState) => {
if (!logs || logs.length === 0) {
return;
}
const { wallet } = getState();
const { contract } = wallet;
const walletInstance = contract.instance;
const signatures = {
OwnerChanged: toHex(walletInstance.OwnerChanged.signature),
OwnerAdded: toHex(walletInstance.OwnerAdded.signature),
OwnerRemoved: toHex(walletInstance.OwnerRemoved.signature),
RequirementChanged: toHex(walletInstance.RequirementChanged.signature),
Confirmation: toHex(walletInstance.Confirmation.signature),
Revoke: toHex(walletInstance.Revoke.signature),
Deposit: toHex(walletInstance.Deposit.signature),
SingleTransact: toHex(walletInstance.SingleTransact.signature),
MultiTransact: toHex(walletInstance.MultiTransact.signature),
ConfirmationNeeded: toHex(walletInstance.ConfirmationNeeded.signature)
};
const updates = {};
logs.forEach((log) => {
const { address, topics } = log;
const eventSignature = toHex(topics[0]);
const prev = updates[address] || { address };
switch (eventSignature) {
case signatures.OwnerChanged:
case signatures.OwnerAdded:
case signatures.OwnerRemoved:
updates[address] = {
...prev,
[ UPDATE_OWNERS ]: true
};
return;
case signatures.RequirementChanged:
updates[address] = {
...prev,
[ UPDATE_REQUIRE ]: true
};
return;
case signatures.Confirmation:
case signatures.Revoke:
const operation = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(operation)
)
};
return;
case signatures.Deposit:
case signatures.SingleTransact:
case signatures.MultiTransact:
updates[address] = {
...prev,
[ UPDATE_TRANSACTIONS ]: true
};
return;
case signatures.ConfirmationNeeded:
const op = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(op)
)
};
return;
}
});
fetchWalletsInfo(updates)(dispatch, getState);
};
}
function setOperationPendingState (address, operation, isPending) {
return {
type: 'setOperationPendingState',
address, operation, isPending
};
}
function updateWalletsDetails (wallets) {
return {
type: 'updateWalletsDetails',
wallets
};
}
function setWalletContract (contract) {
return {
type: 'setWalletContract',
contract
};
}
function updateWallets (data) {
return {
type: 'updateWallets',
...data
};
}

View File

@ -0,0 +1,89 @@
// 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 { handleActions } from 'redux-actions';
const initialState = {
wallets: {},
walletsAddresses: [],
filterSubId: null,
contract: null
};
export default handleActions({
updateWallets: (state, action) => {
const { wallets, walletsAddresses, filterSubId } = action;
return {
...state,
wallets, walletsAddresses, filterSubId
};
},
updateWalletsDetails: (state, action) => {
const { wallets } = action;
const prevWallets = state.wallets;
const nextWallets = { ...prevWallets };
Object.values(wallets).forEach((wallet) => {
const prevWallet = prevWallets[wallet.address] || {};
nextWallets[wallet.address] = {
instance: (state.contract && state.contract.instance) || null,
...prevWallet,
...wallet
};
});
return {
...state,
wallets: nextWallets
};
},
setWalletContract: (state, action) => {
const { contract } = action;
return {
...state,
contract
};
},
setOperationPendingState: (state, action) => {
const { address, operation, isPending } = action;
const { wallets } = state;
const wallet = { ...wallets[address] };
wallet.confirmations = wallet.confirmations.map((conf) => {
if (conf.operation === operation) {
conf.pending = isPending;
}
return conf;
});
return {
...state,
wallets: {
...wallets,
[ address ]: wallet
}
};
}
}, initialState);

View File

@ -17,7 +17,7 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer } from './providers';
import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers';
import certificationsReducer from './providers/certifications/reducer';
import errorReducer from '~/ui/Errors/reducers';
@ -40,6 +40,7 @@ export default function () {
nodeStatus: nodeStatusReducer,
personal: personalReducer,
signer: signerReducer,
snackbar: snackbarReducer
snackbar: snackbarReducer,
wallet: walletReducer
});
}

View File

@ -19,6 +19,8 @@ import { applyMiddleware, createStore } from 'redux';
import initMiddleware from './middleware';
import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions';
import {
Balances as BalancesProvider,
Personal as PersonalProvider,
@ -40,5 +42,7 @@ export default function (api) {
new SignerProvider(store, api).start();
new StatusProvider(store, api).start();
store.dispatch(loadWallet(api));
return store;
}

View File

@ -60,7 +60,8 @@ class Errors extends Component {
flexDirection: 'row',
lineHeight: '1.5em',
padding: '0.75em 0',
alignItems: 'center'
alignItems: 'center',
justifyContent: 'space-between'
} }
/>
);

View File

@ -33,6 +33,7 @@ export default class AddressSelect extends Component {
accounts: PropTypes.object,
contacts: PropTypes.object,
contracts: PropTypes.object,
wallets: PropTypes.object,
label: PropTypes.string,
hint: PropTypes.string,
error: PropTypes.string,
@ -49,8 +50,8 @@ export default class AddressSelect extends Component {
}
entriesFromProps (props = this.props) {
const { accounts, contacts, contracts } = props;
const entries = Object.assign({}, accounts || {}, contacts || {}, contracts || {});
const { accounts, contacts, contracts, wallets } = props;
const entries = Object.assign({}, accounts || {}, wallets || {}, contacts || {}, contracts || {});
return entries;
}

View File

@ -78,7 +78,7 @@ export default class Input extends Component {
}
state = {
value: this.props.value || ''
value: typeof this.props.value === 'undefined' ? '' : this.props.value
}
componentWillReceiveProps (newProps) {

View File

@ -21,12 +21,30 @@
.input input {
padding-left: 48px !important;
box-sizing: border-box;
&.small {
padding-left: 40px !important;
}
}
.inputEmpty input {
padding-left: 0 !important;
}
.small {
.input input {
padding-left: 40px !important;
}
.icon,
.iconDisabled {
img {
height: 24px;
width: 24px;
}
}
}
.icon,
.iconDisabled {
position: absolute;
@ -35,6 +53,14 @@
&.noLabel {
top: 10px;
}
&.noCopy {
left: 5px;
}
&.noUnderline {
top: 0;
}
}
.icon {

View File

@ -36,22 +36,38 @@ class InputAddress extends Component {
tokens: PropTypes.object,
text: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
hideUnderline: PropTypes.bool,
allowCopy: PropTypes.bool,
small: PropTypes.bool
};
static defaultProps = {
allowCopy: true,
hideUnderline: false,
small: false
};
render () {
const { className, disabled, error, label, hint, value, text, onSubmit, accountsInfo, tokens } = this.props;
const { className, disabled, error, label, hint, value, text } = this.props;
const { small, allowCopy, hideUnderline, onSubmit, accountsInfo, tokens } = this.props;
const account = accountsInfo[value] || tokens[value];
const hasAccount = account && (!account.meta || !account.meta.deleted);
const hasAccount = account && !(account.meta && account.meta.deleted);
const icon = this.renderIcon();
const classes = [ className ];
classes.push(!icon ? styles.inputEmpty : styles.input);
const containerClasses = [ styles.container ];
if (small) {
containerClasses.push(styles.small);
}
return (
<div className={ styles.container }>
<div className={ containerClasses.join(' ') }>
<Input
className={ classes.join(' ') }
disabled={ disabled }
@ -61,7 +77,8 @@ class InputAddress extends Component {
value={ text && hasAccount ? account.name : value }
onChange={ this.handleInputChange }
onSubmit={ onSubmit }
allowCopy={ disabled ? value : false }
allowCopy={ allowCopy && (disabled ? value : false) }
hideUnderline={ hideUnderline }
/>
{ icon }
</div>
@ -69,7 +86,7 @@ class InputAddress extends Component {
}
renderIcon () {
const { value, disabled, label } = this.props;
const { value, disabled, label, allowCopy, hideUnderline } = this.props;
if (!value || !value.length || !util.isAddressValid(value)) {
return null;
@ -81,6 +98,14 @@ class InputAddress extends Component {
classes.push(styles.noLabel);
}
if (!allowCopy) {
classes.push(styles.noCopy);
}
if (hideUnderline) {
classes.push(styles.noUnderline);
}
return (
<div className={ classes.join(' ') }>
<IdentityIcon

View File

@ -25,6 +25,7 @@ class InputAddressSelect extends Component {
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contracts: PropTypes.object.isRequired,
wallets: PropTypes.object.isRequired,
error: PropTypes.string,
label: PropTypes.string,
hint: PropTypes.string,
@ -33,7 +34,7 @@ class InputAddressSelect extends Component {
};
render () {
const { accounts, contacts, contracts, label, hint, error, value, onChange } = this.props;
const { accounts, contacts, contracts, wallets, label, hint, error, value, onChange } = this.props;
return (
<AddressSelect
@ -41,6 +42,7 @@ class InputAddressSelect extends Component {
accounts={ accounts }
contacts={ contacts }
contracts={ contracts }
wallets={ wallets }
error={ error }
label={ label }
hint={ hint }
@ -51,12 +53,13 @@ class InputAddressSelect extends Component {
}
function mapStateToProps (state) {
const { accounts, contacts, contracts } = state.personal;
const { accounts, contacts, contracts, wallets } = state.personal;
return {
accounts,
contacts,
contracts
contracts,
wallets
};
}

View File

@ -34,12 +34,13 @@ export default class TypedInput extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
param: PropTypes.object.isRequired,
accounts: PropTypes.object,
error: PropTypes.any,
value: PropTypes.any,
label: PropTypes.string
label: PropTypes.string,
hint: PropTypes.string
};
render () {
@ -144,11 +145,12 @@ export default class TypedInput extends Component {
}
renderNumber () {
const { label, value, error, param } = this.props;
const { label, value, error, param, hint } = this.props;
return (
<Input
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
@ -159,11 +161,12 @@ export default class TypedInput extends Component {
}
renderDefault () {
const { label, value, error } = this.props;
const { label, value, error, hint } = this.props;
return (
<Input
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
@ -172,12 +175,13 @@ export default class TypedInput extends Component {
}
renderAddress () {
const { accounts, label, value, error } = this.props;
const { accounts, label, value, error, hint } = this.props;
return (
<InputAddressSelect
accounts={ accounts }
label={ label }
hint={ hint }
value={ value }
error={ error }
onChange={ this.onChange }
@ -187,7 +191,7 @@ export default class TypedInput extends Component {
}
renderBoolean () {
const { label, value, error } = this.props;
const { label, value, error, hint } = this.props;
const boolitems = ['false', 'true'].map((bool) => {
return (
@ -204,6 +208,7 @@ export default class TypedInput extends Component {
return (
<Select
label={ label }
hint={ hint }
value={ value ? 'true' : 'false' }
error={ error }
onChange={ this.onChangeBool }

View File

@ -39,14 +39,20 @@ export class TxRow extends Component {
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest } = this.props;
const { tx, address, isTest, historic, className } = this.props;
return (
<tr>
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
@ -64,7 +70,7 @@ export class TxRow extends Component {
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic
historic={ historic }
address={ address }
transaction={ tx } />
</td>

View File

@ -88,6 +88,10 @@ export default class Header extends Component {
const { txCount } = balance;
if (!txCount) {
return null;
}
return (
<div className={ styles.infoline }>
{ txCount.toFormat() } outgoing transactions

View File

@ -21,7 +21,7 @@ import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq, isEqual } from 'lodash';
import List from './List';
import { CreateAccount } from '~/modals';
import { CreateAccount, CreateWallet } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
@ -34,15 +34,18 @@ class Accounts extends Component {
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired,
accounts: PropTypes.object,
hasAccounts: PropTypes.bool,
balances: PropTypes.object
}
state = {
addressBook: false,
newDialog: false,
newWalletDialog: false,
sortOrder: '',
searchValues: [],
searchTokens: [],
@ -58,8 +61,8 @@ class Accounts extends Component {
}
componentWillReceiveProps (nextProps) {
const prevAddresses = Object.keys(this.props.accounts);
const nextAddresses = Object.keys(nextProps.accounts);
const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets });
const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets });
if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
this.setVisibleAccounts(nextProps);
@ -71,8 +74,8 @@ class Accounts extends Component {
}
setVisibleAccounts (props = this.props) {
const { accounts, setVisibleAccounts } = props;
const addresses = Object.keys(accounts);
const { accounts, wallets, setVisibleAccounts } = props;
const addresses = Object.keys({ ...accounts, ...wallets });
setVisibleAccounts(addresses);
}
@ -80,17 +83,17 @@ class Accounts extends Component {
return (
<div className={ styles.accounts }>
{ this.renderNewDialog() }
{ this.renderNewWalletDialog() }
{ this.renderActionbar() }
{ this.state.show ? this.renderAccounts() : this.renderLoading() }
{ this.renderAccounts() }
{ this.renderWallets() }
</div>
);
}
renderLoading () {
const { accounts } = this.props;
const loadings = ((accounts && Object.keys(accounts)) || []).map((_, idx) => (
renderLoading (object) {
const loadings = ((object && Object.keys(object)) || []).map((_, idx) => (
<div key={ idx } className={ styles.loading }>
<div />
</div>
@ -104,6 +107,10 @@ class Accounts extends Component {
}
renderAccounts () {
if (!this.state.show) {
return this.renderLoading(this.props.accounts);
}
const { accounts, hasAccounts, balances } = this.props;
const { searchValues, sortOrder } = this.state;
@ -123,6 +130,29 @@ class Accounts extends Component {
);
}
renderWallets () {
if (!this.state.show) {
return this.renderLoading(this.props.wallets);
}
const { wallets, hasWallets, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<Page>
<List
link='wallet'
search={ searchValues }
accounts={ wallets }
balances={ balances }
empty={ !hasWallets }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
/>
</Page>
);
}
renderSearchButton () {
const onChange = (searchTokens, searchValues) => {
this.setState({ searchTokens, searchValues });
@ -160,6 +190,12 @@ class Accounts extends Component {
label='new account'
onClick={ this.onNewAccountClick } />,
<Button
key='newWallet'
icon={ <ContentAdd /> }
label='new wallet'
onClick={ this.onNewWalletClick } />,
<ActionbarExport
key='exportAccounts'
content={ accounts }
@ -198,6 +234,22 @@ class Accounts extends Component {
);
}
renderNewWalletDialog () {
const { accounts } = this.props;
const { newWalletDialog } = this.state;
if (!newWalletDialog) {
return null;
}
return (
<CreateWallet
accounts={ accounts }
onClose={ this.onNewWalletClose }
/>
);
}
onAddSearchToken = (token) => {
const { searchTokens } = this.state;
const newSearchTokens = uniq([].concat(searchTokens, token));
@ -210,21 +262,33 @@ class Accounts extends Component {
});
}
onNewWalletClick = () => {
this.setState({
newWalletDialog: !this.state.newWalletDialog
});
}
onNewAccountClose = () => {
this.onNewAccountClick();
}
onNewWalletClose = () => {
this.onNewWalletClick();
}
onNewAccountUpdate = () => {
}
}
function mapStateToProps (state) {
const { accounts, hasAccounts } = state.personal;
const { accounts, hasAccounts, wallets, hasWallets } = state.personal;
const { balances } = state.balances;
return {
accounts,
hasAccounts,
wallets,
hasWallets,
balances
};
}

View File

@ -28,6 +28,7 @@ import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-t
const TABMAP = {
accounts: 'account',
wallet: 'account',
addresses: 'address',
apps: 'app',
contracts: 'contract',

View File

@ -0,0 +1,372 @@
// 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 { LinearProgress, MenuItem, IconMenu } from 'material-ui';
import ReactTooltip from 'react-tooltip';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { confirmOperation, revokeOperation } from '../../../redux/providers/walletActions';
import { bytesToHex } from '../../../api/util/format';
import { Container, InputAddress, Button, IdentityIcon } from '../../../ui';
import { TxRow } from '../../../ui/TxList/txList';
import styles from '../wallet.css';
import txListStyles from '../../../ui/TxList/txList.css';
class WalletConfirmations extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
owners: PropTypes.array.isRequired,
require: PropTypes.object.isRequired,
confirmOperation: PropTypes.func.isRequired,
revokeOperation: PropTypes.func.isRequired,
confirmations: PropTypes.array
};
static defaultProps = {
confirmations: []
};
render () {
return (
<div>
<Container title='Pending Confirmations'>
{ this.renderConfirmations() }
</Container>
</div>
);
}
renderConfirmations () {
const { confirmations, ...others } = this.props;
if (!confirmations) {
return null;
}
if (confirmations.length === 0) {
return (
<div>
<p>No transactions needs confirmation right now.</p>
</div>
);
}
return confirmations.map((confirmation) => (
<WalletConfirmation
key={ confirmation.operation }
confirmation={ confirmation }
{ ...others }
/>
));
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return { accounts };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
confirmOperation,
revokeOperation
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WalletConfirmations);
class WalletConfirmation extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
confirmation: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
owners: PropTypes.array.isRequired,
require: PropTypes.object.isRequired,
confirmOperation: PropTypes.func.isRequired,
revokeOperation: PropTypes.func.isRequired
};
state = {
openConfirm: false,
openRevoke: false
};
render () {
const { confirmation } = this.props;
const confirmationsRows = [];
const className = styles.light;
const txRow = this.renderTransactionRow(confirmation, className);
const detailsRow = this.renderConfirmedBy(confirmation, className);
const progressRow = this.renderProgress(confirmation, className);
const actionsRow = this.renderActions(confirmation, className);
confirmationsRows.push(progressRow);
confirmationsRows.push(detailsRow);
confirmationsRows.push(txRow);
confirmationsRows.push(actionsRow);
return (
<div className={ styles.confirmationContainer }>
<table className={ [ txListStyles.transactions, styles.confirmations ].join(' ') }>
<tbody>
{ confirmationsRows }
</tbody>
</table>
{ this.renderPending() }
</div>
);
}
renderPending () {
const { pending } = this.props.confirmation;
if (!pending) {
return null;
}
return (
<div className={ styles.pendingOverlay } />
);
}
handleOpenConfirm = () => {
this.setState({
openConfirm: true
});
}
handleCloseConfirm = () => {
this.setState({
openConfirm: false
});
}
handleOpenRevoke = () => {
this.setState({
openRevoke: true
});
}
handleCloseRevoke = () => {
this.setState({
openRevoke: false
});
}
handleConfirm = (e, item) => {
const { confirmOperation, confirmation, address } = this.props;
const owner = item.props.value;
confirmOperation(address, owner, confirmation.operation);
}
handleRevoke = (e, item) => {
const { revokeOperation, confirmation, address } = this.props;
const owner = item.props.value;
revokeOperation(address, owner, confirmation.operation);
}
renderActions (confirmation, className) {
const { owners, accounts } = this.props;
const { operation, confirmedBy, pending } = confirmation;
const { openConfirm, openRevoke } = this.state;
const addresses = Object.keys(accounts);
const possibleConfirm = owners
.filter((owner) => addresses.includes(owner))
.filter((owner) => !confirmedBy.includes(owner));
const possibleRevoke = owners
.filter((owner) => addresses.includes(owner))
.filter((owner) => confirmedBy.includes(owner));
const confirmButton = (
<Button
onClick={ this.handleOpenConfirm }
label='Confirm As...'
disabled={ pending || possibleConfirm.length === 0 }
/>
);
const revokeButton = (
<Button
onClick={ this.handleOpenRevoke }
label='Revoke As...'
disabled={ pending || possibleRevoke.length === 0 }
/>
);
return (
<tr key={ `actions_${operation}` } className={ className }>
<td />
<td colSpan={ 3 }>
<div className={ styles.actions }>
<IconMenu
iconButtonElement={ confirmButton }
open={ openConfirm }
onRequestChange={ this.handleCloseConfirm }
onItemTouchTap={ this.handleConfirm }
>
{ possibleConfirm.map((address) => this.renderAccountItem(address)) }
</IconMenu>
<IconMenu
iconButtonElement={ revokeButton }
open={ openRevoke }
onRequestChange={ this.handleCloseRevoke }
onItemTouchTap={ this.handleRevoke }
>
{ possibleRevoke.map((address) => this.renderAccountItem(address)) }
</IconMenu>
</div>
</td>
<td />
</tr>
);
}
renderAccountItem (address) {
const account = this.props.accounts[address];
return (
<MenuItem value={ address } key={ address }>
<div className={ styles.accountItem }>
<IdentityIcon
inline center
address={ address }
/>
<span>{ account.name.toUpperCase() || account.address }</span>
</div>
</MenuItem>
);
}
renderProgress (confirmation) {
const { require } = this.props;
const { operation, confirmedBy, pending } = confirmation;
const style = { borderRadius: 0 };
return (
<tr key={ `prog_${operation}` }>
<td colSpan={ 5 } style={ { padding: 0, paddingTop: '1em' } }>
<div
data-tip
data-for={ `tooltip_${operation}` }
data-effect='solid'
>
{
pending
? (
<LinearProgress
key={ `pending_${operation}` }
mode='indeterminate'
style={ style }
/>
)
: (
<LinearProgress
key={ `unpending_${operation}` }
mode='determinate'
min={ 0 }
max={ require.toNumber() }
value={ confirmedBy.length }
style={ style }
/>
)
}
</div>
<ReactTooltip id={ `tooltip_${operation}` }>
Confirmed by { confirmedBy.length }/{ require.toNumber() } owners
</ReactTooltip>
</td>
</tr>
);
}
renderTransactionRow (confirmation, className) {
const { address, isTest } = this.props;
const { operation, transactionHash, blockNumber, value, to, data } = confirmation;
return (
<TxRow
className={ className }
key={ operation }
tx={ {
hash: transactionHash,
blockNumber: blockNumber,
from: address,
to: to,
value: value,
input: bytesToHex(data)
} }
address={ address }
isTest={ isTest }
historic={ false }
/>
);
}
renderConfirmedBy (confirmation, className) {
const { operation, confirmedBy } = confirmation;
const confirmed = confirmedBy.map((owner) => (
<InputAddress
key={ owner }
value={ owner }
allowCopy={ false }
hideUnderline
disabled
small
text
/>
));
return (
<tr key={ `details_${operation}` } className={ className }>
<td colSpan={ 5 } style={ { padding: 0 } }>
<div
data-tip
data-for={ `tooltip_${operation}` }
data-effect='solid'
className={ styles.confirmed }
>
{ confirmed }
</div>
</td>
</tr>
);
}
}

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 './confirmations';

View File

@ -0,0 +1,102 @@
// 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 moment from 'moment';
import { Container, InputAddress } from '../../../ui';
import styles from '../wallet.css';
export default class WalletDetails extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
owners: PropTypes.array,
require: PropTypes.object,
dailylimit: PropTypes.object
};
render () {
return (
<div className={ styles.details }>
<Container title='Owners'>
{ this.renderOwners() }
</Container>
<Container title='Details'>
{ this.renderDetails() }
</Container>
</div>
);
}
renderOwners () {
const { owners } = this.props;
if (!owners) {
return null;
}
const ownersList = owners.map((address) => (
<InputAddress
key={ address }
value={ address }
disabled
text
/>
));
return (
<div>
{ ownersList }
</div>
);
}
renderDetails () {
const { require, dailylimit } = this.props;
const { api } = this.context;
if (!dailylimit || !dailylimit.limit) {
return null;
}
const limit = api.util.fromWei(dailylimit.limit).toFormat(3);
const spent = api.util.fromWei(dailylimit.spent).toFormat(3);
const date = moment(dailylimit.last.toNumber() * 24 * 3600 * 1000);
return (
<div>
<p>
<span>This wallet requires at least</span>
<span className={ styles.detail }>{ require.toFormat() } owners</span>
<span>to validate any action (transactions, modifications).</span>
</p>
<p>
<span className={ styles.detail }>{ spent }<span className={ styles.eth } /></span>
<span>has been spent today, out of</span>
<span className={ styles.detail }>{ limit }<span className={ styles.eth } /></span>
<span>set as the daily limit, which has been reset on</span>
<span className={ styles.detail }>{ date.format('LL') }</span>
</p>
</div>
);
}
}

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 './details';

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 './transactions';

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 { bytesToHex } from '../../../api/util/format';
import { Container } from '../../../ui';
import { TxRow } from '../../../ui/TxList/txList';
import txListStyles from '../../../ui/TxList/txList.css';
export default class WalletTransactions extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
transactions: PropTypes.array
};
static defaultProps = {
transactions: []
};
render () {
return (
<div>
<Container title='Transactions'>
{ this.renderTransactions() }
</Container>
</div>
);
}
renderTransactions () {
const { address, isTest, transactions } = this.props;
if (!transactions) {
return null;
}
if (transactions.length === 0) {
return (
<div>
<p>No transactions has been sent.</p>
</div>
);
}
const txRows = transactions.map((transaction) => {
const { transactionHash, blockNumber, from, to, value, data } = transaction;
return (
<TxRow
key={ transactionHash }
tx={ {
hash: transactionHash,
input: data && bytesToHex(data) || '',
blockNumber, from, to, value
} }
address={ address }
isTest={ isTest }
/>
);
});
return (
<table className={ txListStyles.transactions }>
<tbody>
{ txRows }
</tbody>
</table>
);
}
}

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 './wallet';

View File

@ -0,0 +1,107 @@
/* 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/>.
*/
.details {
margin: 0;
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0.125em;
height: auto;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.detail {
font-size: 1.125em;
color: white;
margin: 0 0.375em;
line-height: 1.5em;
&:first-child {
margin-left: 0;
}
}
.eth:after {
content: 'ETH';
font-size: 0.75em;
margin-left: 0.125em;
}
.progressText {
text-align: center;
margin: 0.75em 0 0.25em;
}
.confirmed {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0.75em 0.5em 0;
}
.confirmations {
tr {
&:nth-child(even) {
background-color: initial;
}
&.light {
background-color: rgba(255, 255, 255, 0.04);
}
&.dark {
background-color: transparent;
}
}
}
.actions {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.accountItem {
display: flex;
flex-direction: row;
align-items: center;
}
.confirmationContainer {
position: relative;
}
.pendingOverlay {
position: absolute;
top: 1em;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.1);
}

View File

@ -0,0 +1,273 @@
// 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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ContentCreate from 'material-ui/svg-icons/content/create';
import ContentSend from 'material-ui/svg-icons/content/send';
import { EditMeta, Transfer } from '../../modals';
import { Actionbar, Button, Page, Loading } from '../../ui';
import Header from '../Account/Header';
import WalletDetails from './Details';
import WalletConfirmations from './Confirmations';
import WalletTransactions from './Transactions';
import { setVisibleAccounts } from '../../redux/providers/personalActions';
import styles from './wallet.css';
class WalletContainer extends Component {
static propTypes = {
isTest: PropTypes.any
};
render () {
const { isTest, ...others } = this.props;
if (isTest !== false && isTest !== true) {
return (
<Loading size={ 4 } />
);
}
return (
<Wallet isTest={ isTest } { ...others } />
);
}
}
class Wallet extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
wallets: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired
};
state = {
showEditDialog: false,
showTransferDialog: false
};
componentDidMount () {
this.setVisibleAccounts();
}
componentWillReceiveProps (nextProps) {
const prevAddress = this.props.address;
const nextAddress = nextProps.address;
if (prevAddress !== nextAddress) {
this.setVisibleAccounts(nextProps);
}
}
componentWillUnmount () {
this.props.setVisibleAccounts([]);
}
setVisibleAccounts (props = this.props) {
const { address, setVisibleAccounts } = props;
const addresses = [ address ];
setVisibleAccounts(addresses);
}
render () {
const { wallets, balances, address } = this.props;
const wallet = (wallets || {})[address];
const balance = (balances || {})[address];
if (!wallet) {
return null;
}
return (
<div className={ styles.wallet }>
{ this.renderEditDialog(wallet) }
{ this.renderTransferDialog() }
{ this.renderActionbar() }
<Page>
<Header
account={ wallet }
balance={ balance }
/>
{ this.renderDetails() }
</Page>
</div>
);
}
renderDetails () {
const { address, isTest, wallet } = this.props;
const { owners, require, dailylimit, confirmations, transactions } = wallet;
if (!isTest || !owners || !require) {
return (
<div style={ { marginTop: '4em' } }>
<Loading size={ 4 } />
</div>
);
}
return [
<WalletDetails
key='details'
owners={ owners }
require={ require }
dailylimit={ dailylimit }
/>,
<WalletConfirmations
key='confirmations'
owners={ owners }
require={ require }
confirmations={ confirmations }
isTest={ isTest }
address={ address }
/>,
<WalletTransactions
key='transactions'
transactions={ transactions }
address={ address }
isTest={ isTest }
/>
];
}
renderActionbar () {
const { address, balances } = this.props;
const balance = balances[address];
const showTransferButton = !!(balance && balance.tokens);
const buttons = [
<Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton }
onClick={ this.onTransferClick } />,
<Button
key='editmeta'
icon={ <ContentCreate /> }
label='edit'
onClick={ this.onEditClick } />
];
return (
<Actionbar
title='Wallet Management'
buttons={ buttons } />
);
}
renderEditDialog (wallet) {
const { showEditDialog } = this.state;
if (!showEditDialog) {
return null;
}
return (
<EditMeta
account={ wallet }
keys={ ['description', 'passwordHint'] }
onClose={ this.onEditClick } />
);
}
renderTransferDialog () {
const { showTransferDialog } = this.state;
if (!showTransferDialog) {
return null;
}
const { wallets, balances, images, address } = this.props;
const wallet = wallets[address];
const balance = balances[address];
return (
<Transfer
account={ wallet }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.onTransferClose }
/>
);
}
onEditClick = () => {
this.setState({
showEditDialog: !this.state.showEditDialog
});
}
onTransferClick = () => {
this.setState({
showTransferDialog: !this.state.showTransferDialog
});
}
onTransferClose = () => {
this.onTransferClick();
}
}
function mapStateToProps (_, initProps) {
const { address } = initProps.params;
return (state) => {
const { isTest } = state.nodeStatus;
const { wallets } = state.personal;
const { balances } = state.balances;
const { images } = state;
const wallet = state.wallet.wallets[address] || {};
return {
isTest,
wallets,
balances,
images,
address,
wallet
};
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setVisibleAccounts
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WalletContainer);

View File

@ -35,6 +35,11 @@ const SNIPPETS = {
name: 'HumanStandardToken.sol',
description: 'Implementation of the Human Token Contract',
id: 'snippet2', sourcecode: require('raw-loader!../../contracts/snippets/human-standard-token.sol')
},
snippet3: {
name: 'Wallet.sol',
description: 'Implementation of a multisig Wallet',
id: 'snippet3', sourcecode: require('raw-loader!../../contracts/snippets/wallet.sol')
}
};

View File

@ -28,6 +28,7 @@ import ParityBar from './ParityBar';
import Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
import Signer from './Signer';
import Status from './Status';
import Wallet from './Wallet';
export {
Account,
@ -47,5 +48,6 @@ export {
SettingsProxy,
SettingsViews,
Signer,
Status
Status,
Wallet
};