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');
}
const mediates = tokens.map((token) => Encoder.encodeToken(token));
const mediates = tokens.map((token, index) => Encoder.encodeToken(token, index));
const inits = mediates
.map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx)))
.join('');
@ -36,37 +36,40 @@ export default class Encoder {
return `${inits}${closings}`;
}
static encodeToken (token) {
static encodeToken (token, index = 0) {
if (!isInstanceOf(token, Token)) {
throw new Error('token should be instanceof Token');
}
switch (token.type) {
case 'address':
return new Mediate('raw', padAddress(token.value));
try {
switch (token.type) {
case 'address':
return new Mediate('raw', padAddress(token.value));
case 'int':
case 'uint':
return new Mediate('raw', padU32(token.value));
case 'int':
case 'uint':
return new Mediate('raw', padU32(token.value));
case 'bool':
return new Mediate('raw', padBool(token.value));
case 'bool':
return new Mediate('raw', padBool(token.value));
case 'fixedBytes':
return new Mediate('raw', padFixedBytes(token.value));
case 'fixedBytes':
return new Mediate('raw', padFixedBytes(token.value));
case 'bytes':
return new Mediate('prefixed', padBytes(token.value));
case 'bytes':
return new Mediate('prefixed', padBytes(token.value));
case 'string':
return new Mediate('prefixed', padString(token.value));
case 'string':
return new Mediate('prefixed', padString(token.value));
case 'fixedArray':
case 'array':
return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token)));
default:
throw new Error(`Invalid token type ${token.type} in encodeToken`);
case 'fixedArray':
case 'array':
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}`);
}
throw new Error(`Invalid token type ${token.type} in encodeToken`);
}
}

View File

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

View File

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

View File

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

View File

@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) {
throw new Error('Input to decodeMethodInput should be a hex value');
} else if (paramdata.substr(0, 2) === '0x') {
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/);
});
it('throws on invalid lengths', () => {
expect(() => decodeMethodInput({}, DATA.slice(-32))).to.throw(/not a multiple of/);
});
it('correctly decodes valid inputs', () => {
expect(decodeMethodInput({
type: 'function',

View File

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

View File

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

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// 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 React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
@ -561,13 +561,19 @@ class DeployContract extends Component {
}
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) => {
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return {
accounts: _accounts,
balances,
gasLimit
};

View File

@ -389,6 +389,7 @@ class ExecuteContract extends Component {
const { advancedOptions, amount, func, minBlock, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = {
gas: this.gasStore.gas,
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 format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
const available = new BigNumber(senderBalance.value).div(format);
let { value, valueError } = this;
let totalEth = gasTotal;
@ -428,7 +426,6 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
return this._getTransferMethod().postTransaction(options, values);
@ -440,16 +437,7 @@ export default class TransferStore {
}
estimateGas () {
if (this.isEth || !this.isWallet) {
return this._estimateGas();
}
return Promise
.all([
this._estimateGas(true),
this._estimateGas()
])
.then((results) => results[0].plus(results[1]));
return this._estimateGas();
}
_getTransferMethod (gas = false, forceToken = false) {

View File

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

View File

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

View File

@ -175,7 +175,7 @@ export function fetchBalances (_addresses) {
const { api, personal } = getState();
const { visibleAccounts, accounts } = personal;
const addresses = uniq(_addresses || visibleAccounts || []);
const addresses = uniq((_addresses || visibleAccounts || []).concat(Object.keys(accounts)));
if (addresses.length === 0) {
return Promise.resolve();
@ -183,7 +183,7 @@ export function fetchBalances (_addresses) {
const fullFetch = addresses.length === 1;
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
const addressesToFetch = uniq(addresses);
return Promise
.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
// 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 { attachWallets } from './walletActions';
import Contract from '~/api/contract';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import WalletsUtils from '~/util/wallets';
import { wallet as WalletAbi } from '~/contracts/abi';
export function personalAccountsInfo (accountsInfo) {
const addresses = [];
const accounts = {};
const contacts = {};
const contracts = {};
@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) {
.filter((account) => account.uuid || !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
addresses.push(account.address);
accounts[account.address] = account;
} else if (account.meta.wallet) {
account.wallet = true;
@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) {
// Load user contracts for Method Decoding
MethodDecodingStore.loadContracts(contracts);
return (dispatch) => {
const data = {
accountsInfo,
accounts, contacts, contracts, wallets
};
return (dispatch, getState) => {
const { api } = getState();
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
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 = {
accountsInfo,
accounts, contacts, contracts
};
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
dispatch(fetchBalances());
});
};
}

View File

@ -25,14 +25,13 @@ const initialState = {
hasContacts: false,
contracts: {},
hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: []
};
export default handleActions({
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, {
accountsInfo,
@ -41,9 +40,7 @@ export default handleActions({
contacts,
hasContacts: Object.keys(contacts).length !== 0,
contracts,
hasContracts: Object.keys(contracts).length !== 0,
wallets,
hasWallets: Object.keys(wallets).length !== 0
hasContracts: Object.keys(contracts).length !== 0
});
},

View File

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

View File

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

View File

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

View File

@ -51,6 +51,7 @@ export default class Input extends Component {
PropTypes.string,
PropTypes.bool
]),
autoFocus: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
@ -112,7 +113,7 @@ export default class Input extends Component {
render () {
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 readOnly = this.props.readOnly || disabled;
@ -138,6 +139,7 @@ export default class Input extends Component {
{ this.renderCopyButton() }
<TextField
autoComplete='off'
autoFocus={ autoFocus }
className={ className }
errorText={ error }
floatingLabelFixed

View File

@ -25,7 +25,6 @@ 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,
@ -34,7 +33,7 @@ class InputAddressSelect extends Component {
};
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 (
<AddressSelect
@ -42,7 +41,6 @@ class InputAddressSelect extends Component {
accounts={ accounts }
contacts={ contacts }
contracts={ contracts }
wallets={ wallets }
error={ error }
label={ label }
hint={ hint }
@ -53,13 +51,12 @@ class InputAddressSelect extends Component {
}
function mapStateToProps (state) {
const { accounts, contacts, contracts, wallets } = state.personal;
const { accounts, contacts, contracts } = state.personal;
return {
accounts,
contacts,
contracts,
wallets
contracts
};
}

View File

@ -67,15 +67,7 @@ export default class TypedInput extends Component {
}
render () {
const { param } = this.props;
if (typeof param === 'string') {
const parsedParam = parseAbiType(param);
if (parsedParam) {
return this.renderParam(parsedParam);
}
}
const param = this.getParam();
if (param) {
return this.renderParam(param);
@ -234,7 +226,8 @@ export default class TypedInput extends Component {
}
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'
? value.toNumber()
@ -263,7 +256,8 @@ export default class TypedInput extends Component {
* @see https://github.com/facebook/react/issues/1549
*/
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'
? value.toNumber()
@ -379,7 +373,9 @@ export default class TypedInput extends Component {
}
onAddField = () => {
const { value, onChange, param } = this.props;
const { value, onChange } = this.props;
const param = this.getParam();
const newValues = [].concat(value, param.subtype.default);
onChange(newValues);
@ -392,4 +388,14 @@ export default class TypedInput extends Component {
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);
}
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
.isContract(contractAddress || transaction.creates)
.then((isContract) => {
@ -132,7 +141,7 @@ export default class MethodDecodingStore {
result.params = paramdata;
// Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
if (!signature) {
return Promise.resolve({ ...result, deploy: true });
}
@ -192,7 +201,7 @@ export default class MethodDecodingStore {
*/
isContract (contractAddress) {
// If zero address, it isn't a contract
if (/^(0x)?0*$/.test(contractAddress)) {
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return Promise.resolve(false);
}

View File

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

View File

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

View File

@ -14,10 +14,93 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import WalletsUtils from '~/util/wallets';
const isValidReceipt = (receipt) => {
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) {
return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => {

View File

@ -14,13 +14,92 @@
// You should have received a copy of the GNU General Public License
// 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 { validateAddress } from '~/util/validation';
import WalletAbi from '~/contracts/abi/wallet.json';
const _cachedWalletLookup = {};
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) {
return walletContract.instance.m_required.call();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { omitBy } from 'lodash';
import { AddDapps, DappPermissions } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store';
@ -150,8 +151,15 @@ class Dapps extends Component {
function mapStateToProps (state) {
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 {
accounts
accounts: _accounts
};
}

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton';
@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css';
class TransactionPendingFormConfirm extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
account: PropTypes.object.isRequired,
address: PropTypes.string.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
@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component {
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 () {
const { accounts, address, isSending } = this.props;
const { account, address, isSending } = this.props;
const { password, wallet, walletError } = this.state;
const account = accounts[address] || {};
const isExternal = !account.uuid;
const passwordHint = account.meta && account.meta.passwordHint
@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component {
}
onChange={ this.onModifyPassword }
onKeyDown={ this.onKeyDown }
ref='input'
type='password'
value={ password } />
value={ password }
/>
<div className={ styles.passwordHint }>
{ passwordHint }
</div>
@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component {
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
function mapStateToProps (initState, initProps) {
const { accounts } = initState.personal;
const { address } = initProps;
return {
accounts
const account = accounts[address] || {};
return () => {
return { account };
};
}

View File

@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component {
isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
className: PropTypes.string
className: PropTypes.string,
focus: PropTypes.bool
};
static defaultProps = {
focus: false
};
state = {
@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component {
}
renderForm () {
const { address, isSending, onConfirm, onReject } = this.props;
const { address, focus, isSending, onConfirm, onReject } = this.props;
if (this.state.isRejectOpen) {
return (
@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component {
return (
<TransactionPendingFormConfirm
address={ address }
focus={ focus }
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 { date, id, isSending, payload } = data;
@ -86,6 +86,7 @@ class Embedded extends Component {
<RequestPending
className={ styles.request }
date={ date }
focus={ index === 0 }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }

View File

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

View File

@ -55,14 +55,20 @@ export default class WalletDetails extends Component {
return null;
}
const ownersList = owners.map((address, idx) => (
<InputAddress
key={ `${idx}_${address}` }
value={ address }
disabled
text
/>
));
const ownersList = owners.map((owner, idx) => {
const address = typeof owner === 'object'
? owner.address
: owner;
return (
<InputAddress
key={ `${idx}_${address}` }
value={ address }
disabled
text
/>
);
});
return (
<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;
return (
<TxRow
key={ transactionHash }
key={ `${transactionHash}_${index}` }
tx={ {
hash: transactionHash,
input: data && bytesToHex(data) || '',

View File

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

View File

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

View File

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

View File

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