Make Wallet first-class citizens (#3990)

* Fixed hint in Address Select + Wallet as first-class-citizen

* Separate Owned and not Owned Wallets

* Fix balance not updating

* Fix MethodDecoding for Contract Deployment

* Fix TypedInput params

* Fix Token Transfer for Wallet

* Small change to contracts

* Fix wallets shown twice

* Fix separation of accounts and wallets in Accounts

* Fix linting

* Execute contract methods from Wallet ✓

* Fixing linting

* Wallet as first-class citizen: Part 1 (Manual) #3784

* Lower level wallet transaction convertion

* Fix linting

* Proper autoFocus on right Signer input

* PR Grumble: don't show Wallets in dApps Permissions

* Add postTransaction and gasEstimate wrapper methods

* Extract Wallet postTx and gasEstimate to utils + PATCH api

* Remove invalid test

It's totally valid for input's length not to be a multiple of 32 bytes. EG. for Wallet Contracts

* Merge master

* Fix linting

* Fix merge issue

* Rename Portal

* Rename Protal => Portal (typo)
This commit is contained in:
Nicolas Gotchac 2016-12-30 12:28:12 +01:00 committed by Gav Wood
parent 88c0329a31
commit fd41a10319
46 changed files with 570 additions and 230 deletions

View File

@ -25,7 +25,7 @@ export default class Encoder {
throw new Error('tokens should be array of Token'); throw new Error('tokens should be array of Token');
} }
const mediates = tokens.map((token) => Encoder.encodeToken(token)); const mediates = tokens.map((token, index) => Encoder.encodeToken(token, index));
const inits = mediates const inits = mediates
.map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx))) .map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx)))
.join(''); .join('');
@ -36,11 +36,12 @@ export default class Encoder {
return `${inits}${closings}`; return `${inits}${closings}`;
} }
static encodeToken (token) { static encodeToken (token, index = 0) {
if (!isInstanceOf(token, Token)) { if (!isInstanceOf(token, Token)) {
throw new Error('token should be instanceof Token'); throw new Error('token should be instanceof Token');
} }
try {
switch (token.type) { switch (token.type) {
case 'address': case 'address':
return new Mediate('raw', padAddress(token.value)); return new Mediate('raw', padAddress(token.value));
@ -64,9 +65,11 @@ export default class Encoder {
case 'fixedArray': case 'fixedArray':
case 'array': case 'array':
return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token))); return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token)));
}
} catch (e) {
throw new Error(`Cannot encode token #${index} [${token.type}: ${token.value}]. ${e.message}`);
}
default:
throw new Error(`Invalid token type ${token.type} in encodeToken`); throw new Error(`Invalid token type ${token.type} in encodeToken`);
} }
} }
}

View File

