Minimise transactions progress (#4942)

* Watch the requests and display them throughout the app

* Linting

* Showing Requests

* Fully working Transaction Requests Display

* Add FormattedMessage to Requests

* Clean-up the Transfer dialog

* Update Validations

* Cleanup Create Wallet

* Clean Deploy Contract Dialog

* Cleanup Contract Execution

* Fix Requests

* Cleanup Wallet Settings

* Don't show stepper in Portal if less than 2 steps

* WIP local storage requests

* Caching requests and saving contract deployments

* Add Historic prop to Requests MethodDecoding

* Fix tests

* Add Contract address to MethodDecoding

* PR Grumbles - Part I

* PR Grumbles - Part II

* Use API Subscription methods

* Linting

* Move SavedRequests and add tests

* Added tests for Requests Actions

* Fixing tests

* PR Grumbles + Playground fix

* Revert Playground changes

* PR Grumbles

* Better showEth in MethodDecoding
This commit is contained in:
Nicolas Gotchac 2017-03-28 14:34:31 +02:00 committed by Jaco Greeff
parent e28c477075
commit a99721004b
40 changed files with 1382 additions and 1216 deletions

View File

@ -139,46 +139,46 @@ export function inOptionsCondition (condition) {
return condition; return condition;
} }
export function inOptions (options) { export function inOptions (_options = {}) {
if (options) { const options = { ..._options };
Object.keys(options).forEach((key) => {
switch (key) {
case 'to':
// Don't encode the `to` option if it's empty
// (eg. contract deployments)
if (options[key]) {
options[key] = inAddress(options[key]);
}
break;
case 'from': Object.keys(options).forEach((key) => {
options[key] = inAddress(options[key]); switch (key) {
break; case 'to':
// Don't encode the `to` option if it's empty
// (eg. contract deployments)
if (options[key]) {
options.to = inAddress(options[key]);
}
break;
case 'condition': case 'from':
options[key] = inOptionsCondition(options[key]); options[key] = inAddress(options[key]);
break; break;
case 'gas': case 'condition':
case 'gasPrice': options[key] = inOptionsCondition(options[key]);
options[key] = inNumber16((new BigNumber(options[key])).round()); break;
break;
case 'minBlock': case 'gas':
options[key] = options[key] ? inNumber16(options[key]) : null; case 'gasPrice':
break; options[key] = inNumber16((new BigNumber(options[key])).round());
break;
case 'value': case 'minBlock':
case 'nonce': options[key] = options[key] ? inNumber16(options[key]) : null;
options[key] = inNumber16(options[key]); break;
break;
case 'data': case 'value':
options[key] = inData(options[key]); case 'nonce':
break; options[key] = inNumber16(options[key]);
} break;
});
} case 'data':
options[key] = inData(options[key]);
break;
}
});
return options; return options;
} }

View File

@ -380,7 +380,7 @@ export default class Parity {
.execute('parity_postSign', inAddress(address), inHex(hash)); .execute('parity_postSign', inAddress(address), inHex(hash));
} }
postTransaction (options) { postTransaction (options = {}) {
return this._transport return this._transport
.execute('parity_postTransaction', inOptions(options)); .execute('parity_postTransaction', inOptions(options));
} }

View File

@ -27,6 +27,7 @@ const events = {
'parity_accountsInfo': { module: 'personal' }, 'parity_accountsInfo': { module: 'personal' },
'parity_allAccountsInfo': { module: 'personal' }, 'parity_allAccountsInfo': { module: 'personal' },
'parity_defaultAccount': { module: 'personal' }, 'parity_defaultAccount': { module: 'personal' },
'parity_postTransaction': { module: 'signer' },
'eth_accounts': { module: 'personal' }, 'eth_accounts': { module: 'personal' },
'signer_requestsToConfirm': { module: 'signer' } 'signer_requestsToConfirm': { module: 'signer' }
}; };
@ -83,7 +84,7 @@ export default class Manager {
if (!engine.isStarted) { if (!engine.isStarted) {
engine.start(); engine.start();
} else { } else if (error !== null || data !== null) {
this._sendData(subscriptionId, error, data); this._sendData(subscriptionId, error, data);
} }

View File

@ -124,7 +124,7 @@ describe('api/subscriptions/manager', () => {
}); });
}); });
it('does not call the callback after unsibscription', () => { it('does not call the callback after unsubscription', () => {
expect(cb).to.have.been.calledWith(null, 'test'); expect(cb).to.have.been.calledWith(null, 'test');
expect(cb).to.not.have.been.calledWith(null, 'test2'); expect(cb).to.not.have.been.calledWith(null, 'test2');
}); });

View File

