[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

3
.gitignore vendored
View File

@ -19,6 +19,9 @@
# mac stuff # mac stuff
.DS_Store .DS_Store
# npm stuff
npm-debug.log
# gdb files # gdb files
.gdb_history .gdb_history

View File

@ -351,7 +351,7 @@ impl Engine for AuthorityRound {
if step == parent_step if step == parent_step
|| (header.number() >= self.validate_step_transition && step <= parent_step) { || (header.number() >= self.validate_step_transition && step <= parent_step) {
trace!(target: "engine", "Multiple blocks proposed for step {}.", parent_step); trace!(target: "engine", "Multiple blocks proposed for step {}.", parent_step);
self.validators.report_malicious(header.author(), header.number(), Default::default()); self.validators.report_malicious(header.author());
Err(EngineError::DoubleVote(header.author().clone()))?; Err(EngineError::DoubleVote(header.author().clone()))?;
} }

View File

@ -1008,6 +1008,16 @@ impl MinerService for Miner {
} }
} }
fn remove_pending_transaction(&self, chain: &MiningBlockChainClient, hash: &H256) -> Option<PendingTransaction> {
let mut queue = self.transaction_queue.lock();
let tx = queue.find(hash);
if tx.is_some() {
let fetch_nonce = |a: &Address| chain.latest_nonce(a);
queue.remove_invalid(hash, &fetch_nonce);
}
tx
}
fn pending_receipt(&self, best_block: BlockNumber, hash: &H256) -> Option<RichReceipt> { fn pending_receipt(&self, best_block: BlockNumber, hash: &H256) -> Option<RichReceipt> {
self.from_pending_block( self.from_pending_block(
best_block, best_block,

View File

@ -150,6 +150,10 @@ pub trait MinerService : Send + Sync {
/// Query pending transactions for hash. /// Query pending transactions for hash.
fn transaction(&self, best_block: BlockNumber, hash: &H256) -> Option<PendingTransaction>; fn transaction(&self, best_block: BlockNumber, hash: &H256) -> Option<PendingTransaction>;
/// Removes transaction from the queue.
/// NOTE: The transaction is not removed from pending block if mining.
fn remove_pending_transaction(&self, chain: &MiningBlockChainClient, hash: &H256) -> Option<PendingTransaction>;
/// Get a list of all pending transactions in the queue. /// Get a list of all pending transactions in the queue.
fn pending_transactions(&self) -> Vec<PendingTransaction>; fn pending_transactions(&self) -> Vec<PendingTransaction>;

View File

@ -150,6 +150,7 @@
"blockies": "0.0.2", "blockies": "0.0.2",
"brace": "0.9.0", "brace": "0.9.0",
"bytes": "2.4.0", "bytes": "2.4.0",
"date-difference": "1.0.0",
"debounce": "1.0.0", "debounce": "1.0.0",
"es6-error": "4.0.0", "es6-error": "4.0.0",
"es6-promise": "4.0.5", "es6-promise": "4.0.5",

View File

@ -37,7 +37,11 @@ Object.keys(rustMethods).forEach((group) => {
}); });
}); });
function printType (type) { function printType (type, obj) {
if (!type) {
throw new Error(`Invalid type in ${JSON.stringify(obj)}`);
}
return type.print || `\`${type.name}\``; return type.print || `\`${type.name}\``;
} }
@ -45,7 +49,7 @@ function formatDescription (obj, prefix = '', indent = '') {
const optional = obj.optional ? '(optional) ' : ''; const optional = obj.optional ? '(optional) ' : '';
const defaults = obj.default ? `(default: \`${obj.default}\`) ` : ''; const defaults = obj.default ? `(default: \`${obj.default}\`) ` : '';
return `${indent}${prefix}${printType(obj.type)} - ${optional}${defaults}${obj.desc}`; return `${indent}${prefix}${printType(obj.type, obj)} - ${optional}${defaults}${obj.desc}`;
} }
function formatType (obj) { function formatType (obj) {

View File

@ -211,3 +211,36 @@ export function inTraceType (whatTrace) {
return 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 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'; import { isAddress } from '../../../test/types';
describe('api/format/input', () => { describe('api/format/input', () => {
@ -215,7 +219,7 @@ describe('api/format/input', () => {
expect(formatted.to).to.equal(''); expect(formatted.to).to.equal('');
}); });
['gas', 'gasPrice', 'value', 'minBlock', 'nonce'].forEach((input) => { ['gas', 'gasPrice', 'value', 'nonce'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => { it(`formats ${input} number as hexnumber`, () => {
const block = {}; const block = {};
@ -226,8 +230,8 @@ describe('api/format/input', () => {
}); });
}); });
it('passes minBlock as null when specified as such', () => { it('passes condition as null when specified as such', () => {
expect(inOptions({ minBlock: null })).to.deep.equal({ minBlock: null }); expect(inOptions({ condition: null })).to.deep.equal({ condition: null });
}); });
it('ignores and passes through unknown keys', () => { it('ignores and passes through unknown keys', () => {
@ -272,4 +276,66 @@ describe('api/format/input', () => {
expect(inTraceType(type)).to.deep.equal([type]); 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]); tx[key] = outTransactionCondition(tx[key]);
break; break;
case 'minBlock':
tx[key] = tx[key]
? outNumber(tx[key])
: null;
break;
case 'creates': case 'creates':
case 'from': case 'from':
case 'to': 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`, () => { it(`formats ${input} number as hexnumber`, () => {
const block = {}; const block = {};
@ -396,8 +396,8 @@ describe('api/format/output', () => {
}); });
}); });
it('passes minBlock as null when null', () => { it('passes condition as null when null', () => {
expect(outTransaction({ minBlock: null })).to.deep.equal({ minBlock: null }); expect(outTransaction({ condition: null })).to.deep.equal({ condition: null });
}); });
it('ignores and passes through unknown keys', () => { 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 // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { 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'; import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outHwAccountInfo, outNumber, outPeers, outRecentDapps, outTransaction, outVaultMeta } from '../../format/output';
export default class Parity { export default class Parity {
@ -117,6 +119,18 @@ export default class Parity {
.execute('parity_devLogsLevels'); .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 () { dropNonReservedPeers () {
return this._transport return this._transport
.execute('parity_dropNonReservedPeers'); .execute('parity_dropNonReservedPeers');
@ -137,6 +151,11 @@ export default class Parity {
.execute('parity_executeUpgrade'); .execute('parity_executeUpgrade');
} }
exportAccount (address, password) {
return this._transport
.execute('parity_exportAccount', inAddress(address), password);
}
extraData () { extraData () {
return this._transport return this._transport
.execute('parity_extraData'); .execute('parity_extraData');
@ -389,6 +408,12 @@ export default class Parity {
.execute('parity_removeReservedPeer', encode); .execute('parity_removeReservedPeer', encode);
} }
removeTransaction (hash) {
return this._transport
.execute('parity_removeTransaction', inHex(hash))
.then(outTransaction);
}
rpcSettings () { rpcSettings () {
return this._transport return this._transport
.execute('parity_rpcSettings'); .execute('parity_rpcSettings');

View File

@ -379,7 +379,9 @@ export default {
gasPrice: '0x2d20cff33', gasPrice: '0x2d20cff33',
hash: '0x09e64eb1ae32bb9ac415ce4ddb3dbad860af72d9377bb5f073c9628ab413c532', hash: '0x09e64eb1ae32bb9ac415ce4ddb3dbad860af72d9377bb5f073c9628ab413c532',
input: '0x', input: '0x',
minBlock: null, condition: {
block: 1
},
networkId: null, networkId: null,
nonce: '0x0', nonce: '0x0',
publicKey: '0x3fa8c08c65a83f6b4ea3e04e1cc70cbe3cd391499e3e05ab7dedf28aff9afc538200ff93e3f2b2cb5029f03c7ebee820d63a4c5a9541c83acebe293f54cacf0e', publicKey: '0x3fa8c08c65a83f6b4ea3e04e1cc70cbe3cd391499e3e05ab7dedf28aff9afc538200ff93e3f2b2cb5029f03c7ebee820d63a4c5a9541c83acebe293f54cacf0e',
@ -421,7 +423,7 @@ export default {
netChain: { netChain: {
section: SECTION_NET, 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: [], params: [],
returns: { returns: {
type: String, type: String,
@ -565,7 +567,9 @@ export default {
gasPrice: '0xba43b7400', gasPrice: '0xba43b7400',
hash: '0x160b3c30ab1cf5871083f97ee1cee3901cfba3b0a2258eb337dd20a7e816b36e', hash: '0x160b3c30ab1cf5871083f97ee1cee3901cfba3b0a2258eb337dd20a7e816b36e',
input: '0x095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d5555555555', input: '0x095ea7b3000000000000000000000000bf4ed7b27f1d666546e30d74d50d173d20bca75400000000000000000000000000002643c948210b4bd99244ccd64d5555555555',
minBlock: null, condition: {
block: 1
},
networkId: 1, networkId: 1,
nonce: '0x5', nonce: '0x5',
publicKey: '0x96157302dade55a1178581333e57d60ffe6fdf5a99607890456a578b4e6b60e335037d61ed58aa4180f9fd747dc50d44a7924aa026acbfb988b5062b629d6c36', publicKey: '0x96157302dade55a1178581333e57d60ffe6fdf5a99607890456a578b4e6b60e335037d61ed58aa4180f9fd747dc50d44a7924aa026acbfb988b5062b629d6c36',
@ -585,6 +589,7 @@ export default {
}, },
pendingTransactionsStats: { pendingTransactionsStats: {
section: SECTION_NET,
desc: 'Returns propagation stats for transactions in the queue.', desc: 'Returns propagation stats for transactions in the queue.',
params: [], params: [],
returns: { 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: { phraseToAddress: {
section: SECTION_ACCOUNTS, section: SECTION_ACCOUNTS,
desc: 'Converts a secret phrase into the corresponding address.', desc: 'Converts a secret phrase into the corresponding address.',
@ -892,7 +940,9 @@ export default {
v: '0x25', v: '0x25',
r: '0xb40c6967a7e8bbdfd99a25fd306b9ef23b80e719514aeb7ddd19e2303d6fc139', r: '0xb40c6967a7e8bbdfd99a25fd306b9ef23b80e719514aeb7ddd19e2303d6fc139',
s: '0x6bf770ab08119e67dc29817e1412a0e3086f43da308c314db1b3bca9fb6d32bd', s: '0x6bf770ab08119e67dc29817e1412a0e3086f43da308c314db1b3bca9fb6d32bd',
minBlock: null condition: {
block: 1
}
}, },
new Dummy('{ ... }, { ... }, ...') new Dummy('{ ... }, { ... }, ...')
] ]
@ -1283,12 +1333,14 @@ export default {
params: [ params: [
{ {
type: Array, type: Array,
desc: 'List of the Geth addresses to import.' desc: 'List of the Geth addresses to import.',
example: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1']
} }
], ],
returns: { returns: {
type: Array, type: Array,
desc: 'Array of the imported addresses.' desc: 'Array of the imported addresses.',
example: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1']
} }
}, },
@ -1298,7 +1350,114 @@ export default {
params: [], params: [],
returns: { returns: {
type: Array, 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: { example: {
from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155', from: '0xb60e8dd61c5d32be8058bb8eb970870f07233155',
to: '0xd46e8dd67c5d32be8058bb8eb970870f07244567', 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 // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Quantity, Data, BlockNumber } from '../types'; import { Quantity, Data } from '../types';
import { fromDecimal, Dummy } from '../helpers'; import { fromDecimal, Dummy } from '../helpers';
export default { export default {
@ -71,9 +71,9 @@ export default {
desc: 'Gas provided by the sender in Wei.', desc: 'Gas provided by the sender in Wei.',
optional: true optional: true
}, },
minBlock: { condition: {
type: BlockNumber, type: Object,
desc: 'Integer block number, or the string `\'latest\'`, `\'earliest\'` or `\'pending\'`. Request will not be propagated till the given block is reached.', desc: 'Condition for scheduled transaction. Can be either an integer block number `{ block: 1 }` or UTC timestamp (in seconds) `{ timestamp: 1491290692 }`.',
optional: true optional: true
} }
}, },
@ -114,7 +114,7 @@ export default {
}, },
confirmRequestWithToken: { confirmRequestWithToken: {
desc: 'Confirm specific request with token.', desc: 'Confirm specific request with rolling token.',
params: [ params: [
{ {
type: Quantity, type: Quantity,
@ -135,9 +135,9 @@ export default {
desc: 'Gas provided by the sender in Wei.', desc: 'Gas provided by the sender in Wei.',
optional: true optional: true
}, },
minBlock: { condition: {
type: BlockNumber, type: Object,
desc: 'Integer block number, or the string `\'latest\'`, `\'earliest\'` or `\'pending\'`. Request will not be propagated till the given block is reached.', 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 optional: true
} }
}, },
@ -145,7 +145,7 @@ export default {
}, },
{ {
type: String, type: String,
desc: 'Password.', desc: 'Password (initially) or a token returned by the previous call.',
example: 'hunter2' example: 'hunter2'
} }
], ],
@ -159,7 +159,7 @@ export default {
}, },
token: { token: {
type: String, type: String,
desc: 'Token used to authenticate the request.' desc: 'Token used to authenticate the next request.'
} }
}, },
example: { 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.', desc: 'Integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce.',
optional: true optional: true
}, },
minBlock: { condition: {
type: BlockNumber, type: Object,
desc: 'Delay until this block if specified.', 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 optional: true
} }
} }

