Refactoring of the Dapp Registry (#4589)

* Add React Hot Loader to DappReg dapp

* Updated colours

* Add DappCards

* Dapp Modal with manifest displayed

* Add input to the Dapp Modal

* WIP // Editing a Dapp

* Clean-Up

* Linting

* CleanUp and separate dapp from dappS

* Semi-working updates

* Working Editing of a Dapp

* OCD

* Linting

* Add a Dapp -- WIP

* Register a new Dapp

* WIP Dapps

* Working update / delete / register

* Better promises

* Working updates for DappReg

* Fully functional again !

* Generic Card Component

* Dashed Register Card

* Cleanups

* Cleanups

* Add Actions to Modal

* Clean-Up

* Better Close Icon

* Single place for Registry version // Fetch meta-data from Registry

* Fixing test

* Fix saving changes in dapp reg

* PR Grumbles - Part I

* PR Grumble - Part I

* PR Grumble - Part II

* DappReg Contract owner can delete dapps
This commit is contained in:
Nicolas Gotchac 2017-03-10 13:31:57 +01:00 committed by Jaco Greeff
parent e15f60b819
commit eebb8b87a4
49 changed files with 2351 additions and 1383 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512">
<g>
</g>
<path d="M295.516 216.494h154v78.992h-154v-78.992z" fill="#FFFFFF" />
<path d="M62.474 216.514h154.050v78.971h-154.050v-78.971z" fill="#FFFFFF" />
<path d="M216.525 295.465h79.001v154.050h-79.001v-154.050z" fill="#FFFFFF" />
<path d="M216.525 62.474h79.001v154.041h-79.001v-154.041z" fill="#FFFFFF" />
<path d="M216.525 216.514h79.001v78.971h-79.001v-78.971z" fill="#FFFFFF" />
</svg>

After

Width:  |  Height:  |  Size: 720 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512">
<g>
</g>
<path d="M295.516 216.494h154v78.992h-154v-78.992z" fill="#000000" />
<path d="M62.474 216.514h154.050v78.971h-154.050v-78.971z" fill="#000000" />
<path d="M216.525 295.465h79.001v154.050h-79.001v-154.050z" fill="#000000" />
<path d="M216.525 62.474h79.001v154.041h-79.001v-154.041z" fill="#000000" />
<path d="M216.525 216.514h79.001v78.971h-79.001v-78.971z" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 715 B

View File

@ -16,7 +16,14 @@
import * as abis from './abi'; import * as abis from './abi';
const REGISTRY_V1_HASHES = [
'0x34f7c51bbb1b1902fbdabfdf04811100f5c9f998f26dd535d2f6f977492c748e', // ropsten
'0x64c3ee34851517a9faecd995c102b339f03e564ad6772dc43a26f993238b20ec' // homestead
];
export default class Registry { export default class Registry {
_registryContract = null;
constructor (api) { constructor (api) {
this._api = api; this._api = api;
@ -43,11 +50,10 @@ export default class Registry {
this._fetching = true; this._fetching = true;
return this._api.parity return this.fetchContract()
.registryAddress() .then((contract) => {
.then((address) => {
this._fetching = false; this._fetching = false;
this._instance = this._api.newContract(abis.registry, address).instance; this._instance = contract.instance;
this._queue.forEach((queued) => { this._queue.forEach((queued) => {
queued.resolve(this._instance); queued.resolve(this._instance);
@ -89,6 +95,47 @@ export default class Registry {
.then((contract) => contract.instance); .then((contract) => contract.instance);
} }
fetchContract () {
if (this._registryContract) {
return Promise.resolve(this._registryContract);
}
return this._api.parity
.registryAddress()
.then((address) => Promise.all([ address, this._api.eth.getCode(address) ]))
.then(([ address, code ]) => {
const codeHash = this._api.util.sha3(code);
const version = REGISTRY_V1_HASHES.includes(codeHash)
? 1
: 2;
const abi = version === 1
? abis.registry
: abis.registry2;
const contract = this._api.newContract(abi, address);
// Add support for previous `set` and `get` methods
if (!contract.instance.get && contract.instance.getData) {
contract.instance.get = contract.instance.getData;
}
if (contract.instance.get && !contract.instance.getData) {
contract.instance.getData = contract.instance.get;
}
if (!contract.instance.set && contract.instance.setData) {
contract.instance.set = contract.instance.setData;
}
if (contract.instance.set && !contract.instance.setData) {
contract.instance.setData = contract.instance.set;
}
console.log(`registry at ${address}, code ${codeHash}, version ${version}`);
this._registryContract = contract;
return this._registryContract;
});
}
_createGetParams (_name, key) { _createGetParams (_name, key) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
const sha3 = this._api.util.sha3.text(name); const sha3 = this._api.util.sha3.text(name);

View File

@ -35,6 +35,9 @@ function create () {
} }
}; };
api = { api = {
eth: {
getCode: sinon.stub().resolves('0x123456')
},
parity: { parity: {
registryAddress: sinon.stub().resolves('testRegistryAddress') registryAddress: sinon.stub().resolves('testRegistryAddress')
}, },

View File

@ -17,6 +17,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
import { AppContainer } from 'react-hot-loader';
injectTapEventPlugin(); injectTapEventPlugin();
@ -27,6 +28,21 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
<Application />, <AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./dappreg/Application/index.js', () => {
require('./dappreg/Application/index.js');
ReactDOM.render(
<AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -15,15 +15,17 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import '../_colors.css';
.body { .body {
color: #333; color: $text-color;
background: #eee; background: $background-color;
padding: 4.5em 0; padding: 3em 0 6em;
text-align: center; text-align: center;
} }
.apps { .apps {
background: #fff; background: white;
border-radius: 0.5em; border-radius: 0.5em;
margin: 0 auto; margin: 0 auto;
max-width: 980px; max-width: 980px;
@ -39,9 +41,8 @@
} }
.header { .header {
background: #44e; background: $blue;
border-radius: 0 0 0.25em 0.25em; color: white;
color: #fff;
left: 0; left: 0;
padding: 1em; padding: 1em;
position: fixed; position: fixed;
@ -54,5 +55,5 @@
text-align: center; text-align: center;
padding-top: 5em; padding-top: 5em;
font-size: 2em; font-size: 2em;
color: #999; color: $loading-color;
} }

View File

@ -19,18 +19,14 @@ import { observer } from 'mobx-react';
import DappsStore from '../dappsStore'; import DappsStore from '../dappsStore';
import ButtonBar from '../ButtonBar'; import Dapps from '../Dapps';
import Dapp from '../Dapp'; import Transactions from '../Transactions';
import ModalDelete from '../ModalDelete';
import ModalRegister from '../ModalRegister';
import ModalUpdate from '../ModalUpdate';
import SelectDapp from '../SelectDapp';
import Warning from '../Warning'; import Warning from '../Warning';
import styles from './application.css'; import styles from './application.css';
@observer @observer
export default class Application extends Component { export default class Application extends Component {
dappsStore = DappsStore.instance(); dappsStore = DappsStore.get();
render () { render () {
if (this.dappsStore.isLoading) { if (this.dappsStore.isLoading) {
@ -41,23 +37,32 @@ export default class Application extends Component {
); );
} }
const { ownDapps, otherDapps } = this.dappsStore;
return ( return (
<div className={ styles.body }> <div className={ styles.body }>
<div className={ styles.header }> <div className={ styles.header }>
DAPP REGISTRY, a global view of distributed applications available on the network. Putting the puzzle together. DAPP REGISTRY, a global view of distributed applications available on the network. Putting the puzzle together.
</div> </div>
<div className={ styles.apps }>
<SelectDapp /> <div>
<ButtonBar /> <Dapps
<Dapp /> dapps={ ownDapps }
own
title='My Dapps'
/>
<Dapps
dapps={ otherDapps }
title='Other Dapps'
/>
</div> </div>
<div className={ styles.footer }> <div className={ styles.footer }>
{ this.dappsStore.count } applications registered, { this.dappsStore.ownedCount } owned by user { this.dappsStore.count } applications registered, { this.dappsStore.ownedCount } owned by user
</div> </div>
<Transactions />
<Warning /> <Warning />
<ModalDelete />
<ModalRegister />
<ModalUpdate />
</div> </div>
); );
} }

View File

@ -15,11 +15,16 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import '../_colors.css';
@import '../_utils.css';
.button { .button {
background: #44e; composes: bezier-transform;
background: $blue;
border: none; border: none;
border-radius: 0.25em; border-radius: 0.25em;
color: #fff; color: white;
cursor: pointer; cursor: pointer;
font-size: 1em; font-size: 1em;
margin: 1em 0.375em; margin: 1em 0.375em;
@ -29,10 +34,14 @@
&[disabled] { &[disabled] {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;
background: #aaa; background: $disabled-bg;
} }
&[data-warning="true"] { &[data-warning="true"] {
background: #e44; background: $warning-bg;
}
&:focus {
transform: scale(1.05);
} }
} }

View File

@ -24,27 +24,29 @@ export default class Button extends Component {
disabled: PropTypes.bool, disabled: PropTypes.bool,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
warning: PropTypes.bool, warning: PropTypes.bool,
onClick: PropTypes.func.isRequired onClick: PropTypes.func
} }
render () { render () {
const { className, disabled, label, warning } = this.props; const { className, disabled, label, warning } = this.props;
const classes = `${styles.button} ${className}`; const classes = [ styles.button, className ];
return ( return (
<button <button
className={ classes } className={ classes.join(' ') }
data-warning={ warning } data-warning={ warning }
disabled={ disabled } disabled={ disabled }
onClick={ this.onClick } onClick={ this.handleClick }
> >
{ label } { label }
</button> </button>
); );
} }
onClick = (event) => { handleClick = (event) => {
if (this.props.disabled) { if (this.props.disabled) {
event.preventDefault();
event.stopPropagation();
return; return;
} }

View File

@ -1,106 +0,0 @@
// 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 } from 'react';
import { observer } from 'mobx-react';
import DappsStore from '../dappsStore';
import ModalStore from '../modalStore';
import Button from '../Button';
import styles from './buttonBar.css';
@observer
export default class ButtonBar extends Component {
dappsStore = DappsStore.instance();
modalStore = ModalStore.instance();
render () {
let buttons = [];
if (this.dappsStore.isEditing || this.dappsStore.isNew) {
buttons = [
<Button
key='cancel'
label='Cancel'
warning
onClick={ this.onCancelClick }
/>,
<Button
key='save'
label={ this.dappsStore.isNew ? 'Register' : 'Update' }
disabled={ !this.dappsStore.canSave }
onClick={ this.onSaveClick }
/>
];
} else {
buttons = [
<Button
key='delete'
label='Delete'
warning
disabled={ !this.dappsStore.currentApp || (!this.dappsStore.currentApp.isOwner && !this.dappsStore.isContractOwner) }
onClick={ this.onDeleteClick }
/>,
<Button
key='edit'
label='Edit'
disabled={ !this.dappsStore.currentApp || !this.dappsStore.currentApp.isOwner }
onClick={ this.onEditClick }
/>,
<Button
key='new'
label='New'
onClick={ this.onNewClick }
/>
];
}
return (
<div className={ styles.buttonbar }>
{ buttons }
</div>
);
}
onCancelClick = () => {
if (this.dappsStore.isEditing) {
this.dappsStore.setEditing(false);
} else {
this.dappsStore.setNew(false);
}
}
onDeleteClick = () => {
this.modalStore.showDelete();
}
onEditClick = () => {
this.dappsStore.setEditing(true);
}
onNewClick = () => {
this.dappsStore.setNew(true);
}
onSaveClick = () => {
if (this.dappsStore.isEditing) {
this.modalStore.showUpdate();
} else {
this.modalStore.showRegister();
}
}
}

View File

@ -0,0 +1,66 @@
/* 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 '../_utils.css';
$imgSize: 6rem;
.container {
display: flex;
}
.card {
composes: bezier-transform;
align-items: center;
background-color: rgba(240, 240, 240, 0.75);
display: flex;
flex-direction: column;
margin: 1rem;
padding: 1rem;
width: 10rem;
&:hover,
&:focus {
cursor: pointer;
transform: scale(1.05);
}
}
.dashed {
border: 1px dashed black;
}
.icon {
margin-bottom: 0.75rem;
overflow: hidden;
img {
border-radius: 50%;
height: $imgSize;
width: $imgSize;
}
}
.name {
font-size: 1.25rem;
margin-bottom: 0.5rem;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
width: 100%;
}

View File

@ -0,0 +1,99 @@
// 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 keycode from 'keycode';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import styles from './card.css';
export default class Card extends Component {
static propTypes = {
children: PropTypes.any,
dashed: PropTypes.bool,
focus: PropTypes.bool,
icon: PropTypes.object,
name: PropTypes.object,
onClick: PropTypes.func.isRequired
};
static defaultProps = {
dashed: false,
focus: false,
name: { value: '' }
};
componentWillReceiveProps (nextProps) {
if (nextProps.focus && !this.props.focus) {
this.handleFocus();
}
}
render () {
const { children, dashed, icon, name } = this.props;
const cardClasses = [ styles.card ];
if (dashed) {
cardClasses.push(styles.dashed);
}
return (
<div className={ styles.container }>
<div
className={ cardClasses.join(' ') }
onClick={ this.handleClick }
onKeyPress={ this.handleKeyPress }
ref='card'
tabIndex={ 0 }
>
<div className={ styles.icon }>
{ icon }
</div>
<span
className={ styles.name }
title={ name.title || name.value }
>
{ name.value }
</span>
{ children }
</div>
</div>
);
}
handleKeyPress = (event) => {
const codeName = keycode(event);
if (codeName === 'enter') {
return this.handleClick();
}
return event;
}
handleFocus = () => {
setTimeout(() => {
const element = ReactDOM.findDOMNode(this.refs.card);
element && element.focus();
}, 50);
}
handleClick = () => {
this.props.onClick();
}
}

View File

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

View File

@ -0,0 +1,84 @@
// 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 } from 'react';
import DappsStore from '../dappsStore';
import Card from '../Card';
import ModalRegister from '../ModalRegister';
import PlusImage from '~/../assets/images/dapps/plus.svg';
export default class CreateDappCard extends Component {
state = {
dappId: null,
focus: false,
open: false
};
dappsStore = DappsStore.get();
render () {
const { focus } = this.state;
return (
<div>
{ this.renderModal() }
<Card
dashed
focus={ focus }
icon={ (<img src={ PlusImage } />) }
name={ { value: 'Register a dapp' } }
onClick={ this.handleOpen }
/>
</div>
);
}
renderModal () {
const { dappId, open } = this.state;
if (!open) {
return null;
}
return (
<ModalRegister
dappId={ dappId }
onClose={ this.handleClose }
onRegister={ this.handleRegister }
/>
);
}
handleOpen = () => {
const dappId = this.dappsStore.createDappId();
this.setState({ focus: false, open: true, dappId });
}
handleClose = () => {
this.setState({ focus: true, open: false, dappId: null });
}
handleRegister = () => {
const { dappId } = this.state;
this.dappsStore.register(dappId);
this.handleClose();
}
}

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

View File

@ -1,174 +0,0 @@
// 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 } from 'react';
import { observer } from 'mobx-react';
import { api } from '../parity';
import DappsStore from '../dappsStore';
import Input from '../Input';
import SelectAccount from '../SelectAccount';
import styles from './dapp.css';
@observer
export default class Dapp extends Component {
dappsStore = DappsStore.instance();
render () {
const app = this.dappsStore.isNew || this.dappsStore.isEditing
? this.dappsStore.wipApp
: this.dappsStore.currentApp;
if (!app) {
return null;
}
return (
<div className={ styles.app }>
{ this.dappsStore.isNew ? this.renderOwnerSelect(app) : this.renderOwnerStatic(app) }
{ this.renderInputs(app) }
</div>
);
}
renderInputs (app) {
if (this.dappsStore.isNew) {
return null;
}
return [
this.renderHashInput(app, 'image', 'Image hash, as generated by Githubhint', true),
this.renderHashInput(app, 'manifest', 'Manifest hash, as generated by Githubhint'),
this.renderHashInput(app, 'content', 'Content hash, as generated by Githubhint')
];
}
renderOwnerSelect (app) {
const overlayImage = (
<img
className={ styles.overlayImage }
src={ api.util.createIdentityImg(this.dappsStore.currentAccount.address, 4) }
/>
);
return (
<Input
hint={ this.dappsStore.currentAccount.address }
label='Owner, select the application owner and editor'
overlay={ overlayImage }
>
<SelectAccount />
</Input>
);
}
renderOwnerStatic (app) {
const overlayImage = (
<img
className={ styles.overlayImage }
src={ api.util.createIdentityImg(app.owner, 4) }
/>
);
return (
<Input
hint={ app.owner }
label='Owner, the application owner and editor'
overlay={ overlayImage }
>
<input value={ app.ownerName } readOnly />
</Input>
);
}
renderHashInput (app, type, label, withImage = false) {
const onChange = (event) => this.onChangeHash(event, type);
const hash = app[`${type}Hash`];
let overlayImage = null;
if (withImage && hash) {
overlayImage = (
<img
className={ styles.overlayImage }
src={ `/api/content/${hash.substr(2)}` }
/>
);
}
return (
<Input
hint={ app[`${type}Error`] || app[`${type}Url`] || '...' }
label={ label }
key={ `${type}Edit` }
overlay={ overlayImage }
>
<input
value={ app[`${type}Hash`] || '' }
data-dirty={ app[`${type}Changed`] }
data-error={ !!app[`${type}Error`] }
readOnly={ !this.dappsStore.isEditing && !this.dappsStore.isNew }
onChange={ onChange }
/>
</Input>
);
}
onChangeHash (event, type) {
if (!this.dappsStore.isNew && !this.dappsStore.isEditing) {
return;
}
const hash = event.target.value;
let changed = false;
let url = null;
if (this.dappsStore.isNew) {
if (hash && hash.length) {
changed = true;
}
} else {
if (this.dappsStore.currentApp[`${type}Hash`] !== hash) {
changed = true;
} else {
url = this.dappsStore.currentApp[`${type}Url`];
}
}
this.dappsStore.editWip({
[`${type}Changed`]: changed,
[`${type}Error`]: null,
[`${type}Hash`]: hash,
[`${type}Url`]: changed ? 'Resolving url from hash' : url
});
if (changed) {
if (hash.length) {
this.dappsStore
.lookupHash(hash)
.then((url) => {
this.dappsStore.editWip({
[`${type}Error`]: url ? null : 'Unable to resolve url',
[`${type}Url`]: url
});
});
} else {
this.dappsStore.editWip({ [`${type}Url`]: null });
}
}
}
}

View File

@ -15,7 +15,8 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.buttonbar { .author,
.version {
font-size: 0.75rem;
text-align: center; text-align: center;
margin: 1em 0 0 0;
} }

View File

@ -0,0 +1,110 @@
// 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 Card from '../Card';
import DappsStore from '../dappsStore';
import DappModal from '../DappModal';
import styles from './dappCard.css';
export default class DappCard extends Component {
dappsStore = DappsStore.get();
static propTypes = {
dapp: PropTypes.object.isRequired
};
state = {
focus: false,
open: false
};
render () {
const { dapp } = this.props;
const { focus } = this.state;
const { id, image } = dapp;
const manifest = dapp.manifest.content;
return (
<div>
{ this.renderModal() }
<Card
focus={ focus }
icon={ this.renderImage(image.url) }
name={ { title: id, value: manifest && manifest.name || id } }
onClick={ this.handleOpen }
>
{ this.renderVersion(manifest) }
{ this.renderAuthor(manifest) }
</Card>
</div>
);
}
renderModal () {
const { dapp } = this.props;
const { open } = this.state;
return (
<DappModal
dapp={ dapp }
onClose={ this.handleClose }
open={ open }
/>
);
}
renderImage (url) {
return (
<img src={ url } />
);
}
renderVersion (manifest) {
if (!manifest || !manifest.version) {
return null;
}
return (
<span className={ styles.version }>
v{ manifest.version }
</span>
);
}
renderAuthor (manifest) {
if (!manifest || !manifest.author) {
return null;
}
return (
<span className={ styles.author }>
by { manifest && manifest.author }
</span>
);
}
handleClose = () => {
this.setState({ focus: true, open: false });
}
handleOpen = () => {
this.setState({ focus: false, open: true });
}
}

View File

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

View File

@ -0,0 +1,82 @@
/* 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 '../_colors.css';
@import '../_utils.css';
$imgSize: 5rem;
.code {
color: $code-color;
font-family: 'Roboto Mono', monospace;
margin-top: 1rem;
.codeTitle {
align-items: center;
background-color: $code-title-bg;
color: $code-title-color;
display: inline-flex;
height: 2rem;
padding: 0 0.5rem;
}
.codeContainer {
background-color: $code-bg;
padding: 0.5rem 1rem;
}
code {
font-size: 0.75rem;
white-space: pre-wrap;
}
}
.actions {
height: 0.5rem;
position: relative;
text-align: right;
top: 0.5rem;
.button {
margin-bottom: 0;
margin-top: 0;
padding: 0.5rem 1.5rem;
}
}
.icon {
margin-right: 1.5rem;
overflow: hidden;
img {
border: 2px solid #ddd;
border-radius: 50%;
height: $imgSize;
width: $imgSize;
}
}
.name {
font-size: 1.25rem;
margin-bottom: 0.25rem;
}
.info {
color: #ddd;
font-size: 0.75rem;
margin-top: 0.25rem;
}

View File

@ -0,0 +1,423 @@
// 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 { api } from '../parity';
import DappsStore from '../dappsStore';
import Button from '../Button';
import Input from '../Input';
import Modal from '../Modal';
import ModalDelete from '../ModalDelete';
import ModalUpdate from '../ModalUpdate';
import SelectAccount from '../SelectAccount';
import styles from './dappModal.css';
@observer
export default class DappModal extends Component {
static propTypes = {
dapp: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired
};
state = {
showDelete: false,
showUpdate: false,
updates: null,
updating: false
};
dappsStore = DappsStore.get();
render () {
const { dapp, open } = this.props;
const { showDelete, showUpdate, updates } = this.state;
if (!open) {
return null;
}
return (
<div>
{
showDelete
? (
<ModalDelete
dappId={ dapp.id }
onClose={ this.handleDeleteClose }
onDelete={ this.handleDeleteConfirm }
/>
)
: null
}
{
showUpdate
? (
<ModalUpdate
dappId={ dapp.id }
onClose={ this.handleUpdateClose }
onConfirm={ this.handleUpdateConfirm }
updates={ updates }
/>
)
: null
}
<Modal
header={ this.renderHeader(dapp) }
onClose={ this.handleClose }
>
{ this.renderContent(dapp) }
</Modal>
</div>
);
}
renderContent (dapp) {
const manifest = dapp.manifest.content || {};
return (
<div>
<div>
{ this.renderInputs(dapp) }
</div>
{ this.renderActions(dapp) }
<div className={ styles.code }>
<div className={ styles.codeTitle }>manifest.json</div>
<div className={ styles.codeContainer }>
<code>{ JSON.stringify(manifest, null, 2) }</code>
</div>
</div>
</div>
);
}
renderActions (dapp) {
if (!dapp.isOwner && !dapp.isContractOwner) {
return null;
}
if (!dapp.isOwner && dapp.isContractOwner) {
return (
<div className={ styles.actions }>
<Button
className={ styles.button }
label='Delete'
onClick={ this.handleDelete }
warning
/>
</div>
);
}
const { isEditing } = dapp;
const { updating } = this.state;
if (updating) {
return (
<div className={ styles.actions }>
<Button
className={ styles.button }
disabled
label='Updating...'
/>
</div>
);
}
if (isEditing) {
return (
<div className={ styles.actions }>
<Button
className={ styles.button }
disabled={ !dapp.canSave }
label='Save'
onClick={ this.handleSave }
/>
<Button
className={ styles.button }
label='Fetch Registry'
onClick={ this.handleFetchRegistry }
/>
<Button
className={ styles.button }
label='Cancel'
onClick={ this.handleCancel }
warning
/>
</div>
);
}
return (
<div className={ styles.actions }>
<Button
className={ styles.button }
label='Edit'
onClick={ this.handleEdit }
/>
<Button
className={ styles.button }
label='Delete'
onClick={ this.handleDelete }
warning
/>
</div>
);
}
renderHeader (dapp) {
const { id, image } = dapp;
const manifest = dapp.manifest.content || {};
const infos = [];
if (manifest.version) {
infos.push(`v${manifest.version}`);
}
if (manifest.author) {
infos.push(`by ${manifest.author}`);
}
return (
<div>
<div className={ styles.icon }>
<img src={ image.url } />
</div>
<div>
<div className={ styles.name }>
{ manifest.name || 'Unnamed' }
</div>
<div className={ styles.info }>
{ id }
</div>
<div className={ styles.info }>
{ infos.length > 0 ? infos.join(', ') : null }
</div>
</div>
</div>
);
}
renderInputs (dapp) {
return [
this.renderOwner(dapp),
this.renderHashInput(dapp, 'image', 'Image URL', true),
this.renderHashInput(dapp, 'manifest', 'Manifest URL'),
this.renderHashInput(dapp, 'content', 'Content URL')
];
}
renderOwner (dapp) {
const { isEditing } = dapp;
if (isEditing) {
return this.renderOwnerSelect(dapp);
}
return this.renderOwnerStatic(dapp);
}
renderOwnerSelect (dapp) {
const overlayImage = (
<img
className={ styles.overlayImage }
src={ api.util.createIdentityImg(this.props.dapp.wip.owner.address, 4) }
/>
);
return (
<Input
key='owner_select'
hint={ this.props.dapp.wip.owner.address }
label='Owner, select the application owner and editor'
overlay={ overlayImage }
>
<SelectAccount
onSelect={ this.handleSelectOwner }
value={ dapp.wip.owner.address }
/>
</Input>
);
}
renderOwnerStatic (dapp) {
const overlayImage = (
<img
className={ styles.overlayImage }
src={ api.util.createIdentityImg(dapp.owner.address, 4) }
/>
);
return (
<Input
key='owner_static'
hint={ dapp.owner.address }
label='Owner, the application owner and editor'
overlay={ overlayImage }
>
<input
readOnly
tabIndex={ -1 }
value={ dapp.owner.name || dapp.owner.address }
/>
</Input>
);
}
renderHashInput (dapp, type, label, isImage = false) {
const handleChange = (event) => {
return this.handleChangeHash(event, type);
};
const { isEditing, wip } = dapp;
const changed = wip && wip[type].changed;
const error = wip && wip[type].error;
const hash = dapp[type].hash;
const url = dapp[type].url;
const overlayImage = (isImage && hash)
? (
<img
className={ styles.overlayImage }
src={ `/api/content/${hash.substr(2)}` }
/>
)
: null;
const wipUrl = isEditing && wip && wip[type].url;
const hint = error || (!changed && hash) || '...';
const value = typeof wipUrl !== 'string'
? url || ''
: wipUrl;
return (
<Input
key={ `${type}Edit` }
hint={ hint }
label={ label }
overlay={ overlayImage }
>
<input
data-dirty={ changed }
data-error={ !!error }
onChange={ handleChange }
readOnly={ !isEditing }
tabIndex={ isEditing ? 0 : -1 }
value={ value }
/>
</Input>
);
}
handleClose = () => {
this.handleCancel();
this.props.onClose();
}
handleSelectOwner = (event) => {
const { value } = event.target;
const changed = (this.props.dapp.owner.address !== value);
this.props.dapp.handleChange({
owner: {
address: value,
changed
}
});
}
handleChangeHash = (event, type) => {
if (!this.props.dapp.isEditing) {
return;
}
const url = event.target.value;
const changed = (this.props.dapp[type].url !== url);
this.props.dapp.handleChange({
[ type ]: {
error: null,
changed,
url
}
});
}
handleFetchRegistry = () => {
this.dappsStore.fetchRegistryData(this.props.dapp);
}
handleCancel = () => {
this.props.dapp.setEditing(false);
}
handleEdit = () => {
this.props.dapp.setEditing(true);
}
handleDelete = () => {
this.setState({ showDelete: true });
}
handleDeleteClose = () => {
this.setState({ showDelete: false });
}
handleDeleteConfirm = () => {
this.dappsStore.delete(this.props.dapp);
this.handleDeleteClose();
this.handleClose();
}
handleSave = () => {
const updates = this.props.dapp.handleSave();
this.setState({ showUpdate: true, updates });
}
handleUpdateClose = () => {
this.setState({ showUpdate: false, updates: null });
}
handleUpdateConfirm = () => {
const { id, owner } = this.props.dapp;
const { updates } = this.state;
this.handleUpdateClose();
this.handleCancel();
this.setState({ updating: true });
return this.dappsStore.update(id, owner.address, updates)
.then(() => {
this.setState({ updating: false });
})
.catch((error) => {
this.setState({ updating: false });
throw error;
});
}
}

View File

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

View File

@ -0,0 +1,38 @@
/* 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/>.
*/
.dapps {
background-color: rgba(255, 255, 255, 0.9);
margin: 1rem;
padding: 1rem;
text-align: left;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
> * {
flex: 0 0 auto;
display: flex;
}
}
.title {
margin: 0 0 0.5rem;
}

View File

@ -0,0 +1,73 @@
// 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 CreateDappCard from '../CreateDappCard';
import DappCard from '../DappCard';
import styles from './dapps.css';
export default class Dapps extends Component {
static propTypes = {
dapps: PropTypes.array.isRequired,
title: PropTypes.string.isRequired,
own: PropTypes.bool
};
static defaultProps = {
own: false
};
render () {
const { dapps, title } = this.props;
return (
<div className={ styles.dapps }>
<h2 className={ styles.title }>{ title }</h2>
<div className={ styles.container }>
{ this.renderAddDapp() }
{ this.renderDapps(dapps) }
</div>
</div>
);
}
renderAddDapp () {
const { own } = this.props;
if (!own) {
return null;
}
return (
<CreateDappCard />
);
}
renderDapps (dapps) {
return dapps.map((dapp) => {
const { id } = dapp;
return (
<DappCard
dapp={ dapp }
key={ id }
/>
);
});
}
}

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

View File

@ -30,6 +30,10 @@
width: 100%; width: 100%;
} }
&.withOverlay input {
padding-right: 3em;
}
input { input {
padding-bottom: 1.5em; padding-bottom: 1.5em;

View File

@ -29,8 +29,14 @@ export default class Input extends Component {
render () { render () {
const { children, hint, label, overlay } = this.props; const { children, hint, label, overlay } = this.props;
const inputClasses = [ styles.input ];
if (overlay) {
inputClasses.push(styles.withOverlay);
}
return ( return (
<div className={ styles.input }> <div className={ inputClasses.join(' ') }>
<label> <label>
{ label } { label }
</label> </label>

View File

@ -15,34 +15,95 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import '../_colors.css';
@import '../_utils.css';
.modal { .modal {
.body { align-items: center;
background-color: $modal-bg;
bottom: 0; bottom: 0;
display: flex;
justify-content: center;
left: 0; left: 0;
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
text-align: center; z-index: 150;
z-index: 50;
.dialog { &.secondary {
background: #fff; align-items: flex-start;
border-radius: 0 0 0.25em 0.25em; z-index: 200;
margin: 0 auto;
max-width: 840px;
text-align: left;
.content { .content {
line-height: 1.5em; line-height: 1.5rem;
padding: 2em; padding: 2rem;
text-align: center; text-align: center;
}
.dialog {
max-width: 840px;
width: 85vw;
}
}
}
.close {
composes: bezier-transform;
color: white;
display: inline-block;
font-family: 'Roboto Mono', monospace;
font-size: 4rem;
opacity: 0.75;
padding: 0;
position: fixed;
right: 1rem;
top: 0.25rem;
&:hover {
cursor: pointer;
opacity: 1;
.closeIcon {
transform: rotate(135deg);
}
}
}
.closeIcon {
composes: bezier-transform;
height: 3rem;
width: 3rem;
transform: rotate(45deg);
}
.dialog {
background-color: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
max-height: 90vh;
overflow: hidden;
position: relative;
max-width: 740px;
width: 75vw;
> * {
padding: 0.5rem 1rem;
}
}
.content {
flex: 1 1 auto;
overflow: auto;
.section { .section {
margin: 0; margin: 0;
padding: 0; padding: 0;
&.error { * {
color: #f44; overflow-x: hidden;
text-overflow: ellipsis;
} }
} }
@ -57,54 +118,18 @@
} }
.header { .header {
background: #44e; background-color: $blue;
color: #fff; color: white;
opacity: 0.85; min-height: 3rem;
padding: 1em;
&.error { &,
background: #e44; & > * {
align-items: center;
display: flex;
flex: 0 0 auto;
flex-direction: row;
} }
} }
}
}
.overlay {
background: rgba(204, 204, 204, 0.7);
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 49;
}
}
.account {
div {
display: inline-block;
vertical-align: top;
}
img {
border-radius: 50%;
margin-right: 0.5em;
}
}
.hint {
display: block !important;
color: #888;
font-size: 0.75em;
margin-top: -0.5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.center {
text-align: center;
}
.heading { .heading {
color: #888; color: #888;

View File

@ -14,53 +14,147 @@
// 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 keycode from 'keycode';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Button from '../Button';
import styles from './modal.css'; import styles from './modal.css';
import CloseImage from '~/../assets/images/dapps/close.svg';
export default class Modal extends Component { export default class Modal extends Component {
static propTypes = { static propTypes = {
buttons: PropTypes.node, actions: PropTypes.array,
children: PropTypes.node, children: PropTypes.node,
error: PropTypes.object, header: PropTypes.node,
header: PropTypes.string secondary: PropTypes.bool,
} onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func
};
static defaultProps = {
actions: null,
secondary: false
};
render () { render () {
const { children, buttons, error, header } = this.props; const { children, actions, header, secondary } = this.props;
const modalClasses = [ styles.modal ];
if (secondary) {
modalClasses.push(styles.secondary);
}
return ( return (
<div className={ styles.modal }> <div
<div className={ styles.overlay } /> className={ modalClasses.join(' ') }
<div className={ styles.body }> onClick={ this.handleClose }
<div className={ styles.dialog }> onKeyUp={ this.handleKeyPress }
<div className={ `${styles.header} ${error ? styles.error : ''}` }> >
<div
className={ styles.dialog }
onClick={ this.stopEvent }
ref={ this.handleSetRef }
tabIndex={ open ? 0 : null }
>
<div className={ styles.header }>
{ header } { header }
<div
className={ styles.close }
onClick={ this.handleClose }
onKeyPress={ this.handleCloseKeyPress }
tabIndex={ open ? 0 : null }
title='close'
>
<img
className={ styles.closeIcon }
src={ CloseImage }
/>
</div> </div>
</div>
<div className={ styles.content }> <div className={ styles.content }>
{ error ? this.renderError() : children } { children }
</div>
<div className={ styles.footer }>
{ buttons }
</div>
</div> </div>
{ actions ? this.renderActions(actions) : null }
</div> </div>
</div> </div>
); );
} }
renderError () { renderActions (actions) {
const { error } = this.props; return (
<div className={ styles.footer }>
{ actions.map((action) => {
let onClick = () => {};
switch (action.type) {
case 'confirm':
onClick = this.handleConfirm;
break;
case 'close':
onClick = this.handleClose;
break;
}
return ( return (
<div> <Button
<div className={ styles.section }> key={ action.type }
Your operation failed to complete sucessfully. The following error was returned: label={ action.label }
</div> warning={ action.warning }
<div className={ `${styles.section} ${styles.error}` }> onClick={ onClick }
{ error.toString() } />
</div> );
}) }
</div> </div>
); );
} }
stopEvent = (event) => {
event.stopPropagation();
event.preventDefault();
return false;
}
handleKeyPress = (event) => {
const codeName = keycode(event);
if (codeName === 'esc') {
return this.handleClose();
}
return event;
}
handleCloseKeyPress = (event) => {
const codeName = keycode(event);
if (codeName === 'enter') {
return this.handleClose();
}
return event;
}
handleSetRef = (containerRef) => {
// Focus after the modal is open
setTimeout(() => {
const element = ReactDOM.findDOMNode(containerRef);
element && element.focus();
}, 100);
}
handleConfirm = () => {
this.props.onConfirm && this.props.onConfirm();
}
handleClose = () => {
this.props.onClose();
}
} }

View File

@ -14,150 +14,48 @@
// 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 React, { Component } from 'react'; import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import { api } from '../parity';
import DappsStore from '../dappsStore';
import ModalStore from '../modalStore';
import Button from '../Button';
import Modal from '../Modal'; import Modal from '../Modal';
import styles from '../Modal/modal.css'; import styles from '../Modal/modal.css';
const HEADERS = [
'Error During Deletion',
'Confirm Application Deletion',
'Waiting for Signer Confirmation',
'Waiting for Transaction Receipt',
'Deletion Completed'
];
const STEP_ERROR = 0;
const STEP_CONFIRM = 1;
const STEP_SIGNER = 2;
const STEP_TXRECEIPT = 3;
const STEP_DONE = 4;
@observer
export default class ModalDelete extends Component { export default class ModalDelete extends Component {
dappsStore = DappsStore.instance(); static propTypes = {
modalStore = ModalStore.instance(); dappId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired
};
render () { render () {
if (!this.modalStore.showingDelete) { const { dappId, onClose, onDelete } = this.props;
return null; const actions = [
} { type: 'close', label: 'No, Cancel' },
{ type: 'confirm', label: 'Yes, Delete', warning: true }
];
return ( return (
<Modal <Modal
buttons={ this.renderButtons() } actions={ actions }
error={ this.modalStore.errorDelete } header='Confirm Application Deletion'
header={ HEADERS[this.modalStore.stepDelete] } onClose={ onClose }
onConfirm={ onDelete }
secondary
> >
{ this.renderStep() }
</Modal>
);
}
renderButtons () {
switch (this.modalStore.stepDelete) {
case STEP_ERROR:
case STEP_DONE:
return [
<Button
key='close'
label='Close'
onClick={ this.onClickClose }
/>
];
case STEP_CONFIRM:
return [
<Button
key='cancel'
label='No, Cancel'
onClick={ this.onClickClose }
/>,
<Button
key='delete'
label='Yes, Delete'
warning
onClick={ this.onClickYes }
/>
];
default:
return null;
}
}
renderStep () {
switch (this.modalStore.stepDelete) {
case STEP_CONFIRM:
return this.renderStepConfirm();
case STEP_SIGNER:
return this.renderStepWait('Waiting for transaction confirmation in the Parity secure signer');
case STEP_TXRECEIPT:
return this.renderStepWait('Waiting for the transaction receipt from the network');
case STEP_DONE:
return this.renderStepCompleted();
default:
return null;
}
}
renderStepCompleted () {
return (
<div>
<div className={ styles.section }> <div className={ styles.section }>
Your application has been removed from the registry. You are about to remove a distributed application from the registry,
</div> the details of this application is given below. Removal does not return any fees,
</div> however the application will not be available to users anymore.
);
}
renderStepConfirm () {
return (
<div>
<div className={ styles.section }>
You are about to remove a distributed application from the registry, the details of this application is given below. Removal does not return any fees, however the application will not be available to users anymore.
</div>
<div className={ styles.section }>
<div className={ styles.heading }>
Owner account
</div>
<div className={ styles.account }>
<img src={ api.util.createIdentityImg(this.dappsStore.currentApp.owner, 3) } />
<div>{ this.dappsStore.currentApp.ownerName }</div>
<div className={ styles.address }>{ this.dappsStore.currentApp.owner }</div>
</div>
</div> </div>
<div className={ styles.section }> <div className={ styles.section }>
<div className={ styles.heading }> <div className={ styles.heading }>
Application identifier Application identifier
</div> </div>
<div> <div>
{ this.dappsStore.currentApp.id } { dappId }
</div>
</div> </div>
</div> </div>
</Modal>
); );
} }
renderStepWait (waitingFor) {
return (
<div>
<div className={ styles.section }>
{ waitingFor }
</div>
</div>
);
}
onClickClose = () => {
this.modalStore.hideDelete();
}
onClickYes = () => {
this.modalStore.doDelete();
}
} }

View File

@ -14,150 +14,55 @@
// 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 React, { Component } from 'react'; import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { api } from '../parity'; import { api } from '../parity';
import DappsStore from '../dappsStore'; import DappsStore from '../dappsStore';
import ModalStore from '../modalStore';
import Button from '../Button';
import Modal from '../Modal'; import Modal from '../Modal';
import styles from '../Modal/modal.css'; import styles from '../Modal/modal.css';
const HEADERS = [
'Error During Registration',
'Confirm Application Registration',
'Waiting for Signer Confirmation',
'Waiting for Transaction Receipt',
'Registration Completed'
];
const STEP_ERROR = 0;
const STEP_CONFIRM = 1;
const STEP_SIGNER = 2;
const STEP_TXRECEIPT = 3;
const STEP_DONE = 4;
@observer @observer
export default class ModalRegister extends Component { export default class ModalRegister extends Component {
dappsStore = DappsStore.instance(); static propTypes = {
modalStore = ModalStore.instance(); dappId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onRegister: PropTypes.func.isRequired
};
dappsStore = DappsStore.get();
render () { render () {
if (!this.modalStore.showingRegister) { const { onClose, onRegister } = this.props;
return null; const actions = [
} { type: 'close', label: 'No, Cancel' },
{ type: 'confirm', label: 'Yes, Register', warning: true }
];
return ( return (
<Modal <Modal
buttons={ this.renderButtons() } actions={ actions }
error={ this.modalStore.errorRegister } header='Confirm Application Registration'
header={ HEADERS[this.modalStore.stepRegister] } onClose={ onClose }
onConfirm={ onRegister }
secondary
> >
{ this.renderStep() }
</Modal>
);
}
renderButtons () {
switch (this.modalStore.stepRegister) {
case STEP_ERROR:
case STEP_DONE:
return [
<Button
key='close'
label='Close'
onClick={ this.onClickClose }
/>
];
case STEP_CONFIRM:
return [
<Button
key='cancel'
label='No, Cancel'
onClick={ this.onClickClose }
/>,
<Button
key='register'
label='Yes, Register'
warning
onClick={ this.onClickConfirmYes }
/>
];
default:
return null;
}
}
renderStep () {
switch (this.modalStore.stepRegister) {
case STEP_CONFIRM:
return this.renderStepConfirm();
case STEP_SIGNER:
return this.renderStepWait('Waiting for transaction confirmation in the Parity secure signer');
case STEP_TXRECEIPT:
return this.renderStepWait('Waiting for the transaction receipt from the network');
case STEP_DONE:
return this.renderStepCompleted();
default:
return null;
}
}
renderStepCompleted () {
return (
<div>
<div className={ styles.section }> <div className={ styles.section }>
Your application has been registered in the registry. You are about to register a new distributed application on the network, the details of
</div> this application is given below. This will require a non-refundable fee
</div> of { api.util.fromWei(this.dappsStore.fee).toFormat(3) } <small>ETH</small>
);
}
renderStepConfirm () {
return (
<div>
<div className={ styles.section }>
You are about to register a new distributed application on the network, the details of this application is given below. This will require a non-refundable fee of { api.util.fromWei(this.dappsStore.fee).toFormat(3) }<small>ETH</small>.
</div>
<div className={ styles.section }>
<div className={ styles.heading }>
Selected owner account
</div>
<div className={ styles.account }>
<img src={ api.util.createIdentityImg(this.dappsStore.currentAccount.address, 3) } />
<div>{ this.dappsStore.currentAccount.name }</div>
<div className={ styles.hint }>{ this.dappsStore.currentAccount.address }</div>
</div>
</div> </div>
<div className={ styles.section }> <div className={ styles.section }>
<div className={ styles.heading }> <div className={ styles.heading }>
Unique assigned application identifier Unique assigned application identifier
</div> </div>
<div> <div>
{ this.dappsStore.wipApp.id } { this.props.dappId }
</div>
</div> </div>
</div> </div>
</Modal>
); );
} }
renderStepWait (waitingFor) {
return (
<div>
<div className={ styles.section }>
{ waitingFor }
</div>
</div>
);
}
onClickClose = () => {
this.modalStore.hideRegister();
}
onClickConfirmYes = () => {
this.modalStore.doRegister();
}
} }

View File

@ -14,160 +14,71 @@
// 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 React, { Component } from 'react'; import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import DappsStore from '../dappsStore';
import ModalStore from '../modalStore';
import Button from '../Button';
import Modal from '../Modal'; import Modal from '../Modal';
import styles from '../Modal/modal.css'; import styles from '../Modal/modal.css';
const HEADERS = [
'Error During Update',
'Confirm Application Update',
'Waiting for Signer Confirmation',
'Waiting for Transaction Receipt',
'Update Completed'
];
const STEP_ERROR = 0;
const STEP_CONFIRM = 1;
const STEP_SIGNER = 2;
const STEP_TXRECEIPT = 3;
const STEP_DONE = 4;
@observer
export default class ModalUpdate extends Component { export default class ModalUpdate extends Component {
dappsStore = DappsStore.instance(); static propTypes = {
modalStore = ModalStore.instance(); dappId: PropTypes.string.isRequired,
updates: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired
};
render () { render () {
if (!this.modalStore.showingUpdate) { const { dappId, onClose, onConfirm } = this.props;
return null; const actions = [
} { type: 'close', label: 'No, Cancel' },
{ type: 'confirm', label: 'Yes, Update', warning: true }
];
return ( return (
<Modal <Modal
buttons={ this.renderButtons() } actions={ actions }
error={ this.modalStore.errorUpdate } header='Confirm Application Update'
header={ HEADERS[this.modalStore.stepUpdate] } onClose={ onClose }
onConfirm={ onConfirm }
secondary
> >
{ this.renderStep() }
</Modal>
);
}
renderButtons () {
switch (this.modalStore.stepUpdate) {
case STEP_ERROR:
case STEP_DONE:
return [
<Button
key='close'
label='Close'
onClick={ this.onClickClose }
/>
];
case STEP_CONFIRM:
return [
<Button
key='cancel'
label='No, Cancel'
onClick={ this.onClickClose }
/>,
<Button
key='delete'
label='Yes, Update'
warning
onClick={ this.onClickYes }
/>
];
default:
return null;
}
}
renderStep () {
switch (this.modalStore.stepUpdate) {
case STEP_CONFIRM:
return this.renderStepConfirm();
case STEP_SIGNER:
return this.renderStepWait('Waiting for transaction confirmation in the Parity secure signer');
case STEP_TXRECEIPT:
return this.renderStepWait('Waiting for the transaction receipt from the network');
case STEP_DONE:
return this.renderStepCompleted();
default:
return null;
}
}
renderStepCompleted () {
return (
<div>
<div className={ styles.section }> <div className={ styles.section }>
Your application metadata has been updated in the registry. You are about to update the application details in the registry,
</div> the details of these updates are given below. Please note that each
</div> update will generate a seperate transaction.
);
}
renderStepConfirm () {
return (
<div>
<div className={ styles.section }>
You are about to update the application details in the registry, the details of these updates are given below. Please note that each update will generate a seperate transaction.
</div> </div>
<div className={ styles.section }> <div className={ styles.section }>
<div className={ styles.heading }> <div className={ styles.heading }>
Application identifier Application identifier
</div> </div>
<div> <div>
{ this.dappsStore.wipApp.id } { dappId }
</div> </div>
</div> </div>
{ this.renderChanges() } { this.renderChanges() }
</div> </Modal>
); );
} }
renderChanges () { renderChanges () {
return ['content', 'image', 'manifest'] const { updates } = this.props;
.filter((type) => this.dappsStore.wipApp[`${type}Changed`])
return Object.keys(updates)
.map((type) => { .map((type) => {
return ( return (
<div className={ styles.section } key={ `${type}Update` }> <div
className={ styles.section }
key={ `${type}Update` }
>
<div className={ styles.heading }> <div className={ styles.heading }>
Updates to { type } hash Updates to { type }
</div> </div>
<div> <div>
<div>{ this.dappsStore.wipApp[`${type}Hash`] || '(removed)' }</div> <div>{ updates[type] || '(removed)' }</div>
<div className={ styles.hint }>
{ this.dappsStore.wipApp[`${type}Url`] || 'current url to be removed from registry' }
</div>
</div> </div>
</div> </div>
); );
}); });
} }
renderStepWait (waitingFor) {
return (
<div>
<div className={ styles.section }>
{ waitingFor }
</div>
</div>
);
}
onClickClose = () => {
this.modalStore.hideUpdate();
}
onClickYes = () => {
this.modalStore.doUpdate();
}
} }

View File

@ -14,20 +14,27 @@
// 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 React, { Component } from 'react'; import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import DappsStore from '../dappsStore'; import DappsStore from '../dappsStore';
@observer @observer
export default class SelectAccount extends Component { export default class SelectAccount extends Component {
dappsStore = DappsStore.instance(); dappsStore = DappsStore.get();
static propTypes = {
value: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired
};
render () { render () {
const { value } = this.props;
return ( return (
<select <select
value={ this.dappsStore.currentAccount.address } value={ value }
onChange={ this.onSelect } onChange={ this.handleSelect }
> >
{ this.renderOptions() } { this.renderOptions() }
</select> </select>
@ -44,7 +51,7 @@ export default class SelectAccount extends Component {
}); });
} }
onSelect = (event) => { handleSelect = (event) => {
this.dappsStore.setCurrentAccount(event.target.value); this.props.onSelect && this.props.onSelect(event);
} }
} }

View File

@ -1,85 +0,0 @@
// 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 } from 'react';
import { observer } from 'mobx-react';
import DappsStore from '../dappsStore';
import Input from '../Input';
@observer
export default class SelectDapp extends Component {
dappsStore = DappsStore.instance();
render () {
if (this.dappsStore.isNew) {
return (
<Input
hint='...'
label='Application Id, the unique assigned identifier'
>
<input value={ this.dappsStore.wipApp.id } readOnly />
</Input>
);
}
if (!this.dappsStore.currentApp) {
return null;
}
let overlayImg = null;
if (this.dappsStore.currentApp.imageHash) {
overlayImg = (
<img src={ `/api/content/${this.dappsStore.currentApp.imageHash.substr(2)}` } />
);
}
return (
<Input
hint={ this.dappsStore.currentApp.id }
label='Application, the actual application details to show below'
overlay={ overlayImg }
>
<select
disabled={ this.dappsStore.isEditing }
value={ this.dappsStore.currentApp.id }
onChange={ this.onSelect }
>
{ this.renderOptions() }
</select>
</Input>
);
}
renderOptions () {
return this.dappsStore.apps.map((app) => {
return (
<option
value={ app.id }
key={ app.id }
>
{ app.name }
</option>
);
});
}
onSelect = (event) => {
this.dappsStore.setCurrentApp(event.target.value);
}
}

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

View File

@ -0,0 +1,95 @@
/* 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/>.
*/
.container {
bottom: 4rem;
position: fixed;
right: 0;
z-index: 150;
}
.transaction {
display: flex;
flex-direction: column;
margin-top: 0.5rem;
width: 30rem;
.header {
flex: 0 0 auto;
height: 1.5rem;
position: relative;
width: 100%;
> * {
display: inline-block;
font-family: 'Roboto Mono', monospace;
font-size: 0.75rem;
overflow: hidden;
padding: 0.25rem 0.5rem;
position: absolute;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.name {
background-color: rgba(40, 40, 40, 0.95);
color: white;
left: 0.5rem;
margin-right: 1.5rem;
max-width: 20rem;
}
.date {
background-color: rgba(215, 215, 215, 0.95);
color: black;
max-width: 5rem;
right: 0.5rem;
}
.content {
align-items: center;
background-color: rgba(80, 80, 80, 0.95);
color: white;
display: flex;
flex: 1 1 auto;
padding: 1rem 0.75rem;
> * {
max-width: 100%;
white-space: pre-wrap;
word-wrap: break-word;
}
&:hover {
cursor: pointer;
}
&.error {
background-color: rgba(255, 68, 68, 0.95);
}
&.pending {
background-color: rgba(243, 156, 18, 0.95);
}
&.confirmed {
background-color: rgba(39, 174, 96, 0.95);
color: white;
}
}
}

View File

@ -0,0 +1,136 @@
// 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 } from 'react';
import { observer } from 'mobx-react';
import DappsStore from '../dappsStore';
import styles from './transactions.css';
@observer
export default class Transactions extends Component {
dappsStore = DappsStore.get();
render () {
const { transactions } = this.dappsStore;
const displayedTransactions = Object.values(transactions)
.filter((tx) => !tx.hide)
.sort((txA, txB) => txB.start - txA.start);
return (
<div className={ styles.container }>
{ displayedTransactions.map((transaction) => this.renderTransaction(transaction)) }
</div>
);
}
renderTransaction (transaction) {
const { error, name, requestId, start, transactionHash, transactionReceipt } = transaction;
const date = new Date(start);
const isError = !!error;
const isPendingNetwork = transactionHash && !transactionReceipt;
const isConfirmed = !!transactionReceipt;
const transactionClasses = [ styles.content ];
const handleHideTransaction = (event) => this.handleHideTransaction(event, requestId);
if (isError) {
transactionClasses.push(styles.error);
}
if (isPendingNetwork) {
transactionClasses.push(styles.pending);
}
if (isConfirmed) {
transactionClasses.push(styles.confirmed);
}
return (
<div
className={ styles.transaction }
key={ requestId }
>
<div className={ styles.header }>
{
name
? (
<div
className={ styles.name }
title={ name }
>
{ name }
</div>
)
: null
}
<div
className={ styles.date }
title={ date.toISOString() }
>
{ date.toLocaleTimeString() }
</div>
</div>
<div
className={ transactionClasses.join(' ') }
onClick={ handleHideTransaction }
>
{ this.renderTransactionContent(transaction) }
</div>
</div>
);
}
renderTransactionContent (transaction) {
const { error, transactionHash, transactionReceipt } = transaction;
if (error) {
return (
<div>
{ error.text || error.message || error.toString() }
</div>
);
}
if (transactionReceipt) {
return (
<div>
Transaction mined at block { transactionReceipt.blockNumber.toFormat(0) }
</div>
);
}
if (transactionHash) {
return (
<div>
Transaction sent to network with hash { transactionHash }..
</div>
);
}
return (
<div>
Transaction waiting to be signed...
</div>
);
}
handleHideTransaction = (event, requestId) => {
this.dappsStore.updateTransaction(requestId, { hide: true });
}
}

View File

@ -15,11 +15,13 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import '../_colors.css';
.warning { .warning {
background: #f44; background: $warning-bg;
border-top-right-radius: 0.25em; border-top-right-radius: 0.25em;
bottom: 0; bottom: 0;
color: #fff; color: white;
cursor: pointer; cursor: pointer;
font-size: 0.75em; font-size: 0.75em;
left: 0; left: 0;

View File

@ -15,37 +15,41 @@
// 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 } from 'react'; import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { api } from '../parity'; import { api } from '../parity';
import DappsStore from '../dappsStore'; import DappsStore from '../dappsStore';
import ModalStore from '../modalStore';
import styles from './warning.css'; import styles from './warning.css';
@observer
export default class Warning extends Component { export default class Warning extends Component {
dappsStore = DappsStore.instance(); dappsStore = DappsStore.get();
modalStore = ModalStore.instance();
state = {
show: true
};
render () { render () {
if (!this.modalStore.showingWarning) { if (!this.state.show) {
return null; return null;
} }
return ( return (
<div className={ styles.warning } onClick={ this.onClose }> <div className={ styles.warning } onClick={ this.onClose }>
<div> <div>
WARNING: Registering a dapp is for developers only. Please ensure you understand the steps needed to develop and deploy applications, should you wish to use this dapp for anything apart from queries. WARNING: Registering a dapp is for developers only. Please ensure you understand the
steps needed to develop and deploy applications, should you wish to use this dapp for
anything apart from queries.
</div> </div>
<div> <div>
A non-refundable fee of { api.util.fromWei(this.dappsStore.fee).toFormat(3) }<small>ETH</small> is required for any registration. A non-refundable fee
of { api.util.fromWei(this.dappsStore.fee).toFormat(3) } <small>ETH</small> is required
for any registration.
</div> </div>
</div> </div>
); );
} }
onClose = () => { onClose = () => {
this.modalStore.hideWarning(); this.setState({ show: false });
} }
} }

View File

@ -0,0 +1,30 @@
/* 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/>.
*/
$blue: rgb(41, 128, 185);
$disabled-bg: #aaa;
$warning-bg: #e44;
$modal-bg: rgba(40, 40, 40, 0.75);
$background-color: #eee;
$text-color: #333;
$loading-color: #999;
$code-bg: #002b36;
$code-color: #b58900;
$code-title-bg: #073642;
$code-title-color: #859900;

View File

@ -15,5 +15,8 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.app { .bezier-transform {
transition-duration: 0.1s;
transition-property: all;
transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
} }

View File

@ -0,0 +1,153 @@
// 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, computed, observable, transaction } from 'mobx';
export default class DappStore {
@observable id = null;
@observable content = null;
@observable image = null;
@observable manifest = null;
@observable owner = null;
@observable isOwner = false;
@observable isEditing = false;
@observable wip = null;
contractOwner = '';
isContractOwner = false;
constructor (data) {
const { id, content = {}, image = {}, manifest = {}, owner = {}, isOwner = false, contractOwner = '', isContractOwner = false } = data;
transaction(() => {
this.id = id;
this.content = content;
this.image = image;
this.manifest = manifest;
this.owner = owner;
this.isOwner = isOwner;
this.copyToWip();
});
this.contractOwner = contractOwner;
this.isContractOwner = isContractOwner;
}
@computed get canSave () {
if (!this.wip) {
return false;
}
const { content, image, manifest, owner } = this.wip;
const fields = [ content, image, manifest, owner ];
const hasError = !!fields.find((field) => field.error);
const hasChanged = !!fields.find((field) => field.changed);
const isEditMode = this.isEditing;
return isEditMode && hasChanged && !hasError;
}
@action copyToWip = () => {
const defaults = {
changed: false,
error: null
};
const wip = {
id: this.id,
content: {
...defaults,
url: this.content.url
},
image: {
...defaults,
url: this.image.url
},
manifest: {
...defaults,
url: this.manifest.url
},
owner: {
...defaults,
address: this.owner.address
}
};
this.wip = { ...wip };
}
@action handleChange = (details) => {
if (!this.isEditing) {
return false;
}
this.wip = {
...this.wip,
...details
};
return this.wip;
}
@action handleSave = () => {
const updates = {};
if (this.wip.content.url !== this.content.url) {
updates.content = this.wip.content.url;
}
if (this.wip.image.url !== this.image.url) {
updates.image = this.wip.image.url;
}
if (this.wip.manifest.url !== this.manifest.url) {
updates.manifest = this.wip.manifest.url;
}
if (this.wip.owner.address !== this.owner.address) {
updates.owner = this.wip.owner.address;
}
return updates;
}
@action setEditing = (mode) => {
transaction(() => {
this.isEditing = mode;
this.copyToWip();
});
return mode;
}
update = (updates) => {
const { image, content } = updates;
const changes = {};
if (image && image !== this.wip.image.url) {
changes.image = { url: image, changed: true };
}
if (content && content !== this.wip.content.url) {
changes.content = { url: content, changed: true };
}
return this.handleChange(changes);
}
}

View File

@ -16,11 +16,15 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { action, computed, observable, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import { flatten } from 'lodash';
import * as abis from '~/contracts/abi'; import * as abis from '~/contracts/abi';
import Contracts from '~/contracts';
import builtins from '~/views/Dapps/builtin.json'; import builtins from '~/views/Dapps/builtin.json';
import Dapp from './dappStore.js';
import { deleteDapp, registerDapp, updateDapp } from './utils';
import { api } from './parity'; import { api, trackRequest } from './parity';
let instance = null; let instance = null;
@ -28,23 +32,22 @@ export default class DappsStore {
@observable accounts = []; @observable accounts = [];
@observable apps = []; @observable apps = [];
@observable contractOwner = null; @observable contractOwner = null;
@observable currentAccount = null;
@observable currentApp = null;
@observable count = 0; @observable count = 0;
@observable fee = new BigNumber(0); @observable fee = new BigNumber(0);
@observable isContractOwner = false; @observable isContractOwner = false;
@observable isEditing = false;
@observable isLoading = true; @observable isLoading = true;
@observable isNew = false; @observable transactions = {};
@observable wipApp = null;
_instanceGhh = null;
_instanceReg = null;
_registry = null;
_startTime = Date.now(); _startTime = Date.now();
constructor () { constructor () {
this._loadDapps(); this._loadDapps();
} }
static instance () { static get () {
if (!instance) { if (!instance) {
instance = new DappsStore(); instance = new DappsStore();
} }
@ -52,141 +55,69 @@ export default class DappsStore {
return instance; return instance;
} }
@computed get canSave () { createDappId () {
const app = this.wipApp; return api.util.sha3(`${this._startTime}_${Date.now()}`);
const hasError = app.contentError || app.imageError || app.manifestError;
const isDirty = this.isNew || app.contentChanged || app.imageChanged || app.manifestChanged;
const isEditMode = this.isEditing || this.isNew;
return isEditMode && isDirty && !hasError;
}
@computed get isCurrentEditable () {
return !!this.accounts.find((account) => account.address === this.currentApp.owner);
} }
@computed get ownedCount () { @computed get ownedCount () {
return (this.apps.filter((app) => app.isOwner) || []).length; return this.ownDapps.length;
} }
@action copyToWip = () => { @computed get ownDapps () {
let wipApp; return this.apps.filter((app) => app.isOwner);
}
if (this.isNew) { @computed get otherDapps () {
wipApp = { return this.apps.filter((app) => !app.isOwner);
id: api.util.sha3(`${this._startTime}_${Date.now()}`), }
contentHash: null,
contentUrl: null, @action sortApps = () => {
imageHash: null, // Sort dapps per name, then per id
imageUrl: null, const sort = (a, b) => {
manifestHash: null, if (a.name && b.name) {
manifestUrl: null return a.name.localeCompare(b.name);
}
if (a.name) {
return -1;
}
if (b.name) {
return 1;
}
return a.id - b.id;
}; };
} else {
const app = this.currentApp;
wipApp = {
id: app.id,
contentHash: app.contentHash,
contentUrl: app.contentUrl,
imageHash: app.imageHash,
imageUrl: app.imageUrl,
manifestHash: app.manifestHash,
manifestUrl: app.manifestUrl,
owner: app.owner,
ownerName: app.ownerName
};
}
this.wipApp = Object.assign(wipApp, {
contentChanged: false,
contentError: null,
imageChanged: false,
imageError: null,
manifestChanged: false,
manifestError: null
});
return this.wipApp;
}
@action editWip = (details) => {
if (this.isNew || this.isEditing) {
transaction(() => { transaction(() => {
Object const ownDapps = this.ownDapps.sort(sort);
.keys(details) const otherDapps = this.otherDapps.sort(sort);
.forEach((key) => {
this.wipApp[key] = details[key]; this.apps = ownDapps.concat(otherDapps);
});
}); });
} }
return this.wipApp; @action setApps = (dapps) => {
} const filteredDapps = dapps.filter((dapp) => {
return new BigNumber(dapp.id).gt(0);
});
@action sortApps = (apps = this.apps) => {
transaction(() => { transaction(() => {
const ownApps = apps this.apps = filteredDapps;
.filter((app) => app.isOwner) this.sortApps();
.sort((a, b) => a.name.localeCompare(b.name));
const otherApps = apps
.filter((app) => !app.isOwner)
.sort((a, b) => a.name.localeCompare(b.name));
this.apps = ownApps.concat(otherApps);
if (this.apps.length) {
this.currentApp = this.apps[0];
}
}); });
} }
@action setApps = (apps) => { @action refreshApp = (dappId) => {
this.sortApps(apps.filter((app) => { const dapp = this.apps.find((dapp) => dapp.id === dappId);
const bnid = new BigNumber(app.id);
return bnid.gt(0); this._loadDapp(dapp);
}));
return this.apps;
} }
@action _addApp = (app) => { @action removeApp = (dappId) => {
transaction(() => { const dapps = this.apps.filter((dapp) => dapp.id !== dappId);
this.setApps(this.apps.concat([app]));
this.setCurrentApp(app.id);
});
}
@action addApp = (appId, account) => { this.setApps(dapps);
this
._loadDapp({
id: appId,
isOwner: true,
name: `- ${appId}`,
owner: account.address,
ownerName: account.name
})
.then(this._addApp);
}
@action refreshApp = (appId) => {
this._loadDapp(this.apps.find((app) => app.id === appId));
}
@action removeApp = (appId) => {
this.setApps(this.apps.filter((app) => app.id !== appId));
}
@action setAppInfo = (app, info) => {
transaction(() => {
Object.keys(info).forEach((key) => {
app[key] = info[key];
});
});
return app;
} }
@action setAccounts = (accountsInfo) => { @action setAccounts = (accountsInfo) => {
@ -199,8 +130,6 @@ export default class DappsStore {
account.address = address; account.address = address;
return account; return account;
}); });
this.currentAccount = this.accounts[0];
}); });
return this.accounts; return this.accounts;
@ -214,30 +143,11 @@ export default class DappsStore {
return contractOwner; return contractOwner;
} }
@action setCurrentApp = (id) => {
this.currentApp = this.apps.find((app) => app.id === id);
return this.currentApp;
}
@action setCurrentAccount = (address) => {
this.currentAccount = this.accounts.find((account) => account.address === address);
return this.currentAccount;
}
@action setCount = (count) => { @action setCount = (count) => {
this.count = count; this.count = count;
return count; return count;
} }
@action setEditing = (mode) => {
transaction(() => {
this.isEditing = mode;
this.copyToWip();
});
return mode;
}
@action setFee = (fee) => { @action setFee = (fee) => {
this.fee = fee; this.fee = fee;
return fee; return fee;
@ -248,13 +158,106 @@ export default class DappsStore {
return loading; return loading;
} }
@action setNew = (mode) => { @action updateTransaction = (requestId, nextData) => {
transaction(() => { const prevTransaction = this.transactions[requestId] || { requestId };
this.isNew = mode; const nextTransaction = {
this.copyToWip(); ...prevTransaction,
hide: false,
...nextData
};
this.transactions = {
...this.transactions,
[ requestId ]: nextTransaction
};
}
fetchRegistryData (dapp) {
const ownerAddress = (dapp.wip && dapp.wip.owner.address) || dapp.owner.address;
this._registry.reverse
.call({}, [ ownerAddress ])
.then((name) => {
if (!name) {
return;
}
const key = api.util.sha3.text(name);
return Promise
.all([
this._registry.get.call({}, [ key, 'IMG' ])
.then((bytes) => api.util.bytesToHex(bytes))
.then((hash) => this._instanceGhh.entries.call({}, [ hash ])),
this._registry.get.call({}, [ key, 'CONTENT' ])
.then((bytes) => api.util.bytesToHex(bytes))
.then((hash) => this._instanceGhh.entries.call({}, [ hash ]))
])
.then(([ imageGHH, contentGHH ]) => {
const imageUrl = imageGHH[0];
const contentUrl = contentGHH[0];
return dapp.update({
image: imageUrl,
content: contentUrl
});
});
});
}
register (dappId) {
const dappRegInstance = this._instanceReg;
const dappRegFee = this.fee;
return registerDapp(dappId, dappRegInstance, dappRegFee)
.then((request) => this.trackRequest(request, `Registering ${dappId}`))
.then(() => this._loadDapps());
}
delete (dapp) {
const dappRegInstance = this._instanceReg;
return deleteDapp(dapp, dappRegInstance)
.then((request) => this.trackRequest(request, `Deleting ${dapp.id}`))
.then(() => this._loadDapps());
}
update (dappId, dappOwner, updates) {
const dappRegInstance = this._instanceReg;
const ghhRegInstance = this._instanceGhh;
const promises = updateDapp(dappId, dappOwner, updates, dappRegInstance, ghhRegInstance);
const handledPromises = promises.map((promise) => {
return promise
.then((requests) => {
const requestPromises = flatten([].concat(requests))
.filter((request) => request)
.map((request) => this.trackRequest(request.id, request.name));
return Promise.all(requestPromises);
})
.catch((error) => {
const randomRequestId = api.util.sha3(Date.now()).slice(0, 5);
this.updateTransaction(randomRequestId, { start: Date.now(), error });
});
}); });
return mode; return Promise.all(handledPromises)
.then(() => this._loadDapps());
}
trackRequest (requestId, name) {
const statusCallback = (error, data) => {
if (error) {
return this.updateTransaction(requestId, { error });
}
return this.updateTransaction(requestId, data);
};
this.updateTransaction(requestId, { name, start: Date.now() });
return trackRequest(requestId, statusCallback);
} }
lookupHash (hash) { lookupHash (hash) {
@ -310,26 +313,30 @@ export default class DappsStore {
return Promise.all(promises); return Promise.all(promises);
}) })
.then((appsInfo) => { .then((dappsInfo) => {
return Promise.all( const dapps = dappsInfo.reduce((dapps, [dappId, ownerAddress]) => {
this const id = api.util.bytesToHex(dappId);
.setApps(appsInfo.map(([appId, owner]) => { const owner = this.accounts.find((account) => account.address === ownerAddress);
const isOwner = !!this.accounts.find((account) => account.address === owner); const isOwner = !!owner;
const account = this.accounts.find((account) => account.address === owner); const dapp = {
const id = api.util.bytesToHex(appId);
return {
id, id,
owner, owner: owner || { address: ownerAddress },
ownerName: account ? account.name : owner, isOwner
isOwner,
name: `- ${id}`
}; };
}))
.map(this._loadDapp) dapps[id] = dapp;
); return dapps;
}, {});
const promises = Object.values(dapps)
// Only show dapps with id and owners
.filter((dapp) => dapp.id && dapp.owner && !/^(0x)?0*$/.test(dapp.owner.address))
.map((dapp) => this._loadDapp(dapp));
return Promise.all(promises);
}) })
.then(() => { .then((dapps) => {
this.setApps(dapps);
this.sortApps(); this.sortApps();
this.setLoading(false); this.setLoading(false);
}) })
@ -338,12 +345,14 @@ export default class DappsStore {
}); });
} }
_loadDapp = (app) => { _loadDapp = (dappData) => {
const { id, owner, isOwner } = dappData;
return Promise return Promise
.all([ .all([
this._loadMeta(app.id, 'CONTENT'), this._loadMeta(id, 'CONTENT'),
this._loadMeta(app.id, 'IMG'), this._loadMeta(id, 'IMG'),
this._loadMeta(app.id, 'MANIFEST') this._loadMeta(id, 'MANIFEST')
]) ])
.then(([contentHash, imageHash, manifestHash]) => { .then(([contentHash, imageHash, manifestHash]) => {
return Promise return Promise
@ -354,25 +363,47 @@ export default class DappsStore {
]) ])
.then(([contentUrl, imageUrl, manifestUrl]) => { .then(([contentUrl, imageUrl, manifestUrl]) => {
return this return this
._loadManifest(app.id, manifestHash) ._loadManifest(id, manifestHash, manifestUrl)
.then((manifest) => { .then((manifestContent) => {
this.setAppInfo(app, { const content = {
manifest, hash: contentHash,
manifestHash, url: contentUrl
manifestUrl, };
contentHash,
contentUrl,
imageHash,
imageUrl,
name: (manifest && manifest.name) || `- ${app.id}`
});
return app; const image = {
hash: imageHash,
url: imageUrl
};
const manifest = {
content: manifestContent,
hash: manifestHash,
url: manifestUrl
};
return { content, image, manifest };
}); });
}); });
}) })
.catch((error) => { .catch((error) => {
console.error('Store:loadDapp', error); console.error('dappsStore::loadDapp', error);
return {};
})
.then((data) => {
const { content, image, manifest } = data;
const dapp = new Dapp({
contractOwner: this.contractOwner,
isContractOwner: this.isContractOwner,
id,
content,
image,
manifest,
owner,
isOwner
});
return dapp;
}); });
} }
@ -398,7 +429,9 @@ export default class DappsStore {
if (builtin) { if (builtin) {
return Promise.resolve(builtin); return Promise.resolve(builtin);
} else if (!manifestHash) { }
if (!manifestHash) {
return Promise.resolve(null); return Promise.resolve(null);
} }
@ -453,11 +486,10 @@ export default class DappsStore {
} }
_loadRegistry () { _loadRegistry () {
return api.parity return Contracts.create(api).registry
.registryAddress() .fetchContract()
.then((registryAddress) => { .then((contract) => {
console.log(`the registry was found at ${registryAddress}`); this._registry = contract.instance;
this._registry = api.newContract(abis.registry, registryAddress).instance;
}) })
.catch((error) => { .catch((error) => {
console.error('Store:loadRegistry', error); console.error('Store:loadRegistry', error);
@ -472,9 +504,11 @@ export default class DappsStore {
]) ])
.then(([dappregAddress, ghhAddress]) => { .then(([dappregAddress, ghhAddress]) => {
console.log(`dappreg was found at ${dappregAddress}`); console.log(`dappreg was found at ${dappregAddress}`);
console.log(`githubhint was found at ${ghhAddress}`);
this._contractReg = api.newContract(abis.dappreg, dappregAddress); this._contractReg = api.newContract(abis.dappreg, dappregAddress);
this._instanceReg = this._contractReg.instance; this._instanceReg = this._contractReg.instance;
console.log(`githubhint was found at ${ghhAddress}`);
this._contractGhh = api.newContract(abis.githubhint, ghhAddress); this._contractGhh = api.newContract(abis.githubhint, ghhAddress);
this._instanceGhh = this._contractGhh.instance; this._instanceGhh = this._contractGhh.instance;
}) })

View File

@ -1,266 +0,0 @@
// 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, transaction } from 'mobx';
import { trackRequest } from './parity';
import DappsStore from './dappsStore';
let instance = null;
export default class ModalStore {
@observable errorDelete = null;
@observable errorRegister = null;
@observable errorUpdate = null;
@observable stepDelete = 0;
@observable stepRegister = 0;
@observable stepUpdate = 0;
@observable showingDelete = false;
@observable showingRegister = false;
@observable showingUpdate = false;
@observable showingWarning = true;
_dappsStore = DappsStore.instance();
static instance () {
if (!instance) {
instance = new ModalStore();
}
return instance;
}
@action setDeleteError (error) {
transaction(() => {
this.setDeleteStep(0);
this.errorDelete = error;
});
}
@action setDeleteStep (step) {
this.stepDelete = step;
}
@action showDelete () {
transaction(() => {
this.setDeleteStep(1);
this.errorDelete = null;
this.showingDelete = true;
});
}
@action hideDelete () {
this.showingDelete = false;
}
@action setRegisterError (error) {
transaction(() => {
this.setRegisterStep(0);
this.errorRegister = error;
});
}
@action setRegisterStep (step) {
this.stepRegister = step;
}
@action showRegister () {
transaction(() => {
this.setRegisterStep(1);
this.errorRegister = null;
this.showingRegister = true;
});
}
@action hideRegister () {
transaction(() => {
this._dappsStore.setEditing(false);
this._dappsStore.setNew(false);
this.showingRegister = false;
});
}
@action setUpdateError (error) {
transaction(() => {
this.setUpdateStep(0);
this.errorUpdate = error;
});
}
@action setUpdateStep (step) {
this.stepUpdate = step;
}
@action showUpdate () {
transaction(() => {
this.setUpdateStep(1);
this.errorUpdate = null;
this.showingUpdate = true;
});
}
@action hideUpdate () {
transaction(() => {
this._dappsStore.setEditing(false);
this._dappsStore.setNew(false);
this.showingUpdate = false;
});
}
@action hideWarning () {
this.showingWarning = false;
}
doDelete () {
this.setDeleteStep(2);
const appId = this._dappsStore.currentApp.id;
const values = [appId];
const options = {
from: this._dappsStore.currentApp.isOwner ? this._dappsStore.currentApp.owner : this._dappsStore.contractOwner
};
console.log('ModalStore:doDelete', `performing deletion for ${appId} from ${options.from}`);
this._dappsStore._instanceReg
.unregister.estimateGas(options, values)
.then((gas) => {
const newGas = gas.mul(1.2);
console.log('ModalStore:doDelete', `gas estimated as ${gas.toFormat(0)}, setting to ${newGas.toFormat(0)}`);
options.gas = newGas.toFixed(0);
const request = this._dappsStore._instanceReg.unregister.postTransaction(options, values);
const statusCallback = (error, status) => {
if (error) {
} else if (status.signerRequestId) {
} else if (status.transactionHash) {
this.setDeleteStep(3);
} else if (status.transactionReceipt) {
this.setDeleteStep(4);
this._dappsStore.removeApp(appId);
}
};
return trackRequest(request, statusCallback);
})
.catch((error) => {
console.error('ModalStore:doDelete', error);
this.setDeleteError(error);
});
}
doRegister () {
this.setRegisterStep(2);
const appId = this._dappsStore.wipApp.id;
const values = [appId];
const options = {
from: this._dappsStore.currentAccount.address,
value: this._dappsStore.fee
};
console.log('ModalStore:doRegister', `performing registration for ${appId} from ${this._dappsStore.currentAccount.address}`);
this._dappsStore._instanceReg
.register.estimateGas(options, values)
.then((gas) => {
const newGas = gas.mul(1.2);
console.log('ModalStore:doRegister', `gas estimated as ${gas.toFormat(0)}, setting to ${newGas.toFormat(0)}`);
options.gas = newGas.toFixed(0);
const request = this._dappsStore._instanceReg.register.postTransaction(options, values);
const statusCallback = (error, status) => {
if (error) {
} else if (status.signerRequestId) {
} else if (status.transactionHash) {
this.setRegisterStep(3);
} else if (status.transactionReceipt) {
this.setRegisterStep(4);
this._dappsStore.addApp(appId, this._dappsStore.currentAccount);
}
};
return trackRequest(request, statusCallback);
})
.catch((error) => {
console.error('ModalStore:doRegister', error);
this.setRegisterError(error);
});
}
doUpdate () {
this.setUpdateStep(2);
const appId = this._dappsStore.wipApp.id;
const options = {
from: this._dappsStore.wipApp.owner
};
const types = {
'content': 'CONTENT',
'image': 'IMG',
'manifest': 'MANIFEST'
};
const values = Object
.keys(types)
.filter((type) => this._dappsStore.wipApp[`${type}Changed`])
.map((type) => {
return [appId, types[type], this._dappsStore.wipApp[`${type}Hash`] || '0x0'];
});
console.log('ModalStore:doUpdate', `performing updates for ${appId} from ${options.from}`);
Promise
.all(values.map((value) => this._dappsStore._instanceReg.setMeta.estimateGas(options, value)))
.then((gas) => {
const newGas = gas.map((gas) => gas.mul(1.2));
gas.forEach((gas, index) => {
console.log('ModalStore:doUpdate', `${values[index][1]} gas estimated as ${gas.toFormat(0)}, setting to ${newGas[index].toFormat(0)}`);
});
const statusCallback = (error, status) => {
if (error) {
} else if (status.signerRequestId) {
} else if (status.transactionHash) {
this.setUpdateStep(3);
} else if (status.transactionReceipt) {
this.setUpdateStep(4);
}
};
return Promise.all(
newGas.map((gas, index) => {
return trackRequest(
this._dappsStore._instanceReg.setMeta.postTransaction(
Object.assign(options, { gas: gas.toFixed(0) }), values[index]
), statusCallback
);
})
);
})
.then(() => {
this._dappsStore.refreshApp(appId);
})
.catch((error) => {
console.error('ModalStore:doUpdate', error);
this.setUpdateError(error);
});
}
}

View File

@ -14,16 +14,10 @@
// 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/>.
const { api } = window.parity; const api = window.parent.secureApi;
function trackRequest (requestPromise, statusCallback) { function trackRequest (signerRequestId, statusCallback) {
return requestPromise return api.pollMethod('parity_checkRequest', signerRequestId)
.then((signerRequestId) => {
console.log('trackRequest', `posted to signer with requestId ${signerRequestId.toString()}`);
statusCallback(null, { signerRequestId });
return api.pollMethod('parity_checkRequest', signerRequestId);
})
.then((transactionHash) => { .then((transactionHash) => {
console.log('trackRequest', `received transaction hash ${transactionHash}`); console.log('trackRequest', `received transaction hash ${transactionHash}`);
statusCallback(null, { transactionHash }); statusCallback(null, { transactionHash });
@ -41,9 +35,7 @@ function trackRequest (requestPromise, statusCallback) {
statusCallback(null, { transactionReceipt }); statusCallback(null, { transactionReceipt });
}) })
.catch((error) => { .catch((error) => {
console.error('trackRequest', error);
statusCallback(error); statusCallback(error);
throw error;
}); });
} }

View File

@ -0,0 +1,184 @@
// 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 { api } from './parity';
export const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
/**
* Convert the given URL to a content hash,
* and checks if it is already registered in GHH
*/
export const urlToHash = (api, instance, url) => {
if (!url || !url.length) {
return Promise.resolve(null);
}
return api.parity
.hashContent(url)
.catch((error) => {
const message = error.text || error.message || error.toString();
throw new Error(`${message} (${url})`);
})
.then((contentHash) => {
console.log('lookupHash', url, contentHash);
if (contentHash === INVALID_URL_HASH) {
throw new Error(`"${url}" is not a valid URL`);
}
return instance.entries
.call({}, [contentHash])
.then(([accountSlashRepo, commit, contentHashOwner]) => {
const registered = (contentHashOwner !== ZERO_ADDRESS);
return {
hash: contentHash,
registered
};
});
});
};
/**
* Register the given URL to GithubHint
* registry contract
*/
export const registerGHH = (instance, url, hash, owner) => {
const values = [ hash, url ];
const options = {
from: owner
};
return instance
.hintURL.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return instance.hintURL.postTransaction(options, values);
});
};
export const registerDapp = (dappId, dappRegInstance) => {
const values = [ dappId ];
const options = {};
return dappRegInstance
.fee.call({}, [])
.then((fee) => {
options.value = fee;
return dappRegInstance
.register.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return dappRegInstance.register.postTransaction(options, values);
});
});
};
export const deleteDapp = (dapp, dappRegInstance) => {
const { id, owner } = dapp;
const fromAddress = dapp.isOwner
? owner.address
: dapp.contractOwner;
const values = [ id ];
const options = {
from: fromAddress
};
return dappRegInstance
.unregister.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return dappRegInstance.unregister.postTransaction(options, values);
});
};
export const updateDappOwner = (dappId, dappOwner, nextOwnerAddress, dappRegInstance) => {
const options = {
from: dappOwner
};
const values = [ dappId, nextOwnerAddress ];
return dappRegInstance.setDappOwner
.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2);
return dappRegInstance.setDappOwner.postTransaction(options, values);
});
};
export const updateDapp = (dappId, dappOwner, updates, dappRegInstance, ghhRegInstance) => {
const options = {
from: dappOwner
};
const types = {
content: 'CONTENT',
image: 'IMG',
manifest: 'MANIFEST'
};
const promises = Object
.keys(updates)
.map((type) => {
const key = types[type];
const url = updates[type];
let promise;
if (!url) {
promise = Promise.resolve([ null, '' ]);
} else {
promise = urlToHash(api, ghhRegInstance, url)
.then((ghhResult) => {
const { hash, registered } = ghhResult;
if (!registered) {
return registerGHH(ghhRegInstance, url, hash, dappOwner)
.then((requestId) => [ { id: requestId, name: `Registering ${url}` }, hash ]);
}
return [ null, hash ];
});
}
return promise
.then(([ ghhRequest, hash ]) => {
const values = [ dappId, key, hash ];
return dappRegInstance.setMeta.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return dappRegInstance.setMeta.postTransaction(options, values);
})
.then((requestId) => [ ghhRequest, { id: requestId, name: `Updating ${type} of ${dappId}` } ]);
});
});
if (updates.owner) {
promises.push(updateDappOwner(updates.owner).then((reqId) => ({ id: reqId, name: `Updating owner of ${dappId}` })));
}
return promises;
};

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 { registry as registryAbi, registry2 as registryAbi2 } from '~/contracts/abi'; import Contracts from '~/contracts';
import { api } from './parity.js'; import { api } from './parity.js';
import * as addresses from './addresses/actions.js'; import * as addresses from './addresses/actions.js';
@ -27,11 +27,6 @@ import * as reverse from './Reverse/actions.js';
export { addresses, accounts, lookup, events, names, records, reverse }; export { addresses, accounts, lookup, events, names, records, reverse };
const REGISTRY_V1_HASHES = [
'0x34f7c51bbb1b1902fbdabfdf04811100f5c9f998f26dd535d2f6f977492c748e', // ropsten
'0x64c3ee34851517a9faecd995c102b339f03e564ad6772dc43a26f993238b20ec' // homestead
];
export const setNetVersion = (netVersion) => ({ type: 'set netVersion', netVersion }); export const setNetVersion = (netVersion) => ({ type: 'set netVersion', netVersion });
export const fetchIsTestnet = () => (dispatch) => export const fetchIsTestnet = () => (dispatch) =>
@ -48,36 +43,18 @@ export const fetchIsTestnet = () => (dispatch) =>
export const setContract = (contract) => ({ type: 'set contract', contract }); export const setContract = (contract) => ({ type: 'set contract', contract });
export const fetchContract = () => (dispatch) => export const fetchContract = () => (dispatch) => {
api.parity return Contracts.create(api).registry
.registryAddress() .fetchContract()
.then((address) => { .then((contract) => {
return api.eth
.getCode(address)
.then((code) => {
const codeHash = api.util.sha3(code);
const isVersion1 = REGISTRY_V1_HASHES.includes(codeHash);
console.log(`registry at ${address}, code ${codeHash}, version ${isVersion1 ? 1 : 2}`);
const contract = api.newContract(
isVersion1
? registryAbi
: registryAbi2,
address
);
dispatch(setContract(contract)); dispatch(setContract(contract));
dispatch(fetchFee()); dispatch(fetchFee());
dispatch(fetchOwner()); dispatch(fetchOwner());
});
}) })
.catch((err) => { .catch((error) => {
console.error('could not fetch contract'); console.error('could not fetch contract', error);
if (err) {
console.error(err.stack);
}
}); });
};
export const setFee = (fee) => ({ type: 'set fee', fee }); export const setFee = (fee) => ({ type: 'set fee', fee });

View File

@ -170,6 +170,10 @@ export default class WalletsUtils {
* to make unnecessary calls on non-wallet accounts * to make unnecessary calls on non-wallet accounts
*/ */
static isWallet (api, address) { static isWallet (api, address) {
if (!address) {
return Promise.resolve(false);
}
if (!_cachedWalletLookup[address]) { if (!_cachedWalletLookup[address]) {
const walletContract = new Contract(api, WalletAbi); const walletContract = new Contract(api, WalletAbi);

View File

@ -62,7 +62,8 @@
"description": "Enables the registration and content management of dapps on the network", "description": "Enables the registration and content management of dapps on the network",
"author": "Parity Team <admin@ethcore.io>", "author": "Parity Team <admin@ethcore.io>",
"version": "1.0.0", "version": "1.0.0",
"visible": false "visible": false,
"secure": true
}, },
{ {
"id": "0x9042323cd85c6576992d211de34b3ecc183f15e4f639aa87859882f839c374e5", "id": "0x9042323cd85c6576992d211de34b3ecc183f15e4f639aa87859882f839c374e5",