@ -14,6 +14,8 @@
// 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 { outTransaction } from '../format/output';
export default class Signer { export default class Signer {
constructor (updateSubscriptions, api, subscriber) { constructor (updateSubscriptions, api, subscriber) {
this._subscriber = subscriber; this._subscriber = subscriber;
@ -58,6 +60,15 @@ export default class Signer {
.catch(nextTimeout); .catch(nextTimeout);
} }
_postTransaction (data) {
const request = {
transaction: outTransaction(data.params[0]),
requestId: data.json.result.result
};
this._updateSubscriptions('parity_postTransaction', null, request);
}
_loggingSubscribe () { _loggingSubscribe () {
return this._subscriber.subscribe('logging', (error, data) => { return this._subscriber.subscribe('logging', (error, data) => {
if (error || !data) { if (error || !data) {
@ -65,11 +76,15 @@ export default class Signer {
} }
switch (data.method) { switch (data.method) {
case 'parity_postTransaction': case 'eth_sendTransaction':
case 'eth_sendTranasction':
case 'eth_sendRawTransaction': case 'eth_sendRawTransaction':
this._listRequests(false); this._listRequests(false);
return; return;
case 'parity_postTransaction':
this._postTransaction(data);
this._listRequests(false);
return;
} }
}); });
} }

View File

@ -17,9 +17,12 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { BusyStep, Button, Portal, TxHash } from '~/ui'; import { Button, Portal } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons'; import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
import { setRequest } from '~/redux/providers/requestsActions';
import WalletType from './WalletType'; import WalletType from './WalletType';
import WalletDetails from './WalletDetails'; import WalletDetails from './WalletDetails';
@ -27,56 +30,25 @@ import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore'; import CreateWalletStore from './createWalletStore';
@observer @observer
export default class CreateWallet extends Component { export class CreateWallet extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
}; };
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired,
onSetRequest: PropTypes.func.isRequired
}; };
store = new CreateWalletStore(this.context.api, this.props.accounts); store = new CreateWalletStore(this.context.api, this.props);
render () { render () {
const { stage, steps, waiting, rejected } = this.store; const { stage, steps } = this.store;
if (rejected) {
return (
<Portal
buttons={ this.renderDialogActions() }
onClose={ this.onClose }
open
title={
<FormattedMessage
id='createWallet.rejected.title'
defaultMessage='rejected'
/>
}
>
<BusyStep
title={
<FormattedMessage
id='createWallet.rejected.message'
defaultMessage='The deployment has been rejected'
/>
}
state={
<FormattedMessage
id='createWallet.rejected.state'
defaultMessage='The wallet will not be created. You can safely close this window.'
/>
}
/>
</Portal>
);
}
return ( return (
<Portal <Portal
activeStep={ stage } activeStep={ stage }
busySteps={ waiting }
buttons={ this.renderDialogActions() } buttons={ this.renderDialogActions() }
onClose={ this.onClose } onClose={ this.onClose }
open open
@ -92,25 +64,6 @@ export default class CreateWallet extends Component {
const { accounts } = this.props; const { accounts } = this.props;
switch (step) { switch (step) {
case 'DEPLOYMENT':
return (
<BusyStep
title={
<FormattedMessage
id='createWallet.deployment.message'
defaultMessage='The deployment is currently in progress'
/>
}
state={ this.store.deployState }
>
{
this.store.txhash
? <TxHash hash={ this.store.txhash } />
: null
}
</BusyStep>
);
case 'INFO': case 'INFO':
return ( return (
<WalletInfo <WalletInfo
@ -148,7 +101,7 @@ export default class CreateWallet extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { step, hasErrors, rejected, onCreate, onNext, onAdd } = this.store; const { step, hasErrors, onCreate, onNext, onAdd } = this.store;
const cancelBtn = ( const cancelBtn = (
<Button <Button
@ -164,20 +117,6 @@ export default class CreateWallet extends Component {
/> />
); );
const closeBtn = (
<Button
icon={ <CancelIcon /> }
key='close'
label={
<FormattedMessage
id='createWallet.button.close'
defaultMessage='Close'
/>
}
onClick={ this.onClose }
/>
);
const doneBtn = ( const doneBtn = (
<Button <Button
icon={ <DoneIcon /> } icon={ <DoneIcon /> }
@ -192,20 +131,6 @@ export default class CreateWallet extends Component {
/> />
); );
const sendingBtn = (
<Button
icon={ <DoneIcon /> }
key='sending'
label={
<FormattedMessage
id='createWallet.button.sending'
defaultMessage='Sending...'
/>
}
disabled
/>
);
const nextBtn = ( const nextBtn = (
<Button <Button
icon={ <NextIcon /> } icon={ <NextIcon /> }
@ -220,14 +145,7 @@ export default class CreateWallet extends Component {
/> />
); );
if (rejected) {
return [ closeBtn ];
}
switch (step) { switch (step) {
case 'DEPLOYMENT':
return [ closeBtn, sendingBtn ];
case 'INFO': case 'INFO':
return [ doneBtn ]; return [ doneBtn ];
@ -274,3 +192,14 @@ export default class CreateWallet extends Component {
this.props.onClose(); this.props.onClose();
} }
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onSetRequest: setRequest
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(CreateWallet);

View File

@ -18,7 +18,7 @@ import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import CreateWallet from './'; import { CreateWallet } from './createWallet';
import { ACCOUNTS } from './createWallet.test.js'; import { ACCOUNTS } from './createWallet.test.js';
@ -47,7 +47,7 @@ function render () {
return component; return component;
} }
describe('CreateWallet', () => { describe('modals/CreateWallet', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(render()).to.be.ok; expect(render()).to.be.ok;
}); });

View File

@ -14,12 +14,12 @@
// 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 { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import { ERROR_CODES } from '~/api/transport/error';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
@ -46,15 +46,6 @@ const STEPS = {
/> />
) )
}, },
DEPLOYMENT: {
title: (
<FormattedMessage
id='createWallet.steps.deployment'
defaultMessage='wallet deployment'
/>
),
waiting: true
},
INFO: { INFO: {
title: ( title: (
<FormattedMessage <FormattedMessage
@ -67,13 +58,8 @@ const STEPS = {
export default class CreateWalletStore { export default class CreateWalletStore {
@observable step = null; @observable step = null;
@observable rejected = false;
@observable deployState = null;
@observable deployError = null;
@observable deployed = false;
@observable txhash = null; @observable txhash = null;
@observable walletType = 'MULTISIG';
@observable wallet = { @observable wallet = {
account: '', account: '',
@ -85,7 +71,6 @@ export default class CreateWalletStore {
name: '', name: '',
description: '' description: ''
}; };
@observable walletType = 'MULTISIG';
@observable errors = { @observable errors = {
account: null, account: null,
@ -96,6 +81,9 @@ export default class CreateWalletStore {
name: null name: null
}; };
onClose = noop;
onSetRequest = noop;
@computed get stage () { @computed get stage () {
return this.stepsKeys.findIndex((k) => k === this.step); return this.stepsKeys.findIndex((k) => k === this.step);
} }
@ -125,24 +113,17 @@ export default class CreateWalletStore {
key key
}; };
}) })
.filter((step) => { .filter((step) => this.walletType === 'WATCH' || step.key !== 'INFO');
return (this.walletType !== 'WATCH' || step.key !== 'DEPLOYMENT');
});
} }
@computed get waiting () { constructor (api, { accounts, onClose, onSetRequest }) {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
constructor (api, accounts) {
this.api = api; this.api = api;
this.step = this.stepsKeys[0]; this.step = this.stepsKeys[0];
this.wallet.account = Object.values(accounts)[0].address; this.wallet.account = Object.values(accounts)[0].address;
this.validateWallet(this.wallet); this.validateWallet(this.wallet);
this.onClose = onClose;
this.onSetRequest = onSetRequest;
} }
@action onTypeChange = (type) => { @action onTypeChange = (type) => {
@ -193,8 +174,6 @@ export default class CreateWalletStore {
return; return;
} }
this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit } = this.wallet; const { account, owners, required, daylimit } = this.wallet;
Contracts Contracts
@ -243,25 +222,13 @@ export default class CreateWalletStore {
const contract = this.api.newContract(walletAbi); const contract = this.api.newContract(walletAbi);
this.wallet = this.getWalletWithMeta(this.wallet); this.wallet = this.getWalletWithMeta(this.wallet);
return deploy(contract, options, [ owners, required, daylimit ], this.wallet.metadata, this.onDeploymentState); this.onClose();
}) return deploy(contract, options, [ owners, required, daylimit ])
.then((address) => { .then((requestId) => {
if (!address || /^(0x)?0*$/.test(address)) { const metadata = { ...this.wallet.metadata, deployment: true };
return false;
}
this.deployed = true; this.onSetRequest(requestId, { metadata }, false);
this.wallet.address = address; });
return this.addWallet(this.wallet);
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return;
}
console.error('error deploying contract', error);
this.deployError = error;
}); });
} }
@ -297,75 +264,6 @@ export default class CreateWalletStore {
}; };
} }
onDeploymentState = (error, data) => {
if (error) {
return console.error('createWallet::onDeploymentState', error);
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = (
<FormattedMessage
id='createWallet.states.preparing'
defaultMessage='Preparing transaction for network transmission'
/>
);
return;
case 'checkRequest':
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingConfirm'
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer'
/>
);
return;
case 'getTransactionReceipt':
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt'
/>
);
this.txhash = data.txhash;
return;
case 'hasReceipt':
case 'getCode':
this.deployState = (
<FormattedMessage
id='createWallet.states.validatingCode'
defaultMessage='Validating the deployed contract code'
/>
);
return;
case 'confirmationNeeded':
this.deployState = (
<FormattedMessage
id='createWallet.states.confirmationNeeded'
defaultMessage='The contract deployment needs confirmations from other owners of the Wallet'
/>
);
return;
case 'completed':
this.deployState = (
<FormattedMessage
id='createWallet.states.completed'
defaultMessage='The contract deployment has been completed'
/>
);
return;
default:
console.error('createWallet::onDeploymentState', 'unknow contract deployment state', data);
return;
}
}
@action validateWallet = (_wallet) => { @action validateWallet = (_wallet) => {
const addressValidation = validateAddress(_wallet.address); const addressValidation = validateAddress(_wallet.address);
const accountValidation = validateAddress(_wallet.account); const accountValidation = validateAddress(_wallet.account);

View File

@ -15,19 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.center {
text-align: center;
}
.address {
vertical-align: top;
display: inline-block;
}
.identityicon {
margin: -8px 0.5em;
}
.funcparams { .funcparams {
padding-left: 3em; padding-left: 3em;
} }

View File

@ -20,21 +20,18 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui'; import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
import { CancelIcon, DoneIcon } from '~/ui/Icons'; import { CancelIcon } from '~/ui/Icons';
import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation'; import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation';
import { deploy, deployEstimateGas } from '~/util/tx'; import { deploy, deployEstimateGas } from '~/util/tx';
import { setRequest } from '~/redux/providers/requestsActions';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep'; import ParametersStep from './ParametersStep';
import ErrorStep from './ErrorStep';
import Extras from '../Transfer/Extras'; import Extras from '../Transfer/Extras';
import styles from './deployContract.css';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = { const STEPS = {
CONTRACT_DETAILS: { CONTRACT_DETAILS: {
title: ( title: (
@ -59,23 +56,6 @@ const STEPS = {
defaultMessage='extra information' defaultMessage='extra information'
/> />
) )
},
DEPLOYMENT: {
waiting: true,
title: (
<FormattedMessage
id='deployContract.title.deployment'
defaultMessage='deployment'
/>
)
},
COMPLETED: {
title: (
<FormattedMessage
id='deployContract.title.completed'
defaultMessage='completed'
/>
)
} }
}; };
@ -93,6 +73,7 @@ class DeployContract extends Component {
code: PropTypes.string, code: PropTypes.string,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onSetRequest: PropTypes.func.isRequired,
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
source: PropTypes.string source: PropTypes.string
}; };
@ -112,8 +93,6 @@ class DeployContract extends Component {
amountError: '', amountError: '',
code: '', code: '',
codeError: ERRORS.invalidCode, codeError: ERRORS.invalidCode,
deployState: '',
deployError: null,
description: '', description: '',
descriptionError: null, descriptionError: null,
extras: false, extras: false,
@ -124,9 +103,8 @@ class DeployContract extends Component {
params: [], params: [],
paramsError: [], paramsError: [],
inputs: [], inputs: [],
rejected: false,
step: 'CONTRACT_DETAILS' step: 'CONTRACT_DETAILS'
} };
componentWillMount () { componentWillMount () {
const { abi, code } = this.props; const { abi, code } = this.props;
@ -154,11 +132,9 @@ class DeployContract extends Component {
} }
render () { render () {
const { step, deployError, rejected, inputs } = this.state; const { step, inputs } = this.state;
const realStepKeys = deployError || rejected const realStepKeys = Object.keys(STEPS)
? []
: Object.keys(STEPS)
.filter((k) => { .filter((k) => {
if (k === 'CONTRACT_PARAMETERS') { if (k === 'CONTRACT_PARAMETERS') {
return inputs.length > 0; return inputs.length > 0;
@ -172,45 +148,15 @@ class DeployContract extends Component {
}); });
const realStep = realStepKeys.findIndex((k) => k === step); const realStep = realStepKeys.findIndex((k) => k === step);
const realSteps = realStepKeys.length const realSteps = realStepKeys.map((k) => STEPS[k]);
? realStepKeys.map((k) => STEPS[k])
: null;
const title = realSteps
? null
: (
deployError
? (
<FormattedMessage
id='deployContract.title.failed'
defaultMessage='deployment failed'
/>
)
: (
<FormattedMessage
id='deployContract.title.rejected'
defaultMessage='rejected'
/>
)
);
const waiting = realSteps
? realSteps.map((s, i) => s.waiting ? i : false).filter((v) => v !== false)
: null;
return ( return (
<Portal <Portal
buttons={ this.renderDialogActions() } buttons={ this.renderDialogActions() }
activeStep={ realStep } activeStep={ realStep }
busySteps={ waiting }
onClose={ this.onClose } onClose={ this.onClose }
open open
steps={ steps={ realSteps.map((s) => s.title) }
realSteps
? realSteps.map((s) => s.title)
: null
}
title={ title }
> >
{ this.renderExceptionWarning() } { this.renderExceptionWarning() }
{ this.renderStep() } { this.renderStep() }
@ -264,20 +210,6 @@ class DeployContract extends Component {
/> />
); );
const closeBtnOk = (
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='deployContract.button.done'
defaultMessage='Done'
/>
}
onClick={ this.onClose }
/>
);
if (deployError) { if (deployError) {
return closeBtn; return closeBtn;
} }
@ -346,43 +278,12 @@ class DeployContract extends Component {
cancelBtn, cancelBtn,
createButton createButton
]; ];
case 'DEPLOYMENT':
return [ closeBtn ];
case 'COMPLETED':
return [ closeBtnOk ];
} }
} }
renderStep () { renderStep () {
const { accounts, readOnly, balances } = this.props; const { accounts, readOnly, balances } = this.props;
const { address, deployError, step, deployState, txhash, rejected } = this.state; const { step } = this.state;
if (deployError) {
return (
<ErrorStep error={ deployError } />
);
}
if (rejected) {
return (
<BusyStep
title={
<FormattedMessage
id='deployContract.rejected.title'
defaultMessage='The deployment has been rejected'
/>
}
state={
<FormattedMessage
id='deployContract.rejected.description'
defaultMessage='You can safely close this window, the contract deployment will not occur.'
/>
}
/>
);
}
switch (step) { switch (step) {
case 'CONTRACT_DETAILS': case 'CONTRACT_DETAILS':
@ -416,50 +317,6 @@ class DeployContract extends Component {
case 'EXTRAS': case 'EXTRAS':
return this.renderExtrasPage(); return this.renderExtrasPage();
case 'DEPLOYMENT':
const body = txhash
? <TxHash hash={ txhash } />
: null;
return (
<BusyStep
title={
<FormattedMessage
id='deployContract.busy.title'
defaultMessage='The deployment is currently in progress'
/>
}
state={ deployState }
>
{ body }
</BusyStep>
);
case 'COMPLETED':
return (
<CompletedStep>
<div>
<FormattedMessage
id='deployContract.completed.description'
defaultMessage='Your contract has been deployed at'
/>
</div>
<div>
<CopyToClipboard data={ address } />
<IdentityIcon
address={ address }
center
className={ styles.identityicon }
inline
/>
<div className={ styles.address }>
{ address }
</div>
</div>
<TxHash hash={ txhash } />
</CompletedStep>
);
} }
} }
@ -591,7 +448,7 @@ class DeployContract extends Component {
} }
onDeployStart = () => { onDeployStart = () => {
const { api, store } = this.context; const { api } = this.context;
const { source } = this.props; const { source } = this.props;
const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state; const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state;
@ -611,125 +468,17 @@ class DeployContract extends Component {
value: amountValue value: amountValue
}); });
this.setState({ step: 'DEPLOYMENT' });
const contract = api.newContract(abiParsed); const contract = api.newContract(abiParsed);
deploy(contract, options, params, metadata, this.onDeploymentState, true) this.onClose();
.then((address) => { deploy(contract, options, params, true)
// No contract address given, might need some confirmations .then((requestId) => {
// from the wallet owners... const requestMetadata = { ...metadata, deployment: true };
if (!address || /^(0x)?0*$/.test(address)) {
return false;
}
metadata.blockNumber = contract._receipt this.props.onSetRequest(requestId, { metadata: requestMetadata }, false);
? contract.receipt.blockNumber.toNumber()
: null;
return Promise.all([
api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, metadata)
])
.then(() => {
console.log(`contract deployed at ${address}`);
this.setState({ step: 'COMPLETED', address });
});
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true });
return false;
}
console.error('error deploying contract', error);
this.setState({ deployError: error });
store.dispatch({ type: 'newError', error });
}); });
} }
onDeploymentState = (error, data) => {
if (error) {
console.error('onDeploymentState', error);
return;
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.preparing'
defaultMessage='Preparing transaction for network transmission'
/>
)
});
return;
case 'checkRequest':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.waitSigner'
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer'
/>
)
});
return;
case 'getTransactionReceipt':
this.setState({
txhash: data.txhash,
deployState: (
<FormattedMessage
id='deployContract.state.waitReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt'
/>
)
});
return;
case 'hasReceipt':
case 'getCode':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.validatingCode'
defaultMessage='Validating the deployed contract code'
/>
)
});
return;
case 'confirmationNeeded':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.confirmationNeeded'
defaultMessage='The operation needs confirmations from the other owners of the contract'
/>
)
});
return;
case 'completed':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.completed'
defaultMessage='The contract deployment has been completed'
/>
)
});
return;
default:
console.error('Unknown contract deployment state', data);
return;
}
}
onClose = () => { onClose = () => {
this.props.onClose(); this.props.onClose();
} }
@ -752,6 +501,13 @@ function mapStateToProps (initState, initProps) {
}; };
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onSetRequest: setRequest
}, dispatch);
}
export default connect( export default connect(
mapStateToProps mapStateToProps,
mapDispatchToProps
)(DeployContract); )(DeployContract);

