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,46 +139,46 @@ export function inOptionsCondition (condition) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
export function inOptions (options) {
|
||||
if (options) {
|
||||
Object.keys(options).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'to':
|
||||
// Don't encode the `to` option if it's empty
|
||||
// (eg. contract deployments)
|
||||
if (options[key]) {
|
||||
options[key] = inAddress(options[key]);
|
||||
}
|
||||
break;
|
||||
export function inOptions (_options = {}) {
|
||||
const options = { ..._options };
|
||||
|
||||
case 'from':
|
||||
options[key] = inAddress(options[key]);
|
||||
break;
|
||||
Object.keys(options).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'to':
|
||||
// Don't encode the `to` option if it's empty
|
||||
// (eg. contract deployments)
|
||||
if (options[key]) {
|
||||
options.to = inAddress(options[key]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition':
|
||||
options[key] = inOptionsCondition(options[key]);
|
||||
break;
|
||||
case 'from':
|
||||
options[key] = inAddress(options[key]);
|
||||
break;
|
||||
|
||||
case 'gas':
|
||||
case 'gasPrice':
|
||||
options[key] = inNumber16((new BigNumber(options[key])).round());
|
||||
break;
|
||||
case 'condition':
|
||||
options[key] = inOptionsCondition(options[key]);
|
||||
break;
|
||||
|
||||
case 'minBlock':
|
||||
options[key] = options[key] ? inNumber16(options[key]) : null;
|
||||
break;
|
||||
case 'gas':
|
||||
case 'gasPrice':
|
||||
options[key] = inNumber16((new BigNumber(options[key])).round());
|
||||
break;
|
||||
|
||||
case 'value':
|
||||
case 'nonce':
|
||||
options[key] = inNumber16(options[key]);
|
||||
break;
|
||||
case 'minBlock':
|
||||
options[key] = options[key] ? inNumber16(options[key]) : null;
|
||||
break;
|
||||
|
||||
case 'data':
|
||||
options[key] = inData(options[key]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'value':
|
||||
case 'nonce':
|
||||
options[key] = inNumber16(options[key]);
|
||||
break;
|
||||
|
||||
case 'data':
|
||||
options[key] = inData(options[key]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -380,7 +380,7 @@ export default class Parity {
|
||||
.execute('parity_postSign', inAddress(address), inHex(hash));
|
||||
}
|
||||
|
||||
postTransaction (options) {
|
||||
postTransaction (options = {}) {
|
||||
return this._transport
|
||||
.execute('parity_postTransaction', inOptions(options));
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ const events = {
|
||||
'parity_accountsInfo': { module: 'personal' },
|
||||
'parity_allAccountsInfo': { module: 'personal' },
|
||||
'parity_defaultAccount': { module: 'personal' },
|
||||
'parity_postTransaction': { module: 'signer' },
|
||||
'eth_accounts': { module: 'personal' },
|
||||
'signer_requestsToConfirm': { module: 'signer' }
|
||||
};
|
||||
@ -83,7 +84,7 @@ export default class Manager {
|
||||
|
||||
if (!engine.isStarted) {
|
||||
engine.start();
|
||||
} else {
|
||||
} else if (error !== null || data !== null) {
|
||||
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.not.have.been.calledWith(null, 'test2');
|
||||
});
|
||||
|
@ -14,6 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { outTransaction } from '../format/output';
|
||||
|
||||
export default class Signer {
|
||||
constructor (updateSubscriptions, api, subscriber) {
|
||||
this._subscriber = subscriber;
|
||||
@ -58,6 +60,15 @@ export default class Signer {
|
||||
.catch(nextTimeout);
|
||||
}
|
||||
|
||||
_postTransaction (data) {
|
||||
const request = {
|
||||
transaction: outTransaction(data.params[0]),
|
||||
requestId: data.json.result.result
|
||||
};
|
||||
|
||||
this._updateSubscriptions('parity_postTransaction', null, request);
|
||||
}
|
||||
|
||||
_loggingSubscribe () {
|
||||
return this._subscriber.subscribe('logging', (error, data) => {
|
||||
if (error || !data) {
|
||||
@ -65,11 +76,15 @@ export default class Signer {
|
||||
}
|
||||
|
||||
switch (data.method) {
|
||||
case 'parity_postTransaction':
|
||||
case 'eth_sendTranasction':
|
||||
case 'eth_sendTransaction':
|
||||
case 'eth_sendRawTransaction':
|
||||
this._listRequests(false);
|
||||
return;
|
||||
|
||||
case 'parity_postTransaction':
|
||||
this._postTransaction(data);
|
||||
this._listRequests(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -17,9 +17,12 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { BusyStep, Button, Portal, TxHash } from '~/ui';
|
||||
import { Button, Portal } from '~/ui';
|
||||
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
|
||||
import { setRequest } from '~/redux/providers/requestsActions';
|
||||
|
||||
import WalletType from './WalletType';
|
||||
import WalletDetails from './WalletDetails';
|
||||
@ -27,56 +30,25 @@ import WalletInfo from './WalletInfo';
|
||||
import CreateWalletStore from './createWalletStore';
|
||||
|
||||
@observer
|
||||
export default class CreateWallet extends Component {
|
||||
export class CreateWallet extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
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 () {
|
||||
const { stage, steps, waiting, rejected } = 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>
|
||||
);
|
||||
}
|
||||
const { stage, steps } = this.store;
|
||||
|
||||
return (
|
||||
<Portal
|
||||
activeStep={ stage }
|
||||
busySteps={ waiting }
|
||||
buttons={ this.renderDialogActions() }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
@ -92,25 +64,6 @@ export default class CreateWallet extends Component {
|
||||
const { accounts } = this.props;
|
||||
|
||||
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':
|
||||
return (
|
||||
<WalletInfo
|
||||
@ -148,7 +101,7 @@ export default class CreateWallet extends Component {
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { step, hasErrors, rejected, onCreate, onNext, onAdd } = this.store;
|
||||
const { step, hasErrors, onCreate, onNext, onAdd } = this.store;
|
||||
|
||||
const cancelBtn = (
|
||||
<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 = (
|
||||
<Button
|
||||
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 = (
|
||||
<Button
|
||||
icon={ <NextIcon /> }
|
||||
@ -220,14 +145,7 @@ export default class CreateWallet extends Component {
|
||||
/>
|
||||
);
|
||||
|
||||
if (rejected) {
|
||||
return [ closeBtn ];
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 'DEPLOYMENT':
|
||||
return [ closeBtn, sendingBtn ];
|
||||
|
||||
case 'INFO':
|
||||
return [ doneBtn ];
|
||||
|
||||
@ -274,3 +192,14 @@ export default class CreateWallet extends Component {
|
||||
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 sinon from 'sinon';
|
||||
|
||||
import CreateWallet from './';
|
||||
import { CreateWallet } from './createWallet';
|
||||
|
||||
import { ACCOUNTS } from './createWallet.test.js';
|
||||
|
||||
@ -47,7 +47,7 @@ function render () {
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('CreateWallet', () => {
|
||||
describe('modals/CreateWallet', () => {
|
||||
it('renders defaults', () => {
|
||||
expect(render()).to.be.ok;
|
||||
});
|
||||
|
@ -14,12 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { noop } from 'lodash';
|
||||
import { observable, computed, action, transaction } from 'mobx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Contract from '~/api/contract';
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
import Contracts from '~/contracts';
|
||||
import { wallet as walletAbi } from '~/contracts/abi';
|
||||
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: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
@ -67,13 +58,8 @@ const STEPS = {
|
||||
|
||||
export default class CreateWalletStore {
|
||||
@observable step = null;
|
||||
@observable rejected = false;
|
||||
|
||||
@observable deployState = null;
|
||||
@observable deployError = null;
|
||||
@observable deployed = false;
|
||||
|
||||
@observable txhash = null;
|
||||
@observable walletType = 'MULTISIG';
|
||||
|
||||
@observable wallet = {
|
||||
account: '',
|
||||
@ -85,7 +71,6 @@ export default class CreateWalletStore {
|
||||
name: '',
|
||||
description: ''
|
||||
};
|
||||
@observable walletType = 'MULTISIG';
|
||||
|
||||
@observable errors = {
|
||||
account: null,
|
||||
@ -96,6 +81,9 @@ export default class CreateWalletStore {
|
||||
name: null
|
||||
};
|
||||
|
||||
onClose = noop;
|
||||
onSetRequest = noop;
|
||||
|
||||
@computed get stage () {
|
||||
return this.stepsKeys.findIndex((k) => k === this.step);
|
||||
}
|
||||
@ -125,24 +113,17 @@ export default class CreateWalletStore {
|
||||
key
|
||||
};
|
||||
})
|
||||
.filter((step) => {
|
||||
return (this.walletType !== 'WATCH' || step.key !== 'DEPLOYMENT');
|
||||
});
|
||||
.filter((step) => this.walletType === 'WATCH' || step.key !== 'INFO');
|
||||
}
|
||||
|
||||
@computed get waiting () {
|
||||
this.steps
|
||||
.map((s, idx) => ({ idx, waiting: s.waiting }))
|
||||
.filter((s) => s.waiting)
|
||||
.map((s) => s.idx);
|
||||
}
|
||||
|
||||
constructor (api, accounts) {
|
||||
constructor (api, { accounts, onClose, onSetRequest }) {
|
||||
this.api = api;
|
||||
|
||||
this.step = this.stepsKeys[0];
|
||||
this.wallet.account = Object.values(accounts)[0].address;
|
||||
this.validateWallet(this.wallet);
|
||||
this.onClose = onClose;
|
||||
this.onSetRequest = onSetRequest;
|
||||
}
|
||||
|
||||
@action onTypeChange = (type) => {
|
||||
@ -193,8 +174,6 @@ export default class CreateWalletStore {
|
||||
return;
|
||||
}
|
||||
|
||||
this.step = 'DEPLOYMENT';
|
||||
|
||||
const { account, owners, required, daylimit } = this.wallet;
|
||||
|
||||
Contracts
|
||||
@ -243,25 +222,13 @@ export default class CreateWalletStore {
|
||||
const contract = this.api.newContract(walletAbi);
|
||||
|
||||
this.wallet = this.getWalletWithMeta(this.wallet);
|
||||
return deploy(contract, options, [ owners, required, daylimit ], this.wallet.metadata, this.onDeploymentState);
|
||||
})
|
||||
.then((address) => {
|
||||
if (!address || /^(0x)?0*$/.test(address)) {
|
||||
return false;
|
||||
}
|
||||
this.onClose();
|
||||
return deploy(contract, options, [ owners, required, daylimit ])
|
||||
.then((requestId) => {
|
||||
const metadata = { ...this.wallet.metadata, deployment: true };
|
||||
|
||||
this.deployed = true;
|
||||
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;
|
||||
this.onSetRequest(requestId, { metadata }, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
const addressValidation = validateAddress(_wallet.address);
|
||||
const accountValidation = validateAddress(_wallet.account);
|
||||
|
@ -15,19 +15,6 @@
|
||||
/* 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 {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
@ -20,21 +20,18 @@ import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
|
||||
import { CancelIcon, DoneIcon } from '~/ui/Icons';
|
||||
import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
|
||||
import { CancelIcon } from '~/ui/Icons';
|
||||
import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation';
|
||||
import { deploy, deployEstimateGas } from '~/util/tx';
|
||||
import { setRequest } from '~/redux/providers/requestsActions';
|
||||
|
||||
import DetailsStep from './DetailsStep';
|
||||
import ParametersStep from './ParametersStep';
|
||||
import ErrorStep from './ErrorStep';
|
||||
import Extras from '../Transfer/Extras';
|
||||
|
||||
import styles from './deployContract.css';
|
||||
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
|
||||
const STEPS = {
|
||||
CONTRACT_DETAILS: {
|
||||
title: (
|
||||
@ -59,23 +56,6 @@ const STEPS = {
|
||||
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,
|
||||
gasLimit: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSetRequest: PropTypes.func.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
source: PropTypes.string
|
||||
};
|
||||
@ -112,8 +93,6 @@ class DeployContract extends Component {
|
||||
amountError: '',
|
||||
code: '',
|
||||
codeError: ERRORS.invalidCode,
|
||||
deployState: '',
|
||||
deployError: null,
|
||||
description: '',
|
||||
descriptionError: null,
|
||||
extras: false,
|
||||
@ -124,9 +103,8 @@ class DeployContract extends Component {
|
||||
params: [],
|
||||
paramsError: [],
|
||||
inputs: [],
|
||||
rejected: false,
|
||||
step: 'CONTRACT_DETAILS'
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { abi, code } = this.props;
|
||||
@ -154,11 +132,9 @@ class DeployContract extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { step, deployError, rejected, inputs } = this.state;
|
||||
const { step, inputs } = this.state;
|
||||
|
||||
const realStepKeys = deployError || rejected
|
||||
? []
|
||||
: Object.keys(STEPS)
|
||||
const realStepKeys = Object.keys(STEPS)
|
||||
.filter((k) => {
|
||||
if (k === 'CONTRACT_PARAMETERS') {
|
||||
return inputs.length > 0;
|
||||
@ -172,45 +148,15 @@ class DeployContract extends Component {
|
||||
});
|
||||
|
||||
const realStep = realStepKeys.findIndex((k) => k === step);
|
||||
const realSteps = realStepKeys.length
|
||||
? 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;
|
||||
const realSteps = realStepKeys.map((k) => STEPS[k]);
|
||||
|
||||
return (
|
||||
<Portal
|
||||
buttons={ this.renderDialogActions() }
|
||||
activeStep={ realStep }
|
||||
busySteps={ waiting }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
steps={
|
||||
realSteps
|
||||
? realSteps.map((s) => s.title)
|
||||
: null
|
||||
}
|
||||
title={ title }
|
||||
steps={ realSteps.map((s) => s.title) }
|
||||
>
|
||||
{ this.renderExceptionWarning() }
|
||||
{ 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) {
|
||||
return closeBtn;
|
||||
}
|
||||
@ -346,43 +278,12 @@ class DeployContract extends Component {
|
||||
cancelBtn,
|
||||
createButton
|
||||
];
|
||||
|
||||
case 'DEPLOYMENT':
|
||||
return [ closeBtn ];
|
||||
|
||||
case 'COMPLETED':
|
||||
return [ closeBtnOk ];
|
||||
}
|
||||
}
|
||||
|
||||
renderStep () {
|
||||
const { accounts, readOnly, balances } = this.props;
|
||||
const { address, deployError, step, deployState, txhash, rejected } = 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.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const { step } = this.state;
|
||||
|
||||
switch (step) {
|
||||
case 'CONTRACT_DETAILS':
|
||||
@ -416,50 +317,6 @@ class DeployContract extends Component {
|
||||
|
||||
case 'EXTRAS':
|
||||
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 = () => {
|
||||
const { api, store } = this.context;
|
||||
const { api } = this.context;
|
||||
const { source } = this.props;
|
||||
const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state;
|
||||
|
||||
@ -611,125 +468,17 @@ class DeployContract extends Component {
|
||||
value: amountValue
|
||||
});
|
||||
|
||||
this.setState({ step: 'DEPLOYMENT' });
|
||||
|
||||
const contract = api.newContract(abiParsed);
|
||||
|
||||
deploy(contract, options, params, metadata, this.onDeploymentState, true)
|
||||
.then((address) => {
|
||||
// No contract address given, might need some confirmations
|
||||
// from the wallet owners...
|
||||
if (!address || /^(0x)?0*$/.test(address)) {
|
||||
return false;
|
||||
}
|
||||
this.onClose();
|
||||
deploy(contract, options, params, true)
|
||||
.then((requestId) => {
|
||||
const requestMetadata = { ...metadata, deployment: true };
|
||||
|
||||
metadata.blockNumber = contract._receipt
|
||||
? 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 });
|
||||
this.props.onSetRequest(requestId, { metadata: requestMetadata }, false);
|
||||
});
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
@ -752,6 +501,13 @@ function mapStateToProps (initState, initProps) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
onSetRequest: setRequest
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DeployContract);
|
||||
|
@ -21,8 +21,8 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { toWei } from '~/api/util/wei';
|
||||
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
|
||||
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||
import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
|
||||
import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||
import { MAX_GAS_ESTIMATION } from '~/util/constants';
|
||||
import { validateAddress, validateUint } from '~/util/validation';
|
||||
import { parseAbiType } from '~/util/abi';
|
||||
@ -30,11 +30,7 @@ import { parseAbiType } from '~/util/abi';
|
||||
import AdvancedStep from './AdvancedStep';
|
||||
import DetailsStep from './DetailsStep';
|
||||
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
|
||||
const STEP_DETAILS = 0;
|
||||
const STEP_BUSY_OR_ADVANCED = 1;
|
||||
const STEP_BUSY = 2;
|
||||
|
||||
const TITLES = {
|
||||
transfer: (
|
||||
@ -43,40 +39,22 @@ const TITLES = {
|
||||
defaultMessage='function details'
|
||||
/>
|
||||
),
|
||||
sending: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.sending'
|
||||
defaultMessage='sending'
|
||||
/>
|
||||
),
|
||||
complete: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.complete'
|
||||
defaultMessage='complete'
|
||||
/>
|
||||
),
|
||||
advanced: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.advanced'
|
||||
defaultMessage='advanced options'
|
||||
/>
|
||||
),
|
||||
rejected: (
|
||||
<FormattedMessage
|
||||
id='executeContract.steps.rejected'
|
||||
defaultMessage='rejected'
|
||||
/>
|
||||
)
|
||||
};
|
||||
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
||||
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced, TITLES.sending, TITLES.complete];
|
||||
const STAGES_BASIC = [TITLES.transfer];
|
||||
const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced];
|
||||
|
||||
@observer
|
||||
class ExecuteContract extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired,
|
||||
store: PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object,
|
||||
@ -86,7 +64,7 @@ class ExecuteContract extends Component {
|
||||
gasLimit: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onFromAddressChange: PropTypes.func.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit });
|
||||
|
||||
@ -94,17 +72,13 @@ class ExecuteContract extends Component {
|
||||
advancedOptions: false,
|
||||
amount: '0',
|
||||
amountError: null,
|
||||
busyState: null,
|
||||
fromAddressError: null,
|
||||
func: null,
|
||||
funcError: null,
|
||||
rejected: false,
|
||||
sending: false,
|
||||
step: STEP_DETAILS,
|
||||
txhash: null,
|
||||
values: [],
|
||||
valuesError: []
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { contract } = this.props;
|
||||
@ -122,23 +96,13 @@ class ExecuteContract extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { advancedOptions, rejected, sending, step } = this.state;
|
||||
const { advancedOptions, step } = this.state;
|
||||
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
|
||||
|
||||
if (rejected) {
|
||||
steps[steps.length - 1] = TITLES.rejected;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal
|
||||
activeStep={ step }
|
||||
buttons={ this.renderDialogActions() }
|
||||
busySteps={
|
||||
advancedOptions
|
||||
? [STEP_BUSY]
|
||||
: [STEP_BUSY_OR_ADVANCED]
|
||||
}
|
||||
busy={ sending }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
steps={ steps }
|
||||
@ -150,10 +114,9 @@ class ExecuteContract extends Component {
|
||||
}
|
||||
|
||||
renderExceptionWarning () {
|
||||
const { gasEdit, step } = this.state;
|
||||
const { errorEstimated } = this.gasStore;
|
||||
|
||||
if (!errorEstimated || step >= (gasEdit ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
|
||||
if (!errorEstimated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -164,8 +127,8 @@ class ExecuteContract extends Component {
|
||||
|
||||
renderDialogActions () {
|
||||
const { fromAddress } = this.props;
|
||||
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
|
||||
const hasError = fromAddressError || valuesError.find((error) => error);
|
||||
const { advancedOptions, step, fromAddressError, valuesError } = this.state;
|
||||
const hasError = !!(fromAddressError || valuesError.find((error) => error));
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
@ -189,7 +152,7 @@ class ExecuteContract extends Component {
|
||||
defaultMessage='post transaction'
|
||||
/>
|
||||
}
|
||||
disabled={ !!(sending || hasError) }
|
||||
disabled={ hasError }
|
||||
icon={ <IdentityIcon address={ fromAddress } button /> }
|
||||
onClick={ this.postTransaction }
|
||||
/>
|
||||
@ -226,55 +189,18 @@ class ExecuteContract extends Component {
|
||||
cancelBtn,
|
||||
advancedOptions ? nextBtn : postBtn
|
||||
];
|
||||
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
|
||||
return [
|
||||
cancelBtn
|
||||
];
|
||||
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
|
||||
return [
|
||||
cancelBtn,
|
||||
prevBtn,
|
||||
postBtn
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<Button
|
||||
key='close'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='executeContract.button.done'
|
||||
defaultMessage='done'
|
||||
/>
|
||||
}
|
||||
icon={ <DoneIcon /> }
|
||||
onClick={ this.onClose }
|
||||
/>
|
||||
cancelBtn,
|
||||
prevBtn,
|
||||
postBtn
|
||||
];
|
||||
}
|
||||
|
||||
renderStep () {
|
||||
const { onFromAddressChange } = this.props;
|
||||
const { advancedOptions, step, busyState, txhash, rejected } = 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.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const { step } = this.state;
|
||||
|
||||
if (step === STEP_DETAILS) {
|
||||
return (
|
||||
@ -288,28 +214,10 @@ class ExecuteContract extends Component {
|
||||
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 (
|
||||
<CompletedStep>
|
||||
<TxHash hash={ txhash } />
|
||||
</CompletedStep>
|
||||
<AdvancedStep gasStore={ this.gasStore } />
|
||||
);
|
||||
}
|
||||
|
||||
@ -390,59 +298,17 @@ class ExecuteContract extends Component {
|
||||
}
|
||||
|
||||
postTransaction = () => {
|
||||
const { api, store } = this.context;
|
||||
const { api } = this.context;
|
||||
const { fromAddress } = this.props;
|
||||
const { advancedOptions, amount, func, values } = this.state;
|
||||
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
|
||||
const finalstep = steps.length - 1;
|
||||
const { amount, func, values } = this.state;
|
||||
|
||||
const options = this.gasStore.overrideTransaction({
|
||||
from: fromAddress,
|
||||
value: api.util.toWei(amount || 0)
|
||||
});
|
||||
|
||||
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
|
||||
|
||||
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 });
|
||||
});
|
||||
func.postTransaction(options, values);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
onAdvancedClick = () => {
|
||||
|
@ -14,16 +14,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { noop } from 'lodash';
|
||||
import { observable, computed, action, transaction } from 'mobx';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import { wallet as walletAbi } from '~/contracts/abi';
|
||||
import { bytesToHex } from '~/api/util/format';
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
import Contract from '~/api/contract';
|
||||
import ERRORS from './errors';
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
|
||||
import GasPriceStore from '~/ui/GasPriceEditor/store';
|
||||
import { getLogger, LOG_KEYS } from '~/config';
|
||||
@ -32,13 +30,10 @@ const log = getLogger(LOG_KEYS.TransferModalStore);
|
||||
|
||||
const TITLES = {
|
||||
transfer: 'transfer details',
|
||||
sending: 'sending',
|
||||
complete: 'complete',
|
||||
extras: 'extra information',
|
||||
rejected: 'rejected'
|
||||
extras: 'extra information'
|
||||
};
|
||||
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
||||
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
|
||||
const STAGES_BASIC = [TITLES.transfer];
|
||||
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras];
|
||||
|
||||
export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMIT';
|
||||
|
||||
@ -49,8 +44,6 @@ export default class TransferStore {
|
||||
@observable sending = false;
|
||||
@observable tag = 'ETH';
|
||||
@observable isEth = true;
|
||||
@observable busyState = null;
|
||||
@observable rejected = false;
|
||||
|
||||
@observable data = '';
|
||||
@observable dataError = null;
|
||||
@ -72,8 +65,8 @@ export default class TransferStore {
|
||||
|
||||
account = null;
|
||||
balance = null;
|
||||
onClose = null;
|
||||
|
||||
onClose = noop;
|
||||
senders = null;
|
||||
isWallet = false;
|
||||
wallet = null;
|
||||
@ -83,7 +76,7 @@ export default class TransferStore {
|
||||
constructor (api, props) {
|
||||
this.api = api;
|
||||
|
||||
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
|
||||
const { account, balance, gasLimit, onClose, senders, newError, sendersBalances } = props;
|
||||
|
||||
this.account = account;
|
||||
this.balance = balance;
|
||||
@ -102,15 +95,15 @@ export default class TransferStore {
|
||||
this.sendersBalances = sendersBalances;
|
||||
this.senderError = ERRORS.requireSender;
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
this.onClose = onClose;
|
||||
}
|
||||
}
|
||||
|
||||
@computed get steps () {
|
||||
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
|
||||
|
||||
if (this.rejected) {
|
||||
steps[steps.length - 1] = TITLES.rejected;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
@ -147,6 +140,7 @@ export default class TransferStore {
|
||||
|
||||
@action handleClose = () => {
|
||||
this.stage = 0;
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
@action onUpdateDetails = (type, value) => {
|
||||
@ -186,85 +180,14 @@ export default class TransferStore {
|
||||
|
||||
this
|
||||
.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) => {
|
||||
this.sending = false;
|
||||
this.newError(error);
|
||||
})
|
||||
.then(() => {
|
||||
this.handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
@action _attachWalletOperation = (txhash) => {
|
||||
if (!txhash || /^(0x)?0*$/.test(txhash)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ethSubscriptionId = null;
|
||||
|
||||
// Number of blocks left to look-up (unsub after 15 blocks if nothing)
|
||||
let nBlocksLeft = 15;
|
||||
|
||||
return this.api.subscribe('eth_blockNumber', () => {
|
||||
this.api.eth
|
||||
.getTransactionReceipt(txhash)
|
||||
.then((tx) => {
|
||||
if (nBlocksLeft <= 0) {
|
||||
this.api.unsubscribe(ethSubscriptionId);
|
||||
ethSubscriptionId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
nBlocksLeft--;
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = this.walletContract.parseEventLogs(tx.logs);
|
||||
const operations = uniq(logs
|
||||
.filter((log) => log && log.params && log.params.operation)
|
||||
.map((log) => bytesToHex(log.params.operation.value)));
|
||||
|
||||
if (operations.length > 0) {
|
||||
this.operation = operations[0];
|
||||
}
|
||||
|
||||
this.api.unsubscribe(ethSubscriptionId);
|
||||
ethSubscriptionId = null;
|
||||
})
|
||||
.catch(() => {
|
||||
this.api.unsubscribe(ethSubscriptionId);
|
||||
ethSubscriptionId = null;
|
||||
});
|
||||
}).then((subId) => {
|
||||
ethSubscriptionId = subId;
|
||||
});
|
||||
}
|
||||
|
||||
@action _onUpdateAll = (valueAll) => {
|
||||
this.valueAll = valueAll;
|
||||
this.recalculateGas();
|
||||
|
@ -21,9 +21,9 @@ import { bindActionCreators } from 'redux';
|
||||
import { observer } from 'mobx-react';
|
||||
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 { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||
import { CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||
import { nullableProptype } from '~/util/proptypes';
|
||||
|
||||
import Details from './Details';
|
||||
@ -33,8 +33,7 @@ import TransferStore, { WALLET_WARNING_SPENT_TODAY_LIMIT } from './store';
|
||||
import styles from './transfer.css';
|
||||
|
||||
const STEP_DETAILS = 0;
|
||||
const STEP_ADVANCED_OR_BUSY = 1;
|
||||
const STEP_BUSY = 2;
|
||||
const STEP_EXTRA = 1;
|
||||
|
||||
@observer
|
||||
class Transfer extends Component {
|
||||
@ -57,16 +56,11 @@ class Transfer extends Component {
|
||||
store = new TransferStore(this.context.api, this.props);
|
||||
|
||||
render () {
|
||||
const { stage, extras, steps } = this.store;
|
||||
const { stage, steps } = this.store;
|
||||
|
||||
return (
|
||||
<Portal
|
||||
activeStep={ stage }
|
||||
busySteps={
|
||||
extras
|
||||
? [STEP_BUSY]
|
||||
: [STEP_ADVANCED_OR_BUSY]
|
||||
}
|
||||
buttons={ this.renderDialogActions() }
|
||||
onClose={ this.handleClose }
|
||||
open
|
||||
@ -80,10 +74,9 @@ class Transfer extends Component {
|
||||
}
|
||||
|
||||
renderExceptionWarning () {
|
||||
const { extras, stage } = this.store;
|
||||
const { errorEstimated } = this.store.gasStore;
|
||||
|
||||
if (!errorEstimated || stage >= (extras ? STEP_BUSY : STEP_ADVANCED_OR_BUSY)) {
|
||||
if (!errorEstimated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -144,68 +137,9 @@ class Transfer extends Component {
|
||||
|
||||
if (stage === STEP_DETAILS) {
|
||||
return this.renderDetailsPage();
|
||||
} else if (stage === STEP_ADVANCED_OR_BUSY && extras) {
|
||||
} else if (stage === STEP_EXTRA && extras) {
|
||||
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 () {
|
||||
@ -319,19 +253,6 @@ class Transfer extends Component {
|
||||
onClick={ this.store.onSend }
|
||||
/>
|
||||
);
|
||||
const doneBtn = (
|
||||
<Button
|
||||
icon={ <DoneIcon /> }
|
||||
key='close'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='transfer.buttons.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
}
|
||||
onClick={ this.handleClose }
|
||||
/>
|
||||
);
|
||||
|
||||
switch (stage) {
|
||||
case 0:
|
||||
@ -339,19 +260,14 @@ class Transfer extends Component {
|
||||
? [cancelBtn, nextBtn]
|
||||
: [cancelBtn, sendBtn];
|
||||
case 1:
|
||||
return extras
|
||||
? [cancelBtn, prevBtn, sendBtn]
|
||||
: [doneBtn];
|
||||
return [cancelBtn, prevBtn, sendBtn];
|
||||
default:
|
||||
return [doneBtn];
|
||||
return [cancelBtn];
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
const { onClose } = this.props;
|
||||
|
||||
this.store.handleClose();
|
||||
typeof onClose === 'function' && onClose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,8 @@ import { connect } from 'react-redux';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { BusyStep, AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal, TxHash } from '~/ui';
|
||||
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
|
||||
import { AddressSelect, Button, Form, TypedInput, Input, InputAddress, Portal } from '~/ui';
|
||||
import { CancelIcon, NextIcon } from '~/ui/Icons';
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
|
||||
import WalletSettingsStore from './walletSettingsStore.js';
|
||||
@ -40,46 +40,14 @@ class WalletSettings extends Component {
|
||||
senders: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
store = new WalletSettingsStore(this.context.api, this.props.wallet);
|
||||
store = new WalletSettingsStore(this.context.api, this.props);
|
||||
|
||||
render () {
|
||||
const { stage, steps, waiting, rejected } = 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>
|
||||
);
|
||||
}
|
||||
const { stage, steps } = this.store;
|
||||
|
||||
return (
|
||||
<Portal
|
||||
activeStep={ stage }
|
||||
busySteps={ waiting }
|
||||
buttons={ this.renderDialogActions() }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
@ -94,43 +62,6 @@ class WalletSettings extends Component {
|
||||
const { step } = this.store;
|
||||
|
||||
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':
|
||||
const { changes } = this.store;
|
||||
|
||||
@ -396,7 +327,7 @@ class WalletSettings extends Component {
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { step, hasErrors, rejected, onNext, send, done } = this.store;
|
||||
const { step, hasErrors, onNext, send } = this.store;
|
||||
|
||||
const cancelBtn = (
|
||||
<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 = (
|
||||
<Button
|
||||
icon={ <NextIcon /> }
|
||||
@ -470,16 +387,7 @@ class WalletSettings extends Component {
|
||||
/>
|
||||
);
|
||||
|
||||
if (rejected) {
|
||||
return [ closeBtn ];
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 'SENDING':
|
||||
return done
|
||||
? [ closeBtn ]
|
||||
: [ closeBtn, sendingBtn ];
|
||||
|
||||
case 'CONFIRMATION':
|
||||
const { changes } = this.store;
|
||||
|
||||
|
@ -14,24 +14,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { noop } from 'lodash';
|
||||
import { observable, computed, action, transaction } from 'mobx';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
import { validateUint, validateAddress } from '~/util/validation';
|
||||
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
|
||||
const STEPS = {
|
||||
EDIT: { title: 'wallet settings' },
|
||||
CONFIRMATION: { title: 'confirmation' },
|
||||
SENDING: { title: 'sending transaction', waiting: true }
|
||||
CONFIRMATION: { title: 'confirmation' }
|
||||
};
|
||||
|
||||
export default class WalletSettingsStore {
|
||||
accounts = {};
|
||||
onClose = noop;
|
||||
|
||||
@observable deployState = '';
|
||||
@observable done = false;
|
||||
@observable fromString = false;
|
||||
@observable requests = [];
|
||||
@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
|
||||
changesFromString (json) {
|
||||
try {
|
||||
@ -203,7 +194,7 @@ export default class WalletSettingsStore {
|
||||
});
|
||||
}
|
||||
|
||||
constructor (api, wallet) {
|
||||
constructor (api, { onClose, wallet }) {
|
||||
this.api = api;
|
||||
this.step = this.stepsKeys[0];
|
||||
|
||||
@ -223,6 +214,8 @@ export default class WalletSettingsStore {
|
||||
|
||||
this.validateWallet(this.wallet);
|
||||
});
|
||||
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
@action onNext = () => {
|
||||
@ -267,40 +260,8 @@ export default class WalletSettingsStore {
|
||||
const changes = this.changes;
|
||||
const walletInstance = this.walletInstance;
|
||||
|
||||
this.step = 'SENDING';
|
||||
|
||||
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');
|
||||
});
|
||||
Promise.all(changes.map((change) => this.sendChange(change, walletInstance)));
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
@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) => {
|
||||
const senderValidation = validateAddress(_wallet.sender);
|
||||
const requireValidation = validateUint(_wallet.require);
|
||||
|
@ -25,6 +25,7 @@ export blockchainReducer from './blockchainReducer';
|
||||
export workerReducer from './workerReducer';
|
||||
export imagesReducer from './imagesReducer';
|
||||
export personalReducer from './personalReducer';
|
||||
export requestsReducer from './requestsReducer';
|
||||
export signerReducer from './signerReducer';
|
||||
export snackbarReducer from './snackbarReducer';
|
||||
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 = () => {
|
||||
return this._api
|
||||
.subscribe('eth_blockNumber', (error, blockNumber) => {
|
||||
if (error) {
|
||||
if (error || !blockNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import { routerReducer } from 'react-router-redux';
|
||||
|
||||
import {
|
||||
apiReducer, balancesReducer, blockchainReducer,
|
||||
workerReducer, imagesReducer, personalReducer,
|
||||
workerReducer, imagesReducer, personalReducer, requestsReducer,
|
||||
signerReducer, statusReducer as nodeStatusReducer,
|
||||
snackbarReducer, walletReducer
|
||||
} from './providers';
|
||||
@ -45,6 +45,7 @@ export default function () {
|
||||
nodeStatus: nodeStatusReducer,
|
||||
personal: personalReducer,
|
||||
registry: registryReducer,
|
||||
requests: requestsReducer,
|
||||
signer: signerReducer,
|
||||
snackbar: snackbarReducer,
|
||||
wallet: walletReducer,
|
||||
|
@ -20,6 +20,7 @@ import initMiddleware from './middleware';
|
||||
import initReducers from './reducers';
|
||||
|
||||
import { load as loadWallet } from './providers/walletActions';
|
||||
import { init as initRequests } from './providers/requestsActions';
|
||||
import { setupWorker } from './providers/workerWrapper';
|
||||
|
||||
import {
|
||||
@ -44,6 +45,7 @@ export default function (api, browserHistory, forEmbed = false) {
|
||||
new SignerProvider(store, api).start();
|
||||
|
||||
store.dispatch(loadWallet(api));
|
||||
store.dispatch(initRequests(api));
|
||||
setupWorker(store);
|
||||
|
||||
return store;
|
||||
|
@ -49,6 +49,7 @@ export default class SecureApi extends Api {
|
||||
|
||||
// When the transport is closed, try to reconnect
|
||||
transport.on('close', this.connect, this);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ import styles from './inputAddress.css';
|
||||
|
||||
class InputAddress extends Component {
|
||||
static propTypes = {
|
||||
accountsInfo: PropTypes.object,
|
||||
account: PropTypes.object,
|
||||
allowCopy: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
allowInvalid: PropTypes.bool,
|
||||
@ -47,7 +47,6 @@ class InputAddress extends Component {
|
||||
small: PropTypes.bool,
|
||||
tabIndex: PropTypes.number,
|
||||
text: PropTypes.bool,
|
||||
tokens: PropTypes.object,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
@ -58,10 +57,9 @@ class InputAddress extends Component {
|
||||
};
|
||||
|
||||
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 { tabIndex, text, tokens, value } = this.props;
|
||||
const account = value && (accountsInfo[value] || tokens[value]);
|
||||
const { tabIndex, text, value } = this.props;
|
||||
const icon = this.renderIcon();
|
||||
const classes = [ className ];
|
||||
|
||||
@ -168,13 +166,26 @@ class InputAddress extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { tokens } = state.balances;
|
||||
function mapStateToProps (state, props) {
|
||||
const { text, value } = props;
|
||||
|
||||
if (!text || !value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const lcValue = value.toLowerCase();
|
||||
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 {
|
||||
accountsInfo,
|
||||
tokens
|
||||
account
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,14 +33,20 @@ const TOKEN_METHODS = {
|
||||
class MethodDecoding extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
token: PropTypes.object,
|
||||
transaction: PropTypes.object,
|
||||
historic: PropTypes.bool
|
||||
}
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
compact: false,
|
||||
historic: false
|
||||
};
|
||||
|
||||
state = {
|
||||
contractAddress: null,
|
||||
@ -54,7 +60,7 @@ class MethodDecoding extends Component {
|
||||
isLoading: true,
|
||||
expandInput: false,
|
||||
inputType: 'auto'
|
||||
}
|
||||
};
|
||||
|
||||
methodDecodingStore = MethodDecodingStore.get(this.context.api);
|
||||
|
||||
@ -106,10 +112,10 @@ class MethodDecoding extends Component {
|
||||
}
|
||||
|
||||
renderGas () {
|
||||
const { historic, transaction } = this.props;
|
||||
const { compact, historic, transaction } = this.props;
|
||||
const { gas, gasPrice, value } = transaction;
|
||||
|
||||
if (!gas || !gasPrice) {
|
||||
if (!gas || !gasPrice || compact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -248,7 +254,12 @@ class MethodDecoding extends Component {
|
||||
}
|
||||
|
||||
renderInputValue () {
|
||||
const { transaction } = this.props;
|
||||
const { compact, transaction } = this.props;
|
||||
|
||||
if (compact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { expandInput, inputType } = this.state;
|
||||
const input = transaction.input || transaction.data;
|
||||
|
||||
@ -347,7 +358,7 @@ class MethodDecoding extends Component {
|
||||
}
|
||||
|
||||
renderDeploy () {
|
||||
const { historic, transaction } = this.props;
|
||||
const { compact, historic, transaction } = this.props;
|
||||
const { methodInputs } = this.state;
|
||||
const { value } = transaction;
|
||||
|
||||
@ -384,21 +395,21 @@ class MethodDecoding extends Component {
|
||||
/>
|
||||
</div>
|
||||
{ this.renderAddressName(transaction.creates, false) }
|
||||
<div>
|
||||
{
|
||||
methodInputs && methodInputs.length
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='ui.methodDecoding.deploy.params'
|
||||
defaultMessage='with the following parameters:'
|
||||
/>
|
||||
)
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<div className={ styles.inputs }>
|
||||
{ this.renderInputs() }
|
||||
</div>
|
||||
{
|
||||
!compact && methodInputs && methodInputs.length
|
||||
? (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='ui.methodDecoding.deploy.params'
|
||||
defaultMessage='with the following parameters:'
|
||||
/>
|
||||
<div className={ styles.inputs }>
|
||||
{ this.renderInputs() }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -474,15 +485,18 @@ class MethodDecoding extends Component {
|
||||
}
|
||||
|
||||
renderSignatureMethod () {
|
||||
const { historic, transaction } = this.props;
|
||||
const { compact, historic, transaction } = this.props;
|
||||
const { methodName, methodInputs } = this.state;
|
||||
|
||||
const showInputs = !compact && methodInputs && methodInputs.length > 0;
|
||||
const showEth = !!(transaction.value && transaction.value.gt(0));
|
||||
|
||||
const method = (
|
||||
<span className={ styles.name }>
|
||||
{ methodName }
|
||||
</span>
|
||||
);
|
||||
const ethValue = (
|
||||
const ethValue = showEth && (
|
||||
<span className={ styles.highlight }>
|
||||
{ this.renderEtherValue(transaction.value) }
|
||||
</span>
|
||||
@ -493,19 +507,27 @@ class MethodDecoding extends Component {
|
||||
<div className={ styles.description }>
|
||||
<FormattedMessage
|
||||
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={ {
|
||||
historic,
|
||||
method,
|
||||
ethValue,
|
||||
showEth,
|
||||
showInputs,
|
||||
address: this.renderAddressName(transaction.to),
|
||||
inputLength: methodInputs.length
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.inputs }>
|
||||
{ this.renderInputs() }
|
||||
</div>
|
||||
{
|
||||
showInputs
|
||||
? (
|
||||
<div className={ styles.inputs }>
|
||||
{ this.renderInputs() }
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -14,22 +14,4 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default from './scrollableText';
|
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 (
|
||||
<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 () {
|
||||
const { activeStep, steps } = this.props;
|
||||
|
||||
if (!steps) {
|
||||
if (!steps || steps.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,7 @@ export Page from './Page';
|
||||
export ParityBackground from './ParityBackground';
|
||||
export Portal from './Portal';
|
||||
export QrCode from './QrCode';
|
||||
export ScrollableText from './ScrollableText';
|
||||
export SectionList from './SectionList';
|
||||
export SelectionList from './SelectionList';
|
||||
export ShortenedHash from './ShortenedHash';
|
||||
|
@ -16,6 +16,25 @@
|
||||
|
||||
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) => {
|
||||
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) {
|
||||
const options = { ..._options };
|
||||
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) {
|
||||
api.patch = {
|
||||
...api.patch,
|
||||
|
@ -25,6 +25,7 @@ import { NULL_ADDRESS } from './constants';
|
||||
export const ERRORS = {
|
||||
invalidAddress: 'address is an invalid network address',
|
||||
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',
|
||||
invalidChecksum: 'address has failed the checksum formatting',
|
||||
invalidName: 'name should not be blank and longer than 2',
|
||||
@ -48,6 +49,7 @@ export function validateAbi (abi) {
|
||||
abiError = ERRORS.invalidAbi;
|
||||
|
||||
return {
|
||||
error: abiError,
|
||||
abi,
|
||||
abiError,
|
||||
abiParsed
|
||||
@ -66,6 +68,7 @@ export function validateAbi (abi) {
|
||||
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
|
||||
|
||||
return {
|
||||
error: abiError,
|
||||
abi,
|
||||
abiError,
|
||||
abiParsed
|
||||
@ -78,6 +81,7 @@ export function validateAbi (abi) {
|
||||
}
|
||||
|
||||
return {
|
||||
error: abiError,
|
||||
abi,
|
||||
abiError,
|
||||
abiParsed
|
||||
@ -123,6 +127,7 @@ export function validateAddress (address) {
|
||||
}
|
||||
|
||||
return {
|
||||
error: addressError,
|
||||
address,
|
||||
addressError
|
||||
};
|
||||
@ -138,6 +143,7 @@ export function validateCode (code) {
|
||||
}
|
||||
|
||||
return {
|
||||
error: codeError,
|
||||
code,
|
||||
codeError
|
||||
};
|
||||
@ -149,6 +155,7 @@ export function validateName (name) {
|
||||
: null;
|
||||
|
||||
return {
|
||||
error: nameError,
|
||||
name,
|
||||
nameError
|
||||
};
|
||||
@ -168,6 +175,27 @@ export function validatePositiveNumber (number) {
|
||||
}
|
||||
|
||||
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,
|
||||
numberError
|
||||
};
|
||||
@ -189,6 +217,7 @@ export function validateUint (value) {
|
||||
}
|
||||
|
||||
return {
|
||||
error: valueError,
|
||||
value,
|
||||
valueError
|
||||
};
|
||||
|
@ -32,7 +32,8 @@ describe('util/validation', () => {
|
||||
name: 'test',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
}]
|
||||
}],
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
@ -47,7 +48,8 @@ describe('util/validation', () => {
|
||||
name: 'test',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
}]
|
||||
}],
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
@ -57,7 +59,8 @@ describe('util/validation', () => {
|
||||
expect(validateAbi(abi)).to.deep.equal({
|
||||
abi,
|
||||
abiError: ERRORS.invalidAbi,
|
||||
abiParsed: null
|
||||
abiParsed: null,
|
||||
error: ERRORS.invalidAbi
|
||||
});
|
||||
});
|
||||
|
||||
@ -67,7 +70,8 @@ describe('util/validation', () => {
|
||||
expect(validateAbi(abi)).to.deep.equal({
|
||||
abi,
|
||||
abiError: ERRORS.invalidAbi,
|
||||
abiParsed: {}
|
||||
abiParsed: {},
|
||||
error: ERRORS.invalidAbi
|
||||
});
|
||||
});
|
||||
|
||||
@ -77,7 +81,8 @@ describe('util/validation', () => {
|
||||
expect(validateAbi(abi)).to.deep.equal({
|
||||
abi,
|
||||
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({
|
||||
abi,
|
||||
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({
|
||||
abi,
|
||||
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({
|
||||
address,
|
||||
addressError: null
|
||||
addressError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
@ -117,14 +125,16 @@ describe('util/validation', () => {
|
||||
|
||||
expect(validateAddress(address.toLowerCase())).to.deep.equal({
|
||||
address,
|
||||
addressError: null
|
||||
addressError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on null addresses', () => {
|
||||
expect(validateAddress(null)).to.deep.equal({
|
||||
address: null,
|
||||
addressError: ERRORS.invalidAddress
|
||||
addressError: ERRORS.invalidAddress,
|
||||
error: ERRORS.invalidAddress
|
||||
});
|
||||
});
|
||||
|
||||
@ -133,7 +143,8 @@ describe('util/validation', () => {
|
||||
|
||||
expect(validateAddress(address)).to.deep.equal({
|
||||
address,
|
||||
addressError: ERRORS.invalidAddress
|
||||
addressError: ERRORS.invalidAddress,
|
||||
error: ERRORS.invalidAddress
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -142,35 +153,40 @@ describe('util/validation', () => {
|
||||
it('validates hex code', () => {
|
||||
expect(validateCode('0x123abc')).to.deep.equal({
|
||||
code: '0x123abc',
|
||||
codeError: null
|
||||
codeError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('validates hex code (non-prefix)', () => {
|
||||
expect(validateCode('123abc')).to.deep.equal({
|
||||
code: '123abc',
|
||||
codeError: null
|
||||
codeError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on invalid code', () => {
|
||||
expect(validateCode(null)).to.deep.equal({
|
||||
code: null,
|
||||
codeError: ERRORS.invalidCode
|
||||
codeError: ERRORS.invalidCode,
|
||||
error: ERRORS.invalidCode
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on empty code', () => {
|
||||
expect(validateCode('')).to.deep.equal({
|
||||
code: '',
|
||||
codeError: ERRORS.invalidCode
|
||||
codeError: ERRORS.invalidCode,
|
||||
error: ERRORS.invalidCode
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on non-hex code', () => {
|
||||
expect(validateCode('123hfg')).to.deep.equal({
|
||||
code: '123hfg',
|
||||
codeError: ERRORS.invalidCode
|
||||
codeError: ERRORS.invalidCode,
|
||||
error: ERRORS.invalidCode
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -179,21 +195,24 @@ describe('util/validation', () => {
|
||||
it('validates names', () => {
|
||||
expect(validateName('Joe Bloggs')).to.deep.equal({
|
||||
name: 'Joe Bloggs',
|
||||
nameError: null
|
||||
nameError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on null names', () => {
|
||||
expect(validateName(null)).to.deep.equal({
|
||||
name: null,
|
||||
nameError: ERRORS.invalidName
|
||||
nameError: ERRORS.invalidName,
|
||||
error: ERRORS.invalidName
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on short names', () => {
|
||||
expect(validateName(' 1 ')).to.deep.equal({
|
||||
name: ' 1 ',
|
||||
nameError: ERRORS.invalidName
|
||||
nameError: ERRORS.invalidName,
|
||||
error: ERRORS.invalidName
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -202,35 +221,40 @@ describe('util/validation', () => {
|
||||
it('validates numbers', () => {
|
||||
expect(validatePositiveNumber(123)).to.deep.equal({
|
||||
number: 123,
|
||||
numberError: null
|
||||
numberError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('validates strings', () => {
|
||||
expect(validatePositiveNumber('123')).to.deep.equal({
|
||||
number: '123',
|
||||
numberError: null
|
||||
numberError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('validates bignumbers', () => {
|
||||
expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({
|
||||
number: new BigNumber(123),
|
||||
numberError: null
|
||||
numberError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on invalid numbers', () => {
|
||||
expect(validatePositiveNumber(null)).to.deep.equal({
|
||||
number: null,
|
||||
numberError: ERRORS.invalidAmount
|
||||
numberError: ERRORS.invalidAmount,
|
||||
error: ERRORS.invalidAmount
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on negative numbers', () => {
|
||||
expect(validatePositiveNumber(-1)).to.deep.equal({
|
||||
number: -1,
|
||||
numberError: ERRORS.invalidAmount
|
||||
numberError: ERRORS.invalidAmount,
|
||||
error: ERRORS.invalidAmount
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -239,42 +263,48 @@ describe('util/validation', () => {
|
||||
it('validates numbers', () => {
|
||||
expect(validateUint(123)).to.deep.equal({
|
||||
value: 123,
|
||||
valueError: null
|
||||
valueError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('validates strings', () => {
|
||||
expect(validateUint('123')).to.deep.equal({
|
||||
value: '123',
|
||||
valueError: null
|
||||
valueError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('validates bignumbers', () => {
|
||||
expect(validateUint(new BigNumber(123))).to.deep.equal({
|
||||
value: new BigNumber(123),
|
||||
valueError: null
|
||||
valueError: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on invalid numbers', () => {
|
||||
expect(validateUint(null)).to.deep.equal({
|
||||
value: null,
|
||||
valueError: ERRORS.invalidNumber
|
||||
valueError: ERRORS.invalidNumber,
|
||||
error: ERRORS.invalidNumber
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on negative numbers', () => {
|
||||
expect(validateUint(-1)).to.deep.equal({
|
||||
value: -1,
|
||||
valueError: ERRORS.negativeNumber
|
||||
valueError: ERRORS.negativeNumber,
|
||||
error: ERRORS.negativeNumber
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error on decimal numbers', () => {
|
||||
expect(validateUint(3.1415927)).to.deep.equal({
|
||||
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
|
||||
// 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 Store from './store';
|
||||
import TabBar from './TabBar';
|
||||
import Requests from './Requests';
|
||||
|
||||
import styles from './application.css';
|
||||
|
||||
@ -103,6 +104,7 @@ class Application extends Component {
|
||||
}
|
||||
<Extension />
|
||||
<Snackbar />
|
||||
<Requests />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user