From 1aea9caf6d0cbf9b3f020d8ebd9df8587fbca4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 18 May 2017 15:19:29 +0200 Subject: [PATCH] [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. --- .gitignore | 3 + ethcore/src/engines/authority_round.rs | 2 +- ethcore/src/miner/miner.rs | 10 + ethcore/src/miner/mod.rs | 4 + js/package.json | 1 + js/scripts/build-rpc-markdown.js | 8 +- js/src/api/format/input.js | 33 +++ js/src/api/format/input.spec.js | 74 +++++- js/src/api/format/output.js | 6 - js/src/api/format/output.spec.js | 6 +- js/src/api/rpc/parity/parity.js | 27 ++- js/src/jsonrpc/interfaces/parity.js | 182 ++++++++++++++- js/src/jsonrpc/interfaces/signer.js | 20 +- js/src/jsonrpc/types.js | 6 +- js/src/ui/TxList/TxRow/txRow.js | 221 +++++++++++++++++- js/src/ui/TxList/store.js | 209 ++++++++--------- js/src/ui/TxList/store.spec.js | 23 +- js/src/ui/TxList/txList.css | 19 +- js/src/ui/TxList/txList.js | 22 +- .../containers/RequestsPage/requestsPage.js | 35 +-- js/src/views/Signer/store.js | 6 +- local-store/src/lib.rs | 18 +- parity/cli/config.full.toml | 1 + parity/cli/mod.rs | 5 + parity/cli/usage.txt | 3 + parity/configuration.rs | 2 + parity/run.rs | 27 ++- rpc/src/v1/impls/light/parity_set.rs | 6 +- rpc/src/v1/impls/parity_set.rs | 24 +- rpc/src/v1/tests/helpers/miner_service.rs | 4 + rpc/src/v1/tests/mocked/parity_set.rs | 28 +++ rpc/src/v1/traits/parity_set.rs | 11 +- 32 files changed, 831 insertions(+), 215 deletions(-) diff --git a/.gitignore b/.gitignore index f31145039..c36ae1c90 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ # mac stuff .DS_Store +# npm stuff +npm-debug.log + # gdb files .gdb_history diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 22fb622bb..8408d3b1e 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -351,7 +351,7 @@ impl Engine for AuthorityRound { if step == parent_step || (header.number() >= self.validate_step_transition && 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()))?; } diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index 6d4d216cf..c8f47029b 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -1008,6 +1008,16 @@ impl MinerService for Miner { } } + fn remove_pending_transaction(&self, chain: &MiningBlockChainClient, hash: &H256) -> Option { + 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 { self.from_pending_block( best_block, diff --git a/ethcore/src/miner/mod.rs b/ethcore/src/miner/mod.rs index 403aca760..a9e7a9a5d 100644 --- a/ethcore/src/miner/mod.rs +++ b/ethcore/src/miner/mod.rs @@ -150,6 +150,10 @@ pub trait MinerService : Send + Sync { /// Query pending transactions for hash. fn transaction(&self, best_block: BlockNumber, hash: &H256) -> Option; + /// 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; + /// Get a list of all pending transactions in the queue. fn pending_transactions(&self) -> Vec; diff --git a/js/package.json b/js/package.json index d78d14139..3650620da 100644 --- a/js/package.json +++ b/js/package.json @@ -150,6 +150,7 @@ "blockies": "0.0.2", "brace": "0.9.0", "bytes": "2.4.0", + "date-difference": "1.0.0", "debounce": "1.0.0", "es6-error": "4.0.0", "es6-promise": "4.0.5", diff --git a/js/scripts/build-rpc-markdown.js b/js/scripts/build-rpc-markdown.js index 2407cf208..7a78e1ce9 100644 --- a/js/scripts/build-rpc-markdown.js +++ b/js/scripts/build-rpc-markdown.js @@ -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}\``; } @@ -45,7 +49,7 @@ function formatDescription (obj, prefix = '', indent = '') { const optional = obj.optional ? '(optional) ' : ''; 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) { diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 34871c911..442c32aae 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -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) + }; + }); +} diff --git a/js/src/api/format/input.spec.js b/js/src/api/format/input.spec.js index 4b82bd1ef..eb74b7bd6 100644 --- a/js/src/api/format/input.spec.js +++ b/js/src/api/format/input.spec.js @@ -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' + } + ]); + }); + }); }); diff --git a/js/src/api/format/output.js b/js/src/api/format/output.js index 952002b60..b3f37d9e8 100644 --- a/js/src/api/format/output.js +++ b/js/src/api/format/output.js @@ -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': diff --git a/js/src/api/format/output.spec.js b/js/src/api/format/output.spec.js index c23751670..2afef7c6d 100644 --- a/js/src/api/format/output.spec.js +++ b/js/src/api/format/output.spec.js @@ -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', () => { diff --git a/js/src/api/rpc/parity/parity.js b/js/src/api/rpc/parity/parity.js index 31de948dc..183f3c0ae 100644 --- a/js/src/api/rpc/parity/parity.js +++ b/js/src/api/rpc/parity/parity.js @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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'); diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js index cf62213a5..1050b073e 100644 --- a/js/src/jsonrpc/interfaces/parity.js +++ b/js/src/jsonrpc/interfaces/parity.js @@ -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() + } } } ], diff --git a/js/src/jsonrpc/interfaces/signer.js b/js/src/jsonrpc/interfaces/signer.js index e38bad68f..495b8e0e8 100644 --- a/js/src/jsonrpc/interfaces/signer.js +++ b/js/src/jsonrpc/interfaces/signer.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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: { diff --git a/js/src/jsonrpc/types.js b/js/src/jsonrpc/types.js index 8803fdd5c..52e79019e 100644 --- a/js/src/jsonrpc/types.js +++ b/js/src/jsonrpc/types.js @@ -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 } } diff --git a/js/src/ui/TxList/TxRow/txRow.js b/js/src/ui/TxList/TxRow/txRow.js index ef30fc8d1..bad9ca9d2 100644 --- a/js/src/ui/TxList/TxRow/txRow.js +++ b/js/src/ui/TxList/TxRow/txRow.js @@ -15,6 +15,8 @@ // along with Parity. If not, see . 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 (
{ blockNumber && block ? moment(block.timestamp).fromNow() : null }
-
{ blockNumber ? _blockNumber.toFormat() : 'Pending' }
+
{ blockNumber ? _blockNumber.toFormat() : this.renderCancelToggle() }
); } + renderCancelToggle () { + const { canceled, editing, isCancelOpen, isEditOpen } = this.state; + + if (canceled) { + return ( +
+ +
+ ); + } + + if (editing) { + return ( +
+
+ +
+
+ ); + } + + if (!isCancelOpen && !isEditOpen) { + const pendingStatus = this.getCondition(); + + if (pendingStatus === 'submitting') { + return ( +
+
+
+ +
+
+ ); + } + return ( +
+ + { pendingStatus } + +
+ +
+ + + + {' | '} + + + +
+ ); + } + + let which; + + if (isCancelOpen) { + which = ( + + ); + } else { + which = ( + + ); + } + + return ( +
+
+
+ +
+ + { which } + + {' | '} + + + +
+ ); + } + + 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 ( + + ); + } 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 ( + + ); + } 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) { diff --git a/js/src/ui/TxList/store.js b/js/src/ui/TxList/store.js index 1e670e31d..99a081d57 100644 --- a/js/src/ui/TxList/store.js +++ b/js/src/ui/TxList/store.js @@ -14,150 +14,135 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 }); }); } } diff --git a/js/src/ui/TxList/store.spec.js b/js/src/ui/TxList/store.spec.js index 44685b3ba..501065b95 100644 --- a/js/src/ui/TxList/store.spec.js +++ b/js/src/ui/TxList/store.spec.js @@ -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']); - }); }); }); diff --git a/js/src/ui/TxList/txList.css b/js/src/ui/TxList/txList.css index a38ba14fd..0bfd898d2 100644 --- a/js/src/ui/TxList/txList.css +++ b/js/src/ui/TxList/txList.css @@ -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; + } } diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js index c2224903f..140217a23 100644 --- a/js/src/ui/TxList/txList.js +++ b/js/src/ui/TxList/txList.js @@ -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 ( ); }); diff --git a/js/src/views/Signer/containers/RequestsPage/requestsPage.js b/js/src/views/Signer/containers/RequestsPage/requestsPage.js index d90ed7693..5136467c7 100644 --- a/js/src/views/Signer/containers/RequestsPage/requestsPage.js +++ b/js/src/views/Signer/containers/RequestsPage/requestsPage.js @@ -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 { ); @@ -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( diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index 76e3522f8..e99333159 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -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); diff --git a/local-store/src/lib.rs b/local-store/src/lib.rs index f9e5fe385..11a5c8283 100644 --- a/local-store/src/lib.rs +++ b/local-store/src/lib.rs @@ -173,14 +173,26 @@ impl LocalDataStore { pub fn update(&self) -> Result<(), Error> { trace!(target: "local_store", "Updating local store entries."); - let mut batch = self.db.transaction(); - let local_entries: Vec = self.node.pending_transactions() .into_iter() .map(Into::into) .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); batch.put_vec(self.col, LOCAL_TRANSACTIONS_KEY, json_str.into_bytes()); diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 980457887..ed736a71b 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -6,6 +6,7 @@ auto_update = "none" release_track = "current" no_download = false no_consensus = false +no_persistent_txqueue = false chain = "homestead" base_path = "$HOME/.parity" diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index cb256f0b7..ac12137a2 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -93,6 +93,8 @@ usage! { 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_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 flag_unlock: Option = None, @@ -370,6 +372,7 @@ struct Operating { db_path: Option, keys_path: Option, identity: Option, + no_persistent_txqueue: Option, } #[derive(Default, Debug, PartialEq, RustcDecodable)] @@ -627,6 +630,7 @@ mod tests { flag_db_path: Some("$HOME/.parity/chains".into()), flag_keys_path: "$HOME/.parity/keys".into(), flag_identity: "".into(), + flag_no_persistent_txqueue: false, // -- Account Options flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()), @@ -828,6 +832,7 @@ mod tests { db_path: None, keys_path: None, identity: None, + no_persistent_txqueue: None, }), account: Some(Account { unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]), diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index da7a72db9..d3baeede1 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -280,6 +280,9 @@ Sealing/Mining Options: execution time limit. Also number of offending actions have to reach the threshold within that time. (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 instead of cloning them. This gives a slightly faster import speed, but means that extra solutions diff --git a/parity/configuration.rs b/parity/configuration.rs index c0756a771..866fb3a3a 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -370,6 +370,7 @@ impl Configuration { check_seal: !self.args.flag_no_seal_check, download_old_blocks: !self.args.flag_no_ancient_blocks, verifier_settings: verifier_settings, + no_persistent_txqueue: self.args.flag_no_persistent_txqueue, }; Cmd::Run(run_cmd) }; @@ -1184,6 +1185,7 @@ mod tests { check_seal: true, download_old_blocks: true, verifier_settings: Default::default(), + no_persistent_txqueue: false, }; expected.secretstore_conf.enabled = cfg!(feature = "secretstore"); assert_eq!(conf.into_command().unwrap().cmd, Cmd::Run(expected)); diff --git a/parity/run.rs b/parity/run.rs index b25ed3188..fde16e8a4 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -107,6 +107,7 @@ pub struct RunCmd { pub check_seal: bool, pub download_old_blocks: bool, pub verifier_settings: VerifierSettings, + pub no_persistent_txqueue: bool, } 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. struct FullNodeInfo { - miner: Arc, // TODO: only TXQ needed, just use that after decoupling. + miner: Option>, // TODO: only TXQ needed, just use that after decoupling. } impl ::local_store::NodeInfo for FullNodeInfo { fn pending_transactions(&self) -> Vec<::ethcore::transaction::PendingTransaction> { - let local_txs = self.miner.local_transactions(); - self.miner.pending_transactions() + let miner = match self.miner.as_ref() { + Some(m) => m, + None => return Vec::new(), + }; + + let local_txs = miner.local_transactions(); + miner.pending_transactions() .into_iter() - .chain(self.miner.future_transactions()) + .chain(miner.future_transactions()) .filter(|tx| local_txs.contains_key(&tx.hash())) .collect() } @@ -337,11 +343,22 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R let store = { let db = service.db(); 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); + 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. match store.pending_transactions() { Ok(pending) => { diff --git a/rpc/src/v1/impls/light/parity_set.rs b/rpc/src/v1/impls/light/parity_set.rs index 720af0dd9..6f329264d 100644 --- a/rpc/src/v1/impls/light/parity_set.rs +++ b/rpc/src/v1/impls/light/parity_set.rs @@ -28,7 +28,7 @@ use util::sha3; use jsonrpc_core::Error; use v1::helpers::errors; 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. pub struct ParitySetClient { @@ -135,4 +135,8 @@ impl ParitySet for ParitySetClient { fn execute_upgrade(&self) -> Result { Err(errors::light_unimplemented(None)) } + + fn remove_transaction(&self, _hash: H256) -> Result, Error> { + Err(errors::light_unimplemented(None)) + } } diff --git a/rpc/src/v1/impls/parity_set.rs b/rpc/src/v1/impls/parity_set.rs index a6f1129ba..002517162 100644 --- a/rpc/src/v1/impls/parity_set.rs +++ b/rpc/src/v1/impls/parity_set.rs @@ -30,15 +30,10 @@ use updater::{Service as UpdateService}; use jsonrpc_core::Error; use v1::helpers::errors; 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. -pub struct ParitySetClient where - C: MiningBlockChainClient, - M: MinerService, - U: UpdateService, - F: Fetch, -{ +pub struct ParitySetClient { client: Weak, miner: Weak, updater: Weak, @@ -46,12 +41,7 @@ pub struct ParitySetClient where fetch: F, } -impl ParitySetClient where - C: MiningBlockChainClient, - M: MinerService, - U: UpdateService, - F: Fetch, -{ +impl ParitySetClient { /// Creates new `ParitySetClient` with given `Fetch`. pub fn new(client: &Arc, miner: &Arc, updater: &Arc, net: &Arc, fetch: F) -> Self { ParitySetClient { @@ -176,4 +166,12 @@ impl ParitySet for ParitySetClient where let updater = take_weak!(self.updater); Ok(updater.execute_upgrade()) } + + fn remove_transaction(&self, hash: H256) -> Result, 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)) + } } diff --git a/rpc/src/v1/tests/helpers/miner_service.rs b/rpc/src/v1/tests/helpers/miner_service.rs index 01dd9edc7..d6d7c18cf 100644 --- a/rpc/src/v1/tests/helpers/miner_service.rs +++ b/rpc/src/v1/tests/helpers/miner_service.rs @@ -204,6 +204,10 @@ impl MinerService for TestMinerService { self.pending_transactions.lock().get(hash).cloned().map(Into::into) } + fn remove_pending_transaction(&self, _chain: &MiningBlockChainClient, hash: &H256) -> Option { + self.pending_transactions.lock().remove(hash).map(Into::into) + } + fn pending_transactions(&self) -> Vec { self.pending_transactions.lock().values().cloned().map(Into::into).collect() } diff --git a/rpc/src/v1/tests/mocked/parity_set.rs b/rpc/src/v1/tests/mocked/parity_set.rs index 337090499..65d69775a 100644 --- a/rpc/src/v1/tests/mocked/parity_set.rs +++ b/rpc/src/v1/tests/mocked/parity_set.rs @@ -204,3 +204,31 @@ fn rpc_parity_set_hash_content() { 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())); +} diff --git a/rpc/src/v1/traits/parity_set.rs b/rpc/src/v1/traits/parity_set.rs index cffcc787f..9d7ba9bba 100644 --- a/rpc/src/v1/traits/parity_set.rs +++ b/rpc/src/v1/traits/parity_set.rs @@ -19,7 +19,7 @@ use jsonrpc_core::Error; use futures::BoxFuture; -use v1::types::{Bytes, H160, H256, U256, ReleaseInfo}; +use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction}; build_rpc_trait! { /// 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(). #[rpc(name = "parity_executeUpgrade")] fn execute_upgrade(&self) -> Result; + + /// 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, Error>; } }