View File

@ -21,8 +21,8 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toWei } from '~/api/util/wei'; import { toWei } from '~/api/util/wei';
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui'; import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons'; import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { MAX_GAS_ESTIMATION } from '~/util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '~/util/validation'; import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi'; import { parseAbiType } from '~/util/abi';
@ -30,11 +30,7 @@ import { parseAbiType } from '~/util/abi';
import AdvancedStep from './AdvancedStep'; import AdvancedStep from './AdvancedStep';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import { ERROR_CODES } from '~/api/transport/error';
const STEP_DETAILS = 0; const STEP_DETAILS = 0;
const STEP_BUSY_OR_ADVANCED = 1;
const STEP_BUSY = 2;
const TITLES = { const TITLES = {
transfer: ( transfer: (
@ -43,40 +39,22 @@ const TITLES = {
defaultMessage='function details' defaultMessage='function details'
/> />
), ),
sending: (
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending'
/>
),
complete: (
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete'
/>
),
advanced: ( advanced: (
<FormattedMessage <FormattedMessage
id='executeContract.steps.advanced' id='executeContract.steps.advanced'
defaultMessage='advanced options' defaultMessage='advanced options'
/> />
),
rejected: (
<FormattedMessage
id='executeContract.steps.rejected'
defaultMessage='rejected'
/>
) )
}; };
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; const STAGES_BASIC = [TITLES.transfer];
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced, TITLES.sending, TITLES.complete]; const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced];
@observer @observer
class ExecuteContract extends Component { class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
accounts: PropTypes.object, accounts: PropTypes.object,
@ -86,7 +64,7 @@ class ExecuteContract extends Component {
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} };
gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit }); gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit });
@ -94,17 +72,13 @@ class ExecuteContract extends Component {
advancedOptions: false, advancedOptions: false,
amount: '0', amount: '0',
amountError: null, amountError: null,
busyState: null,
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
rejected: false,
sending: false,
step: STEP_DETAILS, step: STEP_DETAILS,
txhash: null,
values: [], values: [],
valuesError: [] valuesError: []
} };
componentDidMount () { componentDidMount () {
const { contract } = this.props; const { contract } = this.props;
@ -122,23 +96,13 @@ class ExecuteContract extends Component {
} }
render () { render () {
const { advancedOptions, rejected, sending, step } = this.state; const { advancedOptions, step } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC; const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return ( return (
<Portal <Portal
activeStep={ step } activeStep={ step }
buttons={ this.renderDialogActions() } buttons={ this.renderDialogActions() }
busySteps={
advancedOptions
? [STEP_BUSY]
: [STEP_BUSY_OR_ADVANCED]
}
busy={ sending }
onClose={ this.onClose } onClose={ this.onClose }
open open
steps={ steps } steps={ steps }
@ -150,10 +114,9 @@ class ExecuteContract extends Component {
} }
renderExceptionWarning () { renderExceptionWarning () {
const { gasEdit, step } = this.state;
const { errorEstimated } = this.gasStore; const { errorEstimated } = this.gasStore;
if (!errorEstimated || step >= (gasEdit ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) { if (!errorEstimated) {
return null; return null;
} }
@ -164,8 +127,8 @@ class ExecuteContract extends Component {
renderDialogActions () { renderDialogActions () {
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state; const { advancedOptions, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = !!(fromAddressError || valuesError.find((error) => error));
const cancelBtn = ( const cancelBtn = (
<Button <Button
@ -189,7 +152,7 @@ class ExecuteContract extends Component {
defaultMessage='post transaction' defaultMessage='post transaction'
/> />
} }
disabled={ !!(sending || hasError) } disabled={ hasError }
icon={ <IdentityIcon address={ fromAddress } button /> } icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } onClick={ this.postTransaction }
/> />
@ -226,55 +189,18 @@ class ExecuteContract extends Component {
cancelBtn, cancelBtn,
advancedOptions ? nextBtn : postBtn advancedOptions ? nextBtn : postBtn
]; ];
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return [
cancelBtn
];
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return [
cancelBtn,
prevBtn,
postBtn
];
} }
return [ return [
<Button cancelBtn,
key='close' prevBtn,
label={ postBtn
<FormattedMessage
id='executeContract.button.done'
defaultMessage='done'
/>
}
icon={ <DoneIcon /> }
onClick={ this.onClose }
/>
]; ];
} }
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { advancedOptions, step, busyState, txhash, rejected } = this.state; const { step } = this.state;
if (rejected) {
return (
<BusyStep
title={
<FormattedMessage
id='executeContract.rejected.title'
defaultMessage='The execution has been rejected'
/>
}
state={
<FormattedMessage
id='executeContract.rejected.state'
defaultMessage='You can safely close this window, the function execution will not occur.'
/>
}
/>
);
}
if (step === STEP_DETAILS) { if (step === STEP_DETAILS) {
return ( return (
@ -288,28 +214,10 @@ class ExecuteContract extends Component {
onValueChange={ this.onValueChange } onValueChange={ this.onValueChange }
/> />
); );
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return (
<BusyStep
title={
<FormattedMessage
id='executeContract.busy.title'
defaultMessage='The function execution is in progress'
/>
}
state={ busyState }
/>
);
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return (
<AdvancedStep gasStore={ this.gasStore } />
);
} }
return ( return (
<CompletedStep> <AdvancedStep gasStore={ this.gasStore } />
<TxHash hash={ txhash } />
</CompletedStep>
); );
} }
@ -390,59 +298,17 @@ class ExecuteContract extends Component {
} }
postTransaction = () => { postTransaction = () => {
const { api, store } = this.context; const { api } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { advancedOptions, amount, func, values } = this.state; const { amount, func, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = this.gasStore.overrideTransaction({ const options = this.gasStore.overrideTransaction({
from: fromAddress, from: fromAddress,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}); });
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED }); func.postTransaction(options, values);
this.onClose();
func
.postTransaction(options, values)
.then((requestId) => {
this.setState({
busyState: (
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
)
});
return api
.pollMethod('parity_checkRequest', requestId)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true, step: finalstep });
return false;
}
throw error;
});
})
.then((txhash) => {
this.setState({
sending: false,
step: finalstep,
txhash,
busyState: (
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network'
/>
)
});
})
.catch((error) => {
console.error('postTransaction', error);
store.dispatch({ type: 'newError', error });
});
} }
onAdvancedClick = () => { onAdvancedClick = () => {

View File

@ -14,16 +14,14 @@
// 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 { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { uniq } from 'lodash';
import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletAbi } from '~/contracts/abi';
import { bytesToHex } from '~/api/util/format';
import { fromWei } from '~/api/util/wei'; import { fromWei } from '~/api/util/wei';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store'; import GasPriceStore from '~/ui/GasPriceEditor/store';
import { getLogger, LOG_KEYS } from '~/config'; import { getLogger, LOG_KEYS } from '~/config';
@ -32,13 +30,10 @@ const log = getLogger(LOG_KEYS.TransferModalStore);
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
sending: 'sending', extras: 'extra information'
complete: 'complete',
extras: 'extra information',
rejected: 'rejected'
}; };
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; const STAGES_BASIC = [TITLES.transfer];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete]; const STAGES_EXTRA = [TITLES.transfer, TITLES.extras];
export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMIT'; export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMIT';
@ -49,8 +44,6 @@ export default class TransferStore {
@observable sending = false; @observable sending = false;
@observable tag = 'ETH'; @observable tag = 'ETH';
@observable isEth = true; @observable isEth = true;
@observable busyState = null;
@observable rejected = false;
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@ -72,8 +65,8 @@ export default class TransferStore {
account = null; account = null;
balance = null; balance = null;
onClose = null;
onClose = noop;
senders = null; senders = null;
isWallet = false; isWallet = false;
wallet = null; wallet = null;
@ -83,7 +76,7 @@ export default class TransferStore {
constructor (api, props) { constructor (api, props) {
this.api = api; this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props; const { account, balance, gasLimit, onClose, senders, newError, sendersBalances } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
@ -102,15 +95,15 @@ export default class TransferStore {
this.sendersBalances = sendersBalances; this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender; this.senderError = ERRORS.requireSender;
} }
if (onClose) {
this.onClose = onClose;
}
} }
@computed get steps () { @computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
if (this.rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return steps; return steps;
} }
@ -147,6 +140,7 @@ export default class TransferStore {
@action handleClose = () => { @action handleClose = () => {
this.stage = 0; this.stage = 0;
this.onClose();
} }
@action onUpdateDetails = (type, value) => { @action onUpdateDetails = (type, value) => {
@ -186,85 +180,14 @@ export default class TransferStore {
this this
.send() .send()
.then((requestId) => {
this.busyState = 'Waiting for authorization in the Parity Signer';
return this.api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return false;
}
throw e;
});
})
.then((txhash) => {
transaction(() => {
this.onNext();
this.sending = false;
this.txhash = txhash;
this.busyState = 'Your transaction has been posted to the network';
});
if (this.isWallet) {
return this._attachWalletOperation(txhash);
}
})
.catch((error) => { .catch((error) => {
this.sending = false;
this.newError(error); this.newError(error);
})
.then(() => {
this.handleClose();
}); });
} }
@action _attachWalletOperation = (txhash) => {
if (!txhash || /^(0x)?0*$/.test(txhash)) {
return;
}
let ethSubscriptionId = null;
// Number of blocks left to look-up (unsub after 15 blocks if nothing)
let nBlocksLeft = 15;
return this.api.subscribe('eth_blockNumber', () => {
this.api.eth
.getTransactionReceipt(txhash)
.then((tx) => {
if (nBlocksLeft <= 0) {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
return;
}
if (!tx) {
nBlocksLeft--;
return;
}
const logs = this.walletContract.parseEventLogs(tx.logs);
const operations = uniq(logs
.filter((log) => log && log.params && log.params.operation)
.map((log) => bytesToHex(log.params.operation.value)));
if (operations.length > 0) {
this.operation = operations[0];
}
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
})
.catch(() => {
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
});
}).then((subId) => {
ethSubscriptionId = subId;
});
}
@action _onUpdateAll = (valueAll) => { @action _onUpdateAll = (valueAll) => {
this.valueAll = valueAll; this.valueAll = valueAll;
this.recalculateGas(); this.recalculateGas();

View File

@ -21,9 +21,9 @@ import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { BusyStep, CompletedStep, Button, IdentityIcon, Input, Portal, TxHash, Warning } from '~/ui'; import { Button, IdentityIcon, Portal, Warning } from '~/ui';
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons'; import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import Details from './Details'; import Details from './Details';
@ -33,8 +33,7 @@ import TransferStore, { WALLET_WARNING_SPENT_TODAY_LIMIT } from './store';
import styles from './transfer.css'; import styles from './transfer.css';
const STEP_DETAILS = 0; const STEP_DETAILS = 0;
const STEP_ADVANCED_OR_BUSY = 1; const STEP_EXTRA = 1;
const STEP_BUSY = 2;
@observer @observer
class Transfer extends Component { class Transfer extends Component {
@ -57,16 +56,11 @@ class Transfer extends Component {
store = new TransferStore(this.context.api, this.props); store = new TransferStore(this.context.api, this.props);
render () { render () {
const { stage, extras, steps } = this.store; const { stage, steps } = this.store;
return ( return (
<Portal <Portal
activeStep={ stage } activeStep={ stage }
busySteps={
extras
? [STEP_BUSY]
: [STEP_ADVANCED_OR_BUSY]
}
buttons={ this.renderDialogActions() } buttons={ this.renderDialogActions() }
onClose={ this.handleClose } onClose={ this.handleClose }
open open
@ -80,10 +74,9 @@ class Transfer extends Component {
} }
renderExceptionWarning () { renderExceptionWarning () {
const { extras, stage } = this.store;
const { errorEstimated } = this.store.gasStore; const { errorEstimated } = this.store.gasStore;
if (!errorEstimated || stage >= (extras ? STEP_BUSY : STEP_ADVANCED_OR_BUSY)) { if (!errorEstimated) {
return null; return null;
} }
@ -144,68 +137,9 @@ class Transfer extends Component {
if (stage === STEP_DETAILS) { if (stage === STEP_DETAILS) {
return this.renderDetailsPage(); return this.renderDetailsPage();
} else if (stage === STEP_ADVANCED_OR_BUSY && extras) { } else if (stage === STEP_EXTRA && extras) {
return this.renderExtrasPage(); return this.renderExtrasPage();
} }
return this.renderCompletePage();
}
renderCompletePage () {
const { sending, txhash, busyState, rejected } = this.store;
if (rejected) {
return (
<BusyStep
title='The transaction has been rejected'
state='You can safely close this window, the transfer will not occur.'
/>
);
}
if (sending) {
return (
<BusyStep
title='The transaction is in progress'
state={ busyState }
/>
);
}
return (
<CompletedStep>
<TxHash hash={ txhash } />
{
this.store.operation
? (
<div>
<br />
<div>
<p>
<FormattedMessage
id='transfer.wallet.confirmation'
defaultMessage='This transaction needs confirmation from other owners.'
/>
</p>
<Input
style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation }
label={
<FormattedMessage
id='transfer.wallet.operationHash'
defaultMessage='operation hash'
/>
}
readOnly
allowCopy
/>
</div>
</div>
)
: null
}
</CompletedStep>
);
} }
renderDetailsPage () { renderDetailsPage () {
@ -319,19 +253,6 @@ class Transfer extends Component {
onClick={ this.store.onSend } onClick={ this.store.onSend }
/> />
); );
const doneBtn = (
<Button
icon={ <DoneIcon /> }
key='close'
label={
<FormattedMessage
id='transfer.buttons.close'
defaultMessage='Close'
/>
}
onClick={ this.handleClose }
/>
);
switch (stage) { switch (stage) {
case 0: case 0:
@ -339,19 +260,14 @@ class Transfer extends Component {
? [cancelBtn, nextBtn] ? [cancelBtn, nextBtn]
: [cancelBtn, sendBtn]; : [cancelBtn, sendBtn];
case 1: case 1:
return extras return [cancelBtn, prevBtn, sendBtn];
? [cancelBtn, prevBtn, sendBtn]
: [doneBtn];
default: default:
return [doneBtn]; return [cancelBtn];
} }
} }
handleClose = () => { handleClose = () => {
const { onClose } = this.props;
this.store.handleClose(); this.store.handleClose();
typeof onClose === 'function' && onClose();
} }
} }

View File

@ -20,8 +20,8 @@ import { connect } from 'react-redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { BusyStep, AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal, TxHash } from '~/ui'; import { AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons'; import { CancelIcon, NextIcon } from '~/ui/Icons';
import { fromWei } from '~/api/util/wei'; import { fromWei } from '~/api/util/wei';
import WalletSettingsStore from './walletSettingsStore.js'; import WalletSettingsStore from './walletSettingsStore.js';
@ -40,46 +40,14 @@ class WalletSettings extends Component {
senders: PropTypes.object.isRequired senders: PropTypes.object.isRequired
}; };
store = new WalletSettingsStore(this.context.api, this.props.wallet); store = new WalletSettingsStore(this.context.api, this.props);
render () { render () {
const { stage, steps, waiting, rejected } = this.store; const { stage, steps } = this.store;
if (rejected) {
return (
<Portal
onClose={ this.onClose }
open
title={
<FormattedMessage
id='walletSettings.rejected.title'
defaultMessage='rejected'
/>
}
actions={ this.renderDialogActions() }
>
<BusyStep
title={
<FormattedMessage
id='walletSettings.rejected.busyStep.title'
defaultMessage='The modifications have been rejected'
/>
}
state={
<FormattedMessage
id='walletSettings.rejected.busyStep.state'
defaultMessage='The wallet settings will not be modified. You can safely close this window.'
/>
}
/>
</Portal>
);
}
return ( return (
<Portal <Portal
activeStep={ stage } activeStep={ stage }
busySteps={ waiting }
buttons={ this.renderDialogActions() } buttons={ this.renderDialogActions() }
onClose={ this.onClose } onClose={ this.onClose }
open open
@ -94,43 +62,6 @@ class WalletSettings extends Component {
const { step } = this.store; const { step } = this.store;
switch (step) { switch (step) {
case 'SENDING':
return (
<BusyStep
title='The modifications are currently being sent'
state={ this.store.deployState }
>
{
this.store.requests.map((req) => {
const key = req.id;
if (req.txhash) {
return (
<TxHash
key={ key }
hash={ req.txhash }
/>
);
}
if (req.rejected) {
return (
<p key={ key }>
<FormattedMessage
id='walletSettings.rejected'
defaultMessage='The transaction #{txid} has been rejected'
values={ {
txid: parseInt(key, 16)
} }
/>
</p>
);
}
})
}
</BusyStep>
);
case 'CONFIRMATION': case 'CONFIRMATION':
const { changes } = this.store; const { changes } = this.store;
@ -396,7 +327,7 @@ class WalletSettings extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { step, hasErrors, rejected, onNext, send, done } = this.store; const { step, hasErrors, onNext, send } = this.store;
const cancelBtn = ( const cancelBtn = (
<Button <Button
@ -426,20 +357,6 @@ class WalletSettings extends Component {
/> />
); );
const sendingBtn = (
<Button
icon={ <DoneIcon /> }
key='sendingBtn'
label={
<FormattedMessage
id='walletSettings.buttons.sending'
defaultMessage='Sending...'
/>
}
disabled
/>
);
const nextBtn = ( const nextBtn = (
<Button <Button
icon={ <NextIcon /> } icon={ <NextIcon /> }
@ -470,16 +387,7 @@ class WalletSettings extends Component {
/> />
); );
if (rejected) {
return [ closeBtn ];
}
switch (step) { switch (step) {
case 'SENDING':
return done
? [ closeBtn ]
: [ closeBtn, sendingBtn ];
case 'CONFIRMATION': case 'CONFIRMATION':
const { changes } = this.store; const { changes } = this.store;

View File

@ -14,24 +14,22 @@
// 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 { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { validateUint, validateAddress } from '~/util/validation'; import { validateUint, validateAddress } from '~/util/validation';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = { const STEPS = {
EDIT: { title: 'wallet settings' }, EDIT: { title: 'wallet settings' },
CONFIRMATION: { title: 'confirmation' }, CONFIRMATION: { title: 'confirmation' }
SENDING: { title: 'sending transaction', waiting: true }
}; };
export default class WalletSettingsStore { export default class WalletSettingsStore {
accounts = {}; accounts = {};
onClose = noop;
@observable deployState = '';
@observable done = false;
@observable fromString = false; @observable fromString = false;
@observable requests = []; @observable requests = [];
@observable step = null; @observable step = null;
@ -73,13 +71,6 @@ export default class WalletSettingsStore {
}); });
} }
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
@action @action
changesFromString (json) { changesFromString (json) {
try { try {
@ -203,7 +194,7 @@ export default class WalletSettingsStore {
}); });
} }
constructor (api, wallet) { constructor (api, { onClose, wallet }) {
this.api = api; this.api = api;
this.step = this.stepsKeys[0]; this.step = this.stepsKeys[0];
@ -223,6 +214,8 @@ export default class WalletSettingsStore {
this.validateWallet(this.wallet); this.validateWallet(this.wallet);
}); });
this.onClose = onClose;
} }
@action onNext = () => { @action onNext = () => {
@ -267,40 +260,8 @@ export default class WalletSettingsStore {
const changes = this.changes; const changes = this.changes;
const walletInstance = this.walletInstance; const walletInstance = this.walletInstance;
this.step = 'SENDING'; Promise.all(changes.map((change) => this.sendChange(change, walletInstance)));
this.onClose();
this.onTransactionsState('postTransaction');
Promise
.all(changes.map((change) => this.sendChange(change, walletInstance)))
.then((requestIds) => {
this.onTransactionsState('checkRequest');
this.requests = requestIds.map((id) => ({ id, rejected: false, txhash: null }));
return Promise
.all(requestIds.map((id) => {
return this.api
.pollMethod('parity_checkRequest', id)
.then((txhash) => {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].txhash = txhash;
})
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].rejected = true;
return false;
}
throw e;
});
}));
})
.then(() => {
this.done = true;
this.onTransactionsState('completed');
});
} }
@action sendChange = (change, walletInstance) => { @action sendChange = (change, walletInstance) => {
@ -356,23 +317,6 @@ export default class WalletSettingsStore {
} }
} }
@action onTransactionsState = (state) => {
switch (state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'completed':
this.deployState = '';
return;
}
}
@action validateWallet = (_wallet) => { @action validateWallet = (_wallet) => {
const senderValidation = validateAddress(_wallet.sender); const senderValidation = validateAddress(_wallet.sender);
const requireValidation = validateUint(_wallet.require); const requireValidation = validateUint(_wallet.require);

View File

@ -25,6 +25,7 @@ export blockchainReducer from './blockchainReducer';
export workerReducer from './workerReducer'; export workerReducer from './workerReducer';
export imagesReducer from './imagesReducer'; export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer'; export personalReducer from './personalReducer';
export requestsReducer from './requestsReducer';
export signerReducer from './signerReducer'; export signerReducer from './signerReducer';
export snackbarReducer from './snackbarReducer'; export snackbarReducer from './snackbarReducer';
export statusReducer from './statusReducer'; export statusReducer from './statusReducer';

View File

@ -0,0 +1,171 @@
// 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 { outTransaction } from '~/api/format/output';
import { trackRequest as trackRequestUtil, parseTransactionReceipt } from '~/util/tx';
import SavedRequests from '~/views/Application/Requests/savedRequests';
const savedRequests = new SavedRequests();
export const init = (api) => (dispatch) => {
api.subscribe('parity_postTransaction', (error, request) => {
if (error) {
return console.error(error);
}
dispatch(watchRequest(request));
});
api.on('connected', () => {
savedRequests.load(api).then((requests) => {
requests.forEach((request) => dispatch(watchRequest(request)));
});
});
};
export const watchRequest = (request) => (dispatch, getState) => {
const { requestId } = request;
// Convert value to BigNumber
request.transaction = outTransaction(request.transaction);
dispatch(setRequest(requestId, request));
dispatch(trackRequest(requestId, request));
};
export const trackRequest = (requestId, { transactionHash = null } = {}) => (dispatch, getState) => {
const { api } = getState();
trackRequestUtil(api, { requestId, transactionHash }, (error, data) => {
if (error) {
console.error(error);
return dispatch(setRequest(requestId, { error }));
}
// Hide the request after 6 mined blocks
if (data.transactionReceipt) {
const { transactionReceipt } = data;
const { requests } = getState();
const requestData = requests[requestId];
let blockSubscriptionId = -1;
// If the request was a contract deployment,
// then add the contract with the saved metadata to the account
if (requestData.metadata && requestData.metadata.deployment) {
const { metadata } = requestData;
const options = {
...requestData.transaction,
metadata
};
parseTransactionReceipt(api, options, data.transactionReceipt)
.then((contractAddress) => {
// No contract address given, might need some confirmations
// from the wallet owners...
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return false;
}
metadata.blockNumber = data.transactionReceipt
? data.transactionReceipt.blockNumber.toNumber()
: null;
const prevRequest = getState().requests[requestId];
const nextTransaction = {
...prevRequest.transaction,
creates: contractAddress
};
dispatch(setRequest(requestId, { transaction: nextTransaction }));
return Promise.all([
api.parity.setAccountName(contractAddress, metadata.name),
api.parity.setAccountMeta(contractAddress, metadata)
]);
})
.catch((error) => {
console.error(error);
});
}
api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error || !blockNumber) {
return;
}
// Transaction included in `blockHeight` blocks
const blockHeight = blockNumber.minus(transactionReceipt.blockNumber).plus(1);
const nextData = { blockHeight };
// Hide the transaction after 6 blocks
if (blockHeight.gt(6)) {
return dispatch(hideRequest(requestId));
}
return dispatch(setRequest(requestId, nextData, false));
})
.then((subId) => {
blockSubscriptionId = subId;
return dispatch(setRequest(requestId, { blockSubscriptionId }, false));
});
}
return dispatch(setRequest(requestId, data));
});
};
export const hideRequest = (requestId) => (dispatch, getState) => {
const { api, requests } = getState();
const request = requests[requestId];
dispatch(setRequest(requestId, { show: false }));
// Delete it if an error occured or if completed
if (request.error || request.transactionReceipt) {
// Wait for the animation to be done to delete the request
setTimeout(() => {
dispatch(deleteRequest(requestId));
}, 1000);
}
// Unsubscribe to eth-blockNumber if subscribed
if (request.blockSubscriptionId) {
api.unsubscribe(request.blockSubscriptionId);
dispatch(setRequest(requestId, { blockSubscriptionId: null }, false));
}
};
export const setRequest = (requestId, requestData, autoSetShow = true) => {
if (autoSetShow && requestData.show === undefined) {
requestData.show = true;
}
savedRequests.save(requestId, requestData);
return {
type: 'setRequest',
requestId, requestData
};
};
export const deleteRequest = (requestId) => {
savedRequests.remove(requestId);
return {
type: 'deleteRequest',
requestId
};
};

View File

@ -0,0 +1,111 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import { hideRequest, trackRequest, watchRequest } from './requestsActions';
const TX_HASH = '0x123456';
const BASE_REQUEST = {
requestId: '0x1',
transaction: {
from: '0x0',
to: '0x1'
}
};
let api;
let store;
let dispatcher;
function createApi () {
return {
pollMethod: (method, data) => {
switch (method) {
case 'parity_checkRequest':
return Promise.resolve(TX_HASH);
default:
return Promise.resolve();
}
}
};
}
function createRedux (dispatcher) {
return {
dispatch: (arg) => {
if (typeof arg === 'function') {
return arg(store.dispatch, store.getState);
}
return dispatcher(arg);
},
getState: () => {
return {
api,
requests: {
[BASE_REQUEST.requestId]: BASE_REQUEST
}
};
}
};
}
describe('redux/requests', () => {
beforeEach(() => {
api = createApi();
dispatcher = sinon.spy();
store = createRedux(dispatcher);
});
it('watches new requests', () => {
store.dispatch(watchRequest(BASE_REQUEST));
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: BASE_REQUEST
});
});
it('tracks requests', (done) => {
store.dispatch(trackRequest(BASE_REQUEST.requestId));
setTimeout(() => {
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: {
transactionHash: TX_HASH,
show: true
}
});
done();
}, 50);
});
it('hides requests', () => {
store.dispatch(hideRequest(BASE_REQUEST.requestId));
expect(dispatcher).to.be.calledWith({
type: 'setRequest',
requestId: BASE_REQUEST.requestId,
requestData: { show: false }
});
});
});

