[beta] Cancel Transaction (#5656)

* option to disable persistent txqueue (#5544)

* option to disable persistent txqueue

* New option goes with kin

* Remove transaction RPC (#4949)

* Cancel tx JS (#4958)

* Remove transaction RPC

* Bumping multihash and libc

* Updating nanomsg

* bump nanomsg

* cancel tx

* cancel-tx-js

* cancel-tx-js

* cancel-tx-js

* cancel-tx-hs

* cancel-tx-js

* cancel-tx-js

* cancel-tx-js

* small fixes

* edit & time till submit

* edit & time till submit

* updates

* updates

* udpates

* udpates

* grumbles

* step 1

* Wonderful updates

* ready

* small refact

* small refact

* grumbles 1

* ffx2

* good ol' fashioned updates

* latest and greatest

* removeHash

* removeHash

* spec

* fix 1

* fix 1

* fix 2

* fix 2

* ff

* ff

* ff

* updates

* Updating documentation for RPCs (#5392)

* Removing minBlocks occurrencies

* Docs for new RPCs.

* Fixing linting issues, updating *withToken documentatiojn.

* Adding missing RPCs. Fixing tests.

* Fixing lint issues.
This commit is contained in:
Tomasz Drwięga
2017-05-18 15:19:29 +02:00
committed by Arkadiy Paronyan
parent 8dfc10ede9
commit 1aea9caf6d
32 changed files with 831 additions and 215 deletions

View File

@@ -211,3 +211,36 @@ export function inTraceType (whatTrace) {
return whatTrace;
}
function inDeriveType (derive) {
return derive && derive.type === 'hard' ? 'hard' : 'soft';
}
export function inDeriveHash (derive) {
const hash = derive && derive.hash ? derive.hash : derive;
const type = inDeriveType(derive);
return {
hash: inHex(hash),
type
};
}
export function inDeriveIndex (derive) {
if (!derive) {
return [];
}
if (!isArray(derive)) {
derive = [derive];
}
return derive.map(item => {
const index = inNumber10(item && item.index ? item.index : item);
return {
index,
type: inDeriveType(item)
};
});
}

View File

@@ -16,7 +16,11 @@
import BigNumber from 'bignumber.js';
import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions, inTraceType } from './input';
import {
inAddress, inBlockNumber, inData, inFilter, inHex,
inNumber10, inNumber16, inOptions, inTraceType,
inDeriveHash, inDeriveIndex
} from './input';
import { isAddress } from '../../../test/types';
describe('api/format/input', () => {
@@ -215,7 +219,7 @@ describe('api/format/input', () => {
expect(formatted.to).to.equal('');
});
['gas', 'gasPrice', 'value', 'minBlock', 'nonce'].forEach((input) => {
['gas', 'gasPrice', 'value', 'nonce'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => {
const block = {};
@@ -226,8 +230,8 @@ describe('api/format/input', () => {
});
});
it('passes minBlock as null when specified as such', () => {
expect(inOptions({ minBlock: null })).to.deep.equal({ minBlock: null });
it('passes condition as null when specified as such', () => {
expect(inOptions({ condition: null })).to.deep.equal({ condition: null });
});
it('ignores and passes through unknown keys', () => {
@@ -272,4 +276,66 @@ describe('api/format/input', () => {
expect(inTraceType(type)).to.deep.equal([type]);
});
});
describe('inDeriveHash', () => {
it('returns derive hash', () => {
expect(inDeriveHash(1)).to.deep.equal({
hash: '0x1',
type: 'soft'
});
expect(inDeriveHash(null)).to.deep.equal({
hash: '0x',
type: 'soft'
});
expect(inDeriveHash({
hash: 5
})).to.deep.equal({
hash: '0x5',
type: 'soft'
});
expect(inDeriveHash({
hash: 5,
type: 'hard'
})).to.deep.equal({
hash: '0x5',
type: 'hard'
});
});
});
describe('inDeriveIndex', () => {
it('returns derive hash', () => {
expect(inDeriveIndex(null)).to.deep.equal([]);
expect(inDeriveIndex([])).to.deep.equal([]);
expect(inDeriveIndex([1])).to.deep.equal([{
index: 1,
type: 'soft'
}]);
expect(inDeriveIndex({
index: 1
})).to.deep.equal([{
index: 1,
type: 'soft'
}]);
expect(inDeriveIndex([{
index: 1,
type: 'hard'
}, 5])).to.deep.equal([
{
index: 1,
type: 'hard'
},
{
index: 5,
type: 'soft'
}
]);
});
});
});

View File

@@ -280,12 +280,6 @@ export function outTransaction (tx) {
tx[key] = outTransactionCondition(tx[key]);
break;
case 'minBlock':
tx[key] = tx[key]
? outNumber(tx[key])
: null;
break;
case 'creates':
case 'from':
case 'to':

View File

@@ -384,7 +384,7 @@ describe('api/format/output', () => {
});
});
['blockNumber', 'gasPrice', 'gas', 'minBlock', 'nonce', 'transactionIndex', 'value'].forEach((input) => {
['blockNumber', 'gasPrice', 'gas', 'nonce', 'transactionIndex', 'value'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => {
const block = {};
@@ -396,8 +396,8 @@ describe('api/format/output', () => {
});
});
it('passes minBlock as null when null', () => {
expect(outTransaction({ minBlock: null })).to.deep.equal({ minBlock: null });
it('passes condition as null when null', () => {
expect(outTransaction({ condition: null })).to.deep.equal({ condition: null });
});
it('ignores and passes through unknown keys', () => {

View File

@@ -14,7 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
import {
inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber, inDeriveHash, inDeriveIndex
} from '../../format/input';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outHwAccountInfo, outNumber, outPeers, outRecentDapps, outTransaction, outVaultMeta } from '../../format/output';
export default class Parity {
@@ -117,6 +119,18 @@ export default class Parity {
.execute('parity_devLogsLevels');
}
deriveAddressHash (address, password, hash, shouldSave) {
return this._transport
.execute('parity_deriveAddressHash', inAddress(address), password, inDeriveHash(hash), !!shouldSave)
.then(outAddress);
}
deriveAddressIndex (address, password, index, shouldSave) {
return this._transport
.execute('parity_deriveAddressIndex', inAddress(address), password, inDeriveIndex(index), !!shouldSave)
.then(outAddress);
}
dropNonReservedPeers () {
return this._transport
.execute('parity_dropNonReservedPeers');
@@ -137,6 +151,11 @@ export default class Parity {
.execute('parity_executeUpgrade');
}
exportAccount (address, password) {
return this._transport
.execute('parity_exportAccount', inAddress(address), password);
}
extraData () {
return this._transport
.execute('parity_extraData');
@@ -389,6 +408,12 @@ export default class Parity {
.execute('parity_removeReservedPeer', encode);
}
removeTransaction (hash) {
return this._transport
.execute('parity_removeTransaction', inHex(hash))
.then(outTransaction);
}
rpcSettings () {
return this._transport
.execute('parity_rpcSettings');

View File

@@ -379,7 +379,9 @@ export default {
gasPrice: '0x2d20cff33',
hash: '0x09e64eb1ae32bb9ac415ce4ddb3dbad860af72d9377bb5f073c9628ab413c532',
input: '0x',
minBlock: null,
condition: {
block: 1
},
networkId: null,
nonce: '0x0',
publicKey: '0x3fa8c08c65a83f6b4ea3e04e1cc70cbe3cd391499e3e05ab7dedf28aff9afc538200ff93e3f2b2cb5029f03c7ebee820d63a4c5a9541c83acebe293f54cacf0e',
@@ -421,7 +423,7 @@ export default {
netChain: {
section: SECTION_NET,
desc: 'Returns the name of the connected chain.',
desc: 'Returns the name of the connected chain. DEPRECATED use `parity_chain` instead.',
params: [],
returns: {
type: String,
@@ -565,7 +567,9 @@ export default {
gasPrice: '0xba43b7400',
hash: '0x160b3c30ab1cf5871083f97ee1cee3901cfba3b0a2258eb337dd20a7e816b36e',
input: '0x095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d5555555555',
minBlock: null,
condition: {
block: 1
},
networkId: 1,
nonce: '0x5',
publicKey: '0x96157302dade55a1178581333e57d60ffe6fdf5a99607890456a578b4e6b60e335037d61ed58aa4180f9fd747dc50d44a7924aa026acbfb988b5062b629d6c36',
@@ -585,6 +589,7 @@ export default {
},
pendingTransactionsStats: {
section: SECTION_NET,
desc: 'Returns propagation stats for transactions in the queue.',
params: [],
returns: {
@@ -602,6 +607,49 @@ export default {
}
},
removeTransaction: {
section: SECTION_NET,
desc: 'Removes transaction from local transaction pool. Scheduled transactions and not-propagated transactions are safe to remove, removal of other transactions may have no effect though.',
params: [{
type: Hash,
desc: 'Hash of transaction to remove.',
example: '0x2547ea3382099c7c76d33dd468063b32d41016aacb02cbd51ebc14ff5d2b6a43'
}],
returns: {
type: Object,
desc: 'Removed transaction or `null`.',
details: TransactionResponse.details,
example: [
{
blockHash: null,
blockNumber: null,
creates: null,
from: '0xee3ea02840129123d5397f91be0391283a25bc7d',
gas: '0x23b58',
gasPrice: '0xba43b7400',
hash: '0x160b3c30ab1cf5871083f97ee1cee3901cfba3b0a2258eb337dd20a7e816b36e',
input: '0x095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d5555555555',
condition: {
block: 1
},
networkId: 1,
nonce: '0x5',
publicKey: '0x96157302dade55a1178581333e57d60ffe6fdf5a99607890456a578b4e6b60e335037d61ed58aa4180f9fd747dc50d44a7924aa026acbfb988b5062b629d6c36',
r: '0x92e8beb19af2bad0511d516a86e77fa73004c0811b2173657a55797bdf8558e1',
raw: '0xf8aa05850ba43b740083023b5894bb9bc244d798123fde783fcc1c72d3bb8c18941380b844095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d555555555526a092e8beb19af2bad0511d516a86e77fa73004c0811b2173657a55797bdf8558e1a062b4d4d125bbcb9c162453bc36ca156537543bb4414d59d1805d37fb63b351b8',
s: '0x62b4d4d125bbcb9c162453bc36ca156537543bb4414d59d1805d37fb63b351b8',
standardV: '0x1',
to: '0xbb9bc244d798123fde783fcc1c72d3bb8c189413',
transactionIndex: null,
v: '0x26',
value: '0x0'
},
new Dummy('{ ... }'),
new Dummy('{ ... }')
]
}
},
phraseToAddress: {
section: SECTION_ACCOUNTS,
desc: 'Converts a secret phrase into the corresponding address.',
@@ -892,7 +940,9 @@ export default {
v: '0x25',
r: '0xb40c6967a7e8bbdfd99a25fd306b9ef23b80e719514aeb7ddd19e2303d6fc139',
s: '0x6bf770ab08119e67dc29817e1412a0e3086f43da308c314db1b3bca9fb6d32bd',
minBlock: null
condition: {
block: 1
}
},
new Dummy('{ ... }, { ... }, ...')
]
@@ -1283,12 +1333,14 @@ export default {
params: [
{
type: Array,
desc: 'List of the Geth addresses to import.'
desc: 'List of the Geth addresses to import.',
example: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1']
}
],
returns: {
type: Array,
desc: 'Array of the imported addresses.'
desc: 'Array of the imported addresses.',
example: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1']
}
},
@@ -1298,7 +1350,114 @@ export default {
params: [],
returns: {
type: Array,
desc: '20 Bytes addresses owned by the client.'
desc: '20 Bytes addresses owned by the client.',
example: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1']
}
},
deriveAddressHash: {
subdoc: SUBDOC_ACCOUNTS,
desc: 'Derive new address from given account address using specific hash.',
params: [
{
type: Address,
desc: 'Account address to derive from.',
example: '0x407d73d8a49eeb85d32cf465507dd71d507100c1'
},
{
type: String,
desc: 'Password to the account.',
example: 'hunter2'
},
{
type: Object,
desc: 'Derivation hash and type (`soft` or `hard`). E.g. `{ hash: "0x123..123", type: "hard" }`.',
example: {
hash: '0x2547ea3382099c7c76d33dd468063b32d41016aacb02cbd51ebc14ff5d2b6a43',
type: 'hard'
}
},
{
type: Boolean,
desc: 'Flag indicating if the account should be saved.',
example: false
}
],
returns: {
type: Address,
desc: '20 Bytes new derived address.',
example: '0x407d73d8a49eeb85d32cf465507dd71d507100c1'
}
},
deriveAddressIndex: {
subdoc: SUBDOC_ACCOUNTS,
desc: 'Derive new address from given account address using hierarchical derivation (sequence of 32-bit integer indices).',
params: [
{
type: Address,
desc: 'Account address to export.',
example: '0x407d73d8a49eeb85d32cf465507dd71d507100c1'
},
{
type: String,
desc: 'Password to the account.',
example: 'hunter2'
},
{
type: Array,
desc: 'Hierarchical derivation sequence of index and type (`soft` or `hard`). E.g. `[{index:1,type:"hard"},{index:2,type:"soft"}]`.',
example: [
{ index: 1, type: 'hard' },
{ index: 2, type: 'soft' }
]
},
{
type: Boolean,
desc: 'Flag indicating if the account should be saved.',
example: false
}
],
returns: {
type: Address,
desc: '20 Bytes new derived address.',
example: '0x407d73d8a49eeb85d32cf465507dd71d507100c1'
}
},
exportAccount: {
subdoc: SUBDOC_ACCOUNTS,
desc: 'Returns a standard wallet file for given account if password matches.',
params: [
{
type: Address,
desc: 'Account address to export.',
example: '0x407d73d8a49eeb85d32cf465507dd71d507100c1'
},
{
type: String,
desc: 'Password to the account.',
example: 'hunter2'
}
],
returns: {
type: Object,
desc: 'Standard wallet JSON.',
example: {
'address': '0042e5d2a662eeaca8a7e828c174f98f35d8925b',
'crypto': {
'cipher': 'aes-128-ctr',
'cipherparams': { 'iv': 'a1c6ff99070f8032ca1c4e8add006373' },
'ciphertext': 'df27e3db64aa18d984b6439443f73660643c2d119a6f0fa2fa9a6456fc802d75',
'kdf': 'pbkdf2',
'kdfparams': { 'c': 10240, 'dklen': 32, 'prf': 'hmac-sha256', 'salt': 'ddc325335cda5567a1719313e73b4842511f3e4a837c9658eeb78e51ebe8c815' },
'mac': '3dc888ae79cbb226ff9c455669f6cf2d79be72120f2298f6cb0d444fddc0aa3d'
},
'id': '6a186c80-7797-cff2-bc2e-7c1d6a6cc76e',
'meta': '{"passwordHint":"parity-export-test","timestamp":1490017814987}',
'name': 'parity-export-test',
'version': 3
}
}
},
@@ -1622,7 +1781,14 @@ export default {
example: {
from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155',
to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567',
value: fromDecimal(2441406250)
gas: fromDecimal(30400),
gasPrice: fromDecimal(10000000000000),
value: fromDecimal(2441406250),
data: '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675',
condition: {
block: 354221,
time: new Date()
}
}
}
],

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Quantity, Data, BlockNumber } from '../types';
import { Quantity, Data } from '../types';
import { fromDecimal, Dummy } from '../helpers';
export default {
@@ -71,9 +71,9 @@ export default {
desc: 'Gas provided by the sender in Wei.',
optional: true
},
minBlock: {
type: BlockNumber,
desc: 'Integer block number, or the string `\'latest\'`, `\'earliest\'` or `\'pending\'`. Request will not be propagated till the given block is reached.',
condition: {
type: Object,
desc: 'Condition for scheduled transaction. Can be either an integer block number `{ block: 1 }` or UTC timestamp (in seconds) `{ timestamp: 1491290692 }`.',
optional: true
}
},
@@ -114,7 +114,7 @@ export default {
},
confirmRequestWithToken: {
desc: 'Confirm specific request with token.',
desc: 'Confirm specific request with rolling token.',
params: [
{
type: Quantity,
@@ -135,9 +135,9 @@ export default {
desc: 'Gas provided by the sender in Wei.',
optional: true
},
minBlock: {
type: BlockNumber,
desc: 'Integer block number, or the string `\'latest\'`, `\'earliest\'` or `\'pending\'`. Request will not be propagated till the given block is reached.',
condition: {
type: Object,
desc: 'Conditional submission of the transaction. Can be either an integer block number `{ block: 1 }` or UTC timestamp (in seconds) `{ time: 1491290692 }` or `null`.',
optional: true
}
},
@@ -145,7 +145,7 @@ export default {
},
{
type: String,
desc: 'Password.',
desc: 'Password (initially) or a token returned by the previous call.',
example: 'hunter2'
}
],
@@ -159,7 +159,7 @@ export default {
},
token: {
type: String,
desc: 'Token used to authenticate the request.'
desc: 'Token used to authenticate the next request.'
}
},
example: {

View File

@@ -102,9 +102,9 @@ export class TransactionRequest {
desc: 'Integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce.',
optional: true
},
minBlock: {
type: BlockNumber,
desc: 'Delay until this block if specified.',
condition: {
type: Object,
desc: 'Conditional submission of the transaction. Can be either an integer block number `{ block: 1 }` or UTC timestamp (in seconds) `{ time: 1491290692 }` or `null`.',
optional: true
}
}

View File

@@ -15,6 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment';
import dateDifference from 'date-difference';
import { FormattedMessage } from 'react-intl';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
@@ -24,6 +26,7 @@ import { txLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName';
import MethodDecoding from '../../MethodDecoding';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import styles from '../txList.css';
@@ -35,11 +38,15 @@ class TxRow extends Component {
static propTypes = {
accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired,
blockNumber: PropTypes.object,
contractAddresses: PropTypes.array.isRequired,
netVersion: PropTypes.string.isRequired,
tx: PropTypes.object.isRequired,
block: PropTypes.object,
className: PropTypes.string,
cancelTransaction: PropTypes.func,
editTransaction: PropTypes.func,
historic: PropTypes.bool
};
@@ -47,6 +54,33 @@ class TxRow extends Component {
historic: true
};
state = {
isCancelOpen: false,
isEditOpen: false,
canceled: false,
editing: false,
isContract: false,
isDeploy: false
};
methodDecodingStore = MethodDecodingStore.get(this.context.api);
componentWillMount () {
const { address, tx } = this.props;
this
.methodDecodingStore
.lookup(address, tx)
.then((lookup) => {
const newState = {
isContract: lookup.contract,
isDeploy: lookup.deploy
};
this.setState(newState);
});
}
render () {
const { address, className, historic, netVersion, tx } = this.props;
@@ -135,11 +169,132 @@ class TxRow extends Component {
return (
<td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : this.renderCancelToggle() }</div>
</td>
);
}
renderCancelToggle () {
const { canceled, editing, isCancelOpen, isEditOpen } = this.state;
if (canceled) {
return (
<div className={ styles.pending }>
<FormattedMessage
lassName={ styles.uppercase }
id='ui.txList.txRow.canceled'
defaultMessage='Canceled'
/>
</div>
);
}
if (editing) {
return (
<div className={ styles.pending }>
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.editing'
defaultMessage='Editing'
/>
</div>
</div>
);
}
if (!isCancelOpen && !isEditOpen) {
const pendingStatus = this.getCondition();
if (pendingStatus === 'submitting') {
return (
<div className={ styles.pending }>
<div />
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.submitting'
defaultMessage='Submitting'
/>
</div>
</div>
);
}
return (
<div className={ styles.pending }>
<span>
{ pendingStatus }
</span>
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.scheduled'
defaultMessage='Scheduled'
/>
</div>
<a onClick={ this.setEdit } className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.edit'
defaultMessage='Edit'
/>
</a>
<span>{' | '}</span>
<a onClick={ this.setCancel } className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.cancel'
defaultMessage='Cancel'
/>
</a>
</div>
);
}
let which;
if (isCancelOpen) {
which = (
<FormattedMessage
id='ui.txList.txRow.verify.cancelEditCancel'
defaultMessage='Cancel'
/>
);
} else {
which = (
<FormattedMessage
id='ui.txList.txRow.verify.cancelEditEdit'
defaultMessage='Edit'
/>
);
}
return (
<div className={ styles.pending }>
<div />
<div className={ styles.uppercase }>
<FormattedMessage
id='ui.txList.txRow.verify'
defaultMessage='Are you sure?'
/>
</div>
<a onClick={ (isCancelOpen) ? this.cancelTx : this.editTx }>
{ which }
</a>
<span>{' | '}</span>
<a onClick={ this.revertEditCancel }>
<FormattedMessage
id='ui.txList.txRow.verify.nevermind'
defaultMessage='Nevermind'
/>
</a>
</div>
);
}
getIsKnownContract (address) {
const { contractAddresses } = this.props;
return contractAddresses
.map((a) => a.toLowerCase())
.includes(address.toLowerCase());
}
addressLink (address) {
const { accountAddresses } = this.props;
const isAccount = accountAddresses.includes(address);
@@ -150,6 +305,70 @@ class TxRow extends Component {
return `/addresses/${address}`;
}
getCondition = () => {
const { blockNumber, tx } = this.props;
let { time, block } = tx.condition;
if (time) {
if ((time.getTime() - Date.now()) >= 0) {
// return `${dateDifference(new Date(), time, { compact: true })} left`;
return (
<FormattedMessage
id='ui.txList.txRow.pendingStatus.time'
defaultMessage='{time} left'
values={ {
time: dateDifference(new Date(), time, { compact: true })
} }
/>
);
} else {
return 'submitting';
}
} else if (blockNumber) {
block = blockNumber.minus(block);
// return (block.toNumber() < 0)
// ? block.abs().toFormat(0) + ' blocks left'
// : 'submitting';
if (block.toNumber() < 0) {
return (
<FormattedMessage
id='ui.txList.txRow.pendingStatus.blocksLeft'
defaultMessage='{blockNumber} blocks left'
values={ {
blockNumber: block.abs().toFormat(0)
} }
/>
);
} else {
return 'submitting';
}
}
}
cancelTx = () => {
const { cancelTransaction, tx } = this.props;
cancelTransaction(this, tx);
}
editTx = () => {
const { editTransaction, tx } = this.props;
editTransaction(this, tx);
}
setCancel = () => {
this.setState({ isCancelOpen: true });
}
setEdit = () => {
this.setState({ isEditOpen: true });
}
revertEditCancel = () => {
this.setState({ isCancelOpen: false, isEditOpen: false });
}
}
function mapStateToProps (initState) {

View File

@@ -14,150 +14,135 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable, transaction } from 'mobx';
import { uniq } from 'lodash';
import { action, observable } from 'mobx';
export default class Store {
@observable blocks = {};
@observable sortedHashes = [];
@observable transactions = {};
constructor (api) {
constructor (api, onNewError, hashes) {
this._api = api;
this._subscriptionId = 0;
this._pendingHashes = [];
this.subscribe();
this._onNewError = onNewError;
this.loadTransactions(hashes);
}
@action addBlocks = (blocks) => {
this.blocks = Object.assign({}, this.blocks, blocks);
@action addHash = (hash) => {
if (!this.sortedHashes.includes(hash)) {
this.sortedHashes.push(hash);
this.sortHashes();
}
}
@action addTransactions = (transactions) => {
transaction(() => {
this.transactions = Object.assign({}, this.transactions, transactions);
this.sortedHashes = Object
.keys(this.transactions)
.sort((ahash, bhash) => {
const bnA = this.transactions[ahash].blockNumber;
const bnB = this.transactions[bhash].blockNumber;
@action removeHash = (hash) => {
this.sortedHashes.remove(hash);
let tx = this.transactions[hash];
if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) {
return -1;
}
if (tx) {
delete this.transactions[hash];
delete this.blocks[tx.blockNumber];
}
this.sortHashes();
}
return bnB.comparedTo(bnA);
});
containsAll = (arr1, arr2) => {
return arr2.every((arr2Item) => arr1.includes(arr2Item));
}
this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0));
sameHashList = (transactions) => {
return this.containsAll(transactions, this.sortedHashes) && this.containsAll(this.sortedHashes, transactions);
}
sortHashes = () => {
this.sortedHashes = this.sortedHashes.sort((hashA, hashB) => {
const bnA = this.transactions[hashA].blockNumber;
const bnB = this.transactions[hashB].blockNumber;
// 0 is a special case (has not been added to the blockchain yet)
if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) {
return -1;
}
return bnB.comparedTo(bnA);
});
}
@action clearPending () {
this._pendingHashes = [];
}
loadTransactions (_txhashes) {
const { eth } = this._api;
subscribe () {
this._api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
return;
}
if (this._pendingHashes.length) {
this.loadTransactions(this._pendingHashes);
this.clearPending();
}
})
.then((subscriptionId) => {
this._subscriptionId = subscriptionId;
});
}
unsubscribe () {
if (!this._subscriptionId) {
// Ignore special cases and if the contents of _txhashes && this.sortedHashes are the same
if (Array.isArray(_txhashes) || this.sameHashList(_txhashes)) {
return;
}
this._api.unsubscribe(this._subscriptionId);
this._subscriptionId = 0;
}
// Remove any tx that are edited/cancelled
this.sortedHashes
.forEach((hash) => {
if (!_txhashes.includes(hash)) {
this.removeHash(hash);
}
});
loadTransactions (_txhashes = []) {
const promises = _txhashes
.filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash))
.map((txhash) => {
return Promise
.all([
this._api.eth.getTransactionByHash(txhash),
this._api.eth.getTransactionReceipt(txhash)
])
.then(([
transaction = {},
transactionReceipt = {}
]) => {
return {
...transactionReceipt,
...transaction
};
// Add any new tx
_txhashes
.forEach((txhash) => {
if (this.sortedHashes.includes(txhash)) { return; }
eth.getTransactionByHash(txhash)
.then((tx) => {
if (!tx) { return; }
this.transactions[txhash] = tx;
// If the tx has a blockHash, let's get the blockNumber, otherwise it's ready to be added
if (tx.blockHash) {
eth.getBlockByNumber(tx.blockNumber)
.then((block) => {
this.blocks[tx.blockNumber] = block;
this.addHash(txhash);
});
} else {
this.addHash(txhash);
}
});
});
}
if (!promises.length) {
return;
}
cancelTransaction = (txComponent, tx) => {
const { parity } = this._api;
const { hash } = tx;
Promise
.all(promises)
.then((_transactions) => {
const blockNumbers = [];
const transactions = _transactions
.filter((tx) => tx && tx.hash)
.reduce((txs, tx) => {
txs[tx.hash] = tx;
if (tx.blockNumber && tx.blockNumber.gt(0)) {
blockNumbers.push(tx.blockNumber.toNumber());
}
return txs;
}, {});
// No need to add transactions if there are none
if (Object.keys(transactions).length === 0) {
return false;
}
this.addTransactions(transactions);
this.loadBlocks(blockNumbers);
parity
.removeTransaction(hash)
.then(() => {
txComponent.setState({ canceled: true });
})
.catch((error) => {
console.warn('loadTransactions', error);
.catch((err) => {
this._onNewError({ message: err });
});
}
loadBlocks (_blockNumbers) {
const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]);
editTransaction = (txComponent, tx) => {
const { parity } = this._api;
const { hash, gas, gasPrice, to, from, value, input, condition } = tx;
if (!blockNumbers || !blockNumbers.length) {
return;
}
Promise
.all(blockNumbers.map((blockNumber) => this._api.eth.getBlockByNumber(blockNumber)))
.then((blocks) => {
this.addBlocks(
blocks.reduce((blocks, block, index) => {
blocks[blockNumbers[index]] = block;
return blocks;
}, {})
);
parity
.removeTransaction(hash)
.then(() => {
parity.postTransaction({
from,
to,
gas,
gasPrice,
value,
condition,
data: input
});
})
.catch((error) => {
console.warn('loadBlocks', error);
.then(() => {
txComponent.setState({ editing: true });
})
.catch((err) => {
this._onNewError({ message: err });
});
}
}

View File

@@ -44,7 +44,7 @@ describe('ui/TxList/store', () => {
}
}
};
store = new Store(api);
store = new Store(api, null, []);
});
describe('create', () => {
@@ -53,16 +53,14 @@ describe('ui/TxList/store', () => {
expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).to.deep.equal({});
});
it('subscribes to eth_blockNumber', () => {
expect(api.subscribe).to.have.been.calledWith('eth_blockNumber');
expect(store._subscriptionId).to.equal(SUBID);
});
});
describe('addBlocks', () => {
beforeEach(() => {
store.addBlocks(BLOCKS);
Object.keys(BLOCKS)
.forEach((blockNumber) => {
store.blocks[blockNumber] = BLOCKS[blockNumber];
});
});
it('adds the blocks to the list', () => {
@@ -72,7 +70,12 @@ describe('ui/TxList/store', () => {
describe('addTransactions', () => {
beforeEach(() => {
store.addTransactions(TRANSACTIONS);
Object.keys(TRANSACTIONS)
.forEach((hash) => {
store.transactions[hash] = TRANSACTIONS[hash];
store.addHash(hash);
});
store.sortHashes();
});
it('adds all transactions to the list', () => {
@@ -82,9 +85,5 @@ describe('ui/TxList/store', () => {
it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']);
});
it('adds pending transactions to the pending queue', () => {
expect(store._pendingHashes).to.deep.equal(['0x234', '0x456']);
});
});
});

View File

@@ -42,10 +42,11 @@
}
&.timestamp {
padding-top: 1.5em;
text-align: right;
max-width: 5em;
padding-top: 0.75em;
text-align: center;
line-height: 1.5em;
opacity: 0.5;
color: grey;
}
&.transaction {
@@ -83,4 +84,16 @@
.left {
text-align: left;
}
.pending {
padding: 0em;
}
.pending div {
padding-bottom: 1.25em;
}
.uppercase {
text-transform: uppercase;
}
}

View File

@@ -35,17 +35,13 @@ class TxList extends Component {
PropTypes.array,
PropTypes.object
]).isRequired,
netVersion: PropTypes.string.isRequired
blockNumber: PropTypes.object,
netVersion: PropTypes.string.isRequired,
onNewError: PropTypes.func
};
store = new Store(this.context.api);
componentWillMount () {
this.store.loadTransactions(this.props.hashes);
}
componentWillUnmount () {
this.store.unsubscribe();
this.store = new Store(this.context.api, this.props.onNewError, this.props.hashes);
}
componentWillReceiveProps (newProps) {
@@ -63,20 +59,24 @@ class TxList extends Component {
}
renderRows () {
const { address, netVersion } = this.props;
const { address, netVersion, blockNumber } = this.props;
const { editTransaction, cancelTransaction } = this.store;
return this.store.sortedHashes.map((txhash) => {
const tx = this.store.transactions[txhash];
const blockNumber = tx.blockNumber.toNumber();
const block = this.store.blocks[blockNumber];
const txBlockNumber = tx.blockNumber.toNumber();
const block = this.store.blocks[txBlockNumber];
return (
<TxRow
key={ tx.hash }
tx={ tx }
block={ block }
blockNumber={ blockNumber }
address={ address }
netVersion={ netVersion }
editTransaction={ editTransaction }
cancelTransaction={ cancelTransaction }
/>
);
});

View File

@@ -21,7 +21,8 @@ import { connect } from 'react-redux';
import { observer } from 'mobx-react';
import Store from '../../store';
import * as RequestsActions from '~/redux/providers/signerActions';
import { newError } from '~/redux/actions';
import { startConfirmRequest, startRejectRequest } from '~/redux/providers/signerActions';
import { Container, Page, TxList } from '~/ui';
import RequestPending from '../../components/RequestPending';
@@ -35,12 +36,13 @@ class RequestsPage extends Component {
};
static propTypes = {
actions: PropTypes.shape({
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired
}).isRequired,
gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired,
blockNumber: PropTypes.object,
newError: PropTypes.func,
signer: PropTypes.shape({
pending: PropTypes.array.isRequired,
finished: PropTypes.array.isRequired
@@ -68,6 +70,7 @@ class RequestsPage extends Component {
renderLocalQueue () {
const { localHashes } = this.store;
const { blockNumber, newError } = this.props;
if (!localHashes.length) {
return null;
@@ -77,7 +80,9 @@ class RequestsPage extends Component {
<Container title='Local Transactions'>
<TxList
address=''
blockNumber={ blockNumber }
hashes={ localHashes }
onNewError={ newError }
/>
</Container>
);
@@ -106,7 +111,7 @@ class RequestsPage extends Component {
}
renderPending = (data, index) => {
const { actions, gasLimit, netVersion } = this.props;
const { startConfirmRequest, startRejectRequest, gasLimit, netVersion } = this.props;
const { date, id, isSending, payload, origin } = data;
return (
@@ -119,8 +124,8 @@ class RequestsPage extends Component {
isSending={ isSending }
netVersion={ netVersion }
key={ id }
onConfirm={ actions.startConfirmRequest }
onReject={ actions.startRejectRequest }
onConfirm={ startConfirmRequest }
onReject={ startRejectRequest }
origin={ origin }
payload={ payload }
signerstore={ this.store }
@@ -130,11 +135,11 @@ class RequestsPage extends Component {
}
function mapStateToProps (state) {
const { gasLimit, netVersion } = state.nodeStatus;
const { actions, signer } = state;
const { gasLimit, netVersion, blockNumber } = state.nodeStatus;
const { signer } = state;
return {
actions,
blockNumber,
gasLimit,
netVersion,
signer
@@ -142,9 +147,11 @@ function mapStateToProps (state) {
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(RequestsActions, dispatch)
};
return bindActionCreators({
newError,
startConfirmRequest,
startRejectRequest
}, dispatch);
}
export default connect(

View File

@@ -95,7 +95,11 @@ export default class SignerStore {
this._api.parity
.localTransactions()
.then((localTransactions) => {
this.setLocalHashes(Object.keys(localTransactions));
const keys = Object
.keys(localTransactions)
.filter((key) => localTransactions[key].status !== 'canceled');
this.setLocalHashes(keys);
})
.then(nextTimeout)
.catch(nextTimeout);