Visible accounts for dapps (default whitelist) (#3898)

* Add APIs for Dapp management

* Move AddDapps modal

* Add DappsPermissions Modal (basics)

* Allow whitelist editing

* Add select/unselect tests

* Case

* Case

* Modal render/non-render tests

* UI made slightly prettier

* Adjust spacing

* Allow get/set of null for default whitelist (all)

* Allow null = all for selection

* Adjust selected background

* Address valid comment on formatters location
This commit is contained in:
Jaco Greeff 2016-12-27 16:23:41 +01:00 committed by Gav Wood
parent 80eae8cc49
commit b27c809c64
17 changed files with 650 additions and 51 deletions

View File

@ -24,6 +24,10 @@ export function inAddress (address) {
return inHex(address);
}
export function inAddresses (addresses) {
return (addresses || []).map(inAddress);
}
export function inBlockNumber (blockNumber) {
if (isString(blockNumber)) {
switch (blockNumber) {

View File

@ -42,6 +42,10 @@ export function outAddress (address) {
return toChecksumAddress(address);
}
export function outAddresses (addresses) {
return (addresses || []).map(outAddress);
}
export function outBlock (block) {
if (block) {
Object.keys(block).forEach((key) => {

View File

@ -14,8 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inData, inHex, inNumber16, inOptions } from '../../format/input';
import { outAccountInfo, outAddress, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions } from '../../format/input';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
export default class Parity {
constructor (transport) {
@ -128,6 +128,18 @@ export default class Parity {
.execute('parity_generateSecretPhrase');
}
getDappsAddresses (dappId) {
return this._transport
.execute('parity_getDappsAddresses', dappId)
.then(outAddresses);
}
getNewDappsWhitelist () {
return this._transport
.execute('parity_getNewDappsWhitelist')
.then((addresses) => addresses ? addresses.map(outAddress) : null);
}
hashContent (url) {
return this._transport
.execute('parity_hashContent', url);
@ -135,8 +147,8 @@ export default class Parity {
importGethAccounts (accounts) {
return this._transport
.execute('parity_importGethAccounts', (accounts || []).map(inAddress))
.then((accounts) => (accounts || []).map(outAddress));
.execute('parity_importGethAccounts', inAddresses)
.then(outAddresses);
}
killAccount (account, password) {
@ -144,6 +156,11 @@ export default class Parity {
.execute('parity_killAccount', inAddress(account), password);
}
listRecentDapps () {
return this._transport
.execute('parity_listRecentDapps');
}
removeAddress (address) {
return this._transport
.execute('parity_removeAddress', inAddress(address));
@ -152,7 +169,7 @@ export default class Parity {
listGethAccounts () {
return this._transport
.execute('parity_listGethAccounts')
.then((accounts) => (accounts || []).map(outAddress));
.then(outAddresses);
}
localTransactions () {
@ -289,6 +306,11 @@ export default class Parity {
.execute('parity_setAuthor', inAddress(address));
}
setDappsAddresses (dappId, addresses) {
return this._transport
.execute('parity_setDappsAddresses', dappId, inAddresses(addresses));
}
setExtraData (data) {
return this._transport
.execute('parity_setExtraData', inData(data));
@ -309,6 +331,11 @@ export default class Parity {
.execute('parity_setMode', mode);
}
setNewDappsWhitelist (addresses) {
return this._transport
.execute('parity_setNewDappsWhitelist', addresses ? inAddresses(addresses) : null);
}
setTransactionsLimit (quantity) {
return this._transport
.execute('parity_setTransactionsLimit', inNumber16(quantity));

View File

@ -236,6 +236,29 @@ export default {
}
},
getDappsAddresses: {
desc: 'Returns the list of accounts available to a specific dapp',
params: [
{
type: String,
desc: 'Dapp Id'
}
],
returns: {
type: Array,
desc: 'The list of available accounts'
}
},
getNewDappsWhitelist: {
desc: 'Returns the list of accounts available to a new dapps',
params: [],
returns: {
type: Array,
desc: 'The list of available accounts'
}
},
hashContent: {
desc: 'Creates a hash of the file as retrieved',
params: [
@ -282,6 +305,15 @@ export default {
}
},
listRecentDapps: {
desc: 'Returns a list of the most recent active dapps',
params: [],
returns: {
type: Array,
desc: 'Array of Dapp Ids'
}
},
removeAddress: {
desc: 'Removes an address from the addressbook',
params: [
@ -586,6 +618,24 @@ export default {
}
},
setDappsAddresses: {
desc: 'Sets the available addresses for a dapp',
params: [
{
type: String,
desc: 'Dapp Id'
},
{
type: Array,
desc: 'Array of available accounts available to the dapp'
}
],
returns: {
type: Boolean,
desc: 'True if the call succeeded'
}
},
setExtraData: {
desc: 'Changes extra data for newly mined blocks',
params: [
@ -645,6 +695,20 @@ export default {
}
},
setNewDappsWhitelist: {
desc: 'Sets the list of accounts available to new dapps',
params: [
{
type: Array,
desc: 'List of accounts available by default'
}
],
returns: {
type: Boolean,
desc: 'True if the call succeeded'
}
},
setTransactionsLimit: {
desc: 'Changes limit for transactions in queue.',
params: [

View File

@ -14,16 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { observer } from 'mobx-react';
import DoneIcon from 'material-ui/svg-icons/action/done';
import { List, ListItem } from 'material-ui/List';
import Checkbox from 'material-ui/Checkbox';
import { Modal, Button } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import styles from './AddDapps.css';
import styles from './addDapps.css';
@observer
export default class AddDapps extends Component {
@ -40,25 +40,24 @@ export default class AddDapps extends Component {
return (
<Modal
visible
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.add.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications' />
}
actions={ [
<Button
label={
<FormattedMessage
id='dapps.add.button.done'
defaultMessage='Done' />
}
key='done'
onClick={ store.closeModal }
icon={ <DoneIcon /> }
/>
] }>
visible>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal,

View File

@ -0,0 +1,46 @@
// Copyright 2015, 2016 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 AddDapps from './';
function renderShallow (store = {}) {
return shallow(
<AddDapps store={ store } />
);
}
describe('modals/AddDapps', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true }).find('Connect(Modal)')
).to.have.length(1);
});
});
});

View File

@ -14,4 +14,4 @@
// 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 './AddDapps';
export default from './addDapps';

View File

@ -0,0 +1,47 @@
/* Copyright 2015, 2016 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/>.
*/
.item {
.info {
display: inline-block;
.address {
opacity: 0.75;
}
.description {
margin-top: 0.5em;
opacity: 0.75;
}
.name {
margin: 0.5em 0;
text-transform: uppercase;
}
}
}
.selected, .unselected {
margin-bottom: 0.25em;
}
.selected {
background: rgba(255, 255, 255, 0.15);
}
.unselected {
}

View File

@ -0,0 +1,112 @@
// Copyright 2015, 2016 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 { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, IdentityIcon, Modal } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import styles from './dappPermissions.css';
@observer
export default class DappPermissions extends Component {
static propTypes = {
store: PropTypes.object.isRequired
};
render () {
const { store } = this.props;
if (!store.modalOpen) {
return null;
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.permissions.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
title={
<FormattedMessage
id='dapps.permissions.label'
defaultMessage='visible dapp accounts' />
}
visible>
<List>
{ this.renderListItems() }
</List>
</Modal>
);
}
renderListItems () {
const { store } = this.props;
return store.accounts.map((account) => {
const onCheck = () => {
store.selectAccount(account.address);
};
// TODO: Once new modal & account selection is in, this should be updated
// to conform to the new (as of this code WIP) look & feel for selection.
// For now in the current/old style, not as pretty but consistent.
return (
<ListItem
className={
account.checked
? styles.selected
: styles.unselected
}
key={ account.address }
leftCheckbox={
<Checkbox
checked={ account.checked }
onCheck={ onCheck }
/>
}
primaryText={
<div className={ styles.item }>
<IdentityIcon address={ account.address } />
<div className={ styles.info }>
<h3 className={ styles.name }>
{ account.name }
</h3>
<div className={ styles.address }>
{ account.address }
</div>
<div className={ styles.description }>
{ account.description }
</div>
</div>
</div>
} />
);
});
}
}

View File

@ -0,0 +1,46 @@
// Copyright 2015, 2016 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 DappPermissions from './';
function renderShallow (store = {}) {
return shallow(
<DappPermissions store={ store } />
);
}
describe('modals/DappPermissions', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)')
).to.have.length(1);
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 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 './dappPermissions';

View File

@ -0,0 +1,94 @@
// Copyright 2015, 2016 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, transaction } from 'mobx';
export default class Store {
@observable accounts = [];
@observable modalOpen = false;
@observable whitelist = [];
constructor (api) {
this._api = api;
this.loadWhitelist();
}
@action closeModal = () => {
transaction(() => {
const accounts = this.accounts
.filter((account) => account.checked)
.map((account) => account.address);
this.modalOpen = false;
this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts);
});
}
@action openModal = (accounts) => {
transaction(() => {
this.accounts = Object
.values(accounts)
.map((account) => {
return {
address: account.address,
checked: this.whitelist
? this.whitelist.includes(account.address)
: true,
description: account.meta.description,
name: account.name
};
});
this.modalOpen = true;
});
}
@action selectAccount = (address) => {
this.accounts = this.accounts.map((account) => {
if (account.address === address) {
account.checked = !account.checked;
}
return account;
});
}
@action setWhitelist = (whitelist) => {
this.whitelist = whitelist;
}
loadWhitelist () {
return this._api.parity
.getNewDappsWhitelist()
.then((whitelist) => {
this.setWhitelist(whitelist);
})
.catch((error) => {
console.warn('loadWhitelist', error);
});
}
updateWhitelist (whitelist) {
return this._api.parity
.setNewDappsWhitelist(whitelist)
.then(() => {
this.setWhitelist(whitelist);
})
.catch((error) => {
console.warn('updateWhitelist', error);
});
}
}

View File

@ -0,0 +1,100 @@
// Copyright 2015, 2016 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 Store from './store';
const ACCOUNTS = {
'123': { address: '123', name: '123', meta: { description: '123' } },
'456': { address: '456', name: '456', meta: { description: '456' } },
'789': { address: '789', name: '789', meta: { description: '789' } }
};
const WHITELIST = ['123', '456'];
describe('modals/DappPermissions/store', () => {
let api;
let store;
beforeEach(() => {
api = {
parity: {
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
setNewDappsWhitelist: sinon.stub().resolves(true)
}
};
store = new Store(api);
});
describe('constructor', () => {
it('retrieves the whitelist via api', () => {
expect(api.parity.getNewDappsWhitelist).to.be.calledOnce;
});
it('sets the retrieved whitelist', () => {
expect(store.whitelist.peek()).to.deep.equal(WHITELIST);
});
});
describe('@actions', () => {
describe('openModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
});
it('sets the modalOpen status', () => {
expect(store.modalOpen).to.be.true;
});
it('sets accounts with checked interfaces', () => {
expect(store.accounts.peek()).to.deep.equal([
{ address: '123', name: '123', description: '123', checked: true },
{ address: '456', name: '456', description: '456', checked: true },
{ address: '789', name: '789', description: '789', checked: false }
]);
});
});
describe('closeModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('789');
store.closeModal();
});
it('calls setNewDappsWhitelist', () => {
expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce;
});
});
describe('selectAccount', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('123');
store.selectAccount('789');
});
it('unselects previous selected accounts', () => {
expect(store.accounts.find((account) => account.address === '123').checked).to.be.false;
});
it('selects previous unselected accounts', () => {
expect(store.accounts.find((account) => account.address === '789').checked).to.be.true;
});
});
});
});

View File

@ -16,8 +16,10 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import AddDapps from './AddDapps';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DappPermissions from './DappPermissions';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@ -35,8 +37,10 @@ import WalletSettings from './WalletSettings';
export {
AddAddress,
AddContract,
AddDapps,
CreateAccount,
CreateWallet,
DappPermissions,
DeleteAccount,
DeployContract,
EditMeta,

View File

@ -20,10 +20,12 @@ import CheckIcon from 'material-ui/svg-icons/navigation/check';
import CloseIcon from 'material-ui/svg-icons/navigation/close';
import ContractIcon from 'material-ui/svg-icons/action/code';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
export {
AddIcon,
@ -32,8 +34,10 @@ export {
CloseIcon,
ContractIcon,
DoneIcon,
PrevIcon,
LockedIcon,
NextIcon,
PrevIcon,
SaveIcon,
SnoozeIcon
SnoozeIcon,
VisibleIcon
};

View File

@ -14,29 +14,35 @@
// 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 { Checkbox } from 'material-ui';
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 { Actionbar, Page } from '~/ui';
import FlatButton from 'material-ui/FlatButton';
import EyeIcon from 'material-ui/svg-icons/image/remove-red-eye';
import { AddDapps, DappPermissions } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store';
import { Actionbar, Button, Page } from '~/ui';
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
import DappsStore from './dappsStore';
import AddDapps from './AddDapps';
import Summary from './Summary';
import styles from './dapps.css';
@observer
export default class Dapps extends Component {
class Dapps extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
accounts: PropTypes.object.isRequired
};
store = DappsStore.get(this.context.api);
permissionStore = new PermissionStore(this.context.api);
render () {
let externalOverlay = null;
@ -68,6 +74,7 @@ export default class Dapps extends Component {
return (
<div>
<AddDapps store={ this.store } />
<DappPermissions store={ this.permissionStore } />
<Actionbar
className={ styles.toolbar }
title={
@ -76,30 +83,31 @@ export default class Dapps extends Component {
defaultMessage='Decentralized Applications' />
}
buttons={ [
<FlatButton
<Button
icon={ <VisibleIcon /> }
key='edit'
label={
<FormattedMessage
id='dapps.button.edit'
defaultMessage='edit' />
}
key='edit'
icon={ <EyeIcon /> }
onTouchTap={ this.store.openModal }
/>
onClick={ this.store.openModal }
/>,
<Button
icon={ <LockedIcon /> }
key='permissions'
label={
<FormattedMessage
id='dapps.button.permissions'
defaultMessage='permissions' />
}
onClick={ this.openPermissionsModal } />
] }
/>
<Page>
<div>
{ this.renderList(this.store.visibleLocal) }
</div>
<div>
{ this.renderList(this.store.visibleBuiltin) }
</div>
<div>
{ this.renderList(this.store.visibleNetwork, externalOverlay) }
</div>
<div>{ this.renderList(this.store.visibleLocal) }</div>
<div>{ this.renderList(this.store.visibleBuiltin) }</div>
<div>{ this.renderList(this.store.visibleNetwork, externalOverlay) }</div>
</Page>
</div>
);
@ -131,4 +139,27 @@ export default class Dapps extends Component {
onClickAcceptExternal = () => {
this.store.closeExternalOverlay();
}
openPermissionsModal = () => {
const { accounts } = this.props;
this.permissionStore.openModal(accounts);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return {
accounts
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Dapps);