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:
parent
e28c477075
commit
a99721004b
@ -139,15 +139,16 @@ 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) => {
|
Object.keys(options).forEach((key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'to':
|
case 'to':
|
||||||
// Don't encode the `to` option if it's empty
|
// Don't encode the `to` option if it's empty
|
||||||
// (eg. contract deployments)
|
// (eg. contract deployments)
|
||||||
if (options[key]) {
|
if (options[key]) {
|
||||||
options[key] = inAddress(options[key]);
|
options.to = inAddress(options[key]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -178,7 +179,6 @@ export function inOptions (options) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,123 +468,15 @@ 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 = () => {
|
||||||
@ -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);
|
||||||
|
@ -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,11 +189,8 @@ 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 [
|
return [
|
||||||
cancelBtn,
|
cancelBtn,
|
||||||
prevBtn,
|
prevBtn,
|
||||||
@ -238,43 +198,9 @@ class ExecuteContract extends Component {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
|
||||||
<Button
|
|
||||||
key='close'
|
|
||||||
label={
|
|
||||||
<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 = () => {
|
||||||
|
@ -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,82 +180,11 @@ 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);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(() => {
|
.then(() => {
|
||||||
this.api.unsubscribe(ethSubscriptionId);
|
this.handleClose();
|
||||||
ethSubscriptionId = null;
|
|
||||||
});
|
|
||||||
}).then((subId) => {
|
|
||||||
ethSubscriptionId = subId;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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';
|
||||||
|
171
js/src/redux/providers/requestsActions.js
Normal file
171
js/src/redux/providers/requestsActions.js
Normal 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
|
||||||
|
};
|
||||||
|
};
|
111
js/src/redux/providers/requestsActions.spec.js
Normal file
111
js/src/redux/providers/requestsActions.spec.js
Normal 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 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
43
js/src/redux/providers/requestsReducer.js
Normal file
43
js/src/redux/providers/requestsReducer.js
Normal 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);
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,22 +395,22 @@ class MethodDecoding extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ this.renderAddressName(transaction.creates, false) }
|
{ this.renderAddressName(transaction.creates, false) }
|
||||||
<div>
|
|
||||||
{
|
{
|
||||||
methodInputs && methodInputs.length
|
!compact && 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>
|
|
||||||
<div className={ styles.inputs }>
|
<div className={ styles.inputs }>
|
||||||
{ this.renderInputs() }
|
{ this.renderInputs() }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
{
|
||||||
|
showInputs
|
||||||
|
? (
|
||||||
<div className={ styles.inputs }>
|
<div className={ styles.inputs }>
|
||||||
{ this.renderInputs() }
|
{ this.renderInputs() }
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
34
js/src/ui/ScrollableText/scrollableText.css
Normal file
34
js/src/ui/ScrollableText/scrollableText.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
40
js/src/ui/ScrollableText/scrollableText.js
Normal file
40
js/src/ui/ScrollableText/scrollableText.js
Normal 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
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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';
|
127
js/src/views/Application/Requests/requests.css
Normal file
127
js/src/views/Application/Requests/requests.css
Normal 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;
|
||||||
|
}
|
233
js/src/views/Application/Requests/requests.js
Normal file
233
js/src/views/Application/Requests/requests.js
Normal 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);
|
89
js/src/views/Application/Requests/savedRequests.js
Normal file
89
js/src/views/Application/Requests/savedRequests.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
88
js/src/views/Application/Requests/savedRequests.spec.js
Normal file
88
js/src/views/Application/Requests/savedRequests.spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user