View File

@ -0,0 +1,43 @@
// 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 { handleActions } from 'redux-actions';
const initialState = {};
export default handleActions({
setRequest (state, action) {
const { requestId, requestData } = action;
const nextState = {
...state,
[requestId]: {
...(state[requestId] || {}),
...requestData
}
};
return nextState;
},
deleteRequest (state, action) {
const { requestId } = action;
const nextState = { ...state };
delete nextState[requestId];
return nextState;
}
}, initialState);

View File

@ -121,7 +121,7 @@ export default class Status {
_subscribeBlockNumber = () => { _subscribeBlockNumber = () => {
return this._api return this._api
.subscribe('eth_blockNumber', (error, blockNumber) => { .subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) { if (error || !blockNumber) {
return; return;
} }

View File

@ -19,7 +19,7 @@ import { routerReducer } from 'react-router-redux';
import { import {
apiReducer, balancesReducer, blockchainReducer, apiReducer, balancesReducer, blockchainReducer,
workerReducer, imagesReducer, personalReducer, workerReducer, imagesReducer, personalReducer, requestsReducer,
signerReducer, statusReducer as nodeStatusReducer, signerReducer, statusReducer as nodeStatusReducer,
snackbarReducer, walletReducer snackbarReducer, walletReducer
} from './providers'; } from './providers';
@ -45,6 +45,7 @@ export default function () {
nodeStatus: nodeStatusReducer, nodeStatus: nodeStatusReducer,
personal: personalReducer, personal: personalReducer,
registry: registryReducer, registry: registryReducer,
requests: requestsReducer,
signer: signerReducer, signer: signerReducer,
snackbar: snackbarReducer, snackbar: snackbarReducer,
wallet: walletReducer, wallet: walletReducer,

View File

@ -20,6 +20,7 @@ import initMiddleware from './middleware';
import initReducers from './reducers'; import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions'; import { load as loadWallet } from './providers/walletActions';
import { init as initRequests } from './providers/requestsActions';
import { setupWorker } from './providers/workerWrapper'; import { setupWorker } from './providers/workerWrapper';
import { import {
@ -44,6 +45,7 @@ export default function (api, browserHistory, forEmbed = false) {
new SignerProvider(store, api).start(); new SignerProvider(store, api).start();
store.dispatch(loadWallet(api)); store.dispatch(loadWallet(api));
store.dispatch(initRequests(api));
setupWorker(store); setupWorker(store);
return store; return store;

View File

@ -49,6 +49,7 @@ export default class SecureApi extends Api {
// When the transport is closed, try to reconnect // When the transport is closed, try to reconnect
transport.on('close', this.connect, this); transport.on('close', this.connect, this);
this.connect(); this.connect();
} }

View File

@ -28,7 +28,7 @@ import styles from './inputAddress.css';
class InputAddress extends Component { class InputAddress extends Component {
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object, account: PropTypes.object,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
allowInvalid: PropTypes.bool, allowInvalid: PropTypes.bool,
@ -47,7 +47,6 @@ class InputAddress extends Component {
small: PropTypes.bool, small: PropTypes.bool,
tabIndex: PropTypes.number, tabIndex: PropTypes.number,
text: PropTypes.bool, text: PropTypes.bool,
tokens: PropTypes.object,
value: PropTypes.string value: PropTypes.string
}; };
@ -58,10 +57,9 @@ class InputAddress extends Component {
}; };
render () { render () {
const { accountsInfo, allowCopy, autoFocus, className, disabled, error, focused, hint } = this.props; const { account, allowCopy, autoFocus, className, disabled, error, focused, hint } = this.props;
const { hideUnderline, label, onClick, onFocus, readOnly, small } = this.props; const { hideUnderline, label, onClick, onFocus, readOnly, small } = this.props;
const { tabIndex, text, tokens, value } = this.props; const { tabIndex, text, value } = this.props;
const account = value && (accountsInfo[value] || tokens[value]);
const icon = this.renderIcon(); const icon = this.renderIcon();
const classes = [ className ]; const classes = [ className ];
@ -168,13 +166,26 @@ class InputAddress extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (state, props) {
const { tokens } = state.balances; const { text, value } = props;
if (!text || !value) {
return {};
}
const lcValue = value.toLowerCase();
const { accountsInfo } = state.personal; const { accountsInfo } = state.personal;
const { tokens } = state.balances;
const accountsInfoAddress = Object.keys(accountsInfo).find((address) => address.toLowerCase() === lcValue);
const tokensAddress = Object.keys(tokens).find((address) => address.toLowerCase() === lcValue);
const account = (accountsInfoAddress && accountsInfo[accountsInfoAddress]) ||
(tokensAddress && tokens[tokensAddress]) ||
null;
return { return {
accountsInfo, account
tokens
}; };
} }

View File

@ -33,14 +33,20 @@ const TOKEN_METHODS = {
class MethodDecoding extends Component { class MethodDecoding extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
compact: PropTypes.bool,
token: PropTypes.object, token: PropTypes.object,
transaction: PropTypes.object, transaction: PropTypes.object,
historic: PropTypes.bool historic: PropTypes.bool
} };
static defaultProps = {
compact: false,
historic: false
};
state = { state = {
contractAddress: null, contractAddress: null,
@ -54,7 +60,7 @@ class MethodDecoding extends Component {
isLoading: true, isLoading: true,
expandInput: false, expandInput: false,
inputType: 'auto' inputType: 'auto'
} };
methodDecodingStore = MethodDecodingStore.get(this.context.api); methodDecodingStore = MethodDecodingStore.get(this.context.api);
@ -106,10 +112,10 @@ class MethodDecoding extends Component {
} }
renderGas () { renderGas () {
const { historic, transaction } = this.props; const { compact, historic, transaction } = this.props;
const { gas, gasPrice, value } = transaction; const { gas, gasPrice, value } = transaction;
if (!gas || !gasPrice) { if (!gas || !gasPrice || compact) {
return null; return null;
} }
@ -248,7 +254,12 @@ class MethodDecoding extends Component {
} }
renderInputValue () { renderInputValue () {
const { transaction } = this.props; const { compact, transaction } = this.props;
if (compact) {
return null;
}
const { expandInput, inputType } = this.state; const { expandInput, inputType } = this.state;
const input = transaction.input || transaction.data; const input = transaction.input || transaction.data;
@ -347,7 +358,7 @@ class MethodDecoding extends Component {
} }
renderDeploy () { renderDeploy () {
const { historic, transaction } = this.props; const { compact, historic, transaction } = this.props;
const { methodInputs } = this.state; const { methodInputs } = this.state;
const { value } = transaction; const { value } = transaction;
@ -384,21 +395,21 @@ class MethodDecoding extends Component {
/> />
</div> </div>
{ this.renderAddressName(transaction.creates, false) } { this.renderAddressName(transaction.creates, false) }
<div> {
{ !compact && methodInputs && methodInputs.length
methodInputs && methodInputs.length ? (
? ( <div>
<FormattedMessage <FormattedMessage
id='ui.methodDecoding.deploy.params' id='ui.methodDecoding.deploy.params'
defaultMessage='with the following parameters:' defaultMessage='with the following parameters:'
/> />
) <div className={ styles.inputs }>
: '' { this.renderInputs() }
} </div>
</div> </div>
<div className={ styles.inputs }> )
{ this.renderInputs() } : null
</div> }
</div> </div>
); );
} }
@ -474,15 +485,18 @@ class MethodDecoding extends Component {
} }
renderSignatureMethod () { renderSignatureMethod () {
const { historic, transaction } = this.props; const { compact, historic, transaction } = this.props;
const { methodName, methodInputs } = this.state; const { methodName, methodInputs } = this.state;
const showInputs = !compact && methodInputs && methodInputs.length > 0;
const showEth = !!(transaction.value && transaction.value.gt(0));
const method = ( const method = (
<span className={ styles.name }> <span className={ styles.name }>
{ methodName } { methodName }
</span> </span>
); );
const ethValue = ( const ethValue = showEth && (
<span className={ styles.highlight }> <span className={ styles.highlight }>
{ this.renderEtherValue(transaction.value) } { this.renderEtherValue(transaction.value) }
</span> </span>
@ -493,19 +507,27 @@ class MethodDecoding extends Component {
<div className={ styles.description }> <div className={ styles.description }>
<FormattedMessage <FormattedMessage
id='ui.methodDecoding.signature.info' id='ui.methodDecoding.signature.info'
defaultMessage='{historic, select, true {Executed} false {Will execute}} the {method} function on the contract {address} trsansferring {ethValue}{inputLength, plural, zero {,} other {passing the following {inputLength, plural, one {parameter} other {parameters}}}}' defaultMessage='{historic, select, true {Executed} false {Will execute}} the {method} function on the contract {address} {showEth, select, true {transferring {ethValue}} false {}} {showInputs, select, false {} true {passing the following {inputLength, plural, one {parameter} other {parameters}}}}'
values={ { values={ {
historic, historic,
method, method,
ethValue, ethValue,
showEth,
showInputs,
address: this.renderAddressName(transaction.to), address: this.renderAddressName(transaction.to),
inputLength: methodInputs.length inputLength: methodInputs.length
} } } }
/> />
</div> </div>
<div className={ styles.inputs }> {
{ this.renderInputs() } showInputs
</div> ? (
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
)
: null
}
</div> </div>
); );
} }

View File

@ -14,22 +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/>.
import React, { Component, PropTypes } from 'react'; export default from './scrollableText';
import styles from '../deployContract.css';
export default class ErrorStep extends Component {
static propTypes = {
error: PropTypes.object
}
render () {
const { error } = this.props;
return (
<div className={ styles.center }>
The contract deployment failed: { error.message }
</div>
);
}
}

View File

@ -0,0 +1,34 @@
/* 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/>.
*/
.input {
background-color: transparent;
border: none;
box-sizing: border-box;
color: inherit;
cursor: text;
display: inline-block;
font-family: inherit;
font-size: inherit;
margin-left: 0.25em;
padding: 0;
width: 100%;
&.small {
width: 10em;
}
}

View File

@ -0,0 +1,40 @@
// 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, { PropTypes } from 'react';
import styles from './scrollableText.css';
export default function ScrollableText ({ small = false, text }) {
const classes = [ styles.input ];
if (small) {
classes.push(styles.small);
}
return (
<input
className={ classes.join(' ') }
readOnly
value={ text }
/>
);
}
ScrollableText.propTypes = {
text: PropTypes.string.isRequired,
small: PropTypes.bool
};

View File

@ -36,7 +36,7 @@ export default class ShortenedHash extends Component {
} }
return ( return (
<abbr className={ styles.hash } title={ shortened }>{ shortened }</abbr> <abbr className={ styles.hash } title={ data }>{ shortened }</abbr>
); );
} }
} }

View File

@ -76,7 +76,7 @@ export default class Title extends Component {
renderSteps () { renderSteps () {
const { activeStep, steps } = this.props; const { activeStep, steps } = this.props;
if (!steps) { if (!steps || steps.length < 2) {
return; return;
} }

View File

@ -46,6 +46,7 @@ export Page from './Page';
export ParityBackground from './ParityBackground'; export ParityBackground from './ParityBackground';
export Portal from './Portal'; export Portal from './Portal';
export QrCode from './QrCode'; export QrCode from './QrCode';
export ScrollableText from './ScrollableText';
export SectionList from './SectionList'; export SectionList from './SectionList';
export SelectionList from './SelectionList'; export SelectionList from './SelectionList';
export ShortenedHash from './ShortenedHash'; export ShortenedHash from './ShortenedHash';

View File

@ -16,6 +16,25 @@
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
export function trackRequest (api, options, statusCallback) {
const { requestId, transactionHash } = options;
const txHashPromise = transactionHash
? Promise.resolve(transactionHash)
: api.pollMethod('parity_checkRequest', requestId);
return txHashPromise
.then((transactionHash) => {
statusCallback(null, { transactionHash });
return api.pollMethod('eth_getTransactionReceipt', transactionHash, isValidReceipt);
})
.then((transactionReceipt) => {
statusCallback(null, { transactionReceipt });
})
.catch((error) => {
statusCallback(error);
});
}
const isValidReceipt = (receipt) => { const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
}; };
@ -73,100 +92,6 @@ export function postTransaction (_func, _options, _values = []) {
}); });
} }
export function deploy (contract, _options, values, metadata = {}, statecb = () => {}, skipGasEstimate = false) {
const options = { ..._options };
const { api } = contract;
const address = options.from;
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return contract.deploy(options, values, statecb, skipGasEstimate);
}
let gasEstPromise;
if (skipGasEstimate) {
gasEstPromise = Promise.resolve(null);
} else {
statecb(null, { state: 'estimateGas' });
gasEstPromise = deployEstimateGas(contract, options, values)
.then(([gasEst, gas]) => gas);
}
return gasEstPromise
.then((gas) => {
if (gas) {
options.gas = gas.toFixed(0);
}
statecb(null, { state: 'postTransaction', gas: options.gas });
return WalletsUtils.getDeployArgs(contract, options, values);
})
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values)
.then((requestId) => {
statecb(null, { state: 'checkRequest', requestId });
return contract._pollCheckRequest(requestId);
})
.then((txhash) => {
statecb(null, { state: 'getTransactionReceipt', txhash });
return contract._pollTransactionReceipt(txhash, options.gas);
})
.then((receipt) => {
if (receipt.gasUsed.eq(options.gas)) {
throw new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`);
}
const logs = WalletsUtils.parseLogs(api, receipt.logs || []);
const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded');
const transactionLog = logs.find((log) => log.event === 'SingleTransact');
if (!confirmationLog && !transactionLog) {
throw new Error('Something went wrong in the Wallet Contract (no logs have been emitted)...');
}
// Confirmations are needed from the other owners
if (confirmationLog) {
const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value);
// Add the contract to pending contracts
WalletsUtils.addPendingContract(address, operationHash, metadata);
statecb(null, { state: 'confirmationNeeded' });
return;
}
// Set the contract address in the receip
receipt.contractAddress = transactionLog.params.created.value;
const contractAddress = receipt.contractAddress;
statecb(null, { state: 'hasReceipt', receipt });
contract._receipt = receipt;
contract._address = contractAddress;
statecb(null, { state: 'getCode' });
return api.eth.getCode(contractAddress)
.then((code) => {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
statecb(null, { state: 'completed' });
return contractAddress;
});
});
});
});
}
export function deployEstimateGas (contract, _options, values) { export function deployEstimateGas (contract, _options, values) {
const options = { ..._options }; const options = { ..._options };
const { api } = contract; const { api } = contract;
@ -192,6 +117,86 @@ export function deployEstimateGas (contract, _options, values) {
}); });
} }
export function deploy (contract, options, values, skipGasEstimate = false) {
const { api } = contract;
const address = options.from;
const gasEstPromise = skipGasEstimate
? Promise.resolve(null)
: deployEstimateGas(contract, options, values).then(([gasEst, gas]) => gas);
return gasEstPromise
.then((gas) => {
if (gas) {
options.gas = gas.toFixed(0);
}
return WalletsUtils.isWallet(api, address);
})
.then((isWallet) => {
if (!isWallet) {
const encodedOptions = contract._encodeOptions(contract.constructors[0], options, values);
return api.parity.postTransaction(encodedOptions);
}
return WalletsUtils.getDeployArgs(contract, options, values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values);
});
});
}
export function parseTransactionReceipt (api, options, receipt) {
const { metadata } = options;
const address = options.from;
if (receipt.gasUsed.eq(options.gas)) {
const error = new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`);
return Promise.reject(error);
}
const logs = WalletsUtils.parseLogs(api, receipt.logs || []);
const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded');
const transactionLog = logs.find((log) => log.event === 'SingleTransact');
if (!confirmationLog && !transactionLog && !receipt.contractAddress) {
const error = new Error('Something went wrong in the contract deployment...');
return Promise.reject(error);
}
// Confirmations are needed from the other owners
if (confirmationLog) {
const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value);
// Add the contract to pending contracts
WalletsUtils.addPendingContract(address, operationHash, metadata);
return Promise.resolve(null);
}
if (transactionLog) {
// Set the contract address in the receipt
receipt.contractAddress = transactionLog.params.created.value;
}
const contractAddress = receipt.contractAddress;
return api.eth
.getCode(contractAddress)
.then((code) => {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
return contractAddress;
});
}
export function patchApi (api) { export function patchApi (api) {
api.patch = { api.patch = {
...api.patch, ...api.patch,

View File

@ -25,6 +25,7 @@ import { NULL_ADDRESS } from './constants';
export const ERRORS = { export const ERRORS = {
invalidAddress: 'address is an invalid network address', invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number', invalidAmount: 'the supplied amount should be a valid positive number',
invalidAmountDecimals: 'the supplied amount exceeds the allowed decimals',
duplicateAddress: 'the address is already in your address book', duplicateAddress: 'the address is already in your address book',
invalidChecksum: 'address has failed the checksum formatting', invalidChecksum: 'address has failed the checksum formatting',
invalidName: 'name should not be blank and longer than 2', invalidName: 'name should not be blank and longer than 2',
@ -48,6 +49,7 @@ export function validateAbi (abi) {
abiError = ERRORS.invalidAbi; abiError = ERRORS.invalidAbi;
return { return {
error: abiError,
abi, abi,
abiError, abiError,
abiParsed abiParsed
@ -66,6 +68,7 @@ export function validateAbi (abi) {
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`; abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
return { return {
error: abiError,
abi, abi,
abiError, abiError,
abiParsed abiParsed
@ -78,6 +81,7 @@ export function validateAbi (abi) {
} }
return { return {
error: abiError,
abi, abi,
abiError, abiError,
abiParsed abiParsed
@ -123,6 +127,7 @@ export function validateAddress (address) {
} }
return { return {
error: addressError,
address, address,
addressError addressError
}; };
@ -138,6 +143,7 @@ export function validateCode (code) {
} }
return { return {
error: codeError,
code, code,
codeError codeError
}; };
@ -149,6 +155,7 @@ export function validateName (name) {
: null; : null;
return { return {
error: nameError,
name, name,
nameError nameError
}; };
@ -168,6 +175,27 @@ export function validatePositiveNumber (number) {
} }
return { return {
error: numberError,
number,
numberError
};
}
export function validateDecimalsNumber (number, base = 1) {
let numberError = null;
try {
const s = new BigNumber(number).mul(base).toFixed();
if (s.indexOf('.') !== -1) {
numberError = ERRORS.invalidAmountDecimals;
}
} catch (e) {
numberError = ERRORS.invalidAmount;
}
return {
error: numberError,
number, number,
numberError numberError
}; };
@ -189,6 +217,7 @@ export function validateUint (value) {
} }
return { return {
error: valueError,
value, value,
valueError valueError
}; };

View File

@ -32,7 +32,8 @@ describe('util/validation', () => {
name: 'test', name: 'test',
inputs: [], inputs: [],
outputs: [] outputs: []
}] }],
error: null
}); });
}); });
@ -47,7 +48,8 @@ describe('util/validation', () => {
name: 'test', name: 'test',
inputs: [], inputs: [],
outputs: [] outputs: []
}] }],
error: null
}); });
}); });
@ -57,7 +59,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({ expect(validateAbi(abi)).to.deep.equal({
abi, abi,
abiError: ERRORS.invalidAbi, abiError: ERRORS.invalidAbi,
abiParsed: null abiParsed: null,
error: ERRORS.invalidAbi
}); });
}); });
@ -67,7 +70,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({ expect(validateAbi(abi)).to.deep.equal({
abi, abi,
abiError: ERRORS.invalidAbi, abiError: ERRORS.invalidAbi,
abiParsed: {} abiParsed: {},
error: ERRORS.invalidAbi
}); });
}); });
@ -77,7 +81,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({ expect(validateAbi(abi)).to.deep.equal({
abi, abi,
abiError: `${ERRORS.invalidAbi} (#0: event)`, abiError: `${ERRORS.invalidAbi} (#0: event)`,
abiParsed: [{ type: 'event' }] abiParsed: [{ type: 'event' }],
error: `${ERRORS.invalidAbi} (#0: event)`
}); });
}); });
@ -87,7 +92,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({ expect(validateAbi(abi)).to.deep.equal({
abi, abi,
abiError: `${ERRORS.invalidAbi} (#0: function)`, abiError: `${ERRORS.invalidAbi} (#0: function)`,
abiParsed: [{ type: 'function' }] abiParsed: [{ type: 'function' }],
error: `${ERRORS.invalidAbi} (#0: function)`
}); });
}); });
@ -97,7 +103,8 @@ describe('util/validation', () => {
expect(validateAbi(abi)).to.deep.equal({ expect(validateAbi(abi)).to.deep.equal({
abi, abi,
abiError: `${ERRORS.invalidAbi} (#0: somethingElse)`, abiError: `${ERRORS.invalidAbi} (#0: somethingElse)`,
abiParsed: [{ type: 'somethingElse' }] abiParsed: [{ type: 'somethingElse' }],
error: `${ERRORS.invalidAbi} (#0: somethingElse)`
}); });
}); });
}); });
@ -108,7 +115,8 @@ describe('util/validation', () => {
expect(validateAddress(address)).to.deep.equal({ expect(validateAddress(address)).to.deep.equal({
address, address,
addressError: null addressError: null,
error: null
}); });
}); });
@ -117,14 +125,16 @@ describe('util/validation', () => {
expect(validateAddress(address.toLowerCase())).to.deep.equal({ expect(validateAddress(address.toLowerCase())).to.deep.equal({
address, address,
addressError: null addressError: null,
error: null
}); });
}); });
it('sets error on null addresses', () => { it('sets error on null addresses', () => {
expect(validateAddress(null)).to.deep.equal({ expect(validateAddress(null)).to.deep.equal({
address: null, address: null,
addressError: ERRORS.invalidAddress addressError: ERRORS.invalidAddress,
error: ERRORS.invalidAddress
}); });
}); });
@ -133,7 +143,8 @@ describe('util/validation', () => {
expect(validateAddress(address)).to.deep.equal({ expect(validateAddress(address)).to.deep.equal({
address, address,
addressError: ERRORS.invalidAddress addressError: ERRORS.invalidAddress,
error: ERRORS.invalidAddress
}); });
}); });
}); });
@ -142,35 +153,40 @@ describe('util/validation', () => {
it('validates hex code', () => { it('validates hex code', () => {
expect(validateCode('0x123abc')).to.deep.equal({ expect(validateCode('0x123abc')).to.deep.equal({
code: '0x123abc', code: '0x123abc',
codeError: null codeError: null,
error: null
}); });
}); });
it('validates hex code (non-prefix)', () => { it('validates hex code (non-prefix)', () => {
expect(validateCode('123abc')).to.deep.equal({ expect(validateCode('123abc')).to.deep.equal({
code: '123abc', code: '123abc',
codeError: null codeError: null,
error: null
}); });
}); });
it('sets error on invalid code', () => { it('sets error on invalid code', () => {
expect(validateCode(null)).to.deep.equal({ expect(validateCode(null)).to.deep.equal({
code: null, code: null,
codeError: ERRORS.invalidCode codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
}); });
}); });
it('sets error on empty code', () => { it('sets error on empty code', () => {
expect(validateCode('')).to.deep.equal({ expect(validateCode('')).to.deep.equal({
code: '', code: '',
codeError: ERRORS.invalidCode codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
}); });
}); });
it('sets error on non-hex code', () => { it('sets error on non-hex code', () => {
expect(validateCode('123hfg')).to.deep.equal({ expect(validateCode('123hfg')).to.deep.equal({
code: '123hfg', code: '123hfg',
codeError: ERRORS.invalidCode codeError: ERRORS.invalidCode,
error: ERRORS.invalidCode
}); });
}); });
}); });
@ -179,21 +195,24 @@ describe('util/validation', () => {
it('validates names', () => { it('validates names', () => {
expect(validateName('Joe Bloggs')).to.deep.equal({ expect(validateName('Joe Bloggs')).to.deep.equal({
name: 'Joe Bloggs', name: 'Joe Bloggs',
nameError: null nameError: null,
error: null
}); });
}); });
it('sets error on null names', () => { it('sets error on null names', () => {
expect(validateName(null)).to.deep.equal({ expect(validateName(null)).to.deep.equal({
name: null, name: null,
nameError: ERRORS.invalidName nameError: ERRORS.invalidName,
error: ERRORS.invalidName
}); });
}); });
it('sets error on short names', () => { it('sets error on short names', () => {
expect(validateName(' 1 ')).to.deep.equal({ expect(validateName(' 1 ')).to.deep.equal({
name: ' 1 ', name: ' 1 ',
nameError: ERRORS.invalidName nameError: ERRORS.invalidName,
error: ERRORS.invalidName
}); });
}); });
}); });
@ -202,35 +221,40 @@ describe('util/validation', () => {
it('validates numbers', () => { it('validates numbers', () => {
expect(validatePositiveNumber(123)).to.deep.equal({ expect(validatePositiveNumber(123)).to.deep.equal({
number: 123, number: 123,
numberError: null numberError: null,
error: null
}); });
}); });
it('validates strings', () => { it('validates strings', () => {
expect(validatePositiveNumber('123')).to.deep.equal({ expect(validatePositiveNumber('123')).to.deep.equal({
number: '123', number: '123',
numberError: null numberError: null,
error: null
}); });
}); });
it('validates bignumbers', () => { it('validates bignumbers', () => {
expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({ expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({
number: new BigNumber(123), number: new BigNumber(123),
numberError: null numberError: null,
error: null
}); });
}); });
it('sets error on invalid numbers', () => { it('sets error on invalid numbers', () => {
expect(validatePositiveNumber(null)).to.deep.equal({ expect(validatePositiveNumber(null)).to.deep.equal({
number: null, number: null,
numberError: ERRORS.invalidAmount numberError: ERRORS.invalidAmount,
error: ERRORS.invalidAmount
}); });
}); });
it('sets error on negative numbers', () => { it('sets error on negative numbers', () => {
expect(validatePositiveNumber(-1)).to.deep.equal({ expect(validatePositiveNumber(-1)).to.deep.equal({
number: -1, number: -1,
numberError: ERRORS.invalidAmount numberError: ERRORS.invalidAmount,
error: ERRORS.invalidAmount
}); });
}); });
}); });
@ -239,42 +263,48 @@ describe('util/validation', () => {
it('validates numbers', () => { it('validates numbers', () => {
expect(validateUint(123)).to.deep.equal({ expect(validateUint(123)).to.deep.equal({
value: 123, value: 123,
valueError: null valueError: null,
error: null
}); });
}); });
it('validates strings', () => { it('validates strings', () => {
expect(validateUint('123')).to.deep.equal({ expect(validateUint('123')).to.deep.equal({
value: '123', value: '123',
valueError: null valueError: null,
error: null
}); });
}); });
it('validates bignumbers', () => { it('validates bignumbers', () => {
expect(validateUint(new BigNumber(123))).to.deep.equal({ expect(validateUint(new BigNumber(123))).to.deep.equal({
value: new BigNumber(123), value: new BigNumber(123),
valueError: null valueError: null,
error: null
}); });
}); });
it('sets error on invalid numbers', () => { it('sets error on invalid numbers', () => {
expect(validateUint(null)).to.deep.equal({ expect(validateUint(null)).to.deep.equal({
value: null, value: null,
valueError: ERRORS.invalidNumber valueError: ERRORS.invalidNumber,
error: ERRORS.invalidNumber
}); });
}); });
it('sets error on negative numbers', () => { it('sets error on negative numbers', () => {
expect(validateUint(-1)).to.deep.equal({ expect(validateUint(-1)).to.deep.equal({
value: -1, value: -1,
valueError: ERRORS.negativeNumber valueError: ERRORS.negativeNumber,
error: ERRORS.negativeNumber
}); });
}); });
it('sets error on decimal numbers', () => { it('sets error on decimal numbers', () => {
expect(validateUint(3.1415927)).to.deep.equal({ expect(validateUint(3.1415927)).to.deep.equal({
value: 3.1415927, value: 3.1415927,
valueError: ERRORS.decimalNumber valueError: ERRORS.decimalNumber,
error: ERRORS.decimalNumber
}); });
}); });
}); });

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 './errorStep'; export default from './requests';

