diff --git a/Cargo.lock b/Cargo.lock index 98cf273e1..b73c3e3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1265,7 +1265,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#f46188126257e03c775e76a3cea82b5f70549400" +source = "git+https://github.com/ethcore/js-precompiled.git#55741cc9850ad6dcbcb7e0a7ac16e805df85de9a" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index 29f28265f..f0e526fea 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -580,29 +580,29 @@ impl Miner { let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into(); let best_block_header: Header = ::rlp::decode(&chain.best_block_header()); transactions.into_iter() - .filter(|tx| match self.engine.verify_transaction_basic(tx, &best_block_header) { - Ok(()) => true, + .map(|tx| { + match self.engine.verify_transaction_basic(&tx, &best_block_header) { Err(e) => { debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e); - false - } - } - ) - .map(|tx| { - let origin = accounts.as_ref().and_then(|accounts| { - tx.sender().ok().and_then(|sender| match accounts.contains(&sender) { - true => Some(TransactionOrigin::Local), - false => None, - }) - }).unwrap_or(default_origin); - - match origin { - TransactionOrigin::Local | TransactionOrigin::RetractedBlock => { - transaction_queue.add(tx, origin, &fetch_account, &gas_required) + Err(e) + }, + Ok(()) => { + let origin = accounts.as_ref().and_then(|accounts| { + tx.sender().ok().and_then(|sender| match accounts.contains(&sender) { + true => Some(TransactionOrigin::Local), + false => None, + }) + }).unwrap_or(default_origin); + + match origin { + TransactionOrigin::Local | TransactionOrigin::RetractedBlock => { + transaction_queue.add(tx, origin, &fetch_account, &gas_required) + }, + TransactionOrigin::External => { + transaction_queue.add_with_banlist(tx, &fetch_account, &gas_required) + } + } }, - TransactionOrigin::External => { - transaction_queue.add_with_banlist(tx, &fetch_account, &gas_required) - } } }) .collect() diff --git a/ethcore/src/verification/queue/mod.rs b/ethcore/src/verification/queue/mod.rs index 99e09784d..686a1d093 100644 --- a/ethcore/src/verification/queue/mod.rs +++ b/ethcore/src/verification/queue/mod.rs @@ -35,6 +35,9 @@ pub mod kind; const MIN_MEM_LIMIT: usize = 16384; const MIN_QUEUE_LIMIT: usize = 512; +// maximum possible number of verification threads. +const MAX_VERIFIERS: usize = 8; + /// Type alias for block queue convenience. pub type BlockQueue = VerificationQueue; @@ -61,6 +64,37 @@ impl Default for Config { } } +struct VerifierHandle { + deleting: Arc, + sleep: Arc, + thread: JoinHandle<()>, +} + +impl VerifierHandle { + // signal to the verifier thread that it should sleep. + fn sleep(&self) { + self.sleep.store(true, AtomicOrdering::SeqCst); + } + + // signal to the verifier thread that it should wake up. + fn wake_up(&self) { + self.sleep.store(false, AtomicOrdering::SeqCst); + self.thread.thread().unpark(); + } + + // signal to the verifier thread that it should conclude its + // operations. + fn conclude(&self) { + self.wake_up(); + self.deleting.store(true, AtomicOrdering::Release); + } + + // join the verifier thread. + fn join(self) { + self.thread.join().expect("Verifier thread panicked"); + } +} + /// An item which is in the process of being verified. pub struct Verifying { hash: H256, @@ -97,11 +131,12 @@ pub struct VerificationQueue { engine: Arc, more_to_verify: Arc, verification: Arc>, - verifiers: Vec>, + verifiers: Mutex<(Vec, usize)>, deleting: Arc, ready_signal: Arc, empty: Arc, processing: RwLock>, + ticks_since_adjustment: AtomicUsize, max_queue_size: usize, max_mem_use: usize, } @@ -157,6 +192,7 @@ struct Verification { more_to_verify: SMutex<()>, empty: SMutex<()>, sizes: Sizes, + check_seal: bool, } impl VerificationQueue { @@ -173,7 +209,8 @@ impl VerificationQueue { unverified: AtomicUsize::new(0), verifying: AtomicUsize::new(0), verified: AtomicUsize::new(0), - } + }, + check_seal: check_seal, }); let more_to_verify = Arc::new(SCondvar::new()); let deleting = Arc::new(AtomicBool::new(false)); @@ -185,44 +222,82 @@ impl VerificationQueue { let empty = Arc::new(SCondvar::new()); let panic_handler = PanicHandler::new_in_arc(); - let mut verifiers: Vec> = Vec::new(); - let thread_count = max(::num_cpus::get(), 3) - 2; - for i in 0..thread_count { - let verification = verification.clone(); - let engine = engine.clone(); - let more_to_verify = more_to_verify.clone(); - let ready_signal = ready_signal.clone(); - let empty = empty.clone(); + let max_verifiers = min(::num_cpus::get(), MAX_VERIFIERS); + let default_amount = max(::num_cpus::get(), 3) - 2; + let mut verifiers = Vec::with_capacity(max_verifiers); + + debug!(target: "verification", "Allocating {} verifiers, {} initially active", max_verifiers, default_amount); + + for i in 0..max_verifiers { + debug!(target: "verification", "Adding verification thread #{}", i); + let deleting = deleting.clone(); let panic_handler = panic_handler.clone(); - verifiers.push( - thread::Builder::new() - .name(format!("Verifier #{}", i)) - .spawn(move || { - panic_handler.catch_panic(move || { - VerificationQueue::verify(verification, engine, more_to_verify, ready_signal, deleting, empty, check_seal) - }).unwrap() - }) - .expect("Error starting block verification thread") - ); + let verification = verification.clone(); + let engine = engine.clone(); + let wait = more_to_verify.clone(); + let ready = ready_signal.clone(); + let empty = empty.clone(); + + // enable only the first few verifiers. + let sleep = if i < default_amount { + Arc::new(AtomicBool::new(false)) + } else { + Arc::new(AtomicBool::new(true)) + }; + + verifiers.push(VerifierHandle { + deleting: deleting.clone(), + sleep: sleep.clone(), + thread: thread::Builder::new() + .name(format!("Verifier #{}", i)) + .spawn(move || { + panic_handler.catch_panic(move || { + VerificationQueue::verify(verification, engine, wait, ready, deleting, empty, sleep) + }).unwrap() + }) + .expect("Failed to create verifier thread.") + }); } + VerificationQueue { engine: engine, panic_handler: panic_handler, - ready_signal: ready_signal.clone(), - more_to_verify: more_to_verify.clone(), - verification: verification.clone(), - verifiers: verifiers, - deleting: deleting.clone(), + ready_signal: ready_signal, + more_to_verify: more_to_verify, + verification: verification, + verifiers: Mutex::new((verifiers, default_amount)), + deleting: deleting, processing: RwLock::new(HashSet::new()), - empty: empty.clone(), + empty: empty, + ticks_since_adjustment: AtomicUsize::new(0), max_queue_size: max(config.max_queue_size, MIN_QUEUE_LIMIT), max_mem_use: max(config.max_mem_use, MIN_MEM_LIMIT), } } - fn verify(verification: Arc>, engine: Arc, wait: Arc, ready: Arc, deleting: Arc, empty: Arc, check_seal: bool) { + fn verify( + verification: Arc>, + engine: Arc, + wait: Arc, + ready: Arc, + deleting: Arc, + empty: Arc, + sleep: Arc, + ) { while !deleting.load(AtomicOrdering::Acquire) { + { + while sleep.load(AtomicOrdering::SeqCst) { + trace!(target: "verification", "Verifier sleeping"); + ::std::thread::park(); + trace!(target: "verification", "Verifier waking up"); + + if deleting.load(AtomicOrdering::Acquire) { + return; + } + } + } + { let mut more_to_verify = verification.more_to_verify.lock().unwrap(); @@ -255,7 +330,7 @@ impl VerificationQueue { }; let hash = item.hash(); - let is_ready = match K::verify(item, &*engine, check_seal) { + let is_ready = match K::verify(item, &*engine, verification.check_seal) { Ok(verified) => { let mut verifying = verification.verifying.lock(); let mut idx = None; @@ -302,9 +377,15 @@ impl VerificationQueue { } } - fn drain_verifying(verifying: &mut VecDeque>, verified: &mut VecDeque, bad: &mut HashSet, sizes: &Sizes) { + fn drain_verifying( + verifying: &mut VecDeque>, + verified: &mut VecDeque, + bad: &mut HashSet, + sizes: &Sizes, + ) { let mut removed_size = 0; let mut inserted_size = 0; + while let Some(output) = verifying.front_mut().and_then(|x| x.output.take()) { assert!(verifying.pop_front().is_some()); let size = output.heap_size_of_children(); @@ -487,14 +568,85 @@ impl VerificationQueue { } } - /// Optimise memory footprint of the heap fields. + /// Optimise memory footprint of the heap fields, and adjust the number of threads + /// to better suit the workload. pub fn collect_garbage(&self) { - { - self.verification.unverified.lock().shrink_to_fit(); + // number of ticks to average queue stats over + // when deciding whether to change the number of verifiers. + #[cfg(not(test))] + const READJUSTMENT_PERIOD: usize = 12; + + #[cfg(test)] + const READJUSTMENT_PERIOD: usize = 1; + + let (u_len, v_len) = { + let u_len = { + let mut q = self.verification.unverified.lock(); + q.shrink_to_fit(); + q.len() + }; self.verification.verifying.lock().shrink_to_fit(); - self.verification.verified.lock().shrink_to_fit(); - } + + let v_len = { + let mut q = self.verification.verified.lock(); + q.shrink_to_fit(); + q.len() + }; + + (u_len as isize, v_len as isize) + }; + self.processing.write().shrink_to_fit(); + + if self.ticks_since_adjustment.fetch_add(1, AtomicOrdering::SeqCst) + 1 >= READJUSTMENT_PERIOD { + self.ticks_since_adjustment.store(0, AtomicOrdering::SeqCst); + } else { + return; + } + + let current = self.verifiers.lock().1; + + let diff = (v_len - u_len).abs(); + let total = v_len + u_len; + + self.scale_verifiers( + if u_len < 20 { + 1 + } else if diff <= total / 10 { + current + } else if v_len > u_len { + current - 1 + } else { + current + 1 + } + ); + } + + // wake up or sleep verifiers to get as close to the target as + // possible, never going over the amount of initially allocated threads + // or below 1. + fn scale_verifiers(&self, target: usize) { + let mut verifiers = self.verifiers.lock(); + let &mut (ref mut verifiers, ref mut verifier_count) = &mut *verifiers; + + let target = min(verifiers.len(), target); + let target = max(1, target); + + debug!(target: "verification", "Scaling from {} to {} verifiers", verifier_count, target); + + // scaling up + for i in *verifier_count..target { + debug!(target: "verification", "Waking up verifier {}", i); + verifiers[i].wake_up(); + } + + // scaling down. + for i in target..*verifier_count { + debug!(target: "verification", "Putting verifier {} to sleep", i); + verifiers[i].sleep(); + } + + *verifier_count = target; } } @@ -509,10 +661,23 @@ impl Drop for VerificationQueue { trace!(target: "shutdown", "[VerificationQueue] Closing..."); self.clear(); self.deleting.store(true, AtomicOrdering::Release); - self.more_to_verify.notify_all(); - for t in self.verifiers.drain(..) { - t.join().unwrap(); + + let mut verifiers = self.verifiers.get_mut(); + let mut verifiers = &mut verifiers.0; + + // first pass to signal conclusion. must be done before + // notify or deadlock possible. + for handle in verifiers.iter() { + handle.conclude(); } + + self.more_to_verify.notify_all(); + + // second pass to join. + for handle in verifiers.drain(..) { + handle.join(); + } + trace!(target: "shutdown", "[VerificationQueue] Closed."); } } @@ -611,4 +776,56 @@ mod tests { } assert!(queue.queue_info().is_full()); } + + #[test] + fn scaling_limits() { + use super::MAX_VERIFIERS; + + let queue = get_test_queue(); + queue.scale_verifiers(MAX_VERIFIERS + 1); + + assert!(queue.verifiers.lock().1 < MAX_VERIFIERS + 1); + + queue.scale_verifiers(0); + + assert!(queue.verifiers.lock().1 == 1); + } + + #[test] + fn readjust_verifiers() { + let queue = get_test_queue(); + + // put all the verifiers to sleep to ensure + // the test isn't timing sensitive. + let num_verifiers = { + let verifiers = queue.verifiers.lock(); + for i in 0..verifiers.1 { + verifiers.0[i].sleep(); + } + + verifiers.1 + }; + + for block in get_good_dummy_block_seq(5000) { + queue.import(Unverified::new(block)).expect("Block good by definition; qed"); + } + + // almost all unverified == bump verifier count. + queue.collect_garbage(); + assert_eq!(queue.verifiers.lock().1, num_verifiers + 1); + + // wake them up again and verify everything. + { + let verifiers = queue.verifiers.lock(); + for i in 0..verifiers.1 { + verifiers.0[i].wake_up(); + } + } + + queue.flush(); + + // nothing to verify == use minimum number of verifiers. + queue.collect_garbage(); + assert_eq!(queue.verifiers.lock().1, 1); + } } diff --git a/js/package.json b/js/package.json index 62a09e1d1..c762db3de 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.61", + "version": "0.2.67", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -102,6 +102,7 @@ "postcss-nested": "^1.0.0", "postcss-simple-vars": "^3.0.0", "raw-loader": "^0.5.1", + "react-addons-perf": "~15.3.2", "react-addons-test-utils": "~15.3.2", "react-copy-to-clipboard": "^4.2.3", "react-dom": "~15.3.2", diff --git a/js/src/3rdparty/shapeshift/shapeshift.js b/js/src/3rdparty/shapeshift/shapeshift.js index 344a44802..8f388d0a7 100644 --- a/js/src/3rdparty/shapeshift/shapeshift.js +++ b/js/src/3rdparty/shapeshift/shapeshift.js @@ -15,7 +15,8 @@ // along with Parity. If not, see . export default function (rpc) { - const subscriptions = []; + let subscriptions = []; + let pollStatusIntervalId = null; function getCoins () { return rpc.get('getcoins'); @@ -45,6 +46,24 @@ export default function (rpc) { callback, idx }); + + // Only poll if there are subscriptions... + if (!pollStatusIntervalId) { + pollStatusIntervalId = setInterval(_pollStatus, 2000); + } + } + + function unsubscribe (depositAddress) { + const newSubscriptions = [] + .concat(subscriptions) + .filter((sub) => sub.depositAddress !== depositAddress); + + subscriptions = newSubscriptions; + + if (subscriptions.length === 0) { + clearInterval(pollStatusIntervalId); + pollStatusIntervalId = null; + } } function _getSubscriptionStatus (subscription) { @@ -81,13 +100,12 @@ export default function (rpc) { subscriptions.forEach(_getSubscriptionStatus); } - setInterval(_pollStatus, 2000); - return { getCoins, getMarketInfo, getStatus, shift, - subscribe + subscribe, + unsubscribe }; } diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 06afb0d9d..6fe497551 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -48,7 +48,11 @@ export default class Contract { this._instance[fn.signature] = fn; }); - this._sendSubscriptionChanges(); + this._subscribedToPendings = false; + this._pendingsSubscriptionId = null; + + this._subscribedToBlock = false; + this._blockSubscriptionId = null; } get address () { @@ -239,44 +243,71 @@ export default class Contract { return event; } - subscribe (eventName = null, options = {}, callback) { - return new Promise((resolve, reject) => { - let event = null; + _findEvent (eventName = null) { + const event = eventName + ? this._events.find((evt) => evt.name === eventName) + : null; - if (eventName) { - event = this._events.find((evt) => evt.name === eventName); + if (eventName && !event) { + const events = this._events.map((evt) => evt.name).join(', '); + throw new Error(`${eventName} is not a valid eventName, subscribe using one of ${events} (or null to include all)`); + } - if (!event) { - const events = this._events.map((evt) => evt.name).join(', '); - reject(new Error(`${eventName} is not a valid eventName, subscribe using one of ${events} (or null to include all)`)); - return; - } - } + return event; + } - return this._subscribe(event, options, callback).then(resolve).catch(reject); + _createEthFilter (event = null, _options) { + const optionTopics = _options.topics || []; + const signature = event && event.signature || null; + + // If event provided, remove the potential event signature + // as the first element of the topics + const topics = signature + ? [ signature ].concat(optionTopics.filter((t, idx) => idx > 0 || t !== signature)) + : optionTopics; + + const options = Object.assign({}, _options, { + address: this._address, + topics }); + + return this._api.eth.newFilter(options); + } + + subscribe (eventName = null, options = {}, callback) { + try { + const event = this._findEvent(eventName); + return this._subscribe(event, options, callback); + } catch (e) { + return Promise.reject(e); + } } _subscribe (event = null, _options, callback) { const subscriptionId = nextSubscriptionId++; - const options = Object.assign({}, _options, { - address: this._address, - topics: [event ? event.signature : null] - }); + const { skipInitFetch } = _options; + delete _options['skipInitFetch']; - return this._api.eth - .newFilter(options) + return this + ._createEthFilter(event, _options) .then((filterId) => { + this._subscriptions[subscriptionId] = { + options: _options, + callback, + filterId + }; + + if (skipInitFetch) { + this._subscribeToChanges(); + return subscriptionId; + } + return this._api.eth .getFilterLogs(filterId) .then((logs) => { callback(null, this.parseEventLogs(logs)); - this._subscriptions[subscriptionId] = { - options, - callback, - filterId - }; + this._subscribeToChanges(); return subscriptionId; }); }); @@ -285,19 +316,89 @@ export default class Contract { unsubscribe (subscriptionId) { return this._api.eth .uninstallFilter(this._subscriptions[subscriptionId].filterId) - .then(() => { - delete this._subscriptions[subscriptionId]; - }) .catch((error) => { console.error('unsubscribe', error); + }) + .then(() => { + delete this._subscriptions[subscriptionId]; + this._unsubscribeFromChanges(); }); } - _sendSubscriptionChanges = () => { + _subscribeToChanges = () => { const subscriptions = Object.values(this._subscriptions); - const timeout = () => setTimeout(this._sendSubscriptionChanges, 1000); - Promise + const pendingSubscriptions = subscriptions + .filter((s) => s.options.toBlock && s.options.toBlock === 'pending'); + + const otherSubscriptions = subscriptions + .filter((s) => !(s.options.toBlock && s.options.toBlock === 'pending')); + + if (pendingSubscriptions.length > 0 && !this._subscribedToPendings) { + this._subscribedToPendings = true; + this._subscribeToPendings(); + } + + if (otherSubscriptions.length > 0 && !this._subscribedToBlock) { + this._subscribedToBlock = true; + this._subscribeToBlock(); + } + } + + _unsubscribeFromChanges = () => { + const subscriptions = Object.values(this._subscriptions); + + const pendingSubscriptions = subscriptions + .filter((s) => s.options.toBlock && s.options.toBlock === 'pending'); + + const otherSubscriptions = subscriptions + .filter((s) => !(s.options.toBlock && s.options.toBlock === 'pending')); + + if (pendingSubscriptions.length === 0 && this._subscribedToPendings) { + this._subscribedToPendings = false; + clearTimeout(this._pendingsSubscriptionId); + } + + if (otherSubscriptions.length === 0 && this._subscribedToBlock) { + this._subscribedToBlock = false; + this._api.unsubscribe(this._blockSubscriptionId); + } + } + + _subscribeToBlock = () => { + this._api + .subscribe('eth_blockNumber', (error) => { + if (error) { + console.error('::_subscribeToBlock', error, error && error.stack); + } + + const subscriptions = Object.values(this._subscriptions) + .filter((s) => !(s.options.toBlock && s.options.toBlock === 'pending')); + + this._sendSubscriptionChanges(subscriptions); + }) + .then((blockSubId) => { + this._blockSubscriptionId = blockSubId; + }) + .catch((e) => { + console.error('::_subscribeToBlock', e, e && e.stack); + }); + } + + _subscribeToPendings = () => { + const subscriptions = Object.values(this._subscriptions) + .filter((s) => s.options.toBlock && s.options.toBlock === 'pending'); + + const timeout = () => setTimeout(() => this._subscribeToPendings(), 1000); + + this._sendSubscriptionChanges(subscriptions) + .then(() => { + this._pendingsSubscriptionId = timeout(); + }); + } + + _sendSubscriptionChanges = (subscriptions) => { + return Promise .all( subscriptions.map((subscription) => { return this._api.eth.getFilterChanges(subscription.filterId); @@ -315,12 +416,9 @@ export default class Contract { console.error('_sendSubscriptionChanges', error); } }); - - timeout(); }) .catch((error) => { console.error('_sendSubscriptionChanges', error); - timeout(); }); } } diff --git a/js/src/api/contract/contract.spec.js b/js/src/api/contract/contract.spec.js index 9c08024a9..970dd606d 100644 --- a/js/src/api/contract/contract.spec.js +++ b/js/src/api/contract/contract.spec.js @@ -437,6 +437,7 @@ describe('api/contract/Contract', () => { ] } ]; + const logs = [{ address: '0x22bff18ec62281850546a664bb63a5c06ac5f76c', blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b', @@ -450,6 +451,7 @@ describe('api/contract/Contract', () => { transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917', transactionIndex: '0x0' }]; + const parsed = [{ address: '0x22bfF18ec62281850546a664bb63a5C06AC5F76C', blockHash: '0xa9280530a3b47bee2fc80f2862fd56502ae075350571d724d6442ea4c597347b', @@ -466,11 +468,13 @@ describe('api/contract/Contract', () => { sender: { type: 'address', value: '0x63Cf90D3f0410092FC0fca41846f596223979195' } }, topics: [ - '0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5', '0x0000000000000000000000000000000000000000000000000001000000004fe0' + '0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5', + '0x0000000000000000000000000000000000000000000000000001000000004fe0' ], transactionHash: '0xca16f537d761d13e4e80953b754e2b15541f267d6cad9381f750af1bae1e4917', transactionIndex: new BigNumber(0) }]; + let contract; beforeEach(() => { @@ -496,18 +500,19 @@ describe('api/contract/Contract', () => { scope = mockHttp([ { method: 'eth_newFilter', reply: { result: '0x123' } }, { method: 'eth_getFilterLogs', reply: { result: logs } }, + { method: 'eth_getFilterChanges', reply: { result: logs } }, { method: 'eth_newFilter', reply: { result: '0x123' } }, { method: 'eth_getFilterLogs', reply: { result: logs } } ]); cbb = sinon.stub(); cbe = sinon.stub(); - return contract.subscribe('Message', {}, cbb); + return contract.subscribe('Message', { toBlock: 'pending' }, cbb); }); it('sets the subscriptionId returned', () => { return contract - .subscribe('Message', {}, cbe) + .subscribe('Message', { toBlock: 'pending' }, cbe) .then((subscriptionId) => { expect(subscriptionId).to.equal(1); }); @@ -515,7 +520,7 @@ describe('api/contract/Contract', () => { it('creates a new filter and retrieves the logs on it', () => { return contract - .subscribe('Message', {}, cbe) + .subscribe('Message', { toBlock: 'pending' }, cbe) .then((subscriptionId) => { expect(scope.isDone()).to.be.true; }); @@ -523,7 +528,7 @@ describe('api/contract/Contract', () => { it('returns the logs to the callback', () => { return contract - .subscribe('Message', {}, cbe) + .subscribe('Message', { toBlock: 'pending' }, cbe) .then((subscriptionId) => { expect(cbe).to.have.been.calledWith(null, parsed); }); diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 4cd1c8a56..55c85e4f3 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import BigNumber from 'bignumber.js'; +import { range } from 'lodash'; import { isArray, isHex, isInstanceOf, isString } from '../util/types'; @@ -50,14 +51,19 @@ export function inHash (hash) { return inHex(hash); } +export function pad (input, length) { + const value = inHex(input).substr(2, length * 2); + return '0x' + value + range(length * 2 - value.length).map(() => '0').join(''); +} + export function inTopics (_topics) { let topics = (_topics || []) - .filter((topic) => topic) - .map(inHex); + .filter((topic) => topic === null || topic) + .map((topic) => topic === null ? null : pad(topic, 32)); - while (topics.length < 4) { - topics.push(null); - } + // while (topics.length < 4) { + // topics.push(null); + // } return topics; } diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js index 198e456ee..93f31a161 100644 --- a/js/src/api/util/format.js +++ b/js/src/api/util/format.js @@ -29,3 +29,7 @@ export function hex2Ascii (_hex) { return str; } + +export function asciiToHex (string) { + return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join(''); +} diff --git a/js/src/api/util/index.js b/js/src/api/util/index.js index 55cf008c5..2058cd011 100644 --- a/js/src/api/util/index.js +++ b/js/src/api/util/index.js @@ -16,7 +16,7 @@ import { isAddress as isAddressValid, toChecksumAddress } from '../../abi/util/address'; import { decodeCallData, decodeMethodInput, methodToAbi } from './decode'; -import { bytesToHex, hex2Ascii } from './format'; +import { bytesToHex, hex2Ascii, asciiToHex } from './format'; import { fromWei, toWei } from './wei'; import { sha3 } from './sha3'; import { isArray, isFunction, isHex, isInstanceOf, isString } from './types'; @@ -31,6 +31,7 @@ export default { isString, bytesToHex, hex2Ascii, + asciiToHex, createIdentityImg, decodeCallData, decodeMethodInput, diff --git a/js/src/contracts/sms-verification.js b/js/src/contracts/sms-verification.js index c6893e639..2d32556ea 100644 --- a/js/src/contracts/sms-verification.js +++ b/js/src/contracts/sms-verification.js @@ -19,17 +19,34 @@ export const checkIfVerified = (contract, account) => { }; export const checkIfRequested = (contract, account) => { + let subId = null; + let resolved = false; + return new Promise((resolve, reject) => { - contract.subscribe('Requested', { - fromBlock: 0, toBlock: 'pending' - }, (err, logs) => { - if (err) { - return reject(err); - } - const e = logs.find((l) => { - return l.type === 'mined' && l.params.who && l.params.who.value === account; + contract + .subscribe('Requested', { + fromBlock: 0, toBlock: 'pending' + }, (err, logs) => { + if (err) { + return reject(err); + } + const e = logs.find((l) => { + return l.type === 'mined' && l.params.who && l.params.who.value === account; + }); + + resolve(e ? e.transactionHash : false); + resolved = true; + + if (subId) { + contract.unsubscribe(subId); + } + }) + .then((_subId) => { + subId = _subId; + + if (resolved) { + contract.unsubscribe(subId); + } }); - resolve(e ? e.transactionHash : false); - }); }); }; diff --git a/js/src/dapps/dappreg.html b/js/src/dapps/dappreg.html new file mode 100644 index 000000000..89c95c472 --- /dev/null +++ b/js/src/dapps/dappreg.html @@ -0,0 +1,17 @@ + + + + + + + + Dapp Registry + + +
+ + + + + + diff --git a/js/src/dapps/dappreg.js b/js/src/dapps/dappreg.js new file mode 100644 index 000000000..243576a34 --- /dev/null +++ b/js/src/dapps/dappreg.js @@ -0,0 +1,35 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React from 'react'; +import ReactDOM from 'react-dom'; +import injectTapEventPlugin from 'react-tap-event-plugin'; +import { useStrict } from 'mobx'; + +injectTapEventPlugin(); +useStrict(true); + +import Application from './dappreg/Application'; + +import '../../assets/fonts/Roboto/font.css'; +import '../../assets/fonts/RobotoMono/font.css'; +import './style.css'; +import './dappreg.html'; + +ReactDOM.render( + , + document.querySelector('#container') +); diff --git a/js/src/dapps/dappreg/Application/application.css b/js/src/dapps/dappreg/Application/application.css new file mode 100644 index 000000000..f171d8127 --- /dev/null +++ b/js/src/dapps/dappreg/Application/application.css @@ -0,0 +1,58 @@ +/* Copyright 2015, 2016 Ethcore (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.body { + color: #333; + background: #eee; + padding: 4.5em 0; + text-align: center; +} + +.apps { + background: #fff; + border-radius: 0.5em; + margin: 0 auto; + max-width: 980px; + padding: 1.5em; + text-align: left; +} + +.footer { + font-size: 0.75em; + margin: 1em; + padding: 1.5em; + text-align: center; +} + +.header { + background: #44e; + border-radius: 0 0 0.25em 0.25em; + color: #fff; + left: 0; + padding: 1em; + position: fixed; + right: 0; + top: 0; + z-index: 25; +} + +.loading { + text-align: center; + padding-top: 5em; + font-size: 2em; + color: #999; +} diff --git a/js/src/dapps/dappreg/Application/application.js b/js/src/dapps/dappreg/Application/application.js new file mode 100644 index 000000000..b5e4d5a97 --- /dev/null +++ b/js/src/dapps/dappreg/Application/application.js @@ -0,0 +1,64 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; + +import DappsStore from '../dappsStore'; + +import ButtonBar from '../ButtonBar'; +import Dapp from '../Dapp'; +import ModalDelete from '../ModalDelete'; +import ModalRegister from '../ModalRegister'; +import ModalUpdate from '../ModalUpdate'; +import SelectDapp from '../SelectDapp'; +import Warning from '../Warning'; +import styles from './application.css'; + +@observer +export default class Application extends Component { + dappsStore = DappsStore.instance(); + + render () { + if (this.dappsStore.isLoading) { + return ( +
+ Loading application +
+ ); + } + + return ( +
+
+ DAPP REGISTRY, a global view of distributed applications available on the network. Putting the puzzle together. +
+
+ + + +
+
+ { this.dappsStore.count } applications registered, { this.dappsStore.ownedCount } owned by user +
+ + + + +
+ ); + } +} diff --git a/js/src/dapps/dappreg/Application/index.js b/js/src/dapps/dappreg/Application/index.js new file mode 100644 index 000000000..236578226 --- /dev/null +++ b/js/src/dapps/dappreg/Application/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './application'; diff --git a/js/src/dapps/dappreg/Button/button.css b/js/src/dapps/dappreg/Button/button.css new file mode 100644 index 000000000..a66736e46 --- /dev/null +++ b/js/src/dapps/dappreg/Button/button.css @@ -0,0 +1,38 @@ +/* Copyright 2015, 2016 Ethcore (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.button { + background: #44e; + border: none; + border-radius: 0.25em; + color: #fff; + cursor: pointer; + font-size: 1em; + margin: 1em 0.375em; + opacity: 0.85; + padding: 0.75em 2em; + + &[disabled] { + opacity: 0.5; + cursor: default; + background: #aaa; + } + + &[data-warning="true"] { + background: #e44; + } +} diff --git a/js/src/dapps/dappreg/Button/button.js b/js/src/dapps/dappreg/Button/button.js new file mode 100644 index 000000000..1f3b67b4c --- /dev/null +++ b/js/src/dapps/dappreg/Button/button.js @@ -0,0 +1,52 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +import styles from './button.css'; + +export default class Button extends Component { + static propTypes = { + className: PropTypes.string, + disabled: PropTypes.bool, + label: PropTypes.string.isRequired, + warning: PropTypes.bool, + onClick: PropTypes.func.isRequired + } + + render () { + const { className, disabled, label, warning } = this.props; + const classes = `${styles.button} ${className}`; + + return ( + + ); + } + + onClick = (event) => { + if (this.props.disabled) { + return; + } + + this.props.onClick(event); + } +} diff --git a/js/src/dapps/dappreg/Button/index.js b/js/src/dapps/dappreg/Button/index.js new file mode 100644 index 000000000..f69a65e3d --- /dev/null +++ b/js/src/dapps/dappreg/Button/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './button'; diff --git a/js/src/dapps/dappreg/ButtonBar/buttonBar.css b/js/src/dapps/dappreg/ButtonBar/buttonBar.css new file mode 100644 index 000000000..0aa84ee29 --- /dev/null +++ b/js/src/dapps/dappreg/ButtonBar/buttonBar.css @@ -0,0 +1,21 @@ +/* Copyright 2015, 2016 Ethcore (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.buttonbar { + text-align: center; + margin: 1em 0 0 0; +} diff --git a/js/src/dapps/dappreg/ButtonBar/buttonBar.js b/js/src/dapps/dappreg/ButtonBar/buttonBar.js new file mode 100644 index 000000000..289def0ea --- /dev/null +++ b/js/src/dapps/dappreg/ButtonBar/buttonBar.js @@ -0,0 +1,101 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; + +import DappsStore from '../dappsStore'; +import ModalStore from '../modalStore'; + +import Button from '../Button'; +import styles from './buttonBar.css'; + +@observer +export default class ButtonBar extends Component { + dappsStore = DappsStore.instance(); + modalStore = ModalStore.instance(); + + render () { + let buttons = []; + + if (this.dappsStore.isEditing || this.dappsStore.isNew) { + buttons = [ +