Export acc js (#4973)

* Export account RPC

* Removing GethDirectory and ParityDirectory

* js export accounts as json

* js export accounts as json

* api - then - catch

* final touches

* pass

* oops

* individual accounts

* refactoring

* refactor one

* refactor one

* refactor two

* some grumble fixes

* file name changes

* constructor

* constructor

* git recognize file name change

* spec and updates

* specs

* one tiny fix

* one tiny fix

* grumbles

* more grumbles

* sliders

* ff

* pointer default

* grumbles

* almost ready

* lots of updates

* accountList

* stupid debuglog

* bug fix

* bug fix

* some more good ol fashioned updates

* filter accounts

* clean

* update spec

* ff

* ff-f

* balances fix
This commit is contained in:
Craig O'Connor
2017-04-26 05:34:48 -04:00
committed by Jaco Greeff
parent 3be3b78c90
commit cf904b6b2f
15 changed files with 765 additions and 15 deletions

View File

@@ -0,0 +1,177 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { personalAccountsInfo } from '~/redux/providers/personalActions';
import { AccountCard, Button, Portal, SelectionList } from '~/ui';
import { CancelIcon, CheckIcon } from '~/ui/Icons';
import ExportInput from './exportInput';
import ExportStore from './exportStore';
@observer
class ExportAccount extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
personalAccountsInfo: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
componentWillMount () {
const { accounts, newError } = this.props;
this.exportStore = new ExportStore(this.context.api, accounts, newError, null);
}
render () {
const { canExport } = this.exportStore;
return (
<Portal
buttons={ [
<Button
icon={ <CancelIcon /> }
key='cancel'
label={
<FormattedMessage
id='export.accounts.button.cancel'
defaultMessage='Cancel'
/>
}
onClick={ this.onClose }
/>,
<Button
disabled={ !canExport }
icon={ <CheckIcon /> }
key='execute'
label={
<FormattedMessage
id='export.accounts.button.export'
defaultMessage='Export'
/>
}
onClick={ this.onExport }
/>
] }
onClose={ this.onClose }
open
title={
<FormattedMessage
id='export.accounts.title'
defaultMessage='Export an Account'
/>
}
>
{ this.renderList() }
</Portal>
);
}
renderList () {
const { accounts } = this.props;
const { selectedAccounts } = this.exportStore;
const accountList = Object.values(accounts)
.filter((account) => account.uuid)
.map((account) => {
account.checked = !!(selectedAccounts[account.address]);
return account;
});
return (
<SelectionList
items={ accountList }
noStretch
onSelectClick={ this.onSelect }
renderItem={ this.renderAccount }
/>
);
}
renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
const { changePassword, getPassword } = this.exportStore;
const password = getPassword(account);
return (
<AccountCard
account={ account }
balance={ balance }
>
<div>
<ExportInput
account={ account }
value={ password }
onClick={ this.onClick }
onChange={ changePassword }
/>
</div>
</AccountCard>
);
}
onSelect = (account) => {
this.exportStore.toggleSelectedAccount(account.address);
}
onClick = (address) => {
this.exportStore.onClick(address);
}
onClose = () => {
this.props.onClose();
}
onExport = () => {
this.exportStore.onExport();
}
}
function mapStateToProps (state) {
const { balances } = state;
const { accounts } = state.personal;
return {
accounts,
balances
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError,
personalAccountsInfo
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ExportAccount);

View File

@@ -0,0 +1,76 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import ExportAccount from './';
const ADDRESS = '0x0123456789012345678901234567890123456789';
let component;
let NEWERROR;
let PAI;
let ONCLOSE;
let reduxStore;
function createReduxStore () {
reduxStore = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {
[ADDRESS]: {}
}
},
personal: {
accounts: {
[ADDRESS]: {
address: ADDRESS
}
}
}
};
}
};
return reduxStore;
}
function render () {
component = shallow(
<ExportAccount
newError={ NEWERROR }
personalAccountsInfo={ PAI }
onClose={ ONCLOSE }
/>,
{
context: { api: {}, store: createReduxStore() }
}
);
return component;
}
describe('CreateExportModal', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@@ -0,0 +1,61 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Input } from '~/ui/Form';
export default class ExportInput extends Component {
static propTypes = {
account: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string
};
render () {
const { value, onChange } = this.props;
return (
<Input
type='password'
label={
<FormattedMessage
id='export.setPassword.label'
defaultMessage='Password'
/>
}
hint={
<FormattedMessage
id='export.setPassword.hint'
defaultMessage='Enter password Here'
/>
}
value={ value }
onClick={ this.onClick }
onChange={ onChange }
/>
);
}
onClick = (event) => {
const { account, onClick } = this.props;
event.stopPropagation();
onClick && onClick(account.address);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './exportInput';

View File

@@ -0,0 +1,106 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable } from 'mobx';
import FileSaver from 'file-saver';
export default class ExportStore {
@observable canExport = false;
@observable selectedAccount = '';
@observable selectedAccounts = {};
@observable passwordInputs = {};
constructor (api, accounts, newError, address) {
this.accounts = accounts;
this._api = api;
this._newError = newError;
if (address) {
this.selectedAccounts[address] = true;
this.selectedAccount = address;
}
}
@action changePassword = (event, password) => {
this.passwordInputs[this.selectedAccount] = password;
}
@action getPassword = (address) => {
return this.passwordInputs[address];
}
@action onClick = (address) => {
this.selectedAccount = address;
}
@action resetAccountValue = () => {
this.passwordInputs[this.selectedAccount] = '';
}
@action setAccounts = (accounts) => {
this.accounts = accounts;
}
@action setSelectedAccount = (addr) => {
this.selectedAccounts[addr] = true;
this.canExport = true;
}
@action toggleSelectedAccount = (addr) => {
if (this.selectedAccounts[addr]) {
delete this.selectedAccounts[addr];
} else {
this.selectedAccounts[addr] = true;
}
this.canExport = false;
Object
.keys(this.selectedAccounts)
.forEach((address) => {
if (this.selectedAccounts[address]) {
this.canExport = true;
}
});
}
onExport = (event) => {
const { parity } = this._api;
const accounts = Object.keys(this.selectedAccounts);
accounts.forEach((address) => {
let password = this.passwordInputs[address];
parity
.exportAccount(address, password)
.then((content) => {
const text = JSON.stringify(content, null, 4);
const blob = new Blob([ text ], { type: 'application/json' });
const filename = this.accounts[address].uuid;
FileSaver.saveAs(blob, `${filename}.json`);
this.accountValue = '';
if (event) { event(); }
})
.catch((err) => {
const { name, meta } = this.accounts[address];
const { passwordHint } = meta;
this._newError({
message: `[${err.code}] Account "${name}" - Incorrect password. (Password Hint: ${passwordHint})`
});
});
});
}
}

View File

@@ -0,0 +1,133 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import ExportStore from './exportStore';
const ADDRESS = '0x00000123456789abcdef123456789abcdef123456789abcdef';
const ADDRESS_2 = '0x123456789abcdef123456789abcdef123456789abcdef00000';
const ACCOUNTS = { ADDRESS: {}, ADDRESS_2: {} };
let api;
let AccountStore;
function createApi () {
return {
eth: {
},
parity: {
exportAccount: sinon.stub().resolves({})
}
};
}
function createMultiAccountStore (loadGeth) {
api = createApi();
AccountStore = new ExportStore(api, ACCOUNTS, null, null);
return AccountStore;
}
describe('modals/exportAccount/Store', () => {
beforeEach(() => {
createMultiAccountStore();
});
describe('constructor', () => {
it('insert api', () => {
expect(AccountStore._api).to.deep.equal(api);
});
it('insert accounts', () => {
expect(AccountStore.accounts).to.deep.equal(ACCOUNTS);
});
it('newError created', () => {
expect(AccountStore._newError).to.deep.equal(null);
});
});
describe('@action', () => {
describe('toggleSelectedAccount', () => {
it('Updates the selected accounts', () => {
// First set selectedAccounts
AccountStore.selectedAccounts = {
[ADDRESS]: true,
[ADDRESS_2]: false
};
// Toggle
AccountStore.toggleSelectedAccount(ADDRESS_2);
// Prep eqality
const eq = {
[ADDRESS]: true,
[ADDRESS_2]: true
};
// Check equality
expect(JSON.stringify(AccountStore.selectedAccounts)).to.deep.equal(JSON.stringify(eq));
});
});
describe('getPassword', () => {
it('Grab from the selected accounts input', () => {
// First set passwordInputs
AccountStore.passwordInputs = {
[ADDRESS]: 'abc'
};
// getPassword
const pass = AccountStore.getPassword(ADDRESS);
// Check equality
expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal(pass);
});
});
describe('setPassword & getPassword', () => {
it('First save the input of the selected account, than get the input.', () => {
// Set password
AccountStore.selectedAccount = ADDRESS;
// Set new pass
AccountStore.changePassword(null, 'abc');
// getPassword
const pass = AccountStore.getPassword(ADDRESS);
// Check equality
expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal(pass);
});
});
describe('changePassword', () => {
it('Change the stored value with the new input.', () => {
// First set selectedAccounts
AccountStore.selectedAccounts = {
[ADDRESS]: true,
[ADDRESS_2]: false
};
// First set passwordInputs
AccountStore.passwordInputs = {
[ADDRESS]: 'abc'
};
// 'Click' on the address:
AccountStore.onClick(ADDRESS);
// Change password
AccountStore.changePassword(null, '123');
// Check equality
expect(AccountStore.passwordInputs[ADDRESS]).to.deep.equal('123');
});
});
});
});

View File

@@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './exportAccount';

View File

@@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount';
export DeployContract from './DeployContract';
export EditMeta from './EditMeta';
export ExecuteContract from './ExecuteContract';
export ExportAccount from './ExportAccount';
export Faucet from './Faucet';
export FirstRun from './FirstRun';
export LoadContract from './LoadContract';