View File

@ -0,0 +1,127 @@
/* 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/>.
*/
$baseColor: 255;
$baseOpacity: 0.95;
.requests {
align-items: flex-end;
bottom: 2em;
display: flex;
flex-direction: column;
position: fixed;
right: 0.175em;
z-index: 750;
* {
font-size: 0.85rem !important;
}
}
.request {
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
background-color: rgba($baseColor, $baseColor, $baseColor, $baseOpacity);
color: black;
cursor: pointer;
margin-top: 0.5em;
opacity: 1;
&.hide {
animation-duration: 0.5s;
animation-name: fadeout;
}
.status {
padding: 0.5em;
&.error {
background-color: rgba(200, 40, 40, 0.95);
color: white;
}
}
.container {
display: flex;
flex-direction: row;
padding: 1em;
* {
color: black !important;
}
}
&:hover .container {
background-color: rgba($baseColor, $baseColor, $baseColor, 1);
}
p {
margin: 0;
}
}
@keyframes fadeout {
from {
display: block;
height: inherit;
opacity: 1;
}
49% {
display: block;
height: inherit;
opacity: 0;
}
98% {
display: block;
height: 0;
opacity: 0;
}
to {
display: none;
height: 0;
opacity: 0;
}
}
.identity {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 1em;
.icon {
margin-bottom: 0.5rem;
}
}
.inline {
align-items: center;
display: flex;
flex-direction: row;
.fill {
flex: 1 0 auto;
}
}
.hash {
margin-left: 0.25em;
}