@ -41,6 +41,10 @@ export default class Interface {
} }
encodeTokens (paramTypes, values) { encodeTokens (paramTypes, values) {
return Interface.encodeTokens(paramTypes, values);
}
static encodeTokens (paramTypes, values) {
const createToken = function (paramType, value) { const createToken = function (paramType, value) {
if (paramType.subtype) { if (paramType.subtype) {
return new Token(paramType.type, value.map((entry) => createToken(paramType.subtype, entry))); return new Token(paramType.type, value.map((entry) => createToken(paramType.subtype, entry)));

View File

@ -114,7 +114,11 @@ export default class Api {
} }
}) })
.catch((error) => { .catch((error) => {
// Don't print if the request is rejected: that's ok
if (error.type !== 'REQUEST_REJECTED') {
console.error('pollMethod', error); console.error('pollMethod', error);
}
reject(error); reject(error);
}); });
}; };

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Abi from '../../abi'; import Abi from '~/abi';
let nextSubscriptionId = 0; let nextSubscriptionId = 0;
@ -53,6 +53,10 @@ export default class Contract {
this._subscribedToBlock = false; this._subscribedToBlock = false;
this._blockSubscriptionId = null; this._blockSubscriptionId = null;
if (api && api.patch && api.patch.contract) {
api.patch.contract(this);
}
} }
get address () { get address () {
@ -90,8 +94,10 @@ export default class Contract {
} }
deployEstimateGas (options, values) { deployEstimateGas (options, values) {
const _options = this._encodeOptions(this.constructors[0], options, values);
return this._api.eth return this._api.eth
.estimateGas(this._encodeOptions(this.constructors[0], options, values)) .estimateGas(_options)
.then((gasEst) => { .then((gasEst) => {
return [gasEst, gasEst.mul(1.2)]; return [gasEst, gasEst.mul(1.2)];
}); });
@ -115,8 +121,10 @@ export default class Contract {
setState({ state: 'postTransaction', gas }); setState({ state: 'postTransaction', gas });
const _options = this._encodeOptions(this.constructors[0], options, values);
return this._api.parity return this._api.parity
.postTransaction(this._encodeOptions(this.constructors[0], options, values)) .postTransaction(_options)
.then((requestId) => { .then((requestId) => {
setState({ state: 'checkRequest', requestId }); setState({ state: 'checkRequest', requestId });
return this._pollCheckRequest(requestId); return this._pollCheckRequest(requestId);
@ -199,7 +207,7 @@ export default class Contract {
getCallData = (func, options, values) => { getCallData = (func, options, values) => {
let data = options.data; let data = options.data;
const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null; const tokens = func ? Abi.encodeTokens(func.inputParamTypes(), values) : null;
const call = tokens ? func.encodeCall(tokens) : null; const call = tokens ? func.encodeCall(tokens) : null;
if (data && data.substr(0, 2) === '0x') { if (data && data.substr(0, 2) === '0x') {
@ -221,6 +229,8 @@ export default class Contract {
} }
_bindFunction = (func) => { _bindFunction = (func) => {
func.contract = this;
func.call = (options, values = []) => { func.call = (options, values = []) => {
const callParams = this._encodeOptions(func, this._addOptionsTo(options), values); const callParams = this._encodeOptions(func, this._addOptionsTo(options), values);
@ -233,13 +243,13 @@ export default class Contract {
if (!func.constant) { if (!func.constant) {
func.postTransaction = (options, values = []) => { func.postTransaction = (options, values = []) => {
return this._api.parity const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
.postTransaction(this._encodeOptions(func, this._addOptionsTo(options), values)); return this._api.parity.postTransaction(_options);
}; };
func.estimateGas = (options, values = []) => { func.estimateGas = (options, values = []) => {
return this._api.eth const _options = this._encodeOptions(func, this._addOptionsTo(options), values);
.estimateGas(this._encodeOptions(func, this._addOptionsTo(options), values)); return this._api.eth.estimateGas(_options);
}; };
} }

View File

@ -209,7 +209,10 @@ export default class Ws extends JsonRpcBase {
if (result.error) { if (result.error) {
this.error(event.data); this.error(event.data);
// Don't print error if request rejected...
if (!/rejected/.test(result.error.message)) {
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
}
const error = new TransportError(method, result.error.code, result.error.message); const error = new TransportError(method, result.error.code, result.error.message);
reject(error); reject(error);

View File

@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) {
throw new Error('Input to decodeMethodInput should be a hex value'); throw new Error('Input to decodeMethodInput should be a hex value');
} else if (paramdata.substr(0, 2) === '0x') { } else if (paramdata.substr(0, 2) === '0x') {
return decodeMethodInput(methodAbi, paramdata.slice(2)); return decodeMethodInput(methodAbi, paramdata.slice(2));
} else if (paramdata.length % 64 !== 0) {
throw new Error('Parameter length in decodeMethodInput not a multiple of 64 characters');
} }
} }

View File

@ -48,10 +48,6 @@ describe('api/util/decode', () => {
expect(() => decodeMethodInput({}, 'invalid')).to.throw(/should be a hex value/); expect(() => decodeMethodInput({}, 'invalid')).to.throw(/should be a hex value/);
}); });
it('throws on invalid lengths', () => {
expect(() => decodeMethodInput({}, DATA.slice(-32))).to.throw(/not a multiple of/);
});
it('correctly decodes valid inputs', () => { it('correctly decodes valid inputs', () => {
expect(decodeMethodInput({ expect(decodeMethodInput({
type: 'function', type: 'function',

View File

@ -36,6 +36,7 @@ import ContextProvider from '~/ui/ContextProvider';
import muiTheme from '~/ui/Theme'; import muiTheme from '~/ui/Theme';
import MainApplication from './main'; import MainApplication from './main';
import { patchApi } from '~/util/tx';
import { setApi } from '~/redux/providers/apiActions'; import { setApi } from '~/redux/providers/apiActions';
import './environment'; import './environment';
@ -60,6 +61,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
} }
const api = new SecureApi(`ws://${parityUrl}`, token); const api = new SecureApi(`ws://${parityUrl}`, token);
patchApi(api);
ContractInstances.create(api); ContractInstances.create(api);
const store = initStore(api, hashHistory); const store = initStore(api, hashHistory);

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { omitBy } from 'lodash';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui'; import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
@ -73,6 +74,9 @@ export default class WalletDetails extends Component {
renderMultisigDetails () { renderMultisigDetails () {
const { accounts, wallet, errors } = this.props; const { accounts, wallet, errors } = this.props;
// Wallets cannot create contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
return ( return (
<Form> <Form>
<AddressSelect <AddressSelect
@ -81,7 +85,7 @@ export default class WalletDetails extends Component {
value={ wallet.account } value={ wallet.account }
error={ errors.account } error={ errors.account }
onChange={ this.onAccoutChange } onChange={ this.onAccoutChange }
accounts={ accounts } accounts={ _accounts }
/> />
<Input <Input

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick } from 'lodash'; import { pick, omitBy } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -561,13 +561,19 @@ class DeployContract extends Component {
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts); const { accounts } = initProps;
// Skip Wallet accounts : they can't create Contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
const fromAddresses = Object.keys(_accounts);
return (state) => { return (state) => {
const balances = pick(state.balances.balances, fromAddresses); const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus; const { gasLimit } = state.nodeStatus;
return { return {
accounts: _accounts,
balances, balances,
gasLimit gasLimit
}; };

View File

@ -389,6 +389,7 @@ class ExecuteContract extends Component {
const { advancedOptions, amount, func, minBlock, values } = this.state; const { advancedOptions, amount, func, minBlock, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC; const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1; const finalstep = steps.length - 1;
const options = { const options = {
gas: this.gasStore.gas, gas: this.gasStore.gas,
gasPrice: this.gasStore.price, gasPrice: this.gasStore.price,

View File

@ -383,9 +383,7 @@ export default class TransferStore {
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag); const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const format = new BigNumber(senderBalance.token.format || 1); const format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet const available = new BigNumber(senderBalance.value).div(format);
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
let { value, valueError } = this; let { value, valueError } = this;
let totalEth = gasTotal; let totalEth = gasTotal;
@ -428,7 +426,6 @@ export default class TransferStore {
send () { send () {
const { options, values } = this._getTransferParams(); const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null; options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
return this._getTransferMethod().postTransaction(options, values); return this._getTransferMethod().postTransaction(options, values);
@ -440,18 +437,9 @@ export default class TransferStore {
} }
estimateGas () { estimateGas () {
if (this.isEth || !this.isWallet) {
return this._estimateGas(); return this._estimateGas();
} }
return Promise
.all([
this._estimateGas(true),
this._estimateGas()
])
.then((results) => results[0].plus(results[1]));
}
_getTransferMethod (gas = false, forceToken = false) { _getTransferMethod (gas = false, forceToken = false) {
const { isEth, isWallet } = this; const { isEth, isWallet } = this;

View File

@ -36,7 +36,7 @@ class WalletSettings extends Component {
}; };
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accountsInfo: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
senders: PropTypes.object.isRequired senders: PropTypes.object.isRequired
@ -113,7 +113,7 @@ class WalletSettings extends Component {
default: default:
case 'EDIT': case 'EDIT':
const { wallet, errors } = this.store; const { wallet, errors } = this.store;
const { accounts, senders } = this.props; const { accountsInfo, senders } = this.props;
return ( return (
<Form> <Form>
@ -137,7 +137,7 @@ class WalletSettings extends Component {
label='other wallet owners' label='other wallet owners'
value={ wallet.owners.slice() } value={ wallet.owners.slice() }
onChange={ this.store.onOwnersChange } onChange={ this.store.onOwnersChange }
accounts={ accounts } accounts={ accountsInfo }
param='address[]' param='address[]'
/> />
@ -190,7 +190,7 @@ class WalletSettings extends Component {
} }
renderChange (change) { renderChange (change) {
const { accounts } = this.props; const { accountsInfo } = this.props;
switch (change.type) { switch (change.type) {
case 'dailylimit': case 'dailylimit':
@ -229,7 +229,7 @@ class WalletSettings extends Component {
<InputAddress <InputAddress
disabled disabled
value={ change.value } value={ change.value }
accounts={ accounts } accounts={ accountsInfo }
/> />
</div> </div>
</div> </div>
@ -243,7 +243,7 @@ class WalletSettings extends Component {
<InputAddress <InputAddress
disabled disabled
value={ change.value } value={ change.value }
accounts={ accounts } accounts={ accountsInfo }
/> />
</div> </div>
</div> </div>
@ -329,7 +329,7 @@ function mapStateToProps (initState, initProps) {
const senders = pick(accounts, owners); const senders = pick(accounts, owners);
return () => { return () => {
return { accounts: accountsInfo, senders }; return { accountsInfo, senders };
}; };
} }

View File

@ -28,6 +28,8 @@ const STEPS = {
}; };
export default class WalletSettingsStore { export default class WalletSettingsStore {
accounts = {};
@observable step = null; @observable step = null;
@observable requests = []; @observable requests = [];
@observable deployState = ''; @observable deployState = '';

View File

@ -175,7 +175,7 @@ export function fetchBalances (_addresses) {
const { api, personal } = getState(); const { api, personal } = getState();
const { visibleAccounts, accounts } = personal; const { visibleAccounts, accounts } = personal;
const addresses = uniq(_addresses || visibleAccounts || []); const addresses = uniq((_addresses || visibleAccounts || []).concat(Object.keys(accounts)));
if (addresses.length === 0) { if (addresses.length === 0) {
return Promise.resolve(); return Promise.resolve();
@ -183,7 +183,7 @@ export function fetchBalances (_addresses) {
const fullFetch = addresses.length === 1; const fullFetch = addresses.length === 1;
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); const addressesToFetch = uniq(addresses);
return Promise return Promise
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))

View File

@ -14,14 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isEqual } from 'lodash'; import { isEqual, intersection } from 'lodash';
import { fetchBalances } from './balancesActions'; import { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions'; import { attachWallets } from './walletActions';
import Contract from '~/api/contract';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore'; import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import WalletsUtils from '~/util/wallets';
import { wallet as WalletAbi } from '~/contracts/abi';
export function personalAccountsInfo (accountsInfo) { export function personalAccountsInfo (accountsInfo) {
const addresses = [];
const accounts = {}; const accounts = {};
const contacts = {}; const contacts = {};
const contracts = {}; const contracts = {};
@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) {
.filter((account) => account.uuid || !account.meta.deleted) .filter((account) => account.uuid || !account.meta.deleted)
.forEach((account) => { .forEach((account) => {
if (account.uuid) { if (account.uuid) {
addresses.push(account.address);
accounts[account.address] = account; accounts[account.address] = account;
} else if (account.meta.wallet) { } else if (account.meta.wallet) {
account.wallet = true; account.wallet = true;
@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) {
// Load user contracts for Method Decoding // Load user contracts for Method Decoding
MethodDecodingStore.loadContracts(contracts); MethodDecodingStore.loadContracts(contracts);
return (dispatch) => { return (dispatch, getState) => {
const { api } = getState();
const _fetchOwners = Object
.values(wallets)
.map((wallet) => {
const walletContract = new Contract(api, WalletAbi);
return WalletsUtils.fetchOwners(walletContract.at(wallet.address));
});
Promise
.all(_fetchOwners)
.then((walletsOwners) => {
return Object
.values(wallets)
.map((wallet, index) => {
wallet.owners = walletsOwners[index].map((owner) => ({
address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
}));
return wallet;
});
})
.then((_wallets) => {
_wallets.forEach((wallet) => {
const owners = wallet.owners.map((o) => o.address);
// Owners ∩ Addresses not null : Wallet is owned
// by one of the accounts
if (intersection(owners, addresses).length > 0) {
accounts[wallet.address] = wallet;
} else {
contacts[wallet.address] = wallet;
}
});
const data = { const data = {
accountsInfo, accountsInfo,
accounts, contacts, contracts, wallets accounts, contacts, contracts
}; };
dispatch(_personalAccountsInfo(data)); dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets)); dispatch(attachWallets(wallets));
dispatch(fetchBalances());
});
}; };
} }

View File

@ -25,14 +25,13 @@ const initialState = {
hasContacts: false, hasContacts: false,
contracts: {}, contracts: {},
hasContracts: false, hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: [] visibleAccounts: []
}; };
export default handleActions({ export default handleActions({
personalAccountsInfo (state, action) { personalAccountsInfo (state, action) {
const { accountsInfo, accounts, contacts, contracts, wallets } = action; const accountsInfo = action.accountsInfo || state.accountsInfo;
const { accounts, contacts, contracts } = action;
return Object.assign({}, state, { return Object.assign({}, state, {
accountsInfo, accountsInfo,
@ -41,9 +40,7 @@ export default handleActions({
contacts, contacts,
hasContacts: Object.keys(contacts).length !== 0, hasContacts: Object.keys(contacts).length !== 0,
contracts, contracts,
hasContracts: Object.keys(contracts).length !== 0, hasContracts: Object.keys(contracts).length !== 0
wallets,
hasWallets: Object.keys(wallets).length !== 0
}); });
}, },

View File

@ -90,7 +90,7 @@ export default handleActions({
signerSuccessRejectRequest (state, action) { signerSuccessRejectRequest (state, action) {
const { id } = action.payload; const { id } = action.payload;
const rejected = Object.assign( const rejected = Object.assign(
state.pending.find(p => p.id === id), state.pending.find(p => p.id === id) || { id },
{ status: 'rejected' } { status: 'rejected' }
); );
return { return {

View File

@ -26,8 +26,8 @@ import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
import AccountCard from '~/ui/AccountCard'; import AccountCard from '~/ui/AccountCard';
import InputAddress from '~/ui/Form/InputAddress'; import InputAddress from '~/ui/Form/InputAddress';
import Portal from '~/ui/Portal'; import Portal from '~/ui/Portal';
import { validateAddress } from '~/util/validation';
import { nodeOrStringProptype } from '~/util/proptypes'; import { nodeOrStringProptype } from '~/util/proptypes';
import { validateAddress } from '~/util/validation';
import AddressSelectStore from './addressSelectStore'; import AddressSelectStore from './addressSelectStore';
import styles from './addressSelect.css'; import styles from './addressSelect.css';
@ -40,6 +40,7 @@ let currentId = 1;
@observer @observer
class AddressSelect extends Component { class AddressSelect extends Component {
static contextTypes = { static contextTypes = {
intl: React.PropTypes.object.isRequired,
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
muiTheme: PropTypes.object.isRequired muiTheme: PropTypes.object.isRequired
}; };
@ -55,7 +56,6 @@ class AddressSelect extends Component {
contacts: PropTypes.object, contacts: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
tokens: PropTypes.object, tokens: PropTypes.object,
wallets: PropTypes.object,
// Optional props // Optional props
allowInput: PropTypes.bool, allowInput: PropTypes.bool,
@ -160,6 +160,12 @@ class AddressSelect extends Component {
} }
const id = `addressSelect_${++currentId}`; const id = `addressSelect_${++currentId}`;
const ilHint = typeof hint === 'string' || !(hint && hint.props)
? (hint || '')
: this.context.intl.formatMessage(
hint.props,
hint.props.values || {}
);
return ( return (
<Portal <Portal
@ -174,7 +180,7 @@ class AddressSelect extends Component {
<input <input
id={ id } id={ id }
className={ styles.input } className={ styles.input }
placeholder={ hint } placeholder={ ilHint }
onBlur={ this.handleInputBlur } onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus } onFocus={ this.handleInputFocus }

View File

@ -78,14 +78,13 @@ export default class AddressSelectStore {
} }
@action setValues (props) { @action setValues (props) {
const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props; const { accounts = {}, contracts = {}, contacts = {} } = props;
const accountsN = Object.keys(accounts).length; const accountsN = Object.keys(accounts).length;
const contractsN = Object.keys(contracts).length; const contractsN = Object.keys(contracts).length;
const contactsN = Object.keys(contacts).length; const contactsN = Object.keys(contacts).length;
const walletsN = Object.keys(wallets).length;
if (accountsN + contractsN + contactsN + walletsN === 0) { if (accountsN + contractsN + contactsN === 0) {
return; return;
} }
@ -98,10 +97,7 @@ export default class AddressSelectStore {
defaultMessage='accounts' defaultMessage='accounts'
/> />
), ),
values: [].concat( values: Object.values(accounts)
Object.values(wallets),
Object.values(accounts)
)
}, },
{ {
key: 'contacts', key: 'contacts',

View File

@ -51,6 +51,7 @@ export default class Input extends Component {
PropTypes.string, PropTypes.string,
PropTypes.bool PropTypes.bool
]), ]),
autoFocus: PropTypes.bool,
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@ -112,7 +113,7 @@ export default class Input extends Component {
render () { render () {
const { value } = this.state; const { value } = this.state;
const { children, className, hideUnderline, disabled, error, focused, label } = this.props; const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props;
const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props; const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props;
const readOnly = this.props.readOnly || disabled; const readOnly = this.props.readOnly || disabled;
@ -138,6 +139,7 @@ export default class Input extends Component {
{ this.renderCopyButton() } { this.renderCopyButton() }
<TextField <TextField
autoComplete='off' autoComplete='off'
autoFocus={ autoFocus }
className={ className } className={ className }
errorText={ error } errorText={ error }
floatingLabelFixed floatingLabelFixed

View File

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

View File

@ -67,15 +67,7 @@ export default class TypedInput extends Component {
} }
render () { render () {
const { param } = this.props; const param = this.getParam();
if (typeof param === 'string') {
const parsedParam = parseAbiType(param);
if (parsedParam) {
return this.renderParam(parsedParam);
}
}
if (param) { if (param) {
return this.renderParam(param); return this.renderParam(param);
@ -234,7 +226,8 @@ export default class TypedInput extends Component {
} }
renderInteger (value = this.props.value, onChange = this.onChange) { renderInteger (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props; const { label, error, hint, min, max } = this.props;
const param = this.getParam();
const realValue = value && typeof value.toNumber === 'function' const realValue = value && typeof value.toNumber === 'function'
? value.toNumber() ? value.toNumber()
@ -263,7 +256,8 @@ export default class TypedInput extends Component {
* @see https://github.com/facebook/react/issues/1549 * @see https://github.com/facebook/react/issues/1549
*/ */
renderFloat (value = this.props.value, onChange = this.onChange) { renderFloat (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props; const { label, error, hint, min, max } = this.props;
const param = this.getParam();
const realValue = value && typeof value.toNumber === 'function' const realValue = value && typeof value.toNumber === 'function'
? value.toNumber() ? value.toNumber()
@ -379,7 +373,9 @@ export default class TypedInput extends Component {
} }
onAddField = () => { onAddField = () => {
const { value, onChange, param } = this.props; const { value, onChange } = this.props;
const param = this.getParam();
const newValues = [].concat(value, param.subtype.default); const newValues = [].concat(value, param.subtype.default);
onChange(newValues); onChange(newValues);
@ -392,4 +388,14 @@ export default class TypedInput extends Component {
onChange(newValues); onChange(newValues);
} }
getParam = () => {
const { param } = this.props;
if (typeof param === 'string') {
return parseAbiType(param);
}
return param;
}
} }

View File

@ -118,6 +118,15 @@ export default class MethodDecodingStore {
return Promise.resolve(result); return Promise.resolve(result);
} }
try {
const { signature } = this.api.util.decodeCallData(input);
if (signature === CONTRACT_CREATE || transaction.creates) {
result.contract = true;
return Promise.resolve({ ...result, deploy: true });
}
} catch (e) {}
return this return this
.isContract(contractAddress || transaction.creates) .isContract(contractAddress || transaction.creates)
.then((isContract) => { .then((isContract) => {
@ -132,7 +141,7 @@ export default class MethodDecodingStore {
result.params = paramdata; result.params = paramdata;
// Contract deployment // Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) { if (!signature) {
return Promise.resolve({ ...result, deploy: true }); return Promise.resolve({ ...result, deploy: true });
} }
@ -192,7 +201,7 @@ export default class MethodDecodingStore {
*/ */
isContract (contractAddress) { isContract (contractAddress) {
// If zero address, it isn't a contract // If zero address, it isn't a contract
if (/^(0x)?0*$/.test(contractAddress)) { if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return Promise.resolve(false); return Promise.resolve(false);
} }

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Portal from 'react-portal'; import ReactPortal from 'react-portal';
import keycode from 'keycode'; import keycode from 'keycode';
import { CloseIcon } from '~/ui/Icons'; import { CloseIcon } from '~/ui/Icons';
@ -24,7 +24,7 @@ import ParityBackground from '~/ui/ParityBackground';
import styles from './portal.css'; import styles from './portal.css';
export default class Protal extends Component { export default class Portal extends Component {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
@ -65,7 +65,7 @@ export default class Protal extends Component {
} }
return ( return (
<Portal isOpened onClose={ this.handleClose }> <ReactPortal isOpened onClose={ this.handleClose }>
<div <div
className={ classes.join(' ') } className={ classes.join(' ') }
onKeyDown={ this.handleKeyDown } onKeyDown={ this.handleKeyDown }
@ -75,7 +75,7 @@ export default class Protal extends Component {
{ this.renderCloseIcon() } { this.renderCloseIcon() }
{ children } { children }
</div> </div>
</Portal> </ReactPortal>
); );
} }

View File

@ -134,7 +134,7 @@ class TxHash extends Component {
const { api } = this.context; const { api } = this.context;
const { hash } = this.props; const { hash } = this.props;
if (error) { if (error || !hash || /^(0x)?0*$/.test(hash)) {
return; return;
} }

View File

@ -14,10 +14,93 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import WalletsUtils from '~/util/wallets';
const isValidReceipt = (receipt) => { const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
}; };
function getTxArgs (func, options, values = []) {
const { contract } = func;
const { api } = contract;
const address = options.from;
if (!address) {
return Promise.resolve({ func, options, values });
}
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return { func, options, values };
}
options.data = contract.getCallData(func, options, values);
options.to = options.to || contract.address;
if (!options.to) {
return { func, options, values };
}
return WalletsUtils
.getCallArgs(api, options, values)
.then((callArgs) => {
if (!callArgs) {
return { func, options, values };
}
return callArgs;
});
});
}
export function estimateGas (_func, _options, _values = []) {
return getTxArgs(_func, _options, _values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._estimateGas(options, values);
})
.then((gas) => {
return WalletsUtils
.isWallet(_func.contract.api, _options.from)
.then((isWallet) => {
if (isWallet) {
return gas.mul(1.5);
}
return gas;
});
});
}
export function postTransaction (_func, _options, _values = []) {
return getTxArgs(_func, _options, _values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values);
});
}
export function patchApi (api) {
api.patch = {
...api.patch,
contract: patchContract
};
}
export function patchContract (contract) {
contract._functions.forEach((func) => {
if (!func.constant) {
func._postTransaction = func.postTransaction;
func._estimateGas = func.estimateGas;
func.postTransaction = postTransaction.bind(contract, func);
func.estimateGas = estimateGas.bind(contract, func);
}
});
}
export function checkIfTxFailed (api, tx, gasSent) { export function checkIfTxFailed (api, tx, gasSent) {
return api.pollMethod('eth_getTransactionReceipt', tx) return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => { .then((receipt) => {

View File

@ -14,13 +14,92 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range, uniq } from 'lodash'; import BigNumber from 'bignumber.js';
import { intersection, range, uniq } from 'lodash';
import Contract from '~/api/contract';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation'; import { validateAddress } from '~/util/validation';
import WalletAbi from '~/contracts/abi/wallet.json';
const _cachedWalletLookup = {};
export default class WalletsUtils { export default class WalletsUtils {
static getCallArgs (api, options, values = []) {
const walletContract = new Contract(api, WalletAbi);
const promises = [
api.parity.accountsInfo(),
WalletsUtils.fetchOwners(walletContract.at(options.from))
];
return Promise
.all(promises)
.then(([ accounts, owners ]) => {
const addresses = Object.keys(accounts);
const owner = intersection(addresses, owners).pop();
if (!owner) {
return false;
}
return owner;
})
.then((owner) => {
if (!owner) {
return false;
}
const _options = Object.assign({}, options);
const { from, to, value = new BigNumber(0), data } = options;
delete _options.data;
const nextValues = [ to, value, data ];
const nextOptions = {
..._options,
from: owner,
to: from,
value: new BigNumber(0)
};
const execFunc = walletContract.instance.execute;
return { func: execFunc, options: nextOptions, values: nextValues };
});
}
/**
* Check whether the given address could be
* a Wallet. The result is cached in order not
* to make unnecessary calls on non-wallet accounts
*/
static isWallet (api, address) {
if (!_cachedWalletLookup[address]) {
const walletContract = new Contract(api, WalletAbi);
_cachedWalletLookup[address] = walletContract
.at(address)
.instance
.m_numOwners
.call()
.then((result) => {
if (!result || result.equals(0)) {
return false;
}
return true;
})
.then((bool) => {
_cachedWalletLookup[address] = Promise.resolve(bool);
return bool;
});
}
return _cachedWalletLookup[address];
}
static fetchRequire (walletContract) { static fetchRequire (walletContract) {
return walletContract.instance.m_required.call(); return walletContract.instance.m_required.call();
} }

View File

@ -42,7 +42,6 @@ export default class Header extends Component {
render () { render () {
const { account, balance, className, children, hideName } = this.props; const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account; const { address, meta, uuid } = account;
if (!account) { if (!account) {
return null; return null;
} }

View File

@ -34,7 +34,6 @@ class List extends Component {
order: PropTypes.string, order: PropTypes.string,
orderFallback: PropTypes.string, orderFallback: PropTypes.string,
search: PropTypes.array, search: PropTypes.array,
walletsOwners: PropTypes.object,
fetchCertifiers: PropTypes.func.isRequired, fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired, fetchCertifications: PropTypes.func.isRequired,
@ -58,7 +57,7 @@ class List extends Component {
} }
renderAccounts () { renderAccounts () {
const { accounts, balances, empty, link, walletsOwners, handleAddSearchToken } = this.props; const { accounts, balances, empty, link, handleAddSearchToken } = this.props;
if (empty) { if (empty) {
return ( return (
@ -76,7 +75,7 @@ class List extends Component {
const account = accounts[address] || {}; const account = accounts[address] || {};
const balance = balances[address] || {}; const balance = balances[address] || {};
const owners = walletsOwners && walletsOwners[address] || null; const owners = account.owners || null;
return ( return (
<div <div

View File

@ -157,7 +157,11 @@ export default class Summary extends Component {
const { link, noLink, account, name } = this.props; const { link, noLink, account, name } = this.props;
const { address } = account; const { address } = account;
const viewLink = `/${link || 'accounts'}/${address}`; const baseLink = account.wallet
? 'wallet'
: link || 'accounts';
const viewLink = `/${baseLink}/${address}`;
const content = ( const content = (
<IdentityName address={ address } name={ name } unknown /> <IdentityName address={ address } name={ name } unknown />

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add'; import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq, isEqual } from 'lodash'; import { uniq, isEqual, pickBy, omitBy } from 'lodash';
import List from './List'; import List from './List';
import { CreateAccount, CreateWallet } from '~/modals'; import { CreateAccount, CreateWallet } from '~/modals';
@ -36,9 +36,6 @@ class Accounts extends Component {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired, hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired,
walletsOwners: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired,
balances: PropTypes.object balances: PropTypes.object
} }
@ -62,8 +59,8 @@ class Accounts extends Component {
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets }); const prevAddresses = Object.keys(this.props.accounts);
const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets }); const nextAddresses = Object.keys(nextProps.accounts);
if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) { if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
this.setVisibleAccounts(nextProps); this.setVisibleAccounts(nextProps);
@ -75,8 +72,8 @@ class Accounts extends Component {
} }
setVisibleAccounts (props = this.props) { setVisibleAccounts (props = this.props) {
const { accounts, wallets, setVisibleAccounts } = props; const { accounts, setVisibleAccounts } = props;
const addresses = Object.keys({ ...accounts, ...wallets }); const addresses = Object.keys(accounts);
setVisibleAccounts(addresses); setVisibleAccounts(addresses);
} }
@ -115,30 +112,38 @@ class Accounts extends Component {
} }
renderAccounts () { renderAccounts () {
const { accounts, balances } = this.props;
const _accounts = omitBy(accounts, (a) => a.wallet);
const _hasAccounts = Object.keys(_accounts).length > 0;
if (!this.state.show) { if (!this.state.show) {
return this.renderLoading(this.props.accounts); return this.renderLoading(_accounts);
} }
const { accounts, hasAccounts, balances } = this.props;
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
return ( return (
<List <List
search={ searchValues } search={ searchValues }
accounts={ accounts } accounts={ _accounts }
balances={ balances } balances={ balances }
empty={ !hasAccounts } empty={ !_hasAccounts }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } /> handleAddSearchToken={ this.onAddSearchToken } />
); );
} }
renderWallets () { renderWallets () {
const { accounts, balances } = this.props;
const wallets = pickBy(accounts, (a) => a.wallet);
const hasWallets = Object.keys(wallets).length > 0;
if (!this.state.show) { if (!this.state.show) {
return this.renderLoading(this.props.wallets); return this.renderLoading(wallets);
} }
const { wallets, hasWallets, balances, walletsOwners } = this.props;
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
if (!wallets || Object.keys(wallets).length === 0) { if (!wallets || Object.keys(wallets).length === 0) {
@ -154,7 +159,6 @@ class Accounts extends Component {
empty={ !hasWallets } empty={ !hasWallets }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
walletsOwners={ walletsOwners }
/> />
); );
} }
@ -287,34 +291,12 @@ class Accounts extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal; const { accounts, hasAccounts } = state.personal;
const { balances } = state.balances; const { balances } = state.balances;
const walletsInfo = state.wallet.wallets;
const walletsOwners = Object
.keys(walletsInfo)
.map((wallet) => {
const owners = walletsInfo[wallet].owners || [];
return { return {
owners: owners.map((owner) => ({ accounts: accounts,
address: owner, hasAccounts: hasAccounts,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
})),
address: wallet
};
})
.reduce((walletsOwners, wallet) => {
walletsOwners[wallet.address] = wallet.owners;
return walletsOwners;
}, {});
return {
accounts,
hasAccounts,
wallets,
walletsOwners,
hasWallets,
balances balances
}; };
} }

View File

@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { omitBy } from 'lodash';
import { AddDapps, DappPermissions } from '~/modals'; import { AddDapps, DappPermissions } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store'; import PermissionStore from '~/modals/DappPermissions/store';
@ -150,8 +151,15 @@ class Dapps extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts } = state.personal; const { accounts } = state.personal;
/**
* Do not show the Wallet Accounts in the Dapps
* Permissions Modal. This will come in v1.6, but
* for now it would break dApps using Web3...
*/
const _accounts = omitBy(accounts, (account) => account.wallet);
return { return {
accounts accounts: _accounts
}; };
} }

View File

@ -23,6 +23,7 @@ export default class RequestPending extends Component {
static propTypes = { static propTypes = {
className: PropTypes.string, className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired, date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
@ -38,6 +39,7 @@ export default class RequestPending extends Component {
}; };
static defaultProps = { static defaultProps = {
focus: false,
isSending: false isSending: false
}; };
@ -49,7 +51,7 @@ export default class RequestPending extends Component {
}; };
render () { render () {
const { className, date, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props; const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
if (payload.sign) { if (payload.sign) {
const { sign } = payload; const { sign } = payload;
@ -58,6 +60,7 @@ export default class RequestPending extends Component {
<SignRequest <SignRequest
address={ sign.address } address={ sign.address }
className={ className } className={ className }
focus={ focus }
hash={ sign.hash } hash={ sign.hash }
id={ id } id={ id }
isFinished={ false } isFinished={ false }
@ -75,6 +78,7 @@ export default class RequestPending extends Component {
<TransactionPending <TransactionPending
className={ className } className={ className }
date={ date } date={ date }
focus={ focus }
gasLimit={ gasLimit } gasLimit={ gasLimit }
id={ id } id={ id }
isSending={ isSending } isSending={ isSending }

View File

@ -30,13 +30,19 @@ export default class SignRequest extends Component {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
hash: PropTypes.string.isRequired, hash: PropTypes.string.isRequired,
isFinished: PropTypes.bool.isRequired, isFinished: PropTypes.bool.isRequired,
isTest: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired,
className: PropTypes.string,
focus: PropTypes.bool,
isSending: PropTypes.bool, isSending: PropTypes.bool,
onConfirm: PropTypes.func, onConfirm: PropTypes.func,
onReject: PropTypes.func, onReject: PropTypes.func,
status: PropTypes.string, status: PropTypes.string
className: PropTypes.string, };
isTest: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired static defaultProps = {
focus: false
}; };
componentWillMount () { componentWillMount () {
@ -81,7 +87,7 @@ export default class SignRequest extends Component {
} }
renderActions () { renderActions () {
const { address, isFinished, status } = this.props; const { address, focus, isFinished, status } = this.props;
if (isFinished) { if (isFinished) {
if (status === 'confirmed') { if (status === 'confirmed') {
@ -111,6 +117,7 @@ export default class SignRequest extends Component {
return ( return (
<TransactionPendingForm <TransactionPendingForm
address={ address } address={ address }
focus={ focus }
isSending={ this.props.isSending } isSending={ this.props.isSending }
onConfirm={ this.onConfirm } onConfirm={ this.onConfirm }
onReject={ this.onReject } onReject={ this.onReject }

View File

@ -35,6 +35,7 @@ export default class TransactionPending extends Component {
static propTypes = { static propTypes = {
className: PropTypes.string, className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired, date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object, gasLimit: PropTypes.object,
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
@ -53,6 +54,10 @@ export default class TransactionPending extends Component {
}).isRequired }).isRequired
}; };
static defaultProps = {
focus: false
};
gasStore = new GasPriceEditor.Store(this.context.api, { gasStore = new GasPriceEditor.Store(this.context.api, {
gas: this.props.transaction.gas.toFixed(), gas: this.props.transaction.gas.toFixed(),
gasLimit: this.props.gasLimit, gasLimit: this.props.gasLimit,
@ -80,7 +85,7 @@ export default class TransactionPending extends Component {
} }
renderTransaction () { renderTransaction () {
const { className, id, isSending, isTest, store, transaction } = this.props; const { className, focus, id, isSending, isTest, store, transaction } = this.props;
const { totalValue } = this.state; const { totalValue } = this.state;
const { from, value } = transaction; const { from, value } = transaction;
@ -100,6 +105,7 @@ export default class TransactionPending extends Component {
value={ value } /> value={ value } />
<TransactionPendingForm <TransactionPendingForm
address={ from } address={ from }
focus={ focus }
isSending={ isSending } isSending={ isSending }
onConfirm={ this.onConfirm } onConfirm={ this.onConfirm }
onReject={ this.onReject } /> onReject={ this.onReject } />

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css';
class TransactionPendingFormConfirm extends Component { class TransactionPendingFormConfirm extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired onConfirm: PropTypes.func.isRequired,
} focus: PropTypes.bool
};
static defaultProps = {
focus: false
};
id = Math.random(); // for tooltip id = Math.random(); // for tooltip
@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component {
walletError: null walletError: null
} }
componentDidMount () {
this.focus();
}
componentWillReceiveProps (nextProps) {
if (!this.props.focus && nextProps.focus) {
this.focus(nextProps);
}
}
/**
* Properly focus on the input element when needed.
* This might be fixed some day in MaterialUI with
* an autoFocus prop.
*
* @see https://github.com/callemall/material-ui/issues/5632
*/
focus (props = this.props) {
if (props.focus) {
const textNode = ReactDOM.findDOMNode(this.refs.input);
if (!textNode) {
return;
}
const inputNode = textNode.querySelector('input');
inputNode && inputNode.focus();
}
}
render () { render () {
const { accounts, address, isSending } = this.props; const { account, address, isSending } = this.props;
const { password, wallet, walletError } = this.state; const { password, wallet, walletError } = this.state;
const account = accounts[address] || {};
const isExternal = !account.uuid; const isExternal = !account.uuid;
const passwordHint = account.meta && account.meta.passwordHint const passwordHint = account.meta && account.meta.passwordHint
@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component {
} }
onChange={ this.onModifyPassword } onChange={ this.onModifyPassword }
onKeyDown={ this.onKeyDown } onKeyDown={ this.onKeyDown }
ref='input'
type='password' type='password'
value={ password } /> value={ password }
/>
<div className={ styles.passwordHint }> <div className={ styles.passwordHint }>
{ passwordHint } { passwordHint }
</div> </div>
@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (initState, initProps) {
const { accounts } = state.personal; const { accounts } = initState.personal;
const { address } = initProps;
return { const account = accounts[address] || {};
accounts
return () => {
return { account };
}; };
} }

View File

@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component {
isSending: PropTypes.bool.isRequired, isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired,
className: PropTypes.string className: PropTypes.string,
focus: PropTypes.bool
};
static defaultProps = {
focus: false
}; };
state = { state = {
@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component {
} }
renderForm () { renderForm () {
const { address, isSending, onConfirm, onReject } = this.props; const { address, focus, isSending, onConfirm, onReject } = this.props;
if (this.state.isRejectOpen) { if (this.state.isRejectOpen) {
return ( return (
@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component {
return ( return (
<TransactionPendingFormConfirm <TransactionPendingFormConfirm
address={ address } address={ address }
focus={ focus }
isSending={ isSending } isSending={ isSending }
onConfirm={ onConfirm } /> onConfirm={ onConfirm }
/>
); );
} }

View File

@ -78,7 +78,7 @@ class Embedded extends Component {
); );
} }
renderPending = (data) => { renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props; const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data; const { date, id, isSending, payload } = data;
@ -86,6 +86,7 @@ class Embedded extends Component {
<RequestPending <RequestPending
className={ styles.request } className={ styles.request }
date={ date } date={ date }
focus={ index === 0 }
gasLimit={ gasLimit } gasLimit={ gasLimit }
id={ id } id={ id }
isSending={ isSending } isSending={ isSending }

View File

@ -104,7 +104,7 @@ class RequestsPage extends Component {
); );
} }
renderPending = (data) => { renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props; const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data; const { date, id, isSending, payload } = data;
@ -112,6 +112,7 @@ class RequestsPage extends Component {
<RequestPending <RequestPending
className={ styles.request } className={ styles.request }
date={ date } date={ date }
focus={ index === 0 }
gasLimit={ gasLimit } gasLimit={ gasLimit }
id={ id } id={ id }
isSending={ isSending } isSending={ isSending }

View File

@ -55,14 +55,20 @@ export default class WalletDetails extends Component {
return null; return null;
} }
const ownersList = owners.map((address, idx) => ( const ownersList = owners.map((owner, idx) => {
const address = typeof owner === 'object'
? owner.address
: owner;
return (
<InputAddress <InputAddress
key={ `${idx}_${address}` } key={ `${idx}_${address}` }
value={ address } value={ address }
disabled disabled
text text
/> />
)); );
});
return ( return (
<div> <div>

View File

@ -57,12 +57,12 @@ export default class WalletTransactions extends Component {
); );
} }
const txRows = transactions.map((transaction) => { const txRows = transactions.slice(0, 15).map((transaction, index) => {
const { transactionHash, blockNumber, from, to, value, data } = transaction; const { transactionHash, blockNumber, from, to, value, data } = transaction;
return ( return (
<TxRow <TxRow
key={ transactionHash } key={ `${transactionHash}_${index}` }
tx={ { tx={ {
hash: transactionHash, hash: transactionHash,
input: data && bytesToHex(data) || '', input: data && bytesToHex(data) || '',

View File

@ -64,13 +64,14 @@ class Wallet extends Component {
}; };
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, address: PropTypes.string.isRequired,
balance: nullableProptype(PropTypes.object.isRequired), balance: nullableProptype(PropTypes.object.isRequired),
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
address: PropTypes.string.isRequired, isTest: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired, owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired walletAccount: nullableProptype(PropTypes.object).isRequired
}; };
state = { state = {
@ -104,28 +105,26 @@ class Wallet extends Component {
} }
render () { render () {
const { wallets, balance, address } = this.props; const { walletAccount, balance, wallet } = this.props;
const wallet = (wallets || {})[address]; if (!walletAccount) {
if (!wallet) {
return null; return null;
} }
const { owners, require, dailylimit } = this.props.wallet; const { owners, require, dailylimit } = wallet;
return ( return (
<div className={ styles.wallet }> <div className={ styles.wallet }>
{ this.renderEditDialog(wallet) } { this.renderEditDialog(walletAccount) }
{ this.renderSettingsDialog() } { this.renderSettingsDialog() }
{ this.renderTransferDialog() } { this.renderTransferDialog() }
{ this.renderDeleteDialog(wallet) } { this.renderDeleteDialog(walletAccount) }
{ this.renderActionbar() } { this.renderActionbar() }
<Page> <Page>
<div className={ styles.info }> <div className={ styles.info }>
<Header <Header
className={ styles.header } className={ styles.header }
account={ wallet } account={ walletAccount }
balance={ balance } balance={ balance }
isContract isContract
> >
@ -209,32 +208,47 @@ class Wallet extends Component {
} }
renderActionbar () { renderActionbar () {
const { balance } = this.props; const { balance, owned } = this.props;
const showTransferButton = !!(balance && balance.tokens); const showTransferButton = !!(balance && balance.tokens);
const buttons = [ const buttons = [];
if (owned) {
buttons.push(
<Button <Button
key='transferFunds' key='transferFunds'
icon={ <ContentSend /> } icon={ <ContentSend /> }
label='transfer' label='transfer'
disabled={ !showTransferButton } disabled={ !showTransferButton }
onClick={ this.onTransferClick } />, onClick={ this.onTransferClick } />
);
}
buttons.push(
<Button <Button
key='delete' key='delete'
icon={ <ActionDelete /> } icon={ <ActionDelete /> }
label='delete' label='delete'
onClick={ this.showDeleteDialog } />, onClick={ this.showDeleteDialog } />
);
buttons.push(
<Button <Button
key='editmeta' key='editmeta'
icon={ <ContentCreate /> } icon={ <ContentCreate /> }
label='edit' label='edit'
onClick={ this.onEditClick } />, onClick={ this.onEditClick } />
);
if (owned) {
buttons.push(
<Button <Button
key='settings' key='settings'
icon={ <SettingsIcon /> } icon={ <SettingsIcon /> }
label='settings' label='settings'
onClick={ this.onSettingsClick } /> onClick={ this.onSettingsClick } />
]; );
}
return ( return (
<Actionbar <Actionbar
@ -293,12 +307,11 @@ class Wallet extends Component {
return null; return null;
} }
const { wallets, balance, images, address } = this.props; const { walletAccount, balance, images } = this.props;
const wallet = wallets[address];
return ( return (
<Transfer <Transfer
account={ wallet } account={ walletAccount }
balance={ balance } balance={ balance }
images={ images } images={ images }
onClose={ this.onTransferClose } onClose={ this.onTransferClose }
@ -342,20 +355,27 @@ function mapStateToProps (_, initProps) {
return (state) => { return (state) => {
const { isTest } = state.nodeStatus; const { isTest } = state.nodeStatus;
const { wallets } = state.personal; const { accountsInfo = {}, accounts = {} } = state.personal;
const { balances } = state.balances; const { balances } = state.balances;
const { images } = state; const { images } = state;
const walletAccount = accounts[address] || accountsInfo[address] || null;
if (walletAccount) {
walletAccount.address = address;
}
const wallet = state.wallet.wallets[address] || {}; const wallet = state.wallet.wallets[address] || {};
const balance = balances[address] || null; const balance = balances[address] || null;
const owned = !!accounts[address];
return { return {
isTest, address,
wallets,
balance, balance,
images, images,
address, isTest,
wallet owned,
wallet,
walletAccount
}; };
}; };
} }

View File

@ -38,6 +38,13 @@ module.exports = {
library: '[name].js', library: '[name].js',
libraryTarget: 'umd' libraryTarget: 'umd'
}, },
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
}
},
module: { module: {
rules: [ rules: [
{ {

View File

@ -69,6 +69,9 @@ module.exports = {
}, },
resolve: { resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
},
modules: [ modules: [
path.resolve('./src'), path.resolve('./src'),
path.join(__dirname, '../node_modules') path.join(__dirname, '../node_modules')

View File

@ -64,6 +64,13 @@ module.exports = {
} }
] ]
}, },
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
}
},
output: { output: {
filename: '[name].js', filename: '[name].js',
path: path.resolve(__dirname, '../', `${DEST}/`), path: path.resolve(__dirname, '../', `${DEST}/`),