View File

@ -15,6 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment'; import moment from 'moment';
import dateDifference from 'date-difference';
import { FormattedMessage } from 'react-intl';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
@ -24,6 +26,7 @@ import { txLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../../IdentityIcon'; import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName'; import IdentityName from '../../IdentityName';
import MethodDecoding from '../../MethodDecoding'; import MethodDecoding from '../../MethodDecoding';
import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
import styles from '../txList.css'; import styles from '../txList.css';
@ -35,11 +38,15 @@ class TxRow extends Component {
static propTypes = { static propTypes = {
accountAddresses: PropTypes.array.isRequired, accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
blockNumber: PropTypes.object,
contractAddresses: PropTypes.array.isRequired,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
tx: PropTypes.object.isRequired, tx: PropTypes.object.isRequired,
block: PropTypes.object, block: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
cancelTransaction: PropTypes.func,
editTransaction: PropTypes.func,
historic: PropTypes.bool historic: PropTypes.bool
}; };
@ -47,6 +54,33 @@ class TxRow extends Component {
historic: true 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 () { render () {
const { address, className, historic, netVersion, tx } = this.props; const { address, className, historic, netVersion, tx } = this.props;
@ -135,11 +169,132 @@ class TxRow extends Component {
return ( return (
<td className={ styles.timestamp }> <td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div> <div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div> <div>{ blockNumber ? _blockNumber.toFormat() : this.renderCancelToggle() }</div>
</td> </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) { addressLink (address) {
const { accountAddresses } = this.props; const { accountAddresses } = this.props;
const isAccount = accountAddresses.includes(address); const isAccount = accountAddresses.includes(address);
@ -150,6 +305,70 @@ class TxRow extends Component {
return `/addresses/${address}`; 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) { function mapStateToProps (initState) {

View File

@ -14,35 +14,51 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable, transaction } from 'mobx'; import { action, observable } from 'mobx';
import { uniq } from 'lodash';
export default class Store { export default class Store {
@observable blocks = {}; @observable blocks = {};
@observable sortedHashes = []; @observable sortedHashes = [];
@observable transactions = {}; @observable transactions = {};
constructor (api) { constructor (api, onNewError, hashes) {
this._api = api; this._api = api;
this._subscriptionId = 0; this._onNewError = onNewError;
this._pendingHashes = []; this.loadTransactions(hashes);
this.subscribe();
} }
@action addBlocks = (blocks) => { @action addHash = (hash) => {
this.blocks = Object.assign({}, this.blocks, blocks); if (!this.sortedHashes.includes(hash)) {
this.sortedHashes.push(hash);
this.sortHashes();
}
} }
@action addTransactions = (transactions) => { @action removeHash = (hash) => {
transaction(() => { this.sortedHashes.remove(hash);
this.transactions = Object.assign({}, this.transactions, transactions); let tx = this.transactions[hash];
this.sortedHashes = Object
.keys(this.transactions)
.sort((ahash, bhash) => {
const bnA = this.transactions[ahash].blockNumber;
const bnB = this.transactions[bhash].blockNumber;
if (tx) {
delete this.transactions[hash];
delete this.blocks[tx.blockNumber];
}
this.sortHashes();
}
containsAll = (arr1, arr2) => {
return arr2.every((arr2Item) => arr1.includes(arr2Item));
}
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)) { if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1; return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) { } else if (bnA.eq(0)) {
@ -51,113 +67,82 @@ export default class Store {
return bnB.comparedTo(bnA); return bnB.comparedTo(bnA);
}); });
this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0));
});
} }
@action clearPending () { loadTransactions (_txhashes) {
this._pendingHashes = []; const { eth } = this._api;
}
subscribe () { // Ignore special cases and if the contents of _txhashes && this.sortedHashes are the same
this._api if (Array.isArray(_txhashes) || this.sameHashList(_txhashes)) {
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
return; return;
} }
if (this._pendingHashes.length) { // Remove any tx that are edited/cancelled
this.loadTransactions(this._pendingHashes); this.sortedHashes
this.clearPending(); .forEach((hash) => {
if (!_txhashes.includes(hash)) {
this.removeHash(hash);
} }
});
// 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);
}
});
});
}
cancelTransaction = (txComponent, tx) => {
const { parity } = this._api;
const { hash } = tx;
parity
.removeTransaction(hash)
.then(() => {
txComponent.setState({ canceled: true });
}) })
.then((subscriptionId) => { .catch((err) => {
this._subscriptionId = subscriptionId; this._onNewError({ message: err });
}); });
} }
unsubscribe () { editTransaction = (txComponent, tx) => {
if (!this._subscriptionId) { const { parity } = this._api;
return; const { hash, gas, gasPrice, to, from, value, input, condition } = tx;
}
this._api.unsubscribe(this._subscriptionId); parity
this._subscriptionId = 0; .removeTransaction(hash)
} .then(() => {
parity.postTransaction({
loadTransactions (_txhashes = []) { from,
const promises = _txhashes to,
.filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash)) gas,
.map((txhash) => { gasPrice,
return Promise value,
.all([ condition,
this._api.eth.getTransactionByHash(txhash), data: input
this._api.eth.getTransactionReceipt(txhash)
])
.then(([
transaction = {},
transactionReceipt = {}
]) => {
return {
...transactionReceipt,
...transaction
};
}); });
});
if (!promises.length) {
return;
}
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);
}) })
.catch((error) => { .then(() => {
console.warn('loadTransactions', error); txComponent.setState({ editing: true });
});
}
loadBlocks (_blockNumbers) {
const blockNumbers = uniq(_blockNumbers).filter((bn) => !this.blocks[bn]);
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;
}, {})
);
}) })
.catch((error) => { .catch((err) => {
console.warn('loadBlocks', error); 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', () => { describe('create', () => {
@ -53,16 +53,14 @@ describe('ui/TxList/store', () => {
expect(store.sortedHashes.peek()).to.deep.equal([]); expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).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', () => { describe('addBlocks', () => {
beforeEach(() => { beforeEach(() => {
store.addBlocks(BLOCKS); Object.keys(BLOCKS)
.forEach((blockNumber) => {
store.blocks[blockNumber] = BLOCKS[blockNumber];
});
}); });
it('adds the blocks to the list', () => { it('adds the blocks to the list', () => {
@ -72,7 +70,12 @@ describe('ui/TxList/store', () => {
describe('addTransactions', () => { describe('addTransactions', () => {
beforeEach(() => { 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', () => { it('adds all transactions to the list', () => {
@ -82,9 +85,5 @@ describe('ui/TxList/store', () => {
it('sorts transactions based on blockNumber', () => { it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']); 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 { &.timestamp {
padding-top: 1.5em; max-width: 5em;
text-align: right; padding-top: 0.75em;
text-align: center;
line-height: 1.5em; line-height: 1.5em;
opacity: 0.5; color: grey;
} }
&.transaction { &.transaction {
@ -83,4 +84,16 @@
.left { .left {
text-align: 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.array,
PropTypes.object PropTypes.object
]).isRequired, ]).isRequired,
netVersion: PropTypes.string.isRequired blockNumber: PropTypes.object,
netVersion: PropTypes.string.isRequired,
onNewError: PropTypes.func
}; };
store = new Store(this.context.api);
componentWillMount () { componentWillMount () {
this.store.loadTransactions(this.props.hashes); this.store = new Store(this.context.api, this.props.onNewError, this.props.hashes);
}
componentWillUnmount () {
this.store.unsubscribe();
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
@ -63,20 +59,24 @@ class TxList extends Component {
} }
renderRows () { renderRows () {
const { address, netVersion } = this.props; const { address, netVersion, blockNumber } = this.props;
const { editTransaction, cancelTransaction } = this.store;
return this.store.sortedHashes.map((txhash) => { return this.store.sortedHashes.map((txhash) => {
const tx = this.store.transactions[txhash]; const tx = this.store.transactions[txhash];
const blockNumber = tx.blockNumber.toNumber(); const txBlockNumber = tx.blockNumber.toNumber();
const block = this.store.blocks[blockNumber]; const block = this.store.blocks[txBlockNumber];
return ( return (
<TxRow <TxRow
key={ tx.hash } key={ tx.hash }
tx={ tx } tx={ tx }
block={ block } block={ block }
blockNumber={ blockNumber }
address={ address } address={ address }
netVersion={ netVersion } netVersion={ netVersion }
editTransaction={ editTransaction }
cancelTransaction={ cancelTransaction }
/> />
); );
}); });

View File

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

View File

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

View File

@ -173,14 +173,26 @@ impl<T: NodeInfo> LocalDataStore<T> {
pub fn update(&self) -> Result<(), Error> { pub fn update(&self) -> Result<(), Error> {
trace!(target: "local_store", "Updating local store entries."); trace!(target: "local_store", "Updating local store entries.");
let mut batch = self.db.transaction();
let local_entries: Vec<TransactionEntry> = self.node.pending_transactions() let local_entries: Vec<TransactionEntry> = self.node.pending_transactions()
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect(); .collect();
let local_json = ::serde_json::to_value(&local_entries).map_err(Error::Json)?; self.write_txs(&local_entries)
}
/// Clear data in this column.
pub fn clear(&self) -> Result<(), Error> {
trace!(target: "local_store", "Clearing local store entries.");
self.write_txs(&[])
}
// helper for writing a vector of transaction entries to disk.
fn write_txs(&self, txs: &[TransactionEntry]) -> Result<(), Error> {
let mut batch = self.db.transaction();
let local_json = ::serde_json::to_value(txs).map_err(Error::Json)?;
let json_str = format!("{}", local_json); let json_str = format!("{}", local_json);
batch.put_vec(self.col, LOCAL_TRANSACTIONS_KEY, json_str.into_bytes()); batch.put_vec(self.col, LOCAL_TRANSACTIONS_KEY, json_str.into_bytes());

View File

@ -6,6 +6,7 @@ auto_update = "none"
release_track = "current" release_track = "current"
no_download = false no_download = false
no_consensus = false no_consensus = false
no_persistent_txqueue = false
chain = "homestead" chain = "homestead"
base_path = "$HOME/.parity" base_path = "$HOME/.parity"

View File

@ -93,6 +93,8 @@ usage! {
flag_chain: String = "homestead", or |c: &Config| otry!(c.parity).chain.clone(), flag_chain: String = "homestead", or |c: &Config| otry!(c.parity).chain.clone(),
flag_keys_path: String = "$BASE/keys", or |c: &Config| otry!(c.parity).keys_path.clone(), flag_keys_path: String = "$BASE/keys", or |c: &Config| otry!(c.parity).keys_path.clone(),
flag_identity: String = "", or |c: &Config| otry!(c.parity).identity.clone(), flag_identity: String = "", or |c: &Config| otry!(c.parity).identity.clone(),
flag_no_persistent_txqueue: bool = false,
or |c: &Config| otry!(c.parity).no_persistent_txqueue,
// -- Account Options // -- Account Options
flag_unlock: Option<String> = None, flag_unlock: Option<String> = None,
@ -370,6 +372,7 @@ struct Operating {
db_path: Option<String>, db_path: Option<String>,
keys_path: Option<String>, keys_path: Option<String>,
identity: Option<String>, identity: Option<String>,
no_persistent_txqueue: Option<bool>,
} }
#[derive(Default, Debug, PartialEq, RustcDecodable)] #[derive(Default, Debug, PartialEq, RustcDecodable)]
@ -627,6 +630,7 @@ mod tests {
flag_db_path: Some("$HOME/.parity/chains".into()), flag_db_path: Some("$HOME/.parity/chains".into()),
flag_keys_path: "$HOME/.parity/keys".into(), flag_keys_path: "$HOME/.parity/keys".into(),
flag_identity: "".into(), flag_identity: "".into(),
flag_no_persistent_txqueue: false,
// -- Account Options // -- Account Options
flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()), flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()),
@ -828,6 +832,7 @@ mod tests {
db_path: None, db_path: None,
keys_path: None, keys_path: None,
identity: None, identity: None,
no_persistent_txqueue: None,
}), }),
account: Some(Account { account: Some(Account {
unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]), unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]),

View File

@ -280,6 +280,9 @@ Sealing/Mining Options:
execution time limit. Also number of offending actions execution time limit. Also number of offending actions
have to reach the threshold within that time. have to reach the threshold within that time.
(default: {flag_tx_queue_ban_time} seconds) (default: {flag_tx_queue_ban_time} seconds)
--no-persistent-txqueue Don't save pending local transactions to disk to be
restored whenever the node restarts.
(default: {flag_no_persistent_txqueue}).
--remove-solved Move solved blocks from the work package queue --remove-solved Move solved blocks from the work package queue
instead of cloning them. This gives a slightly instead of cloning them. This gives a slightly
faster import speed, but means that extra solutions faster import speed, but means that extra solutions

View File

@ -370,6 +370,7 @@ impl Configuration {
check_seal: !self.args.flag_no_seal_check, check_seal: !self.args.flag_no_seal_check,
download_old_blocks: !self.args.flag_no_ancient_blocks, download_old_blocks: !self.args.flag_no_ancient_blocks,
verifier_settings: verifier_settings, verifier_settings: verifier_settings,
no_persistent_txqueue: self.args.flag_no_persistent_txqueue,
}; };
Cmd::Run(run_cmd) Cmd::Run(run_cmd)
}; };
@ -1184,6 +1185,7 @@ mod tests {
check_seal: true, check_seal: true,
download_old_blocks: true, download_old_blocks: true,
verifier_settings: Default::default(), verifier_settings: Default::default(),
no_persistent_txqueue: false,
}; };
expected.secretstore_conf.enabled = cfg!(feature = "secretstore"); expected.secretstore_conf.enabled = cfg!(feature = "secretstore");
assert_eq!(conf.into_command().unwrap().cmd, Cmd::Run(expected)); assert_eq!(conf.into_command().unwrap().cmd, Cmd::Run(expected));

View File

@ -107,6 +107,7 @@ pub struct RunCmd {
pub check_seal: bool, pub check_seal: bool,
pub download_old_blocks: bool, pub download_old_blocks: bool,
pub verifier_settings: VerifierSettings, pub verifier_settings: VerifierSettings,
pub no_persistent_txqueue: bool,
} }
pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> { pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> {
@ -138,15 +139,20 @@ pub fn open_dapp(dapps_conf: &dapps::Configuration, dapp: &str) -> Result<(), St
// node info fetcher for the local store. // node info fetcher for the local store.
struct FullNodeInfo { struct FullNodeInfo {
miner: Arc<Miner>, // TODO: only TXQ needed, just use that after decoupling. miner: Option<Arc<Miner>>, // TODO: only TXQ needed, just use that after decoupling.
} }
impl ::local_store::NodeInfo for FullNodeInfo { impl ::local_store::NodeInfo for FullNodeInfo {
fn pending_transactions(&self) -> Vec<::ethcore::transaction::PendingTransaction> { fn pending_transactions(&self) -> Vec<::ethcore::transaction::PendingTransaction> {
let local_txs = self.miner.local_transactions(); let miner = match self.miner.as_ref() {
self.miner.pending_transactions() Some(m) => m,
None => return Vec::new(),
};
let local_txs = miner.local_transactions();
miner.pending_transactions()
.into_iter() .into_iter()
.chain(self.miner.future_transactions()) .chain(miner.future_transactions())
.filter(|tx| local_txs.contains_key(&tx.hash())) .filter(|tx| local_txs.contains_key(&tx.hash()))
.collect() .collect()
} }
@ -337,11 +343,22 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
let store = { let store = {
let db = service.db(); let db = service.db();
let node_info = FullNodeInfo { let node_info = FullNodeInfo {
miner: miner.clone(), miner: match cmd.no_persistent_txqueue {
true => None,
false => Some(miner.clone()),
}
}; };
let store = ::local_store::create(db, ::ethcore::db::COL_NODE_INFO, node_info); let store = ::local_store::create(db, ::ethcore::db::COL_NODE_INFO, node_info);
if cmd.no_persistent_txqueue {
info!("Running without a persistent transaction queue.");
if let Err(e) = store.clear() {
warn!("Error clearing persistent transaction queue: {}", e);
}
}
// re-queue pending transactions. // re-queue pending transactions.
match store.pending_transactions() { match store.pending_transactions() {
Ok(pending) => { Ok(pending) => {

View File

@ -28,7 +28,7 @@ use util::sha3;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::errors; use v1::helpers::errors;
use v1::traits::ParitySet; use v1::traits::ParitySet;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction};
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
pub struct ParitySetClient<F> { pub struct ParitySetClient<F> {
@ -135,4 +135,8 @@ impl<F: Fetch> ParitySet for ParitySetClient<F> {
fn execute_upgrade(&self) -> Result<bool, Error> { fn execute_upgrade(&self) -> Result<bool, Error> {
Err(errors::light_unimplemented(None)) Err(errors::light_unimplemented(None))
} }
fn remove_transaction(&self, _hash: H256) -> Result<Option<Transaction>, Error> {
Err(errors::light_unimplemented(None))
}
} }

View File

@ -30,15 +30,10 @@ use updater::{Service as UpdateService};
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::errors; use v1::helpers::errors;
use v1::traits::ParitySet; use v1::traits::ParitySet;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction};
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
pub struct ParitySetClient<C, M, U, F=fetch::Client> where pub struct ParitySetClient<C, M, U, F = fetch::Client> {
C: MiningBlockChainClient,
M: MinerService,
U: UpdateService,
F: Fetch,
{
client: Weak<C>, client: Weak<C>,
miner: Weak<M>, miner: Weak<M>,
updater: Weak<U>, updater: Weak<U>,
@ -46,12 +41,7 @@ pub struct ParitySetClient<C, M, U, F=fetch::Client> where
fetch: F, fetch: F,
} }
impl<C, M, U, F> ParitySetClient<C, M, U, F> where impl<C, M, U, F> ParitySetClient<C, M, U, F> {
C: MiningBlockChainClient,
M: MinerService,
U: UpdateService,
F: Fetch,
{
/// Creates new `ParitySetClient` with given `Fetch`. /// Creates new `ParitySetClient` with given `Fetch`.
pub fn new(client: &Arc<C>, miner: &Arc<M>, updater: &Arc<U>, net: &Arc<ManageNetwork>, fetch: F) -> Self { pub fn new(client: &Arc<C>, miner: &Arc<M>, updater: &Arc<U>, net: &Arc<ManageNetwork>, fetch: F) -> Self {
ParitySetClient { ParitySetClient {
@ -176,4 +166,12 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.execute_upgrade()) Ok(updater.execute_upgrade())
} }
fn remove_transaction(&self, hash: H256) -> Result<Option<Transaction>, Error> {
let miner = take_weak!(self.miner);
let client = take_weak!(self.client);
let hash = hash.into();
Ok(miner.remove_pending_transaction(&*client, &hash).map(Into::into))
}
} }

View File

@ -204,6 +204,10 @@ impl MinerService for TestMinerService {
self.pending_transactions.lock().get(hash).cloned().map(Into::into) self.pending_transactions.lock().get(hash).cloned().map(Into::into)
} }
fn remove_pending_transaction(&self, _chain: &MiningBlockChainClient, hash: &H256) -> Option<PendingTransaction> {
self.pending_transactions.lock().remove(hash).map(Into::into)
}
fn pending_transactions(&self) -> Vec<PendingTransaction> { fn pending_transactions(&self) -> Vec<PendingTransaction> {
self.pending_transactions.lock().values().cloned().map(Into::into).collect() self.pending_transactions.lock().values().cloned().map(Into::into).collect()
} }

View File

@ -204,3 +204,31 @@ fn rpc_parity_set_hash_content() {
assert_eq!(io.handle_request_sync(request), Some(response.to_owned())); assert_eq!(io.handle_request_sync(request), Some(response.to_owned()));
} }
#[test]
fn rpc_parity_remove_transaction() {
use ethcore::transaction::{Transaction, Action};
let miner = miner_service();
let client = client_service();
let network = network_service();
let updater = updater_service();
let mut io = IoHandler::new();
io.extend_with(parity_set_client(&client, &miner, &updater, &network).to_delegate());
let tx = Transaction {
nonce: 1.into(),
gas_price: 0x9184e72a000u64.into(),
gas: 0x76c0.into(),
action: Action::Call(5.into()),
value: 0x9184e72au64.into(),
data: vec![]
};
let signed = tx.fake_sign(2.into());
let hash = signed.hash();
let request = r#"{"jsonrpc": "2.0", "method": "parity_removeTransaction", "params":[""#.to_owned() + &format!("0x{:?}", hash) + r#""], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"condition":null,"creates":null,"from":"0x0000000000000000000000000000000000000002","gas":"0x76c0","gasPrice":"0x9184e72a000","hash":"0x0072c69d780cdfbfc02fed5c7d184151f9a166971d045e55e27695aaa5bcb55e","input":"0x","networkId":null,"nonce":"0x1","publicKey":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","r":"0x0","raw":"0xe9018609184e72a0008276c0940000000000000000000000000000000000000005849184e72a80808080","s":"0x0","standardV":"0x4","to":"0x0000000000000000000000000000000000000005","transactionIndex":null,"v":"0x0","value":"0x9184e72a"},"id":1}"#;
miner.pending_transactions.lock().insert(hash, signed);
assert_eq!(io.handle_request_sync(&request), Some(response.to_owned()));
}

View File

@ -19,7 +19,7 @@
use jsonrpc_core::Error; use jsonrpc_core::Error;
use futures::BoxFuture; use futures::BoxFuture;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction};
build_rpc_trait! { build_rpc_trait! {
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
@ -99,5 +99,14 @@ build_rpc_trait! {
/// Execute a release which is ready according to upgrade_ready(). /// Execute a release which is ready according to upgrade_ready().
#[rpc(name = "parity_executeUpgrade")] #[rpc(name = "parity_executeUpgrade")]
fn execute_upgrade(&self) -> Result<bool, Error>; fn execute_upgrade(&self) -> Result<bool, Error>;
/// Removes transaction from transaction queue.
/// Makes sense only for transactions that were not propagated to other peers yet
/// like scheduled transactions or transactions in future.
/// It might also work for some local transactions with to low gas price
/// or excessive gas limit that are not accepted by other peers whp.
/// Returns `true` when transaction was removed, `false` if it was not found.
#[rpc(name = "parity_removeTransaction")]
fn remove_transaction(&self, H256) -> Result<Option<Transaction>, Error>;
} }
} }