View File

@ -0,0 +1,233 @@
// 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 { LinearProgress } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hideRequest } from '~/redux/providers/requestsActions';
import { MethodDecoding, IdentityIcon, ScrollableText, ShortenedHash } from '~/ui';
import styles from './requests.css';
const ERROR_STATE = 'ERROR_STATE';
const DONE_STATE = 'DONE_STATE';
const WAITING_STATE = 'WAITING_STATE';
class Requests extends Component {
static propTypes = {
requests: PropTypes.object.isRequired,
onHideRequest: PropTypes.func.isRequired
};
state = {
extras: {}
};
render () {
const { requests } = this.props;
const { extras } = this.state;
return (
<div className={ styles.requests }>
{ Object.values(requests).map((request) => this.renderRequest(request, extras[request.requestId])) }
</div>
);
}
renderRequest (request, extras = {}) {
const { show, transaction } = request;
const state = this.getTransactionState(request);
const displayedTransaction = { ...transaction };
// Don't show gas and gasPrice
delete displayedTransaction.gas;
delete displayedTransaction.gasPrice;
const requestClasses = [ styles.request ];
const statusClasses = [ styles.status ];
const requestStyle = {};
const handleHideRequest = () => {
this.handleHideRequest(request.requestId);
};
if (state.type === ERROR_STATE) {
statusClasses.push(styles.error);
}
if (!show) {
requestClasses.push(styles.hide);
}
// Set the Request height (for animation) if found
if (extras.height) {
requestStyle.height = extras.height;
}
return (
<div
className={ requestClasses.join(' ') }
key={ request.requestId }
ref={ `request_${request.requestId}` }
onClick={ handleHideRequest }
style={ requestStyle }
>
<div className={ statusClasses.join(' ') }>
{ this.renderStatus(request) }
</div>
{
state.type === ERROR_STATE
? null
: (
<LinearProgress
max={ 6 }
mode={ state.type === WAITING_STATE ? 'indeterminate' : 'determinate' }
value={ state.type === DONE_STATE ? request.blockHeight.toNumber() : 6 }
/>
)
}
<div className={ styles.container }>
<div className={ styles.identity } title={ transaction.from }>
<IdentityIcon
address={ transaction.from }
inline
center
className={ styles.icon }
/>
</div>
<MethodDecoding
address={ transaction.from }
compact
historic={ state.type === DONE_STATE }
transaction={ displayedTransaction }
/>
</div>
</div>
);
}
renderStatus (request) {
const { error, transactionHash, transactionReceipt } = request;
if (error) {
return (
<div
className={ styles.inline }
title={ error.message }
>
<FormattedMessage
id='requests.status.error'
defaultMessage='An error occured:'
/>
<div className={ styles.fill }>
<ScrollableText
text={ error.text || error.message }
/>
</div>
</div>
);
}
if (transactionReceipt) {
return (
<FormattedMessage
id='requests.status.transactionMined'
defaultMessage='Transaction mined at block #{blockNumber} ({blockHeight} blocks ago)'
values={ {
blockHeight: request.blockHeight.toNumber(),
blockNumber: transactionReceipt.blockNumber.toFormat()
} }
/>
);
}
if (transactionHash) {
return (
<div className={ styles.inline }>
<FormattedMessage
id='requests.status.transactionSent'
defaultMessage='Transaction sent to network with hash'
/>
<div className={ [ styles.fill, styles.hash ].join(' ') }>
<ShortenedHash data={ transactionHash } />
</div>
</div>
);
}
return (
<FormattedMessage
id='requests.status.waitingForSigner'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
);
}
getTransactionState (request) {
const { error, transactionReceipt } = request;
if (error) {
return { type: ERROR_STATE };
}
if (transactionReceipt) {
return { type: DONE_STATE };
}
return { type: WAITING_STATE };
}
handleHideRequest = (requestId) => {
const requestElement = ReactDOM.findDOMNode(this.refs[`request_${requestId}`]);
// Try to get the request element height, to have a nice transition effect
if (requestElement) {
const { height } = requestElement.getBoundingClientRect();
const prevExtras = this.state.extras;
const nextExtras = {
...prevExtras,
[ requestId ]: {
...prevExtras[requestId],
height
}
};
return this.setState({ extras: nextExtras }, () => {
return this.props.onHideRequest(requestId);
});
}
return this.props.onHideRequest(requestId);
}
}
const mapStateToProps = (state) => {
const { requests } = state;
return { requests };
};
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onHideRequest: hideRequest
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Requests);

View File

@ -0,0 +1,89 @@
// 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 store from 'store';
import { ERROR_CODES } from '~/api/transport/error';
export const LS_REQUESTS_KEY = '_parity::requests';
export default class SavedRequests {
load (api) {
const requests = this._get();
const promises = Object.values(requests).map((request) => {
const { requestId, transactionHash } = request;
// The request hasn't been signed yet
if (transactionHash) {
return request;
}
return this._requestExists(api, requestId)
.then((exists) => {
if (!exists) {
return null;
}
return request;
})
.catch(() => {
this.remove(requestId);
});
});
return Promise.all(promises).then((requests) => requests.filter((request) => request));
}
save (requestId, requestData) {
const requests = this._get();
requests[requestId] = {
...(requests[requestId] || {}),
...requestData
};
this._set(requests);
}
remove (requestId) {
const requests = this._get();
delete requests[requestId];
this._set(requests);
}
_get () {
return store.get(LS_REQUESTS_KEY) || {};
}
_set (requests = {}) {
return store.set(LS_REQUESTS_KEY, requests);
}
_requestExists (api, requestId) {
return api.parity
.checkRequest(requestId)
.then(() => true)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_NOT_FOUND) {
return false;
}
throw error;
});
}
}

View File

@ -0,0 +1,88 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import store from 'store';
import SavedRequests, { LS_REQUESTS_KEY } from './savedRequests';
const DEFAULT_REQUEST = {
requestId: '0x1',
transaction: {}
};
const api = createApi();
const savedRequests = new SavedRequests();
function createApi () {
return {
parity: {
checkRequest: sinon.stub().resolves()
}
};
}
describe('views/Application/Requests/savedRequests', () => {
beforeEach(() => {
store.set(LS_REQUESTS_KEY, {
[DEFAULT_REQUEST.requestId]: DEFAULT_REQUEST
});
});
afterEach(() => {
store.set(LS_REQUESTS_KEY, {});
});
it('gets requests from local storage', () => {
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal(DEFAULT_REQUEST);
});
it('sets requests to local storage', () => {
savedRequests._set({});
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('removes requests', () => {
savedRequests.remove(DEFAULT_REQUEST.requestId);
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('saves new requests', () => {
savedRequests.save(DEFAULT_REQUEST.requestId, { extraData: true });
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal({
...DEFAULT_REQUEST,
extraData: true
});
});
it('loads requests', () => {
return savedRequests.load(api)
.then((requests) => {
expect(requests[0]).to.deep.equal(DEFAULT_REQUEST);
});
});
});

View File

@ -31,6 +31,7 @@ import FrameError from './FrameError';
import Status from './Status'; import Status from './Status';
import Store from './store'; import Store from './store';
import TabBar from './TabBar'; import TabBar from './TabBar';
import Requests from './Requests';
import styles from './application.css'; import styles from './application.css';
@ -103,6 +104,7 @@ class Application extends Component {
} }
<Extension /> <Extension />
<Snackbar /> <Snackbar />
<Requests />
</Container> </Container>
); );
} }