Merge master into jr-reverse-caching
This commit is contained in:
commit
ad3b89dc14
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1501,7 +1501,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-ui-precompiled"
|
name = "parity-ui-precompiled"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
source = "git+https://github.com/ethcore/js-precompiled.git#257b3ce8aaa6797507592200dc78b29b8a305c3f"
|
source = "git+https://github.com/ethcore/js-precompiled.git#fee5af5a5c915719dab3ae4507b88262bc79d7f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
@ -37,6 +37,7 @@ use service::ClientIoMessage;
|
|||||||
use transaction::SignedTransaction;
|
use transaction::SignedTransaction;
|
||||||
use env_info::EnvInfo;
|
use env_info::EnvInfo;
|
||||||
use builtin::Builtin;
|
use builtin::Builtin;
|
||||||
|
use state::CleanupMode;
|
||||||
|
|
||||||
/// `AuthorityRound` params.
|
/// `AuthorityRound` params.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -49,6 +50,8 @@ pub struct AuthorityRoundParams {
|
|||||||
pub authorities: Vec<Address>,
|
pub authorities: Vec<Address>,
|
||||||
/// Number of authorities.
|
/// Number of authorities.
|
||||||
pub authority_n: usize,
|
pub authority_n: usize,
|
||||||
|
/// Block reward.
|
||||||
|
pub block_reward: U256,
|
||||||
/// Starting step,
|
/// Starting step,
|
||||||
pub start_step: Option<u64>,
|
pub start_step: Option<u64>,
|
||||||
}
|
}
|
||||||
@ -60,6 +63,7 @@ impl From<ethjson::spec::AuthorityRoundParams> for AuthorityRoundParams {
|
|||||||
step_duration: Duration::from_secs(p.step_duration.into()),
|
step_duration: Duration::from_secs(p.step_duration.into()),
|
||||||
authority_n: p.authorities.len(),
|
authority_n: p.authorities.len(),
|
||||||
authorities: p.authorities.into_iter().map(Into::into).collect::<Vec<_>>(),
|
authorities: p.authorities.into_iter().map(Into::into).collect::<Vec<_>>(),
|
||||||
|
block_reward: p.block_reward.map_or_else(U256::zero, Into::into),
|
||||||
start_step: p.start_step.map(Into::into),
|
start_step: p.start_step.map(Into::into),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,6 +253,20 @@ impl Engine for AuthorityRound {
|
|||||||
Seal::None
|
Seal::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply the block reward on finalisation of the block.
|
||||||
|
fn on_close_block(&self, block: &mut ExecutedBlock) {
|
||||||
|
let reward = self.our_params.block_reward;
|
||||||
|
let fields = block.fields_mut();
|
||||||
|
|
||||||
|
// Bestow block reward
|
||||||
|
fields.state.add_balance(fields.header.author(), &reward, CleanupMode::NoEmpty);
|
||||||
|
|
||||||
|
// Commit state so that we can actually figure out the state root.
|
||||||
|
if let Err(e) = fields.state.commit() {
|
||||||
|
warn!("Encountered error on state commit: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check the number of seal fields.
|
/// Check the number of seal fields.
|
||||||
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
if header.seal().len() != self.seal_fields() {
|
if header.seal().len() != self.seal_fields() {
|
||||||
|
@ -45,6 +45,7 @@ use views::HeaderView;
|
|||||||
use evm::Schedule;
|
use evm::Schedule;
|
||||||
use io::{IoService, IoChannel};
|
use io::{IoService, IoChannel};
|
||||||
use service::ClientIoMessage;
|
use service::ClientIoMessage;
|
||||||
|
use state::CleanupMode;
|
||||||
use self::message::*;
|
use self::message::*;
|
||||||
use self::transition::TransitionHandler;
|
use self::transition::TransitionHandler;
|
||||||
use self::params::TendermintParams;
|
use self::params::TendermintParams;
|
||||||
@ -469,6 +470,20 @@ impl Engine for Tendermint {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply the block reward on finalisation of the block.
|
||||||
|
fn on_close_block(&self, block: &mut ExecutedBlock) {
|
||||||
|
let reward = self.our_params.block_reward;
|
||||||
|
let fields = block.fields_mut();
|
||||||
|
|
||||||
|
// Bestow block reward
|
||||||
|
fields.state.add_balance(fields.header.author(), &reward, CleanupMode::NoEmpty);
|
||||||
|
|
||||||
|
// Commit state so that we can actually figure out the state root.
|
||||||
|
if let Err(e) = fields.state.commit() {
|
||||||
|
warn!("Encountered error on state commit: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
let seal_length = header.seal().len();
|
let seal_length = header.seal().len();
|
||||||
if seal_length == self.seal_fields() {
|
if seal_length == self.seal_fields() {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use ethjson;
|
use ethjson;
|
||||||
use super::transition::TendermintTimeouts;
|
use super::transition::TendermintTimeouts;
|
||||||
use util::{Address, U256};
|
use util::{Address, Uint, U256};
|
||||||
use time::Duration;
|
use time::Duration;
|
||||||
|
|
||||||
/// `Tendermint` params.
|
/// `Tendermint` params.
|
||||||
@ -32,6 +32,8 @@ pub struct TendermintParams {
|
|||||||
pub authority_n: usize,
|
pub authority_n: usize,
|
||||||
/// Timeout durations for different steps.
|
/// Timeout durations for different steps.
|
||||||
pub timeouts: TendermintTimeouts,
|
pub timeouts: TendermintTimeouts,
|
||||||
|
/// Block reward.
|
||||||
|
pub block_reward: U256,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TendermintParams {
|
impl Default for TendermintParams {
|
||||||
@ -42,6 +44,7 @@ impl Default for TendermintParams {
|
|||||||
gas_limit_bound_divisor: 0x0400.into(),
|
gas_limit_bound_divisor: 0x0400.into(),
|
||||||
authorities: authorities,
|
authorities: authorities,
|
||||||
authority_n: val_n,
|
authority_n: val_n,
|
||||||
|
block_reward: U256::zero(),
|
||||||
timeouts: TendermintTimeouts::default(),
|
timeouts: TendermintTimeouts::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,6 +70,7 @@ impl From<ethjson::spec::TendermintParams> for TendermintParams {
|
|||||||
precommit: p.timeout_precommit.map_or(dt.precommit, to_duration),
|
precommit: p.timeout_precommit.map_or(dt.precommit, to_duration),
|
||||||
commit: p.timeout_commit.map_or(dt.commit, to_duration),
|
commit: p.timeout_commit.map_or(dt.commit, to_duration),
|
||||||
},
|
},
|
||||||
|
block_reward: p.block_reward.map_or_else(U256::zero, Into::into),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ fn test_trie(json: &[u8], trie: TrieSpec) -> Vec<String> {
|
|||||||
let key: Vec<u8> = key.into();
|
let key: Vec<u8> = key.into();
|
||||||
let value: Vec<u8> = value.map_or_else(Vec::new, Into::into);
|
let value: Vec<u8> = value.map_or_else(Vec::new, Into::into);
|
||||||
t.insert(&key, &value)
|
t.insert(&key, &value)
|
||||||
.expect(&format!("Trie test '{:?}' failed due to internal error", name))
|
.expect(&format!("Trie test '{:?}' failed due to internal error", name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if *t.root() != test.root.into() {
|
if *t.root() != test.root.into() {
|
||||||
|
@ -23,6 +23,7 @@ use std::cell::Cell;
|
|||||||
use transaction::{SignedTransaction, Action};
|
use transaction::{SignedTransaction, Action};
|
||||||
use transient_hashmap::TransientHashMap;
|
use transient_hashmap::TransientHashMap;
|
||||||
use miner::{TransactionQueue, TransactionImportResult, TransactionOrigin, AccountDetails};
|
use miner::{TransactionQueue, TransactionImportResult, TransactionOrigin, AccountDetails};
|
||||||
|
use miner::transaction_queue::QueuingInstant;
|
||||||
use error::{Error, TransactionError};
|
use error::{Error, TransactionError};
|
||||||
use util::{Uint, U256, H256, Address, Hashable};
|
use util::{Uint, U256, H256, Address, Hashable};
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ impl BanningTransactionQueue {
|
|||||||
pub fn add_with_banlist<F, G>(
|
pub fn add_with_banlist<F, G>(
|
||||||
&mut self,
|
&mut self,
|
||||||
transaction: SignedTransaction,
|
transaction: SignedTransaction,
|
||||||
|
time: QueuingInstant,
|
||||||
account_details: &F,
|
account_details: &F,
|
||||||
gas_estimator: &G,
|
gas_estimator: &G,
|
||||||
) -> Result<TransactionImportResult, Error> where
|
) -> Result<TransactionImportResult, Error> where
|
||||||
@ -115,7 +117,7 @@ impl BanningTransactionQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.queue.add(transaction, TransactionOrigin::External, None, account_details, gas_estimator)
|
self.queue.add(transaction, TransactionOrigin::External, time, None, account_details, gas_estimator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ban transaction with given hash.
|
/// Ban transaction with given hash.
|
||||||
@ -158,7 +160,7 @@ impl BanningTransactionQueue {
|
|||||||
Threshold::BanAfter(threshold) if count > threshold => {
|
Threshold::BanAfter(threshold) if count > threshold => {
|
||||||
// Banlist the sender.
|
// Banlist the sender.
|
||||||
// Remove all transactions from the queue.
|
// Remove all transactions from the queue.
|
||||||
self.remove_all(address, !U256::zero());
|
self.cull(address, !U256::zero());
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
_ => false
|
_ => false
|
||||||
@ -263,7 +265,7 @@ mod tests {
|
|||||||
let mut txq = queue();
|
let mut txq = queue();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.queue().add(tx, TransactionOrigin::External, None, &default_account_details, &gas_required).unwrap();
|
txq.queue().add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_required).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// should also deref to queue
|
// should also deref to queue
|
||||||
@ -279,12 +281,12 @@ mod tests {
|
|||||||
let banlist1 = txq.ban_sender(tx.sender().unwrap());
|
let banlist1 = txq.ban_sender(tx.sender().unwrap());
|
||||||
assert!(!banlist1, "Threshold not reached yet.");
|
assert!(!banlist1, "Threshold not reached yet.");
|
||||||
// Insert once
|
// Insert once
|
||||||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required).unwrap();
|
let import1 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required).unwrap();
|
||||||
assert_eq!(import1, TransactionImportResult::Current);
|
assert_eq!(import1, TransactionImportResult::Current);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let banlist2 = txq.ban_sender(tx.sender().unwrap());
|
let banlist2 = txq.ban_sender(tx.sender().unwrap());
|
||||||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required);
|
let import2 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(banlist2, "Threshold should be reached - banned.");
|
assert!(banlist2, "Threshold should be reached - banned.");
|
||||||
@ -303,12 +305,12 @@ mod tests {
|
|||||||
let banlist1 = txq.ban_recipient(recipient);
|
let banlist1 = txq.ban_recipient(recipient);
|
||||||
assert!(!banlist1, "Threshold not reached yet.");
|
assert!(!banlist1, "Threshold not reached yet.");
|
||||||
// Insert once
|
// Insert once
|
||||||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required).unwrap();
|
let import1 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required).unwrap();
|
||||||
assert_eq!(import1, TransactionImportResult::Current);
|
assert_eq!(import1, TransactionImportResult::Current);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let banlist2 = txq.ban_recipient(recipient);
|
let banlist2 = txq.ban_recipient(recipient);
|
||||||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required);
|
let import2 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(banlist2, "Threshold should be reached - banned.");
|
assert!(banlist2, "Threshold should be reached - banned.");
|
||||||
@ -325,12 +327,12 @@ mod tests {
|
|||||||
let banlist1 = txq.ban_codehash(codehash);
|
let banlist1 = txq.ban_codehash(codehash);
|
||||||
assert!(!banlist1, "Threshold not reached yet.");
|
assert!(!banlist1, "Threshold not reached yet.");
|
||||||
// Insert once
|
// Insert once
|
||||||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required).unwrap();
|
let import1 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required).unwrap();
|
||||||
assert_eq!(import1, TransactionImportResult::Current);
|
assert_eq!(import1, TransactionImportResult::Current);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let banlist2 = txq.ban_codehash(codehash);
|
let banlist2 = txq.ban_codehash(codehash);
|
||||||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details, &gas_required);
|
let import2 = txq.add_with_banlist(tx.clone(), 0, &default_account_details, &gas_required);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(banlist2, "Threshold should be reached - banned.");
|
assert!(banlist2, "Threshold should be reached - banned.");
|
||||||
|
@ -417,15 +417,12 @@ impl Miner {
|
|||||||
|
|
||||||
let block = open_block.close();
|
let block = open_block.close();
|
||||||
|
|
||||||
let fetch_account = |a: &Address| AccountDetails {
|
let fetch_nonce = |a: &Address| chain.latest_nonce(a);
|
||||||
nonce: chain.latest_nonce(a),
|
|
||||||
balance: chain.latest_balance(a),
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut queue = self.transaction_queue.lock();
|
let mut queue = self.transaction_queue.lock();
|
||||||
for hash in invalid_transactions {
|
for hash in invalid_transactions {
|
||||||
queue.remove_invalid(&hash, &fetch_account);
|
queue.remove_invalid(&hash, &fetch_nonce);
|
||||||
}
|
}
|
||||||
for hash in transactions_to_penalize {
|
for hash in transactions_to_penalize {
|
||||||
queue.penalize(&hash);
|
queue.penalize(&hash);
|
||||||
@ -597,6 +594,8 @@ impl Miner {
|
|||||||
let schedule = chain.latest_schedule();
|
let schedule = chain.latest_schedule();
|
||||||
let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into();
|
let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into();
|
||||||
let best_block_header = chain.best_block_header().decode();
|
let best_block_header = chain.best_block_header().decode();
|
||||||
|
let insertion_time = chain.chain_info().best_block_number;
|
||||||
|
|
||||||
transactions.into_iter()
|
transactions.into_iter()
|
||||||
.map(|tx| {
|
.map(|tx| {
|
||||||
if chain.transaction_block(TransactionId::Hash(tx.hash())).is_some() {
|
if chain.transaction_block(TransactionId::Hash(tx.hash())).is_some() {
|
||||||
@ -618,10 +617,10 @@ impl Miner {
|
|||||||
|
|
||||||
match origin {
|
match origin {
|
||||||
TransactionOrigin::Local | TransactionOrigin::RetractedBlock => {
|
TransactionOrigin::Local | TransactionOrigin::RetractedBlock => {
|
||||||
transaction_queue.add(tx, origin, min_block, &fetch_account, &gas_required)
|
transaction_queue.add(tx, origin, insertion_time, min_block, &fetch_account, &gas_required)
|
||||||
},
|
},
|
||||||
TransactionOrigin::External => {
|
TransactionOrigin::External => {
|
||||||
transaction_queue.add_with_banlist(tx, &fetch_account, &gas_required)
|
transaction_queue.add_with_banlist(tx, insertion_time, &fetch_account, &gas_required)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1141,8 +1140,13 @@ impl MinerService for Miner {
|
|||||||
|
|
||||||
// ...and at the end remove the old ones
|
// ...and at the end remove the old ones
|
||||||
{
|
{
|
||||||
|
let fetch_account = |a: &Address| AccountDetails {
|
||||||
|
nonce: chain.latest_nonce(a),
|
||||||
|
balance: chain.latest_balance(a),
|
||||||
|
};
|
||||||
|
let time = chain.chain_info().best_block_number;
|
||||||
let mut transaction_queue = self.transaction_queue.lock();
|
let mut transaction_queue = self.transaction_queue.lock();
|
||||||
transaction_queue.remove_old(|sender| chain.latest_nonce(sender));
|
transaction_queue.remove_old(&fetch_account, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
if enacted.len() > 0 {
|
if enacted.len() > 0 {
|
||||||
|
@ -51,8 +51,8 @@
|
|||||||
//! let gas_estimator = |_tx: &SignedTransaction| 2.into();
|
//! let gas_estimator = |_tx: &SignedTransaction| 2.into();
|
||||||
//!
|
//!
|
||||||
//! let mut txq = TransactionQueue::default();
|
//! let mut txq = TransactionQueue::default();
|
||||||
//! txq.add(st2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
//! txq.add(st2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
//! txq.add(st1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
//! txq.add(st1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
//!
|
//!
|
||||||
//! // Check status
|
//! // Check status
|
||||||
//! assert_eq!(txq.status().pending, 2);
|
//! assert_eq!(txq.status().pending, 2);
|
||||||
@ -64,7 +64,7 @@
|
|||||||
//!
|
//!
|
||||||
//! // And when transaction is removed (but nonce haven't changed)
|
//! // And when transaction is removed (but nonce haven't changed)
|
||||||
//! // it will move subsequent transactions to future
|
//! // it will move subsequent transactions to future
|
||||||
//! txq.remove_invalid(&st1.hash(), &default_account_details);
|
//! txq.remove_invalid(&st1.hash(), &|_| 10.into());
|
||||||
//! assert_eq!(txq.status().pending, 0);
|
//! assert_eq!(txq.status().pending, 0);
|
||||||
//! assert_eq!(txq.status().future, 1);
|
//! assert_eq!(txq.status().future, 1);
|
||||||
//! assert_eq!(txq.top_transactions().len(), 0);
|
//! assert_eq!(txq.top_transactions().len(), 0);
|
||||||
@ -78,11 +78,11 @@
|
|||||||
//! - When it's removed from `future` - all `future` transactions heights are recalculated and then
|
//! - When it's removed from `future` - all `future` transactions heights are recalculated and then
|
||||||
//! we check if the transactions should go to `current` (comparing state nonce)
|
//! we check if the transactions should go to `current` (comparing state nonce)
|
||||||
//! - When it's removed from `current` - all transactions from this sender (`current` & `future`) are recalculated.
|
//! - When it's removed from `current` - all transactions from this sender (`current` & `future`) are recalculated.
|
||||||
//! 3. `remove_all` is used to inform the queue about client (state) nonce changes.
|
//! 3. `cull` is used to inform the queue about client (state) nonce changes.
|
||||||
//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce
|
//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce
|
||||||
//! - It moves matching `future` transactions to `current`
|
//! - It moves matching `future` transactions to `current`
|
||||||
//! 4. `remove_old` is used as convenient method to update the state nonce for all senders in the queue.
|
//! 4. `remove_old` is used as convenient method to update the state nonce for all senders in the queue.
|
||||||
//! - Invokes `remove_all` with latest state nonce for all senders.
|
//! - Invokes `cull` with latest state nonce for all senders.
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
@ -258,16 +258,19 @@ struct VerifiedTransaction {
|
|||||||
transaction: SignedTransaction,
|
transaction: SignedTransaction,
|
||||||
/// Transaction origin.
|
/// Transaction origin.
|
||||||
origin: TransactionOrigin,
|
origin: TransactionOrigin,
|
||||||
|
/// Insertion time
|
||||||
|
insertion_time: QueuingInstant,
|
||||||
/// Delay until specifid block.
|
/// Delay until specifid block.
|
||||||
min_block: Option<BlockNumber>,
|
min_block: Option<BlockNumber>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerifiedTransaction {
|
impl VerifiedTransaction {
|
||||||
fn new(transaction: SignedTransaction, origin: TransactionOrigin, min_block: Option<BlockNumber>) -> Result<Self, Error> {
|
fn new(transaction: SignedTransaction, origin: TransactionOrigin, time: QueuingInstant, min_block: Option<BlockNumber>) -> Result<Self, Error> {
|
||||||
transaction.sender()?;
|
transaction.sender()?;
|
||||||
Ok(VerifiedTransaction {
|
Ok(VerifiedTransaction {
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
|
insertion_time: time,
|
||||||
min_block: min_block,
|
min_block: min_block,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -283,6 +286,10 @@ impl VerifiedTransaction {
|
|||||||
fn sender(&self) -> Address {
|
fn sender(&self) -> Address {
|
||||||
self.transaction.sender().expect("Sender is verified in new; qed")
|
self.transaction.sender().expect("Sender is verified in new; qed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cost(&self) -> U256 {
|
||||||
|
self.transaction.value + self.transaction.gas_price * self.transaction.gas
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@ -488,6 +495,10 @@ pub enum PrioritizationStrategy {
|
|||||||
GasFactorAndGasPrice,
|
GasFactorAndGasPrice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Point in time when transaction was inserted.
|
||||||
|
pub type QueuingInstant = BlockNumber;
|
||||||
|
const DEFAULT_QUEUING_PERIOD: BlockNumber = 128;
|
||||||
|
|
||||||
/// `TransactionQueue` implementation
|
/// `TransactionQueue` implementation
|
||||||
pub struct TransactionQueue {
|
pub struct TransactionQueue {
|
||||||
/// Prioritization strategy for this queue
|
/// Prioritization strategy for this queue
|
||||||
@ -498,6 +509,10 @@ pub struct TransactionQueue {
|
|||||||
tx_gas_limit: U256,
|
tx_gas_limit: U256,
|
||||||
/// Current gas limit (block gas limit * factor). Transactions above the limit will not be accepted (default to !0)
|
/// Current gas limit (block gas limit * factor). Transactions above the limit will not be accepted (default to !0)
|
||||||
gas_limit: U256,
|
gas_limit: U256,
|
||||||
|
/// Maximal time transaction may occupy the queue.
|
||||||
|
/// When we reach `max_time_in_queue / 2^3` we re-validate
|
||||||
|
/// account balance.
|
||||||
|
max_time_in_queue: QueuingInstant,
|
||||||
/// Priority queue for transactions that can go to block
|
/// Priority queue for transactions that can go to block
|
||||||
current: TransactionSet,
|
current: TransactionSet,
|
||||||
/// Priority queue for transactions that has been received but are not yet valid to go to block
|
/// Priority queue for transactions that has been received but are not yet valid to go to block
|
||||||
@ -545,6 +560,7 @@ impl TransactionQueue {
|
|||||||
minimal_gas_price: U256::zero(),
|
minimal_gas_price: U256::zero(),
|
||||||
tx_gas_limit: tx_gas_limit,
|
tx_gas_limit: tx_gas_limit,
|
||||||
gas_limit: !U256::zero(),
|
gas_limit: !U256::zero(),
|
||||||
|
max_time_in_queue: DEFAULT_QUEUING_PERIOD,
|
||||||
current: current,
|
current: current,
|
||||||
future: future,
|
future: future,
|
||||||
by_hash: HashMap::new(),
|
by_hash: HashMap::new(),
|
||||||
@ -624,6 +640,7 @@ impl TransactionQueue {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tx: SignedTransaction,
|
tx: SignedTransaction,
|
||||||
origin: TransactionOrigin,
|
origin: TransactionOrigin,
|
||||||
|
time: QueuingInstant,
|
||||||
min_block: Option<BlockNumber>,
|
min_block: Option<BlockNumber>,
|
||||||
fetch_account: &F,
|
fetch_account: &F,
|
||||||
gas_estimator: &G,
|
gas_estimator: &G,
|
||||||
@ -635,7 +652,7 @@ impl TransactionQueue {
|
|||||||
let hash = tx.hash();
|
let hash = tx.hash();
|
||||||
let cloned_tx = tx.clone();
|
let cloned_tx = tx.clone();
|
||||||
|
|
||||||
let result = self.add_internal(tx, origin, min_block, fetch_account, gas_estimator);
|
let result = self.add_internal(tx, origin, time, min_block, fetch_account, gas_estimator);
|
||||||
match result {
|
match result {
|
||||||
Ok(TransactionImportResult::Current) => {
|
Ok(TransactionImportResult::Current) => {
|
||||||
self.local_transactions.mark_pending(hash);
|
self.local_transactions.mark_pending(hash);
|
||||||
@ -656,7 +673,7 @@ impl TransactionQueue {
|
|||||||
}
|
}
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
self.add_internal(tx, origin, min_block, fetch_account, gas_estimator)
|
self.add_internal(tx, origin, time, min_block, fetch_account, gas_estimator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,6 +682,7 @@ impl TransactionQueue {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tx: SignedTransaction,
|
tx: SignedTransaction,
|
||||||
origin: TransactionOrigin,
|
origin: TransactionOrigin,
|
||||||
|
time: QueuingInstant,
|
||||||
min_block: Option<BlockNumber>,
|
min_block: Option<BlockNumber>,
|
||||||
fetch_account: &F,
|
fetch_account: &F,
|
||||||
gas_estimator: &G,
|
gas_estimator: &G,
|
||||||
@ -734,10 +752,10 @@ impl TransactionQueue {
|
|||||||
// Verify signature
|
// Verify signature
|
||||||
tx.check_low_s()?;
|
tx.check_low_s()?;
|
||||||
|
|
||||||
let vtx = VerifiedTransaction::new(tx, origin, min_block)?;
|
let vtx = VerifiedTransaction::new(tx, origin, time, min_block)?;
|
||||||
let client_account = fetch_account(&vtx.sender());
|
let client_account = fetch_account(&vtx.sender());
|
||||||
|
|
||||||
let cost = vtx.transaction.value + vtx.transaction.gas_price * vtx.transaction.gas;
|
let cost = vtx.cost();
|
||||||
if client_account.balance < cost {
|
if client_account.balance < cost {
|
||||||
trace!(target: "txqueue",
|
trace!(target: "txqueue",
|
||||||
"Dropping transaction without sufficient balance: {:?} ({} < {})",
|
"Dropping transaction without sufficient balance: {:?} ({} < {})",
|
||||||
@ -759,7 +777,7 @@ impl TransactionQueue {
|
|||||||
|
|
||||||
/// Removes all transactions from particular sender up to (excluding) given client (state) nonce.
|
/// Removes all transactions from particular sender up to (excluding) given client (state) nonce.
|
||||||
/// Client (State) Nonce = next valid nonce for this sender.
|
/// Client (State) Nonce = next valid nonce for this sender.
|
||||||
pub fn remove_all(&mut self, sender: Address, client_nonce: U256) {
|
pub fn cull(&mut self, sender: Address, client_nonce: U256) {
|
||||||
// Check if there is anything in current...
|
// Check if there is anything in current...
|
||||||
let should_check_in_current = self.current.by_address.row(&sender)
|
let should_check_in_current = self.current.by_address.row(&sender)
|
||||||
// If nonce == client_nonce nothing is changed
|
// If nonce == client_nonce nothing is changed
|
||||||
@ -775,11 +793,11 @@ impl TransactionQueue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.remove_all_internal(sender, client_nonce);
|
self.cull_internal(sender, client_nonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always updates future and moves transactions from current to future.
|
/// Always updates future and moves transactions from current to future.
|
||||||
fn remove_all_internal(&mut self, sender: Address, client_nonce: U256) {
|
fn cull_internal(&mut self, sender: Address, client_nonce: U256) {
|
||||||
// We will either move transaction to future or remove it completely
|
// We will either move transaction to future or remove it completely
|
||||||
// so there will be no transactions from this sender in current
|
// so there will be no transactions from this sender in current
|
||||||
self.last_nonces.remove(&sender);
|
self.last_nonces.remove(&sender);
|
||||||
@ -794,16 +812,45 @@ impl TransactionQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the current nonce for all transactions' senders in the queue and removes the old transactions.
|
/// Checks the current nonce for all transactions' senders in the queue and removes the old transactions.
|
||||||
pub fn remove_old<F>(&mut self, fetch_nonce: F) where
|
pub fn remove_old<F>(&mut self, fetch_account: &F, current_time: QueuingInstant) where
|
||||||
F: Fn(&Address) -> U256,
|
F: Fn(&Address) -> AccountDetails,
|
||||||
{
|
{
|
||||||
let senders = self.current.by_address.keys()
|
let senders = self.current.by_address.keys()
|
||||||
.chain(self.future.by_address.keys())
|
.chain(self.future.by_address.keys())
|
||||||
.cloned()
|
.map(|sender| (*sender, fetch_account(sender)))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
for sender in senders {
|
for (sender, details) in senders.iter() {
|
||||||
self.remove_all(sender, fetch_nonce(&sender));
|
self.cull(*sender, details.nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_time = self.max_time_in_queue;
|
||||||
|
let balance_check = max_time >> 3;
|
||||||
|
// Clear transactions occupying the queue too long
|
||||||
|
let invalid = self.by_hash.iter()
|
||||||
|
.map(|(hash, tx)| (hash, tx, current_time.saturating_sub(tx.insertion_time)))
|
||||||
|
.filter_map(|(hash, tx, time_diff)| {
|
||||||
|
if time_diff > max_time {
|
||||||
|
return Some(*hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if time_diff > balance_check {
|
||||||
|
return match senders.get(&tx.sender()) {
|
||||||
|
Some(details) if tx.cost() > details.balance => {
|
||||||
|
Some(*hash)
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let fetch_nonce = |a: &Address| senders.get(a)
|
||||||
|
.expect("We fetch details for all senders from both current and future")
|
||||||
|
.nonce;
|
||||||
|
for hash in invalid {
|
||||||
|
self.remove_invalid(&hash, &fetch_nonce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,8 +898,8 @@ impl TransactionQueue {
|
|||||||
/// so transactions left in queue are processed according to client nonce.
|
/// so transactions left in queue are processed according to client nonce.
|
||||||
///
|
///
|
||||||
/// If gap is introduced marks subsequent transactions as future
|
/// If gap is introduced marks subsequent transactions as future
|
||||||
pub fn remove_invalid<T>(&mut self, transaction_hash: &H256, fetch_account: &T)
|
pub fn remove_invalid<T>(&mut self, transaction_hash: &H256, fetch_nonce: &T)
|
||||||
where T: Fn(&Address) -> AccountDetails {
|
where T: Fn(&Address) -> U256 {
|
||||||
|
|
||||||
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
|
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
|
||||||
let transaction = self.by_hash.remove(transaction_hash);
|
let transaction = self.by_hash.remove(transaction_hash);
|
||||||
@ -864,7 +911,7 @@ impl TransactionQueue {
|
|||||||
let transaction = transaction.expect("None is tested in early-exit condition above; qed");
|
let transaction = transaction.expect("None is tested in early-exit condition above; qed");
|
||||||
let sender = transaction.sender();
|
let sender = transaction.sender();
|
||||||
let nonce = transaction.nonce();
|
let nonce = transaction.nonce();
|
||||||
let current_nonce = fetch_account(&sender).nonce;
|
let current_nonce = fetch_nonce(&sender);
|
||||||
|
|
||||||
trace!(target: "txqueue", "Removing invalid transaction: {:?}", transaction.hash());
|
trace!(target: "txqueue", "Removing invalid transaction: {:?}", transaction.hash());
|
||||||
|
|
||||||
@ -889,7 +936,7 @@ impl TransactionQueue {
|
|||||||
if order.is_some() {
|
if order.is_some() {
|
||||||
// This will keep consistency in queue
|
// This will keep consistency in queue
|
||||||
// Moves all to future and then promotes a batch from current:
|
// Moves all to future and then promotes a batch from current:
|
||||||
self.remove_all_internal(sender, current_nonce);
|
self.cull_internal(sender, current_nonce);
|
||||||
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
|
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1039,7 +1086,7 @@ impl TransactionQueue {
|
|||||||
|
|
||||||
/// Finds transaction in the queue by hash (if any)
|
/// Finds transaction in the queue by hash (if any)
|
||||||
pub fn find(&self, hash: &H256) -> Option<SignedTransaction> {
|
pub fn find(&self, hash: &H256) -> Option<SignedTransaction> {
|
||||||
match self.by_hash.get(hash) { Some(transaction_ref) => Some(transaction_ref.transaction.clone()), None => None }
|
self.by_hash.get(hash).map(|tx| tx.transaction.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes all elements (in any state) from the queue
|
/// Removes all elements (in any state) from the queue
|
||||||
@ -1388,14 +1435,14 @@ mod test {
|
|||||||
let (tx1, tx2) = new_tx_pair(123.into(), 1.into(), 1.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair(123.into(), 1.into(), 1.into(), 0.into());
|
||||||
let sender = tx1.sender().unwrap();
|
let sender = tx1.sender().unwrap();
|
||||||
let nonce = tx1.nonce;
|
let nonce = tx1.nonce;
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 2);
|
assert_eq!(txq.status().pending, 2);
|
||||||
assert_eq!(txq.last_nonce(&sender), Some(nonce + 1.into()));
|
assert_eq!(txq.last_nonce(&sender), Some(nonce + 1.into()));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let tx = new_tx(123.into(), 1.into());
|
let tx = new_tx(123.into(), 1.into());
|
||||||
let res = txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// No longer the case as we don't even consider a transaction that isn't above a full
|
// No longer the case as we don't even consider a transaction that isn't above a full
|
||||||
@ -1427,12 +1474,12 @@ mod test {
|
|||||||
gas_limit: !U256::zero(),
|
gas_limit: !U256::zero(),
|
||||||
};
|
};
|
||||||
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let tx1 = VerifiedTransaction::new(tx1, TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx1, TransactionOrigin::External, 0, None).unwrap();
|
||||||
let tx2 = VerifiedTransaction::new(tx2, TransactionOrigin::External, None).unwrap();
|
let tx2 = VerifiedTransaction::new(tx2, TransactionOrigin::External, 0, None).unwrap();
|
||||||
let mut by_hash = {
|
let mut by_hash = {
|
||||||
let mut x = HashMap::new();
|
let mut x = HashMap::new();
|
||||||
let tx1 = VerifiedTransaction::new(tx1.transaction.clone(), TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx1.transaction.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
let tx2 = VerifiedTransaction::new(tx2.transaction.clone(), TransactionOrigin::External, None).unwrap();
|
let tx2 = VerifiedTransaction::new(tx2.transaction.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
x.insert(tx1.hash(), tx1);
|
x.insert(tx1.hash(), tx1);
|
||||||
x.insert(tx2.hash(), tx2);
|
x.insert(tx2.hash(), tx2);
|
||||||
x
|
x
|
||||||
@ -1470,12 +1517,12 @@ mod test {
|
|||||||
// Create two transactions with same nonce
|
// Create two transactions with same nonce
|
||||||
// (same hash)
|
// (same hash)
|
||||||
let (tx1, tx2) = new_tx_pair_default(0.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(0.into(), 0.into());
|
||||||
let tx1 = VerifiedTransaction::new(tx1, TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx1, TransactionOrigin::External, 0, None).unwrap();
|
||||||
let tx2 = VerifiedTransaction::new(tx2, TransactionOrigin::External, None).unwrap();
|
let tx2 = VerifiedTransaction::new(tx2, TransactionOrigin::External, 0, None).unwrap();
|
||||||
let by_hash = {
|
let by_hash = {
|
||||||
let mut x = HashMap::new();
|
let mut x = HashMap::new();
|
||||||
let tx1 = VerifiedTransaction::new(tx1.transaction.clone(), TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx1.transaction.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
let tx2 = VerifiedTransaction::new(tx2.transaction.clone(), TransactionOrigin::External, None).unwrap();
|
let tx2 = VerifiedTransaction::new(tx2.transaction.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
x.insert(tx1.hash(), tx1);
|
x.insert(tx1.hash(), tx1);
|
||||||
x.insert(tx2.hash(), tx2);
|
x.insert(tx2.hash(), tx2);
|
||||||
x
|
x
|
||||||
@ -1517,10 +1564,10 @@ mod test {
|
|||||||
gas_limit: !U256::zero(),
|
gas_limit: !U256::zero(),
|
||||||
};
|
};
|
||||||
let tx = new_tx_default();
|
let tx = new_tx_default();
|
||||||
let tx1 = VerifiedTransaction::new(tx.clone(), TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
let order1 = TransactionOrder::for_transaction(&tx1, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
let order1 = TransactionOrder::for_transaction(&tx1, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
||||||
assert!(set.insert(tx1.sender(), tx1.nonce(), order1).is_none());
|
assert!(set.insert(tx1.sender(), tx1.nonce(), order1).is_none());
|
||||||
let tx2 = VerifiedTransaction::new(tx, TransactionOrigin::External, None).unwrap();
|
let tx2 = VerifiedTransaction::new(tx, TransactionOrigin::External, 0, None).unwrap();
|
||||||
let order2 = TransactionOrder::for_transaction(&tx2, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
let order2 = TransactionOrder::for_transaction(&tx2, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
||||||
assert!(set.insert(tx2.sender(), tx2.nonce(), order2).is_some());
|
assert!(set.insert(tx2.sender(), tx2.nonce(), order2).is_some());
|
||||||
}
|
}
|
||||||
@ -1537,7 +1584,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(set.gas_price_entry_limit(), 0.into());
|
assert_eq!(set.gas_price_entry_limit(), 0.into());
|
||||||
let tx = new_tx_default();
|
let tx = new_tx_default();
|
||||||
let tx1 = VerifiedTransaction::new(tx.clone(), TransactionOrigin::External, None).unwrap();
|
let tx1 = VerifiedTransaction::new(tx.clone(), TransactionOrigin::External, 0, None).unwrap();
|
||||||
let order1 = TransactionOrder::for_transaction(&tx1, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
let order1 = TransactionOrder::for_transaction(&tx1, 0.into(), 1.into(), PrioritizationStrategy::GasPriceOnly);
|
||||||
assert!(set.insert(tx1.sender(), tx1.nonce(), order1.clone()).is_none());
|
assert!(set.insert(tx1.sender(), tx1.nonce(), order1.clone()).is_none());
|
||||||
assert_eq!(set.gas_price_entry_limit(), 2.into());
|
assert_eq!(set.gas_price_entry_limit(), 2.into());
|
||||||
@ -1552,12 +1599,12 @@ mod test {
|
|||||||
!U256::zero() };
|
!U256::zero() };
|
||||||
|
|
||||||
// First insert one transaction to future
|
// First insert one transaction to future
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &prev_nonce, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator);
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Future);
|
assert_eq!(res.unwrap(), TransactionImportResult::Future);
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
|
|
||||||
// now import second transaction to current
|
// now import second transaction to current
|
||||||
let res = txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// and then there should be only one transaction in current (the one with higher gas_price)
|
// and then there should be only one transaction in current (the one with higher gas_price)
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1577,12 +1624,12 @@ mod test {
|
|||||||
!U256::zero() };
|
!U256::zero() };
|
||||||
|
|
||||||
// First insert one transaction to future
|
// First insert one transaction to future
|
||||||
let res = txq.add(tx.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator);
|
let res = txq.add(tx.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator);
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Future);
|
assert_eq!(res.unwrap(), TransactionImportResult::Future);
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
|
|
||||||
// now import second transaction to current
|
// now import second transaction to current
|
||||||
let res = txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1601,7 +1648,7 @@ mod test {
|
|||||||
let tx = new_tx_default();
|
let tx = new_tx_default();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1620,10 +1667,10 @@ mod test {
|
|||||||
txq.set_minimal_gas_price(15.into());
|
txq.set_minimal_gas_price(15.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res1 = txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res1 = txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res2 = txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res2 = txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res3 = txq.add(tx3, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res3 = txq.add(tx3, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res4 = txq.add(tx4, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res4 = txq.add(tx4, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1654,10 +1701,10 @@ mod test {
|
|||||||
txq.set_minimal_gas_price(15.into());
|
txq.set_minimal_gas_price(15.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res1 = txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res1 = txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res2 = txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res2 = txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res3 = txq.add(tx3, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res3 = txq.add(tx3, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res4 = txq.add(tx4, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res4 = txq.add(tx4, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1700,7 +1747,7 @@ mod test {
|
|||||||
txq.set_gas_limit(limit);
|
txq.set_gas_limit(limit);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(unwrap_tx_err(res), TransactionError::GasLimitExceeded {
|
assert_eq!(unwrap_tx_err(res), TransactionError::GasLimitExceeded {
|
||||||
@ -1724,7 +1771,7 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &account, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &account, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(unwrap_tx_err(res), TransactionError::InsufficientBalance {
|
assert_eq!(unwrap_tx_err(res), TransactionError::InsufficientBalance {
|
||||||
@ -1744,7 +1791,7 @@ mod test {
|
|||||||
txq.set_minimal_gas_price(tx.gas_price + U256::one());
|
txq.set_minimal_gas_price(tx.gas_price + U256::one());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(unwrap_tx_err(res), TransactionError::InsufficientGasPrice {
|
assert_eq!(unwrap_tx_err(res), TransactionError::InsufficientGasPrice {
|
||||||
@ -1764,7 +1811,7 @@ mod test {
|
|||||||
txq.set_minimal_gas_price(tx.gas_price + U256::one());
|
txq.set_minimal_gas_price(tx.gas_price + U256::one());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::Local, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||||
@ -1794,7 +1841,7 @@ mod test {
|
|||||||
rlp::decode(s.as_raw())
|
rlp::decode(s.as_raw())
|
||||||
};
|
};
|
||||||
// when
|
// when
|
||||||
let res = txq.add(stx, TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(stx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
@ -1808,8 +1855,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
@ -1828,9 +1875,9 @@ mod test {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
// first insert the one with higher gas price
|
// first insert the one with higher gas price
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
// then the one with lower gas price, but local
|
// then the one with lower gas price, but local
|
||||||
txq.add(tx.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
@ -1847,15 +1894,15 @@ mod test {
|
|||||||
// the second one has same nonce but higher `gas_price`
|
// the second one has same nonce but higher `gas_price`
|
||||||
let (_, tx0) = new_similar_tx_pair();
|
let (_, tx0) = new_similar_tx_pair();
|
||||||
|
|
||||||
txq.add(tx0.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx0.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
// the one with higher gas price is first
|
// the one with higher gas price is first
|
||||||
assert_eq!(txq.top_transactions()[0], tx0);
|
assert_eq!(txq.top_transactions()[0], tx0);
|
||||||
assert_eq!(txq.top_transactions()[1], tx1);
|
assert_eq!(txq.top_transactions()[1], tx1);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
// insert second as local
|
// insert second as local
|
||||||
txq.add(tx2.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// the order should be updated
|
// the order should be updated
|
||||||
@ -1874,9 +1921,9 @@ mod test {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
// first insert local one with higher gas price
|
// first insert local one with higher gas price
|
||||||
txq.add(tx2.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
// then the one with lower gas price, but from retracted block
|
// then the one with lower gas price, but from retracted block
|
||||||
txq.add(tx.clone(), TransactionOrigin::RetractedBlock, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::RetractedBlock, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
@ -1892,8 +1939,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
@ -1912,10 +1959,10 @@ mod test {
|
|||||||
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
||||||
|
|
||||||
// insert everything
|
// insert everything
|
||||||
txq.add(txa.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(txa.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
txq.add(txb.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(txb.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
|
|
||||||
assert_eq!(txq.status().future, 4);
|
assert_eq!(txq.status().future, 4);
|
||||||
|
|
||||||
@ -1940,10 +1987,10 @@ mod test {
|
|||||||
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
||||||
|
|
||||||
// insert everything
|
// insert everything
|
||||||
txq.add(txa.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(txa.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(txb.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(txb.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx1.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
assert_eq!(top[0], tx1);
|
assert_eq!(top[0], tx1);
|
||||||
@ -1973,10 +2020,10 @@ mod test {
|
|||||||
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
let (tx1, tx2) = new_tx_pair_with_gas_price_increment(3.into());
|
||||||
|
|
||||||
// insert everything
|
// insert everything
|
||||||
txq.add(txa.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(txa.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(txb.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(txb.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
let top = txq.top_transactions();
|
let top = txq.top_transactions();
|
||||||
assert_eq!(top[0], tx1);
|
assert_eq!(top[0], tx1);
|
||||||
@ -2005,8 +2052,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let top = txq.pending_hashes();
|
let top = txq.pending_hashes();
|
||||||
@ -2023,8 +2070,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(2.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(2.into(), 0.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res1 = txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res1, TransactionImportResult::Current);
|
assert_eq!(res1, TransactionImportResult::Current);
|
||||||
@ -2045,8 +2092,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res1 = txq.add(tx.clone(), TransactionOrigin::External, Some(1), &default_account_details, &gas_estimator).unwrap();
|
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, Some(1), &default_account_details, &gas_estimator).unwrap();
|
||||||
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res1, TransactionImportResult::Current);
|
assert_eq!(res1, TransactionImportResult::Current);
|
||||||
@ -2067,12 +2114,12 @@ mod test {
|
|||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
|
|
||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 2);
|
assert_eq!(txq.status().future, 2);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_all(tx.sender().unwrap(), next2_nonce);
|
txq.cull(tx.sender().unwrap(), next2_nonce);
|
||||||
// should remove both transactions since they are not valid
|
// should remove both transactions since they are not valid
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@ -2090,13 +2137,13 @@ mod test {
|
|||||||
let tx1 = new_unsigned_tx(124.into(), default_gas_val(), 1.into()).sign(secret, None);
|
let tx1 = new_unsigned_tx(124.into(), default_gas_val(), 1.into()).sign(secret, None);
|
||||||
let tx2 = new_unsigned_tx(125.into(), default_gas_val(), 1.into()).sign(secret, None);
|
let tx2 = new_unsigned_tx(125.into(), default_gas_val(), 1.into()).sign(secret, None);
|
||||||
|
|
||||||
txq.add(tx, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 1);
|
assert_eq!(txq.status().pending, 1);
|
||||||
txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2112,14 +2159,14 @@ mod test {
|
|||||||
// given
|
// given
|
||||||
let mut txq2 = TransactionQueue::default();
|
let mut txq2 = TransactionQueue::default();
|
||||||
let (tx, tx2) = new_tx_pair_default(3.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(3.into(), 0.into());
|
||||||
txq2.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq2.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq2.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq2.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq2.status().pending, 1);
|
assert_eq!(txq2.status().pending, 1);
|
||||||
assert_eq!(txq2.status().future, 1);
|
assert_eq!(txq2.status().future, 1);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq2.remove_all(tx.sender().unwrap(), tx.nonce + U256::one());
|
txq2.cull(tx.sender().unwrap(), tx.nonce + U256::one());
|
||||||
txq2.remove_all(tx2.sender().unwrap(), tx2.nonce + U256::one());
|
txq2.cull(tx2.sender().unwrap(), tx2.nonce + U256::one());
|
||||||
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@ -2134,14 +2181,14 @@ mod test {
|
|||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let tx3 = new_tx_default();
|
let tx3 = new_tx_default();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
txq.add(tx3.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 3);
|
assert_eq!(txq.status().pending, 3);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_invalid(&tx.hash(), &default_account_details);
|
txq.remove_invalid(&tx.hash(), &|_| default_nonce());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2156,8 +2203,8 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
|
||||||
// add
|
// add
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
assert_eq!(stats.pending, 2);
|
assert_eq!(stats.pending, 2);
|
||||||
|
|
||||||
@ -2176,11 +2223,11 @@ mod test {
|
|||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let sender = tx.sender().unwrap();
|
let sender = tx.sender().unwrap();
|
||||||
let nonce = tx.nonce;
|
let nonce = tx.nonce;
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 1);
|
assert_eq!(txq.status().pending, 1);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let t = txq.top_transactions();
|
let t = txq.top_transactions();
|
||||||
@ -2197,14 +2244,14 @@ mod test {
|
|||||||
txq.current.set_limit(10);
|
txq.current.set_limit(10);
|
||||||
let (tx1, tx2) = new_tx_pair_default(4.into(), 1.into());
|
let (tx1, tx2) = new_tx_pair_default(4.into(), 1.into());
|
||||||
let (tx3, tx4) = new_tx_pair_default(4.into(), 2.into());
|
let (tx3, tx4) = new_tx_pair_default(4.into(), 2.into());
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx3.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 2);
|
assert_eq!(txq.status().pending, 2);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
txq.add(tx4.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx4.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
@ -2215,11 +2262,11 @@ mod test {
|
|||||||
let mut txq = TransactionQueue::with_limits(PrioritizationStrategy::GasPriceOnly, 100, default_gas_val() * U256::from(2), !U256::zero());
|
let mut txq = TransactionQueue::with_limits(PrioritizationStrategy::GasPriceOnly, 100, default_gas_val() * U256::from(2), !U256::zero());
|
||||||
let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1));
|
let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1));
|
||||||
let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2));
|
let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2));
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx3.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
// limited by gas
|
// limited by gas
|
||||||
txq.add(tx4.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap_err();
|
txq.add(tx4.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap_err();
|
||||||
assert_eq!(txq.status().pending, 2);
|
assert_eq!(txq.status().pending, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2229,12 +2276,12 @@ mod test {
|
|||||||
let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1));
|
let (tx1, tx2) = new_tx_pair_default(U256::from(1), U256::from(1));
|
||||||
let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2));
|
let (tx3, tx4) = new_tx_pair_default(U256::from(1), U256::from(2));
|
||||||
let (tx5, _) = new_tx_pair_default(U256::from(1), U256::from(2));
|
let (tx5, _) = new_tx_pair_default(U256::from(1), U256::from(2));
|
||||||
txq.add(tx1.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
// Not accepted because of limit
|
// Not accepted because of limit
|
||||||
txq.add(tx5.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap_err();
|
txq.add(tx5.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap_err();
|
||||||
txq.add(tx3.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx4.clone(), TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx4.clone(), TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 4);
|
assert_eq!(txq.status().pending, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2246,7 +2293,7 @@ mod test {
|
|||||||
let fetch_last_nonce = |_a: &Address| AccountDetails { nonce: last_nonce, balance: !U256::zero() };
|
let fetch_last_nonce = |_a: &Address| AccountDetails { nonce: last_nonce, balance: !U256::zero() };
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx, TransactionOrigin::External, None, &fetch_last_nonce, &gas_estimator);
|
let res = txq.add(tx, TransactionOrigin::External, 0, None, &fetch_last_nonce, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(unwrap_tx_err(res), TransactionError::Old);
|
assert_eq!(unwrap_tx_err(res), TransactionError::Old);
|
||||||
@ -2262,12 +2309,12 @@ mod test {
|
|||||||
balance: !U256::zero() };
|
balance: !U256::zero() };
|
||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (_tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (_tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
assert_eq!(txq.status().pending, 0);
|
assert_eq!(txq.status().pending, 0);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx2.clone(), TransactionOrigin::External, None, &nonce, &gas_estimator);
|
let res = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &nonce, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(unwrap_tx_err(res), TransactionError::AlreadyImported);
|
assert_eq!(unwrap_tx_err(res), TransactionError::AlreadyImported);
|
||||||
@ -2281,15 +2328,15 @@ mod test {
|
|||||||
// given
|
// given
|
||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 2);
|
assert_eq!(txq.status().pending, 2);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_invalid(&tx1.hash(), &default_account_details);
|
txq.remove_invalid(&tx1.hash(), &|_| default_nonce());
|
||||||
assert_eq!(txq.status().pending, 0);
|
assert_eq!(txq.status().pending, 0);
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2303,15 +2350,15 @@ mod test {
|
|||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let tx3 = new_tx_default();
|
let tx3 = new_tx_default();
|
||||||
txq.add(tx2.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
txq.add(tx3.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx.clone(), TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx.clone(), TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().pending, 3);
|
assert_eq!(txq.status().pending, 3);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let sender = tx.sender().unwrap();
|
let sender = tx.sender().unwrap();
|
||||||
txq.remove_all(sender, default_nonce() + U256::one());
|
txq.cull(sender, default_nonce() + U256::one());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2333,8 +2380,8 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2361,10 +2408,10 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
txq.add(tx0, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx0, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2376,18 +2423,16 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_recalculate_height_when_removing_from_future() {
|
fn should_recalculate_height_when_removing_from_future() {
|
||||||
// given
|
// given
|
||||||
let previous_nonce = |a: &Address| AccountDetails{ nonce: default_account_details(a).nonce - U256::one(), balance:
|
let previous_nonce = |a: &Address|
|
||||||
!U256::zero() };
|
AccountDetails { nonce: default_account_details(a).nonce - U256::one(), balance: !U256::zero() };
|
||||||
let next_nonce = |a: &Address| AccountDetails{ nonce: default_account_details(a).nonce + U256::one(), balance:
|
|
||||||
!U256::zero() };
|
|
||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
txq.add(tx1.clone(), TransactionOrigin::External, None, &previous_nonce, &gas_estimator).unwrap();
|
txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &previous_nonce, &gas_estimator).unwrap();
|
||||||
txq.add(tx2, TransactionOrigin::External, None, &previous_nonce, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::External, 0, None, &previous_nonce, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.status().future, 2);
|
assert_eq!(txq.status().future, 2);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_invalid(&tx1.hash(), &next_nonce);
|
txq.remove_invalid(&tx1.hash(), &|_| default_nonce() + 1.into());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
let stats = txq.status();
|
let stats = txq.status();
|
||||||
@ -2414,7 +2459,7 @@ mod test {
|
|||||||
let details = |_a: &Address| AccountDetails { nonce: nonce, balance: !U256::zero() };
|
let details = |_a: &Address| AccountDetails { nonce: nonce, balance: !U256::zero() };
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.add(tx, TransactionOrigin::External, None, &details, &gas_estimator).unwrap();
|
txq.add(tx, TransactionOrigin::External, 0, None, &details, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.last_nonce(&from), Some(nonce));
|
assert_eq!(txq.last_nonce(&from), Some(nonce));
|
||||||
@ -2429,17 +2474,17 @@ mod test {
|
|||||||
let details1 = |_a: &Address| AccountDetails { nonce: nonce1, balance: !U256::zero() };
|
let details1 = |_a: &Address| AccountDetails { nonce: nonce1, balance: !U256::zero() };
|
||||||
|
|
||||||
// Insert first transaction
|
// Insert first transaction
|
||||||
txq.add(tx1, TransactionOrigin::External, None, &details1, &gas_estimator).unwrap();
|
txq.add(tx1, TransactionOrigin::External, 0, None, &details1, &gas_estimator).unwrap();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_all(tx2.sender().unwrap(), nonce2 + U256::one());
|
txq.cull(tx2.sender().unwrap(), nonce2 + U256::one());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(txq.top_transactions().is_empty());
|
assert!(txq.top_transactions().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_return_valid_last_nonce_after_remove_all() {
|
fn should_return_valid_last_nonce_after_cull() {
|
||||||
// given
|
// given
|
||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx1, tx2) = new_tx_pair_default(4.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(4.into(), 0.into());
|
||||||
@ -2449,11 +2494,11 @@ mod test {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
// Insert first transaction
|
// Insert first transaction
|
||||||
assert_eq!(txq.add(tx1, TransactionOrigin::External, None, &details1, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
assert_eq!(txq.add(tx1, TransactionOrigin::External, 0, None, &details1, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
||||||
// Second should go to future
|
// Second should go to future
|
||||||
assert_eq!(txq.add(tx2, TransactionOrigin::External, None, &details1, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
assert_eq!(txq.add(tx2, TransactionOrigin::External, 0, None, &details1, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
||||||
// Now block is imported
|
// Now block is imported
|
||||||
txq.remove_all(sender, nonce2 - U256::from(1));
|
txq.cull(sender, nonce2 - U256::from(1));
|
||||||
// tx2 should be not be promoted to current
|
// tx2 should be not be promoted to current
|
||||||
assert_eq!(txq.status().pending, 0);
|
assert_eq!(txq.status().pending, 0);
|
||||||
assert_eq!(txq.status().future, 1);
|
assert_eq!(txq.status().future, 1);
|
||||||
@ -2470,9 +2515,9 @@ mod test {
|
|||||||
assert_eq!(txq.has_local_pending_transactions(), false);
|
assert_eq!(txq.has_local_pending_transactions(), false);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
assert_eq!(txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
assert_eq!(txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
||||||
assert_eq!(txq.has_local_pending_transactions(), false);
|
assert_eq!(txq.has_local_pending_transactions(), false);
|
||||||
assert_eq!(txq.add(tx2, TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
assert_eq!(txq.add(tx2, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap(), TransactionImportResult::Current);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.has_local_pending_transactions(), true);
|
assert_eq!(txq.has_local_pending_transactions(), true);
|
||||||
@ -2487,8 +2532,8 @@ mod test {
|
|||||||
default_account_details(a).balance };
|
default_account_details(a).balance };
|
||||||
|
|
||||||
// when
|
// when
|
||||||
assert_eq!(txq.add(tx2, TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
assert_eq!(txq.add(tx2, TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
||||||
assert_eq!(txq.add(tx1.clone(), TransactionOrigin::External, None, &prev_nonce, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
assert_eq!(txq.add(tx1.clone(), TransactionOrigin::External, 0, None, &prev_nonce, &gas_estimator).unwrap(), TransactionImportResult::Future);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.future.by_priority.len(), 1);
|
assert_eq!(txq.future.by_priority.len(), 1);
|
||||||
@ -2513,14 +2558,14 @@ mod test {
|
|||||||
(tx.sign(secret, None), tx2.sign(secret, None), tx2_2.sign(secret, None), tx3.sign(secret, None))
|
(tx.sign(secret, None), tx2.sign(secret, None), tx2_2.sign(secret, None), tx3.sign(secret, None))
|
||||||
};
|
};
|
||||||
let sender = tx1.sender().unwrap();
|
let sender = tx1.sender().unwrap();
|
||||||
txq.add(tx1, TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2, TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx3, TransactionOrigin::Local, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.future.by_priority.len(), 0);
|
assert_eq!(txq.future.by_priority.len(), 0);
|
||||||
assert_eq!(txq.current.by_priority.len(), 3);
|
assert_eq!(txq.current.by_priority.len(), 3);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res = txq.add(tx2_2, TransactionOrigin::Local, None, &default_account_details, &gas_estimator);
|
let res = txq.add(tx2_2, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.last_nonce(&sender).unwrap(), 125.into());
|
assert_eq!(txq.last_nonce(&sender).unwrap(), 125.into());
|
||||||
@ -2536,8 +2581,8 @@ mod test {
|
|||||||
let high_gas = |_: &SignedTransaction| 100_001.into();
|
let high_gas = |_: &SignedTransaction| 100_001.into();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let res1 = txq.add(tx1, TransactionOrigin::Local, None, &default_account_details, &gas_estimator);
|
let res1 = txq.add(tx1, TransactionOrigin::Local, 0, None, &default_account_details, &gas_estimator);
|
||||||
let res2 = txq.add(tx2, TransactionOrigin::Local, None, &default_account_details, &high_gas);
|
let res2 = txq.add(tx2, TransactionOrigin::Local, 0, None, &default_account_details, &high_gas);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
assert_eq!(res1.unwrap(), TransactionImportResult::Current);
|
||||||
@ -2554,20 +2599,45 @@ mod test {
|
|||||||
let mut txq = TransactionQueue::default();
|
let mut txq = TransactionQueue::default();
|
||||||
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let (tx3, tx4) = new_tx_pair_default(1.into(), 0.into());
|
let (tx3, tx4) = new_tx_pair_default(1.into(), 0.into());
|
||||||
let nonce1 = tx1.nonce;
|
let next_nonce = |_: &Address|
|
||||||
|
AccountDetails { nonce: default_nonce() + U256::one(), balance: !U256::zero() };
|
||||||
|
|
||||||
// Insert all transactions
|
// Insert all transactions
|
||||||
txq.add(tx1, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx2, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx2, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx3, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx3, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
txq.add(tx4, TransactionOrigin::External, None, &default_account_details, &gas_estimator).unwrap();
|
txq.add(tx4, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
assert_eq!(txq.top_transactions().len(), 4);
|
assert_eq!(txq.top_transactions().len(), 4);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
txq.remove_old(|_| nonce1 + U256::one());
|
txq.remove_old(&next_nonce, 0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(txq.top_transactions().len(), 2);
|
assert_eq!(txq.top_transactions().len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_remove_out_of_date_transactions_occupying_queue() {
|
||||||
|
// given
|
||||||
|
let mut txq = TransactionQueue::default();
|
||||||
|
let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||||
|
let (tx3, tx4) = new_tx_pair_default(2.into(), 0.into());
|
||||||
|
|
||||||
|
// Insert all transactions
|
||||||
|
txq.add(tx1, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
txq.add(tx2, TransactionOrigin::External, 5, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
txq.add(tx3.clone(), TransactionOrigin::External, 10, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
txq.add(tx4, TransactionOrigin::External, 0, None, &default_account_details, &gas_estimator).unwrap();
|
||||||
|
assert_eq!(txq.top_transactions().len(), 3);
|
||||||
|
assert_eq!(txq.future_transactions().len(), 1);
|
||||||
|
|
||||||
|
// when
|
||||||
|
txq.remove_old(&default_account_details, 9 + super::DEFAULT_QUEUING_PERIOD);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(txq.top_transactions().len(), 1);
|
||||||
|
assert_eq!(txq.future_transactions().len(), 0);
|
||||||
|
assert_eq!(txq.top_transactions(), vec![tx3]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "parity.js",
|
"name": "parity.js",
|
||||||
"version": "0.2.168",
|
"version": "0.2.172",
|
||||||
"main": "release/index.js",
|
"main": "release/index.js",
|
||||||
"jsnext:main": "src/index.js",
|
"jsnext:main": "src/index.js",
|
||||||
"author": "Parity Team <admin@parity.io>",
|
"author": "Parity Team <admin@parity.io>",
|
||||||
|
@ -218,14 +218,19 @@ export default class Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_encodeOptions (func, options, values) {
|
_encodeOptions (func, options, values) {
|
||||||
options.data = this.getCallData(func, options, values);
|
const data = this.getCallData(func, options, values);
|
||||||
return options;
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
data
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_addOptionsTo (options = {}) {
|
_addOptionsTo (options = {}) {
|
||||||
return Object.assign({
|
return {
|
||||||
to: this._address
|
to: this._address,
|
||||||
}, options);
|
...options
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindFunction = (func) => {
|
_bindFunction = (func) => {
|
||||||
|
@ -31,6 +31,7 @@ const eth = new Api(transport);
|
|||||||
|
|
||||||
describe('api/contract/Contract', () => {
|
describe('api/contract/Contract', () => {
|
||||||
const ADDR = '0x0123456789';
|
const ADDR = '0x0123456789';
|
||||||
|
|
||||||
const ABI = [
|
const ABI = [
|
||||||
{
|
{
|
||||||
type: 'function', name: 'test',
|
type: 'function', name: 'test',
|
||||||
@ -41,12 +42,42 @@ describe('api/contract/Contract', () => {
|
|||||||
type: 'function', name: 'test2',
|
type: 'function', name: 'test2',
|
||||||
outputs: [{ type: 'uint' }, { type: 'uint' }]
|
outputs: [{ type: 'uint' }, { type: 'uint' }]
|
||||||
},
|
},
|
||||||
{ type: 'constructor' },
|
{
|
||||||
|
type: 'constructor',
|
||||||
|
inputs: [{ name: 'boolin', type: 'bool' }, { name: 'stringin', type: 'string' }]
|
||||||
|
},
|
||||||
{ type: 'event', name: 'baz' },
|
{ type: 'event', name: 'baz' },
|
||||||
{ type: 'event', name: 'foo' }
|
{ type: 'event', name: 'foo' }
|
||||||
];
|
];
|
||||||
const VALUES = [true, 'jacogr'];
|
|
||||||
const ENCODED = '0x023562050000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066a61636f67720000000000000000000000000000000000000000000000000000';
|
const ABI_NO_PARAMS = [
|
||||||
|
{
|
||||||
|
type: 'function', name: 'test',
|
||||||
|
inputs: [{ name: 'boolin', type: 'bool' }, { name: 'stringin', type: 'string' }],
|
||||||
|
outputs: [{ type: 'uint' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function', name: 'test2',
|
||||||
|
outputs: [{ type: 'uint' }, { type: 'uint' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'constructor'
|
||||||
|
},
|
||||||
|
{ type: 'event', name: 'baz' },
|
||||||
|
{ type: 'event', name: 'foo' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const VALUES = [ true, 'jacogr' ];
|
||||||
|
const CALLDATA = `
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000001
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000040
|
||||||
|
0000000000000000000000000000000000000000000000000000000000000006
|
||||||
|
6a61636f67720000000000000000000000000000000000000000000000000000
|
||||||
|
`.replace(/\s/g, '');
|
||||||
|
const SIGNATURE = '02356205';
|
||||||
|
|
||||||
|
const ENCODED = `0x${SIGNATURE}${CALLDATA}`;
|
||||||
|
|
||||||
const RETURN1 = '0000000000000000000000000000000000000000000000000000000000123456';
|
const RETURN1 = '0000000000000000000000000000000000000000000000000000000000123456';
|
||||||
const RETURN2 = '0000000000000000000000000000000000000000000000000000000000456789';
|
const RETURN2 = '0000000000000000000000000000000000000000000000000000000000456789';
|
||||||
let scope;
|
let scope;
|
||||||
@ -230,6 +261,33 @@ describe('api/contract/Contract', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deploy without parameters', () => {
|
||||||
|
const contract = new Contract(eth, ABI_NO_PARAMS);
|
||||||
|
const CODE = '0x123';
|
||||||
|
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
|
||||||
|
const RECEIPT_DONE = { contractAddress: ADDRESS.toLowerCase(), gasUsed: 50, blockNumber: 2500 };
|
||||||
|
|
||||||
|
let scope;
|
||||||
|
|
||||||
|
describe('success', () => {
|
||||||
|
before(() => {
|
||||||
|
scope = mockHttp([
|
||||||
|
{ method: 'eth_estimateGas', reply: { result: 1000 } },
|
||||||
|
{ method: 'parity_postTransaction', reply: { result: '0x678' } },
|
||||||
|
{ method: 'parity_checkRequest', reply: { result: '0x890' } },
|
||||||
|
{ method: 'eth_getTransactionReceipt', reply: { result: RECEIPT_DONE } },
|
||||||
|
{ method: 'eth_getCode', reply: { result: CODE } }
|
||||||
|
]);
|
||||||
|
|
||||||
|
return contract.deploy({ data: CODE }, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the options through to postTransaction (incl. gas calculation)', () => {
|
||||||
|
expect(scope.body.parity_postTransaction.params[0].data).to.equal(CODE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
const contract = new Contract(eth, ABI);
|
const contract = new Contract(eth, ABI);
|
||||||
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
|
const ADDRESS = '0xD337e80eEdBdf86eDBba021797d7e4e00Bb78351';
|
||||||
@ -252,7 +310,7 @@ describe('api/contract/Contract', () => {
|
|||||||
{ method: 'eth_getCode', reply: { result: '0x456' } }
|
{ method: 'eth_getCode', reply: { result: '0x456' } }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return contract.deploy({ data: '0x123' }, []);
|
return contract.deploy({ data: '0x123' }, VALUES);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls estimateGas, postTransaction, checkRequest, getTransactionReceipt & getCode in order', () => {
|
it('calls estimateGas, postTransaction, checkRequest, getTransactionReceipt & getCode in order', () => {
|
||||||
@ -261,7 +319,7 @@ describe('api/contract/Contract', () => {
|
|||||||
|
|
||||||
it('passes the options through to postTransaction (incl. gas calculation)', () => {
|
it('passes the options through to postTransaction (incl. gas calculation)', () => {
|
||||||
expect(scope.body.parity_postTransaction.params).to.deep.equal([
|
expect(scope.body.parity_postTransaction.params).to.deep.equal([
|
||||||
{ data: '0x123', gas: '0x4b0' }
|
{ data: `0x123${CALLDATA}`, gas: '0x4b0' }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -280,7 +338,7 @@ describe('api/contract/Contract', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return contract
|
return contract
|
||||||
.deploy({ data: '0x123' }, [])
|
.deploy({ data: '0x123' }, VALUES)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
expect(error.message).to.match(/not deployed, gasUsed/);
|
expect(error.message).to.match(/not deployed, gasUsed/);
|
||||||
});
|
});
|
||||||
@ -296,7 +354,7 @@ describe('api/contract/Contract', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return contract
|
return contract
|
||||||
.deploy({ data: '0x123' }, [])
|
.deploy({ data: '0x123' }, VALUES)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
expect(error.message).to.match(/not deployed, getCode/);
|
expect(error.message).to.match(/not deployed, getCode/);
|
||||||
});
|
});
|
||||||
|
@ -441,9 +441,7 @@ class DeployContract extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCodeChange = (code) => {
|
onCodeChange = (code) => {
|
||||||
const { api } = this.context;
|
this.setState(validateCode(code), this.estimateGas);
|
||||||
|
|
||||||
this.setState(validateCode(code, api), this.estimateGas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeployStart = () => {
|
onDeployStart = () => {
|
||||||
|
@ -14,25 +14,21 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AwaitingDepositStep extends Component {
|
export default class AwaitingDepositStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
coinSymbol: PropTypes.string.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
depositAddress: PropTypes.string,
|
|
||||||
price: PropTypes.shape({
|
|
||||||
rate: PropTypes.number.isRequired,
|
|
||||||
minimum: PropTypes.number.isRequired,
|
|
||||||
limit: PropTypes.number.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { coinSymbol, depositAddress, price } = this.props;
|
const { coinSymbol, depositAddress, price } = this.props.store;
|
||||||
const typeSymbol = (
|
const typeSymbol = (
|
||||||
<div className={ styles.symbol }>
|
<div className={ styles.symbol }>
|
||||||
{ coinSymbol }
|
{ coinSymbol }
|
||||||
@ -43,22 +39,38 @@ export default class AwaitingDepositStep extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.busy }>
|
<div className={ styles.busy }>
|
||||||
Awaiting confirmation of the deposit address for your { typeSymbol } funds exchange
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.awaitingConfirmation'
|
||||||
|
defaultMessage='Awaiting confirmation of the deposit address for your {typeSymbol} funds exchange'
|
||||||
|
values={ { typeSymbol } } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> is awaiting a { typeSymbol } deposit. Send the funds from your { typeSymbol } network client to -
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.awaitingDeposit'
|
||||||
|
defaultMessage='{shapeshiftLink} is awaiting a {typeSymbol} deposit. Send the funds from your {typeSymbol} network client to -'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>,
|
||||||
|
typeSymbol
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
{ depositAddress }
|
{ depositAddress }
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.price }>
|
<div className={ styles.price }>
|
||||||
<div>
|
<div>
|
||||||
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.minimumMaximum'
|
||||||
|
defaultMessage='{minimum} minimum, {maximum} maximum'
|
||||||
|
values={ {
|
||||||
|
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
|
||||||
|
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AwaitingDepositStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<AwaitingDepositStep
|
||||||
|
store={ {
|
||||||
|
coinSymbol: 'BTC',
|
||||||
|
price: { rate: 0.001, minimum: 0, limit: 1.999 }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/AwaitingDepositStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays waiting for address with empty depositAddress', () => {
|
||||||
|
render();
|
||||||
|
expect(component.find('FormattedMessage').props().id).to.match(/awaitingConfirmation/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays waiting for deposit with non-empty depositAddress', () => {
|
||||||
|
render({ depositAddress: 'xyz' });
|
||||||
|
expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/);
|
||||||
|
});
|
||||||
|
});
|
@ -15,32 +15,39 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import Value from '../Value';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
import Value from '../Value';
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AwaitingExchangeStep extends Component {
|
export default class AwaitingExchangeStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
depositInfo: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
incomingCoin: PropTypes.number.isRequired,
|
|
||||||
incomingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { depositInfo } = this.props;
|
const { depositInfo } = this.props.store;
|
||||||
const { incomingCoin, incomingType } = depositInfo;
|
const { incomingCoin, incomingType } = depositInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has received a deposit of -
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingExchangeStep.receivedInfo'
|
||||||
|
defaultMessage='{shapeshiftLink} has received a deposit of -'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
<Value amount={ incomingCoin } symbol={ incomingType } />
|
<Value amount={ incomingCoin } symbol={ incomingType } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
Awaiting the completion of the funds exchange and transfer of funds to your Parity account.
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingExchangeStep.awaitingCompletion'
|
||||||
|
defaultMessage='Awaiting the completion of the funds exchange and transfer of funds to your Parity account.' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AwaitingExchangeStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<AwaitingExchangeStep
|
||||||
|
store={ {
|
||||||
|
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/AwaitingExchangeStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,39 +14,41 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class CompletedStep extends Component {
|
export default class CompletedStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
depositInfo: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
incomingCoin: PropTypes.number.isRequired,
|
|
||||||
incomingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired,
|
|
||||||
exchangeInfo: PropTypes.shape({
|
|
||||||
outgoingCoin: PropTypes.string.isRequired,
|
|
||||||
outgoingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { depositInfo, exchangeInfo } = this.props;
|
const { depositInfo, exchangeInfo } = this.props.store;
|
||||||
const { incomingCoin, incomingType } = depositInfo;
|
const { incomingCoin, incomingType } = depositInfo;
|
||||||
const { outgoingCoin, outgoingType } = exchangeInfo;
|
const { outgoingCoin, outgoingType } = exchangeInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has completed the funds exchange.
|
<FormattedMessage
|
||||||
|
id='shapeshift.completedStep.completed'
|
||||||
|
defaultMessage='{shapeshiftLink} has completed the funds exchange.'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
<Value amount={ incomingCoin } symbol={ incomingType } /> => <Value amount={ outgoingCoin } symbol={ outgoingType } />
|
<Value amount={ incomingCoin } symbol={ incomingType } /> => <Value amount={ outgoingCoin } symbol={ outgoingType } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
The change in funds will be reflected in your Parity account shortly.
|
<FormattedMessage
|
||||||
|
id='shapeshift.completedStep.parityFunds'
|
||||||
|
defaultMessage='The change in funds will be reflected in your Parity account shortly.' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
40
js/src/modals/Shapeshift/CompletedStep/completedStep.spec.js
Normal file
40
js/src/modals/Shapeshift/CompletedStep/completedStep.spec.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CompletedStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<CompletedStep
|
||||||
|
store={ {
|
||||||
|
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' },
|
||||||
|
exchangeInfo: { outgoingCoin: 0.1, outgoingType: 'ETH' }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/CompletedStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,25 +14,30 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class ErrorStep extends Component {
|
export default class ErrorStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
error: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
fatal: PropTypes.bool,
|
|
||||||
message: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { error } = this.props;
|
const { error } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.body }>
|
<div className={ styles.body }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
The funds shifting via <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> failed with a fatal error on the exchange. The error message received from the exchange is as follow:
|
<FormattedMessage
|
||||||
|
id='shapeshift.errorStep.info'
|
||||||
|
defaultMessage='The funds shifting via {shapeshiftLink} failed with a fatal error on the exchange. The error message received from the exchange is as follow:'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.error }>
|
<div className={ styles.error }>
|
||||||
{ error.message }
|
{ error.message }
|
||||||
|
39
js/src/modals/Shapeshift/ErrorStep/errorStep.spec.js
Normal file
39
js/src/modals/Shapeshift/ErrorStep/errorStep.spec.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ErrorStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<ErrorStep
|
||||||
|
store={ {
|
||||||
|
error: new Error('testing')
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/ErrorStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,64 +14,93 @@
|
|||||||
// 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 React, { Component, PropTypes } from 'react';
|
|
||||||
import { Checkbox, MenuItem } from 'material-ui';
|
import { Checkbox, MenuItem } from 'material-ui';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Form, Input, Select } from '~/ui';
|
import { Form, Input, Select, Warning } from '~/ui';
|
||||||
|
|
||||||
import Price from '../Price';
|
import Price from '../Price';
|
||||||
|
import { WARNING_NO_PRICE } from '../store';
|
||||||
import styles from './optionsStep.css';
|
import styles from './optionsStep.css';
|
||||||
|
|
||||||
|
const WARNING_LABELS = {
|
||||||
|
[WARNING_NO_PRICE]: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.warning.noPrice'
|
||||||
|
defaultMessage='No price match was found for the selected type' />
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class OptionsStep extends Component {
|
export default class OptionsStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
refundAddress: PropTypes.string.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
coinSymbol: PropTypes.string.isRequired,
|
|
||||||
coins: PropTypes.array.isRequired,
|
|
||||||
price: PropTypes.object,
|
|
||||||
hasAccepted: PropTypes.bool.isRequired,
|
|
||||||
onChangeSymbol: PropTypes.func.isRequired,
|
|
||||||
onChangeRefund: PropTypes.func.isRequired,
|
|
||||||
onToggleAccept: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { coinSymbol, coins, refundAddress, hasAccepted, onToggleAccept } = this.props;
|
const { coinSymbol, coins, hasAcceptedTerms, price, refundAddress, warning } = this.props.store;
|
||||||
const label = `(optional) ${coinSymbol} return address`;
|
|
||||||
|
|
||||||
if (!coins.length) {
|
if (!coins.length) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.empty }>
|
<div className={ styles.empty }>
|
||||||
There are currently no exchange pairs/coins available to fund with.
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.noPairs'
|
||||||
|
defaultMessage='There are currently no exchange pairs/coins available to fund with.' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = coins.map(this.renderCoinSelectItem);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.body }>
|
<div className={ styles.body }>
|
||||||
<Form>
|
<Form>
|
||||||
<Select
|
<Select
|
||||||
className={ styles.coinselector }
|
className={ styles.coinselector }
|
||||||
label='fund account from'
|
hint={
|
||||||
hint='the type of crypto conversion to do'
|
<FormattedMessage
|
||||||
value={ coinSymbol }
|
id='shapeshift.optionsStep.typeSelect.hint'
|
||||||
onChange={ this.onSelectCoin }>
|
defaultMessage='the type of crypto conversion to do' />
|
||||||
{ items }
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.typeSelect.label'
|
||||||
|
defaultMessage='fund account from' />
|
||||||
|
}
|
||||||
|
onChange={ this.onSelectCoin }
|
||||||
|
value={ coinSymbol }>
|
||||||
|
{
|
||||||
|
coins.map(this.renderCoinSelectItem)
|
||||||
|
}
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
label={ label }
|
hint={
|
||||||
hint='the return address for send failures'
|
<FormattedMessage
|
||||||
value={ refundAddress }
|
id='shapeshift.optionsStep.returnAddr.hint'
|
||||||
onSubmit={ this.onChangeRefund } />
|
defaultMessage='the return address for send failures' />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.returnAddr.label'
|
||||||
|
defaultMessage='(optional) {coinSymbol} return address'
|
||||||
|
values={ { coinSymbol } } />
|
||||||
|
}
|
||||||
|
onSubmit={ this.onChangeRefundAddress }
|
||||||
|
value={ refundAddress } />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
checked={ hasAcceptedTerms }
|
||||||
className={ styles.accept }
|
className={ styles.accept }
|
||||||
label='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity'
|
label={
|
||||||
checked={ hasAccepted }
|
<FormattedMessage
|
||||||
onCheck={ onToggleAccept } />
|
id='shapeshift.optionsStep.terms.label'
|
||||||
|
defaultMessage='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity' />
|
||||||
|
}
|
||||||
|
onCheck={ this.onToggleAcceptTerms } />
|
||||||
</Form>
|
</Form>
|
||||||
<Price { ...this.props } />
|
<Warning warning={ WARNING_LABELS[warning] } />
|
||||||
|
<Price
|
||||||
|
coinSymbol={ coinSymbol }
|
||||||
|
price={ price } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,7 +110,9 @@ export default class OptionsStep extends Component {
|
|||||||
|
|
||||||
const item = (
|
const item = (
|
||||||
<div className={ styles.coinselect }>
|
<div className={ styles.coinselect }>
|
||||||
<img className={ styles.coinimage } src={ image } />
|
<img
|
||||||
|
className={ styles.coinimage }
|
||||||
|
src={ image } />
|
||||||
<div className={ styles.coindetails }>
|
<div className={ styles.coindetails }>
|
||||||
<div className={ styles.coinsymbol }>
|
<div className={ styles.coinsymbol }>
|
||||||
{ symbol }
|
{ symbol }
|
||||||
@ -103,11 +134,15 @@ export default class OptionsStep extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectCoin = (event, idx, value) => {
|
onChangeRefundAddress = (event, refundAddress) => {
|
||||||
this.props.onChangeSymbol(event, value);
|
this.props.store.setRefundAddress(refundAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeAddress = (event, value) => {
|
onSelectCoin = (event, index, coinSymbol) => {
|
||||||
this.props.onChangeRefund(value);
|
this.props.store.setCoinSymbol(coinSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAcceptTerms = () => {
|
||||||
|
this.props.store.toggleAcceptTerms();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
js/src/modals/Shapeshift/OptionsStep/optionsSteps.spec.js
Normal file
126
js/src/modals/Shapeshift/OptionsStep/optionsSteps.spec.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store, { WARNING_NO_PRICE } from '../store';
|
||||||
|
|
||||||
|
import OptionsStep from './';
|
||||||
|
|
||||||
|
const ADDRESS = '0x1234567890123456789012345678901234567890';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = new Store(ADDRESS);
|
||||||
|
component = shallow(
|
||||||
|
<OptionsStep store={ store } />
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/OptionsStep', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no coins when none available', () => {
|
||||||
|
expect(component.find('FormattedMessage').props().id).to.equal('shapeshift.optionsStep.noPairs');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCoins([{ symbol: 'BTC', name: 'Bitcoin' }]);
|
||||||
|
store.toggleAcceptTerms();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('terms Checkbox', () => {
|
||||||
|
it('shows the state of store.hasAcceptedTerms', () => {
|
||||||
|
expect(component.find('Checkbox').props().checked).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warning', () => {
|
||||||
|
let warning;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setWarning(WARNING_NO_PRICE);
|
||||||
|
warning = component.find('Warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a warning message when available', () => {
|
||||||
|
expect(warning.props().warning.props.id).to.equal('shapeshift.warning.noPrice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onChangeRefundAddress', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'setRefundAddress');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setRefundAddress.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the refundAddress on the store', () => {
|
||||||
|
instance.onChangeRefundAddress(null, 'refundAddress');
|
||||||
|
expect(store.setRefundAddress).to.have.been.calledWith('refundAddress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSelectCoin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'setCoinSymbol');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setCoinSymbol.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinSymbol on the store', () => {
|
||||||
|
instance.onSelectCoin(null, 0, 'XMR');
|
||||||
|
expect(store.setCoinSymbol).to.have.been.calledWith('XMR');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onToggleAcceptTerms', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'toggleAcceptTerms');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.toggleAcceptTerms.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles the terms on the store', () => {
|
||||||
|
instance.onToggleAcceptTerms();
|
||||||
|
expect(store.toggleAcceptTerms).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
@ -42,7 +43,13 @@ export default class Price extends Component {
|
|||||||
<Value amount={ 1 } symbol={ coinSymbol } /> = <Value amount={ price.rate } />
|
<Value amount={ 1 } symbol={ coinSymbol } /> = <Value amount={ price.rate } />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
|
<FormattedMessage
|
||||||
|
id='shapeshift.price.minMax'
|
||||||
|
defaultMessage='({minimum} minimum, {maximum} maximum)'
|
||||||
|
values={ {
|
||||||
|
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
|
||||||
|
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
40
js/src/modals/Shapeshift/Price/price.spec.js
Normal file
40
js/src/modals/Shapeshift/Price/price.spec.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Price from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
component = shallow(
|
||||||
|
<Price
|
||||||
|
coinSymbol='BTC'
|
||||||
|
price={ { rate: 0.1, minimum: 0.1, limit: 0.9 } }
|
||||||
|
error={ new Error('testing') }
|
||||||
|
{ ...props } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Price', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
36
js/src/modals/Shapeshift/Value/value.spec.js
Normal file
36
js/src/modals/Shapeshift/Value/value.spec.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Value from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
component = shallow(
|
||||||
|
<Value { ...props } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Value', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,26 +14,44 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
|
||||||
|
|
||||||
|
import shapeshiftLogo from '~/../assets/images/shapeshift-logo.png';
|
||||||
import { Button, IdentityIcon, Modal } from '~/ui';
|
import { Button, IdentityIcon, Modal } from '~/ui';
|
||||||
import initShapeshift from '~/3rdparty/shapeshift';
|
import { CancelIcon, DoneIcon } from '~/ui/Icons';
|
||||||
import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png';
|
|
||||||
|
|
||||||
import AwaitingDepositStep from './AwaitingDepositStep';
|
import AwaitingDepositStep from './AwaitingDepositStep';
|
||||||
import AwaitingExchangeStep from './AwaitingExchangeStep';
|
import AwaitingExchangeStep from './AwaitingExchangeStep';
|
||||||
import CompletedStep from './CompletedStep';
|
import CompletedStep from './CompletedStep';
|
||||||
import ErrorStep from './ErrorStep';
|
import ErrorStep from './ErrorStep';
|
||||||
import OptionsStep from './OptionsStep';
|
import OptionsStep from './OptionsStep';
|
||||||
|
import Store, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
|
||||||
|
|
||||||
import styles from './shapeshift.css';
|
import styles from './shapeshift.css';
|
||||||
|
|
||||||
const shapeshift = initShapeshift();
|
const STAGE_TITLES = [
|
||||||
|
<FormattedMessage
|
||||||
const STAGE_NAMES = ['details', 'awaiting deposit', 'awaiting exchange', 'completed'];
|
id='shapeshift.title.details'
|
||||||
|
defaultMessage='details' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.deposit'
|
||||||
|
defaultMessage='awaiting deposit' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.exchange'
|
||||||
|
defaultMessage='awaiting exchange' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.completed'
|
||||||
|
defaultMessage='completed' />
|
||||||
|
];
|
||||||
|
const ERROR_TITLE = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.error'
|
||||||
|
defaultMessage='exchange failed' />
|
||||||
|
);
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class Shapeshift extends Component {
|
export default class Shapeshift extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
store: PropTypes.object.isRequired
|
store: PropTypes.object.isRequired
|
||||||
@ -44,46 +62,38 @@ export default class Shapeshift extends Component {
|
|||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new Store(this.props.address);
|
||||||
stage: 0,
|
|
||||||
coinSymbol: 'BTC',
|
|
||||||
coinPair: 'btc_eth',
|
|
||||||
coins: [],
|
|
||||||
depositAddress: '',
|
|
||||||
refundAddress: '',
|
|
||||||
price: null,
|
|
||||||
depositInfo: null,
|
|
||||||
exchangeInfo: null,
|
|
||||||
error: {},
|
|
||||||
hasAccepted: false,
|
|
||||||
shifting: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.retrieveCoins();
|
this.store.retrieveCoins();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.unsubscribe();
|
this.store.unsubscribe();
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribe () {
|
|
||||||
// Unsubscribe from Shapeshit
|
|
||||||
const { depositAddress } = this.state;
|
|
||||||
shapeshift.unsubscribe(depositAddress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { error, stage } = this.state;
|
const { error, stage } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
actions={ this.renderDialogActions() }
|
actions={ this.renderDialogActions() }
|
||||||
current={ stage }
|
current={ stage }
|
||||||
steps={ error.fatal ? null : STAGE_NAMES }
|
steps={
|
||||||
title={ error.fatal ? 'exchange failed' : null }
|
error
|
||||||
waiting={ [1, 2] }
|
? null
|
||||||
visible>
|
: STAGE_TITLES
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
error
|
||||||
|
? ERROR_TITLE
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
visible
|
||||||
|
waiting={ [
|
||||||
|
STAGE_WAIT_DEPOSIT,
|
||||||
|
STAGE_WAIT_EXCHANGE
|
||||||
|
] }>
|
||||||
{ this.renderPage() }
|
{ this.renderPage() }
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -91,7 +101,7 @@ export default class Shapeshift extends Component {
|
|||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { address } = this.props;
|
const { address } = this.props;
|
||||||
const { coins, error, stage, hasAccepted, shifting } = this.state;
|
const { coins, error, hasAcceptedTerms, stage } = this.store;
|
||||||
|
|
||||||
const logo = (
|
const logo = (
|
||||||
<a href='http://shapeshift.io' target='_blank' className={ styles.shapeshift }>
|
<a href='http://shapeshift.io' target='_blank' className={ styles.shapeshift }>
|
||||||
@ -100,12 +110,16 @@ export default class Shapeshift extends Component {
|
|||||||
);
|
);
|
||||||
const cancelBtn = (
|
const cancelBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <CancelIcon /> }
|
||||||
label='Cancel'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.cancel'
|
||||||
|
defaultMessage='Cancel' />
|
||||||
|
}
|
||||||
onClick={ this.onClose } />
|
onClick={ this.onClose } />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error.fatal) {
|
if (error) {
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn
|
cancelBtn
|
||||||
@ -113,208 +127,85 @@ export default class Shapeshift extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_OPTIONS:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn,
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
disabled={ !coins.length || !hasAccepted || shifting }
|
disabled={ !coins.length || !hasAcceptedTerms }
|
||||||
icon={ <IdentityIcon address={ address } button /> }
|
icon={
|
||||||
label='Shift Funds'
|
<IdentityIcon
|
||||||
|
address={ address }
|
||||||
|
button />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.shift'
|
||||||
|
defaultMessage='Shift Funds' />
|
||||||
|
}
|
||||||
onClick={ this.onShift } />
|
onClick={ this.onShift } />
|
||||||
];
|
];
|
||||||
|
|
||||||
case 1:
|
case STAGE_WAIT_DEPOSIT:
|
||||||
case 2:
|
case STAGE_WAIT_EXCHANGE:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn
|
cancelBtn
|
||||||
];
|
];
|
||||||
|
|
||||||
case 3:
|
case STAGE_COMPLETED:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDoneAll /> }
|
icon={ <DoneIcon /> }
|
||||||
label='Close'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.done'
|
||||||
|
defaultMessage='Close' />
|
||||||
|
}
|
||||||
onClick={ this.onClose } />
|
onClick={ this.onClose } />
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage () {
|
renderPage () {
|
||||||
const { error, stage } = this.state;
|
const { error, stage } = this.store;
|
||||||
|
|
||||||
if (error.fatal) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorStep error={ error } />
|
<ErrorStep store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_OPTIONS:
|
||||||
return (
|
return (
|
||||||
<OptionsStep
|
<OptionsStep store={ this.store } />
|
||||||
{ ...this.state }
|
|
||||||
onChangeSymbol={ this.onChangeSymbol }
|
|
||||||
onChangeRefund={ this.onChangeRefund }
|
|
||||||
onToggleAccept={ this.onToggleAccept } />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 1:
|
case STAGE_WAIT_DEPOSIT:
|
||||||
return (
|
return (
|
||||||
<AwaitingDepositStep { ...this.state } />
|
<AwaitingDepositStep store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 2:
|
case STAGE_WAIT_EXCHANGE:
|
||||||
return (
|
return (
|
||||||
<AwaitingExchangeStep { ...this.state } />
|
<AwaitingExchangeStep store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 3:
|
case STAGE_COMPLETED:
|
||||||
return (
|
return (
|
||||||
<CompletedStep { ...this.state } />
|
<CompletedStep store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStage (stage) {
|
|
||||||
this.setState({
|
|
||||||
stage,
|
|
||||||
error: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setFatalError (message) {
|
|
||||||
this.setState({
|
|
||||||
stage: 0,
|
|
||||||
error: {
|
|
||||||
fatal: true,
|
|
||||||
message
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setStage(0);
|
this.store.setStage(STAGE_OPTIONS);
|
||||||
this.props.onClose && this.props.onClose();
|
this.props.onClose && this.props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
onShift = () => {
|
onShift = () => {
|
||||||
const { address } = this.props;
|
return this.store.shift();
|
||||||
const { coinPair, refundAddress } = this.state;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
stage: 1,
|
|
||||||
shifting: true
|
|
||||||
});
|
|
||||||
|
|
||||||
shapeshift
|
|
||||||
.shift(address, refundAddress, coinPair)
|
|
||||||
.then((result) => {
|
|
||||||
console.log('onShift', result);
|
|
||||||
const depositAddress = result.deposit;
|
|
||||||
|
|
||||||
if (this.state.depositAddress) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
shapeshift.subscribe(depositAddress, this.onExchangeInfo);
|
|
||||||
this.setState({ depositAddress });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('onShift', error);
|
|
||||||
const message = `Failed to start exchange: ${error.message}`;
|
|
||||||
|
|
||||||
this.newError(new Error(message));
|
|
||||||
this.setFatalError(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeSymbol = (event, coinSymbol) => {
|
|
||||||
const coinPair = `${coinSymbol.toLowerCase()}_eth`;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
coinPair,
|
|
||||||
coinSymbol,
|
|
||||||
price: null
|
|
||||||
});
|
|
||||||
this.getPrice(coinPair);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeRefund = (event, refundAddress) => {
|
|
||||||
this.setState({ refundAddress });
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleAccept = () => {
|
|
||||||
const { hasAccepted } = this.state;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hasAccepted: !hasAccepted
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onExchangeInfo = (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('onExchangeInfo', error);
|
|
||||||
|
|
||||||
if (error.fatal) {
|
|
||||||
this.setFatalError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('onExchangeInfo', result.status, result);
|
|
||||||
|
|
||||||
switch (result.status) {
|
|
||||||
case 'received':
|
|
||||||
this.setState({ depositInfo: result });
|
|
||||||
this.setStage(2);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'complete':
|
|
||||||
this.setState({ exchangeInfo: result });
|
|
||||||
this.setStage(3);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrice (coinPair) {
|
|
||||||
shapeshift
|
|
||||||
.getMarketInfo(coinPair)
|
|
||||||
.then((price) => {
|
|
||||||
this.setState({ price });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('getPrice', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveCoins () {
|
|
||||||
const { coinPair } = this.state;
|
|
||||||
|
|
||||||
shapeshift
|
|
||||||
.getCoins()
|
|
||||||
.then((_coins) => {
|
|
||||||
const coins = Object.values(_coins).filter((coin) => coin.status === 'available');
|
|
||||||
|
|
||||||
this.getPrice(coinPair);
|
|
||||||
this.setState({ coins });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('retrieveCoins', error);
|
|
||||||
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
|
|
||||||
|
|
||||||
this.newError(new Error(message));
|
|
||||||
this.setFatalError(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newError (error) {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
159
js/src/modals/Shapeshift/shapeshift.spec.js
Normal file
159
js/src/modals/Shapeshift/shapeshift.spec.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
|
||||||
|
import Shapeshift from './';
|
||||||
|
|
||||||
|
const ADDRESS = '0x0123456789012345678901234567890123456789';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let onClose;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
onClose = sinon.stub();
|
||||||
|
component = shallow(
|
||||||
|
<Shapeshift
|
||||||
|
address={ ADDRESS }
|
||||||
|
onClose={ onClose }
|
||||||
|
{ ...props } />,
|
||||||
|
{ context: { store: {} } }
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('componentDidMount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
sinon.stub(instance.store, 'retrieveCoins');
|
||||||
|
return instance.componentDidMount();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.retrieveCoins.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the list of coins when mounting', () => {
|
||||||
|
expect(instance.store.retrieveCoins).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('componentWillUnmount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
sinon.stub(instance.store, 'unsubscribe');
|
||||||
|
return instance.componentWillUnmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.unsubscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes any subscriptions when unmounting', () => {
|
||||||
|
expect(instance.store.unsubscribe).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderDialogActions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shift button', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(instance.store, 'shift').resolves();
|
||||||
|
|
||||||
|
instance.store.setCoins(['BTC']);
|
||||||
|
instance.store.toggleAcceptTerms();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.shift.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disabled shift button when not accepted', () => {
|
||||||
|
instance.store.toggleAcceptTerms();
|
||||||
|
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows shift button when accepted', () => {
|
||||||
|
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the shift on store when clicked', () => {
|
||||||
|
shallow(instance.renderDialogActions()[2]).simulate('touchTap');
|
||||||
|
expect(instance.store.shift).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders ErrorStep on error, passing the store', () => {
|
||||||
|
instance.store.setError('testError');
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/ErrorStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders OptionsStep with STAGE_OPTIONS, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_OPTIONS);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/OptionsStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders AwaitingDepositStep with STAGE_WAIT_DEPOSIT, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_WAIT_DEPOSIT);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/AwaitingDepositStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders AwaitingExchangeStep with STAGE_WAIT_EXCHANGE, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_WAIT_EXCHANGE);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/AwaitingExchangeStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders CompletedStep with STAGE_COMPLETED, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_COMPLETED);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/CompletedStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
199
js/src/modals/Shapeshift/store.js
Normal file
199
js/src/modals/Shapeshift/store.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { action, observable, transaction } from 'mobx';
|
||||||
|
|
||||||
|
import initShapeshift from '~/3rdparty/shapeshift';
|
||||||
|
|
||||||
|
const STAGE_OPTIONS = 0;
|
||||||
|
const STAGE_WAIT_DEPOSIT = 1;
|
||||||
|
const STAGE_WAIT_EXCHANGE = 2;
|
||||||
|
const STAGE_COMPLETED = 3;
|
||||||
|
|
||||||
|
const WARNING_NONE = 0;
|
||||||
|
const WARNING_NO_PRICE = -1;
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
@observable address = null;
|
||||||
|
@observable coinPair = 'btc_eth';
|
||||||
|
@observable coinSymbol = 'BTC';
|
||||||
|
@observable coins = [];
|
||||||
|
@observable depositAddress = '';
|
||||||
|
@observable depositInfo = null;
|
||||||
|
@observable exchangeInfo = null;
|
||||||
|
@observable error = null;
|
||||||
|
@observable hasAcceptedTerms = false;
|
||||||
|
@observable price = null;
|
||||||
|
@observable refundAddress = '';
|
||||||
|
@observable stage = STAGE_OPTIONS;
|
||||||
|
@observable warning = 0;
|
||||||
|
|
||||||
|
constructor (address) {
|
||||||
|
this._shapeshiftApi = initShapeshift();
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCoins = (coins) => {
|
||||||
|
this.coins = coins;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCoinSymbol = (coinSymbol) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.coinSymbol = coinSymbol;
|
||||||
|
this.coinPair = `${coinSymbol.toLowerCase()}_eth`;
|
||||||
|
this.price = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getCoinPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDepositAddress = (depositAddress) => {
|
||||||
|
this.depositAddress = depositAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDepositInfo = (depositInfo) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.depositInfo = depositInfo;
|
||||||
|
this.setStage(STAGE_WAIT_EXCHANGE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setError = (error) => {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setExchangeInfo = (exchangeInfo) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.exchangeInfo = exchangeInfo;
|
||||||
|
this.setStage(STAGE_COMPLETED);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPrice = (price) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.price = price;
|
||||||
|
this.setWarning();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setRefundAddress = (refundAddress) => {
|
||||||
|
this.refundAddress = refundAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setStage = (stage) => {
|
||||||
|
this.stage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setWarning = (warning = WARNING_NONE) => {
|
||||||
|
this.warning = warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action toggleAcceptTerms = () => {
|
||||||
|
this.hasAcceptedTerms = !this.hasAcceptedTerms;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoinPrice () {
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.getMarketInfo(this.coinPair)
|
||||||
|
.then((price) => {
|
||||||
|
this.setPrice(price);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('getCoinPrice', error);
|
||||||
|
|
||||||
|
this.setWarning(WARNING_NO_PRICE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveCoins () {
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.getCoins()
|
||||||
|
.then((coins) => {
|
||||||
|
this.setCoins(Object.values(coins).filter((coin) => coin.status === 'available'));
|
||||||
|
|
||||||
|
return this.getCoinPrice();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('retrieveCoins', error);
|
||||||
|
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
|
||||||
|
|
||||||
|
this.setError(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shift () {
|
||||||
|
this.setStage(STAGE_WAIT_DEPOSIT);
|
||||||
|
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.shift(this.address, this.refundAddress, this.coinPair)
|
||||||
|
.then((result) => {
|
||||||
|
console.log('onShift', result);
|
||||||
|
|
||||||
|
this.setDepositAddress(result.deposit);
|
||||||
|
return this.subscribe();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('onShift', error);
|
||||||
|
const message = `Failed to start exchange: ${error.message}`;
|
||||||
|
|
||||||
|
this.setError(new Error(message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onExchangeInfo = (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('onExchangeInfo', error);
|
||||||
|
|
||||||
|
if (error.fatal) {
|
||||||
|
this.setError(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('onExchangeInfo', result.status, result);
|
||||||
|
|
||||||
|
switch (result.status) {
|
||||||
|
case 'received':
|
||||||
|
if (this.stage !== STAGE_WAIT_EXCHANGE) {
|
||||||
|
this.setDepositInfo(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
if (this.stage !== STAGE_COMPLETED) {
|
||||||
|
this.setExchangeInfo(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe () {
|
||||||
|
return this._shapeshiftApi.subscribe(this.depositAddress, this.onExchangeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe () {
|
||||||
|
return this._shapeshiftApi.unsubscribe(this.depositAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
STAGE_COMPLETED,
|
||||||
|
STAGE_OPTIONS,
|
||||||
|
STAGE_WAIT_DEPOSIT,
|
||||||
|
STAGE_WAIT_EXCHANGE,
|
||||||
|
WARNING_NONE,
|
||||||
|
WARNING_NO_PRICE
|
||||||
|
};
|
355
js/src/modals/Shapeshift/store.spec.js
Normal file
355
js/src/modals/Shapeshift/store.spec.js
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE, WARNING_NONE, WARNING_NO_PRICE } from './store';
|
||||||
|
|
||||||
|
const ADDRESS = '0xabcdeffdecbaabcdeffdecbaabcdeffdecbaabcdeffdecba';
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Store', () => {
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new Store(ADDRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the ETH address', () => {
|
||||||
|
expect(store.address).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to BTC-ETH pair', () => {
|
||||||
|
expect(store.coinSymbol).to.equal('BTC');
|
||||||
|
expect(store.coinPair).to.equal('btc_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to stage STAGE_OPTIONS', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_OPTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to terms not accepted', () => {
|
||||||
|
expect(store.hasAcceptedTerms).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@action', () => {
|
||||||
|
describe('setCoins', () => {
|
||||||
|
it('sets the available coins', () => {
|
||||||
|
const coins = ['BTC', 'ETC', 'XMR'];
|
||||||
|
|
||||||
|
store.setCoins(coins);
|
||||||
|
expect(store.coins.peek()).to.deep.equal(coins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCoinSymbol', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'getCoinPrice');
|
||||||
|
store.setCoinSymbol('XMR');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.getCoinPrice.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinSymbol', () => {
|
||||||
|
expect(store.coinSymbol).to.equal('XMR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinPair', () => {
|
||||||
|
expect(store.coinPair).to.equal('xmr_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the price retrieved', () => {
|
||||||
|
expect(store.price).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the pair price', () => {
|
||||||
|
expect(store.getCoinPrice).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDepositAddress', () => {
|
||||||
|
it('sets the depositAddress', () => {
|
||||||
|
store.setDepositAddress('testing');
|
||||||
|
expect(store.depositAddress).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDepositInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setDepositInfo('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.depositInfo).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the stage to STAGE_WAIT_EXCHANGE', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_WAIT_EXCHANGE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setError', () => {
|
||||||
|
it('sets the error', () => {
|
||||||
|
store.setError(new Error('testing'));
|
||||||
|
expect(store.error).to.match(/testing/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setExchangeInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setExchangeInfo('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the exchangeInfo', () => {
|
||||||
|
expect(store.exchangeInfo).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the stage to STAGE_COMPLETED', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_COMPLETED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPrice', () => {
|
||||||
|
it('sets the price', () => {
|
||||||
|
store.setPrice('testing');
|
||||||
|
expect(store.price).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears any warnings once set', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
store.setPrice('testing');
|
||||||
|
expect(store.warning).to.equal(WARNING_NONE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setRefundAddress', () => {
|
||||||
|
it('sets the price', () => {
|
||||||
|
store.setRefundAddress('testing');
|
||||||
|
expect(store.refundAddress).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStage', () => {
|
||||||
|
it('sets the state', () => {
|
||||||
|
store.setStage('testing');
|
||||||
|
expect(store.stage).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWarning', () => {
|
||||||
|
it('sets the warning', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
|
||||||
|
expect(store.warning).to.equal(-999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the warning with no parameters', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
store.setWarning();
|
||||||
|
|
||||||
|
expect(store.warning).to.equal(WARNING_NONE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleAcceptTerms', () => {
|
||||||
|
it('changes state on hasAcceptedTerms', () => {
|
||||||
|
store.toggleAcceptTerms();
|
||||||
|
expect(store.hasAcceptedTerms).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('operations', () => {
|
||||||
|
describe('getCoinPrice', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getMarketInfo').resolves('testPrice');
|
||||||
|
return store.getCoinPrice();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.getMarketInfo.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the market info from ShapeShift', () => {
|
||||||
|
expect(store._shapeshiftApi.getMarketInfo).to.have.been.calledWith('btc_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the price retrieved', () => {
|
||||||
|
expect(store.price).to.equal('testPrice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets a warning on failure', () => {
|
||||||
|
store._shapeshiftApi.getMarketInfo.restore();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getMarketInfo').rejects('someError');
|
||||||
|
|
||||||
|
return store.getCoinPrice().then(() => {
|
||||||
|
expect(store.warning).to.equal(WARNING_NO_PRICE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('retrieveCoins', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getCoins').resolves({
|
||||||
|
BTC: { symbol: 'BTC', status: 'available' },
|
||||||
|
ETC: { symbol: 'ETC' },
|
||||||
|
XMR: { symbol: 'XMR', status: 'available' }
|
||||||
|
});
|
||||||
|
sinon.stub(store, 'getCoinPrice');
|
||||||
|
return store.retrieveCoins();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.getCoins.restore();
|
||||||
|
store.getCoinPrice.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the coins from ShapeShift', () => {
|
||||||
|
expect(store._shapeshiftApi.getCoins).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the available coins', () => {
|
||||||
|
expect(store.coins.peek()).to.deep.equal([
|
||||||
|
{ status: 'available', symbol: 'BTC' },
|
||||||
|
{ status: 'available', symbol: 'XMR' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the price once resolved', () => {
|
||||||
|
expect(store.getCoinPrice).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shift', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'subscribe').resolves();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'shift').resolves({ deposit: 'depositAddress' });
|
||||||
|
store.setRefundAddress('refundAddress');
|
||||||
|
|
||||||
|
return store.shift();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.subscribe.restore();
|
||||||
|
store._shapeshiftApi.shift.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves to stage STAGE_WAIT_DEPOSIT', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_WAIT_DEPOSIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls ShapeShift with the correct parameters', () => {
|
||||||
|
expect(store._shapeshiftApi.shift).to.have.been.calledWith(ADDRESS, 'refundAddress', store.coinPair);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositAddress', () => {
|
||||||
|
expect(store.depositAddress).to.equal('depositAddress');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes to updates', () => {
|
||||||
|
expect(store.subscribe).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when shift fails', () => {
|
||||||
|
store._shapeshiftApi.shift.restore();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'shift').rejects({ message: 'testingError' });
|
||||||
|
|
||||||
|
return store.shift().then(() => {
|
||||||
|
expect(store.error).to.match(/testingError/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribe', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'subscribe');
|
||||||
|
store.setDepositAddress('depositAddress');
|
||||||
|
return store.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.subscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the ShapeShift subscribe', () => {
|
||||||
|
expect(store._shapeshiftApi.subscribe).to.have.been.calledWith('depositAddress', store.onExchangeInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onExchangeInfo', () => {
|
||||||
|
it('sets the error when fatal error retrieved', () => {
|
||||||
|
store.onExchangeInfo({ fatal: true, message: 'testing' });
|
||||||
|
expect(store.error.message).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set the error when non-fatal error retrieved', () => {
|
||||||
|
store.onExchangeInfo({ message: 'testing' });
|
||||||
|
expect(store.error).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('status received', () => {
|
||||||
|
const INFO = { status: 'received' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.onExchangeInfo(null, INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.depositInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only advanced depositInfo once', () => {
|
||||||
|
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
|
||||||
|
expect(store.depositInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('status completed', () => {
|
||||||
|
const INFO = { status: 'complete' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.onExchangeInfo(null, INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.exchangeInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only advanced depositInfo once', () => {
|
||||||
|
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
|
||||||
|
expect(store.exchangeInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'unsubscribe');
|
||||||
|
store.setDepositAddress('depositAddress');
|
||||||
|
return store.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.unsubscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the ShapeShift unsubscribe', () => {
|
||||||
|
expect(store._shapeshiftApi.unsubscribe).to.have.been.calledWith('depositAddress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -113,6 +113,7 @@ const routes = [
|
|||||||
{ path: 'apps', component: Dapps },
|
{ path: 'apps', component: Dapps },
|
||||||
{ path: 'app/:id', component: Dapp },
|
{ path: 'app/:id', component: Dapp },
|
||||||
{ path: 'web', component: Web },
|
{ path: 'web', component: Web },
|
||||||
|
{ path: 'web/:url', component: Web },
|
||||||
{ path: 'signer', component: Signer }
|
{ path: 'signer', component: Signer }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
|
|||||||
import DeleteIcon from 'material-ui/svg-icons/action/delete';
|
import DeleteIcon from 'material-ui/svg-icons/action/delete';
|
||||||
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||||
import EditIcon from 'material-ui/svg-icons/content/create';
|
import EditIcon from 'material-ui/svg-icons/content/create';
|
||||||
|
import LinkIcon from 'material-ui/svg-icons/content/link';
|
||||||
import LockedIcon from 'material-ui/svg-icons/action/lock';
|
import LockedIcon from 'material-ui/svg-icons/action/lock';
|
||||||
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||||
@ -47,6 +48,7 @@ export {
|
|||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
DoneIcon,
|
DoneIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
LinkIcon,
|
||||||
LockedIcon,
|
LockedIcon,
|
||||||
NextIcon,
|
NextIcon,
|
||||||
PrevIcon,
|
PrevIcon,
|
||||||
|
@ -16,10 +16,12 @@
|
|||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
import util from '~/api/util';
|
import apiutil from '~/api/util';
|
||||||
|
|
||||||
import { NULL_ADDRESS } from './constants';
|
import { NULL_ADDRESS } from './constants';
|
||||||
|
|
||||||
|
// TODO: Convert to FormattedMessages as soon as comfortable with the impact, i.e. errors
|
||||||
|
// not being concatted into strings in components, all supporting a non-string format
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
invalidAddress: 'address is an invalid network address',
|
invalidAddress: 'address is an invalid network address',
|
||||||
invalidAmount: 'the supplied amount should be a valid positive number',
|
invalidAmount: 'the supplied amount should be a valid positive number',
|
||||||
@ -42,9 +44,14 @@ export function validateAbi (abi) {
|
|||||||
try {
|
try {
|
||||||
abiParsed = JSON.parse(abi);
|
abiParsed = JSON.parse(abi);
|
||||||
|
|
||||||
if (!util.isArray(abiParsed)) {
|
if (!apiutil.isArray(abiParsed)) {
|
||||||
abiError = ERRORS.invalidAbi;
|
abiError = ERRORS.invalidAbi;
|
||||||
return { abi, abiError, abiParsed };
|
|
||||||
|
return {
|
||||||
|
abi,
|
||||||
|
abiError,
|
||||||
|
abiParsed
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each elements of the Array
|
// Validate each elements of the Array
|
||||||
@ -54,8 +61,15 @@ export function validateAbi (abi) {
|
|||||||
|
|
||||||
if (invalidIndex !== -1) {
|
if (invalidIndex !== -1) {
|
||||||
const invalid = abiParsed[invalidIndex];
|
const invalid = abiParsed[invalidIndex];
|
||||||
|
|
||||||
|
// TODO: Needs seperate error when using FormattedMessage (no concats)
|
||||||
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
|
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
|
||||||
return { abi, abiError, abiParsed };
|
|
||||||
|
return {
|
||||||
|
abi,
|
||||||
|
abiError,
|
||||||
|
abiParsed
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
abi = JSON.stringify(abiParsed);
|
abi = JSON.stringify(abiParsed);
|
||||||
@ -76,7 +90,7 @@ function isValidAbiFunction (object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
|
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
|
||||||
(object.inputs && util.isArray(object.inputs));
|
(object.inputs && apiutil.isArray(object.inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAbiFallback (object) {
|
function isAbiFallback (object) {
|
||||||
@ -94,7 +108,7 @@ function isValidAbiEvent (object) {
|
|||||||
|
|
||||||
return (object.type === 'event') &&
|
return (object.type === 'event') &&
|
||||||
(object.name) &&
|
(object.name) &&
|
||||||
(object.inputs && util.isArray(object.inputs));
|
(object.inputs && apiutil.isArray(object.inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAddress (address) {
|
export function validateAddress (address) {
|
||||||
@ -102,10 +116,10 @@ export function validateAddress (address) {
|
|||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
addressError = ERRORS.invalidAddress;
|
addressError = ERRORS.invalidAddress;
|
||||||
} else if (!util.isAddressValid(address)) {
|
} else if (!apiutil.isAddressValid(address)) {
|
||||||
addressError = ERRORS.invalidAddress;
|
addressError = ERRORS.invalidAddress;
|
||||||
} else {
|
} else {
|
||||||
address = util.toChecksumAddress(address);
|
address = apiutil.toChecksumAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -114,12 +128,12 @@ export function validateAddress (address) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateCode (code, api) {
|
export function validateCode (code) {
|
||||||
let codeError = null;
|
let codeError = null;
|
||||||
|
|
||||||
if (!code.length) {
|
if (!code || !code.length) {
|
||||||
codeError = ERRORS.invalidCode;
|
codeError = ERRORS.invalidCode;
|
||||||
} else if (!api.util.isHex(code)) {
|
} else if (!apiutil.isHex(code)) {
|
||||||
codeError = ERRORS.invalidCode;
|
codeError = ERRORS.invalidCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +144,9 @@ export function validateCode (code, api) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateName (name) {
|
export function validateName (name) {
|
||||||
const nameError = !name || name.trim().length < 2 ? ERRORS.invalidName : null;
|
const nameError = !name || name.trim().length < 2
|
||||||
|
? ERRORS.invalidName
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -162,6 +178,7 @@ export function validateUint (value) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const bn = new BigNumber(value);
|
const bn = new BigNumber(value);
|
||||||
|
|
||||||
if (bn.lt(0)) {
|
if (bn.lt(0)) {
|
||||||
valueError = ERRORS.negativeNumber;
|
valueError = ERRORS.negativeNumber;
|
||||||
} else if (!bn.isInteger()) {
|
} else if (!bn.isInteger()) {
|
||||||
|
303
js/src/util/validation.spec.js
Normal file
303
js/src/util/validation.spec.js
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
import { NULL_ADDRESS } from './constants';
|
||||||
|
import { ERRORS, isNullAddress, validateAbi, validateAddress, validateCode, validateName, validatePositiveNumber, validateUint } from './validation';
|
||||||
|
|
||||||
|
describe('util/validation', () => {
|
||||||
|
describe('validateAbi', () => {
|
||||||
|
it('passes on valid ABI', () => {
|
||||||
|
const abi = '[{"type":"function","name":"test","inputs":[],"outputs":[]}]';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: null,
|
||||||
|
abiParsed: [{
|
||||||
|
type: 'function',
|
||||||
|
name: 'test',
|
||||||
|
inputs: [],
|
||||||
|
outputs: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes on valid ABI & trims ABI', () => {
|
||||||
|
const abi = '[ { "type" : "function" , "name" : "test" , "inputs" : [] , "outputs" : [] } ]';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi: '[{"type":"function","name":"test","inputs":[],"outputs":[]}]',
|
||||||
|
abiError: null,
|
||||||
|
abiParsed: [{
|
||||||
|
type: 'function',
|
||||||
|
name: 'test',
|
||||||
|
inputs: [],
|
||||||
|
outputs: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on invalid JSON', () => {
|
||||||
|
const abi = 'this is not json';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: ERRORS.invalidAbi,
|
||||||
|
abiParsed: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on non-array JSON', () => {
|
||||||
|
const abi = '{}';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: ERRORS.invalidAbi,
|
||||||
|
abiParsed: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with invalid event', () => {
|
||||||
|
const abi = '[{ "type":"event" }]';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: `${ERRORS.invalidAbi} (#0: event)`,
|
||||||
|
abiParsed: [{ type: 'event' }]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with invalid function', () => {
|
||||||
|
const abi = '[{ "type":"function" }]';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: `${ERRORS.invalidAbi} (#0: function)`,
|
||||||
|
abiParsed: [{ type: 'function' }]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with unknown type', () => {
|
||||||
|
const abi = '[{ "type":"somethingElse" }]';
|
||||||
|
|
||||||
|
expect(validateAbi(abi)).to.deep.equal({
|
||||||
|
abi,
|
||||||
|
abiError: `${ERRORS.invalidAbi} (#0: somethingElse)`,
|
||||||
|
abiParsed: [{ type: 'somethingElse' }]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateAddress', () => {
|
||||||
|
it('validates address', () => {
|
||||||
|
const address = '0x1234567890123456789012345678901234567890';
|
||||||
|
|
||||||
|
expect(validateAddress(address)).to.deep.equal({
|
||||||
|
address,
|
||||||
|
addressError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates address and converts to checksum', () => {
|
||||||
|
const address = '0x5A5eFF38DA95b0D58b6C616f2699168B480953C9';
|
||||||
|
|
||||||
|
expect(validateAddress(address.toLowerCase())).to.deep.equal({
|
||||||
|
address,
|
||||||
|
addressError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on null addresses', () => {
|
||||||
|
expect(validateAddress(null)).to.deep.equal({
|
||||||
|
address: null,
|
||||||
|
addressError: ERRORS.invalidAddress
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on invalid addresses', () => {
|
||||||
|
const address = '0x12344567';
|
||||||
|
|
||||||
|
expect(validateAddress(address)).to.deep.equal({
|
||||||
|
address,
|
||||||
|
addressError: ERRORS.invalidAddress
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateCode', () => {
|
||||||
|
it('validates hex code', () => {
|
||||||
|
expect(validateCode('0x123abc')).to.deep.equal({
|
||||||
|
code: '0x123abc',
|
||||||
|
codeError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates hex code (non-prefix)', () => {
|
||||||
|
expect(validateCode('123abc')).to.deep.equal({
|
||||||
|
code: '123abc',
|
||||||
|
codeError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on invalid code', () => {
|
||||||
|
expect(validateCode(null)).to.deep.equal({
|
||||||
|
code: null,
|
||||||
|
codeError: ERRORS.invalidCode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on empty code', () => {
|
||||||
|
expect(validateCode('')).to.deep.equal({
|
||||||
|
code: '',
|
||||||
|
codeError: ERRORS.invalidCode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on non-hex code', () => {
|
||||||
|
expect(validateCode('123hfg')).to.deep.equal({
|
||||||
|
code: '123hfg',
|
||||||
|
codeError: ERRORS.invalidCode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateName', () => {
|
||||||
|
it('validates names', () => {
|
||||||
|
expect(validateName('Joe Bloggs')).to.deep.equal({
|
||||||
|
name: 'Joe Bloggs',
|
||||||
|
nameError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on null names', () => {
|
||||||
|
expect(validateName(null)).to.deep.equal({
|
||||||
|
name: null,
|
||||||
|
nameError: ERRORS.invalidName
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on short names', () => {
|
||||||
|
expect(validateName(' 1 ')).to.deep.equal({
|
||||||
|
name: ' 1 ',
|
||||||
|
nameError: ERRORS.invalidName
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validatePositiveNumber', () => {
|
||||||
|
it('validates numbers', () => {
|
||||||
|
expect(validatePositiveNumber(123)).to.deep.equal({
|
||||||
|
number: 123,
|
||||||
|
numberError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates strings', () => {
|
||||||
|
expect(validatePositiveNumber('123')).to.deep.equal({
|
||||||
|
number: '123',
|
||||||
|
numberError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates bignumbers', () => {
|
||||||
|
expect(validatePositiveNumber(new BigNumber(123))).to.deep.equal({
|
||||||
|
number: new BigNumber(123),
|
||||||
|
numberError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on invalid numbers', () => {
|
||||||
|
expect(validatePositiveNumber(null)).to.deep.equal({
|
||||||
|
number: null,
|
||||||
|
numberError: ERRORS.invalidAmount
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on negative numbers', () => {
|
||||||
|
expect(validatePositiveNumber(-1)).to.deep.equal({
|
||||||
|
number: -1,
|
||||||
|
numberError: ERRORS.invalidAmount
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateUint', () => {
|
||||||
|
it('validates numbers', () => {
|
||||||
|
expect(validateUint(123)).to.deep.equal({
|
||||||
|
value: 123,
|
||||||
|
valueError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates strings', () => {
|
||||||
|
expect(validateUint('123')).to.deep.equal({
|
||||||
|
value: '123',
|
||||||
|
valueError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates bignumbers', () => {
|
||||||
|
expect(validateUint(new BigNumber(123))).to.deep.equal({
|
||||||
|
value: new BigNumber(123),
|
||||||
|
valueError: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on invalid numbers', () => {
|
||||||
|
expect(validateUint(null)).to.deep.equal({
|
||||||
|
value: null,
|
||||||
|
valueError: ERRORS.invalidNumber
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on negative numbers', () => {
|
||||||
|
expect(validateUint(-1)).to.deep.equal({
|
||||||
|
value: -1,
|
||||||
|
valueError: ERRORS.negativeNumber
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error on decimal numbers', () => {
|
||||||
|
expect(validateUint(3.1415927)).to.deep.equal({
|
||||||
|
value: 3.1415927,
|
||||||
|
valueError: ERRORS.decimalNumber
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isNullAddress', () => {
|
||||||
|
it('verifies a prefixed null address', () => {
|
||||||
|
expect(isNullAddress(`0x${NULL_ADDRESS}`)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies a non-prefixed null address', () => {
|
||||||
|
expect(isNullAddress(NULL_ADDRESS)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets false on a null value', () => {
|
||||||
|
expect(isNullAddress(null)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets false on a non-full length 00..00 value', () => {
|
||||||
|
expect(isNullAddress(NULL_ADDRESS.slice(2))).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets false on a valid addess, non 00..00 value', () => {
|
||||||
|
expect(isNullAddress('0x1234567890123456789012345678901234567890')).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
17
js/src/views/Dapps/UrlButton/index.js
Normal file
17
js/src/views/Dapps/UrlButton/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './urlButton';
|
20
js/src/views/Dapps/UrlButton/urlButton.css
Normal file
20
js/src/views/Dapps/UrlButton/urlButton.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
/* This file is part of Parity.
|
||||||
|
/*
|
||||||
|
/* Parity is free software: you can redistribute it and/or modify
|
||||||
|
/* it under the terms of the GNU General Public License as published by
|
||||||
|
/* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
/* (at your option) any later version.
|
||||||
|
/*
|
||||||
|
/* Parity is distributed in the hope that it will be useful,
|
||||||
|
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
/* GNU General Public License for more details.
|
||||||
|
/*
|
||||||
|
/* You should have received a copy of the GNU General Public License
|
||||||
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.button {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
96
js/src/views/Dapps/UrlButton/urlButton.js
Normal file
96
js/src/views/Dapps/UrlButton/urlButton.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
|
||||||
|
import Button from '~/ui/Button';
|
||||||
|
import { LinkIcon } from '~/ui/Icons';
|
||||||
|
import Input from '~/ui/Form/Input';
|
||||||
|
|
||||||
|
import styles from './urlButton.css';
|
||||||
|
|
||||||
|
const INPUT_STYLE = { display: 'inline-block', width: '20em' };
|
||||||
|
|
||||||
|
class UrlButton extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
router: PropTypes.object.isRequired // injected by withRouter
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
inputShown: false
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { inputShown } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ inputShown ? this.renderInput() : null }
|
||||||
|
<Button
|
||||||
|
className={ styles.button }
|
||||||
|
icon={ <LinkIcon /> }
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='dapps.button.url.label'
|
||||||
|
defaultMessage='URL' />
|
||||||
|
}
|
||||||
|
onClick={ this.toggleInput }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInput () {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='dapps.button.url.input'
|
||||||
|
defaultMessage='https://mkr.market' />
|
||||||
|
}
|
||||||
|
onBlur={ this.hideInput }
|
||||||
|
onFocus={ this.showInput }
|
||||||
|
onSubmit={ this.inputOnSubmit }
|
||||||
|
style={ INPUT_STYLE }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleInput = () => {
|
||||||
|
const { inputShown } = this.state;
|
||||||
|
this.setState({
|
||||||
|
inputShown: !inputShown
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideInput = () => {
|
||||||
|
this.setState({ inputShown: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
showInput = () => {
|
||||||
|
this.setState({ inputShown: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
inputOnSubmit = (url) => {
|
||||||
|
const { router } = this.props;
|
||||||
|
|
||||||
|
router.push(`/web/${encodeURIComponent(url)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(UrlButton);
|
@ -27,6 +27,7 @@ import PermissionStore from '~/modals/DappPermissions/store';
|
|||||||
import { Actionbar, Button, Page } from '~/ui';
|
import { Actionbar, Button, Page } from '~/ui';
|
||||||
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
|
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
|
import UrlButton from './UrlButton';
|
||||||
import DappsStore from './dappsStore';
|
import DappsStore from './dappsStore';
|
||||||
import Summary from './Summary';
|
import Summary from './Summary';
|
||||||
|
|
||||||
@ -88,6 +89,7 @@ class Dapps extends Component {
|
|||||||
defaultMessage='Decentralized Applications' />
|
defaultMessage='Decentralized Applications' />
|
||||||
}
|
}
|
||||||
buttons={ [
|
buttons={ [
|
||||||
|
<UrlButton key='url' />,
|
||||||
<Button
|
<Button
|
||||||
icon={ <VisibleIcon /> }
|
icon={ <VisibleIcon /> }
|
||||||
key='edit'
|
key='edit'
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import { parse as parseUrl, format as formatUrl } from 'url';
|
||||||
|
import { parse as parseQuery } from 'querystring';
|
||||||
|
|
||||||
import AddressBar from './AddressBar';
|
import AddressBar from './AddressBar';
|
||||||
|
|
||||||
@ -23,39 +25,53 @@ import styles from './web.css';
|
|||||||
|
|
||||||
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
const LS_LAST_ADDRESS = '_parity::webLastAddress';
|
||||||
|
|
||||||
|
const hasProtocol = /^https?:\/\//;
|
||||||
|
|
||||||
export default class Web extends Component {
|
export default class Web extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
displayedUrl: this.lastAddress(),
|
displayedUrl: null,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
token: null,
|
token: null,
|
||||||
url: this.lastAddress()
|
url: null
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.context.api.signer.generateWebProxyAccessToken().then(token => {
|
const { api } = this.context;
|
||||||
this.setState({ token });
|
const { params } = this.props;
|
||||||
});
|
|
||||||
|
api
|
||||||
|
.signer
|
||||||
|
.generateWebProxyAccessToken()
|
||||||
|
.then((token) => {
|
||||||
|
this.setState({ token });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setUrl(params.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
address () {
|
componentWillReceiveProps (props) {
|
||||||
const { dappsUrl } = this.context.api;
|
this.setUrl(props.params.url);
|
||||||
const { url, token } = this.state;
|
|
||||||
const path = url.replace(/:/g, '').replace(/\/\//g, '/');
|
|
||||||
|
|
||||||
return `${dappsUrl}/web/${token}/${path}/`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAddress () {
|
setUrl = (url) => {
|
||||||
return store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
|
url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
|
||||||
}
|
if (!hasProtocol.test(url)) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ url, displayedUrl: url });
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { displayedUrl, isLoading, token } = this.state;
|
const { displayedUrl, isLoading, token } = this.state;
|
||||||
const address = this.address();
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
@ -67,20 +83,30 @@ export default class Web extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dappsUrl } = this.context.api;
|
||||||
|
const { url } = this.state;
|
||||||
|
if (!url || !token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseUrl(url);
|
||||||
|
const { protocol, host, path } = parsed;
|
||||||
|
const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.wrapper }>
|
<div className={ styles.wrapper }>
|
||||||
<AddressBar
|
<AddressBar
|
||||||
className={ styles.url }
|
className={ styles.url }
|
||||||
isLoading={ isLoading }
|
isLoading={ isLoading }
|
||||||
onChange={ this.handleUpdateUrl }
|
onChange={ this.onUrlChange }
|
||||||
onRefresh={ this.handleOnRefresh }
|
onRefresh={ this.onRefresh }
|
||||||
url={ displayedUrl }
|
url={ displayedUrl }
|
||||||
/>
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
className={ styles.frame }
|
className={ styles.frame }
|
||||||
frameBorder={ 0 }
|
frameBorder={ 0 }
|
||||||
name={ name }
|
name={ name }
|
||||||
onLoad={ this.handleIframeLoad }
|
onLoad={ this.iframeOnLoad }
|
||||||
sandbox='allow-forms allow-same-origin allow-scripts'
|
sandbox='allow-forms allow-same-origin allow-scripts'
|
||||||
scrolling='auto'
|
scrolling='auto'
|
||||||
src={ address } />
|
src={ address } />
|
||||||
@ -88,7 +114,11 @@ export default class Web extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateUrl = (url) => {
|
onUrlChange = (url) => {
|
||||||
|
if (!hasProtocol.test(url)) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
store.set(LS_LAST_ADDRESS, url);
|
store.set(LS_LAST_ADDRESS, url);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -98,18 +128,23 @@ export default class Web extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOnRefresh = (ev) => {
|
onRefresh = () => {
|
||||||
const { displayedUrl } = this.state;
|
const { displayedUrl } = this.state;
|
||||||
const hasQuery = displayedUrl.indexOf('?') > 0;
|
|
||||||
const separator = hasQuery ? '&' : '?';
|
// Insert timestamp
|
||||||
|
// This is a hack to prevent caching.
|
||||||
|
const parsed = parseUrl(displayedUrl);
|
||||||
|
parsed.query = parseQuery(parsed.query);
|
||||||
|
parsed.query.t = Date.now().toString();
|
||||||
|
delete parsed.search;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
url: `${displayedUrl}${separator}t=${Date.now()}`
|
url: formatUrl(parsed)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleIframeLoad = (ev) => {
|
iframeOnLoad = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false
|
isLoading: false
|
||||||
});
|
});
|
||||||
|
@ -458,23 +458,65 @@ class WriteContract extends Component {
|
|||||||
const { bytecode } = contract;
|
const { bytecode } = contract;
|
||||||
const abi = contract.interface;
|
const abi = contract.interface;
|
||||||
|
|
||||||
|
const metadata = contract.metadata
|
||||||
|
? (
|
||||||
|
<Input
|
||||||
|
allowCopy
|
||||||
|
label='Metadata'
|
||||||
|
readOnly
|
||||||
|
value={ contract.metadata }
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
|
allowCopy
|
||||||
|
label='ABI Interface'
|
||||||
readOnly
|
readOnly
|
||||||
value={ abi }
|
value={ abi }
|
||||||
label='ABI Interface'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
allowCopy
|
||||||
|
label='Bytecode'
|
||||||
readOnly
|
readOnly
|
||||||
value={ `0x${bytecode}` }
|
value={ `0x${bytecode}` }
|
||||||
label='Bytecode'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{ metadata }
|
||||||
|
{ this.renderSwarmHash(contract) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSwarmHash (contract) {
|
||||||
|
if (!contract || !contract.metadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { bytecode } = contract;
|
||||||
|
|
||||||
|
// @see https://solidity.readthedocs.io/en/develop/miscellaneous.html#encoding-of-the-metadata-hash-in-the-bytecode
|
||||||
|
const hashRegex = /a165627a7a72305820([a-f0-9]{64})0029$/;
|
||||||
|
|
||||||
|
if (!hashRegex.test(bytecode)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = hashRegex.exec(bytecode)[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
allowCopy
|
||||||
|
label='Swarm Metadata Hash'
|
||||||
|
readOnly
|
||||||
|
value={ `${hash}` }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderErrors () {
|
renderErrors () {
|
||||||
const { annotations } = this.store;
|
const { annotations } = this.store;
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ pub struct AuthorityRoundParams {
|
|||||||
pub step_duration: Uint,
|
pub step_duration: Uint,
|
||||||
/// Valid authorities
|
/// Valid authorities
|
||||||
pub authorities: Vec<Address>,
|
pub authorities: Vec<Address>,
|
||||||
|
/// Block reward.
|
||||||
|
#[serde(rename="blockReward")]
|
||||||
|
pub block_reward: Option<Uint>,
|
||||||
/// Starting step. Determined automatically if not specified.
|
/// Starting step. Determined automatically if not specified.
|
||||||
/// To be used for testing only.
|
/// To be used for testing only.
|
||||||
#[serde(rename="startStep")]
|
#[serde(rename="startStep")]
|
||||||
@ -49,12 +52,13 @@ mod tests {
|
|||||||
use spec::authority_round::AuthorityRound;
|
use spec::authority_round::AuthorityRound;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic_authority_deserialization() {
|
fn authority_round_deserialization() {
|
||||||
let s = r#"{
|
let s = r#"{
|
||||||
"params": {
|
"params": {
|
||||||
"gasLimitBoundDivisor": "0x0400",
|
"gasLimitBoundDivisor": "0x0400",
|
||||||
"stepDuration": "0x02",
|
"stepDuration": "0x02",
|
||||||
"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"],
|
"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"],
|
||||||
|
"blockReward": "0x50",
|
||||||
"startStep" : 24
|
"startStep" : 24
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
@ -39,6 +39,9 @@ pub struct TendermintParams {
|
|||||||
/// Commit step timeout in milliseconds.
|
/// Commit step timeout in milliseconds.
|
||||||
#[serde(rename="timeoutCommit")]
|
#[serde(rename="timeoutCommit")]
|
||||||
pub timeout_commit: Option<Uint>,
|
pub timeout_commit: Option<Uint>,
|
||||||
|
/// Block reward.
|
||||||
|
#[serde(rename="blockReward")]
|
||||||
|
pub block_reward: Option<Uint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tendermint engine deserialization.
|
/// Tendermint engine deserialization.
|
||||||
@ -54,11 +57,12 @@ mod tests {
|
|||||||
use spec::tendermint::Tendermint;
|
use spec::tendermint::Tendermint;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic_authority_deserialization() {
|
fn tendermint_deserialization() {
|
||||||
let s = r#"{
|
let s = r#"{
|
||||||
"params": {
|
"params": {
|
||||||
"gasLimitBoundDivisor": "0x0400",
|
"gasLimitBoundDivisor": "0x400",
|
||||||
"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"]
|
"authorities" : ["0xc6d9d2cd449a754c494264e1809c50e34d64562b"],
|
||||||
|
"blockReward": "0x50"
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
|
@ -100,8 +100,8 @@ use sync_io::SyncIo;
|
|||||||
use time;
|
use time;
|
||||||
use super::SyncConfig;
|
use super::SyncConfig;
|
||||||
use block_sync::{BlockDownloader, BlockRequest, BlockDownloaderImportError as DownloaderImportError, DownloadAction};
|
use block_sync::{BlockDownloader, BlockRequest, BlockDownloaderImportError as DownloaderImportError, DownloadAction};
|
||||||
|
use rand::Rng;
|
||||||
use snapshot::{Snapshot, ChunkType};
|
use snapshot::{Snapshot, ChunkType};
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
use api::{PeerInfo as PeerInfoDigest, WARP_SYNC_PROTOCOL_ID};
|
use api::{PeerInfo as PeerInfoDigest, WARP_SYNC_PROTOCOL_ID};
|
||||||
use transactions_stats::{TransactionsStats, Stats as TransactionStats};
|
use transactions_stats::{TransactionsStats, Stats as TransactionStats};
|
||||||
|
|
||||||
@ -329,6 +329,17 @@ impl PeerInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
mod random {
|
||||||
|
use rand;
|
||||||
|
pub fn new() -> rand::ThreadRng { rand::thread_rng() }
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod random {
|
||||||
|
use rand::{self, SeedableRng};
|
||||||
|
pub fn new() -> rand::XorShiftRng { rand::XorShiftRng::from_seed([0, 1, 2, 3]) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Blockchain sync handler.
|
/// Blockchain sync handler.
|
||||||
/// See module documentation for more details.
|
/// See module documentation for more details.
|
||||||
pub struct ChainSync {
|
pub struct ChainSync {
|
||||||
@ -1120,7 +1131,7 @@ impl ChainSync {
|
|||||||
fn continue_sync(&mut self, io: &mut SyncIo) {
|
fn continue_sync(&mut self, io: &mut SyncIo) {
|
||||||
let mut peers: Vec<(PeerId, U256, u8)> = self.peers.iter().filter_map(|(k, p)|
|
let mut peers: Vec<(PeerId, U256, u8)> = self.peers.iter().filter_map(|(k, p)|
|
||||||
if p.can_sync() { Some((*k, p.difficulty.unwrap_or_else(U256::zero), p.protocol_version)) } else { None }).collect();
|
if p.can_sync() { Some((*k, p.difficulty.unwrap_or_else(U256::zero), p.protocol_version)) } else { None }).collect();
|
||||||
thread_rng().shuffle(&mut peers); //TODO: sort by rating
|
random::new().shuffle(&mut peers); //TODO: sort by rating
|
||||||
// prefer peers with higher protocol version
|
// prefer peers with higher protocol version
|
||||||
peers.sort_by(|&(_, _, ref v1), &(_, _, ref v2)| v1.cmp(v2));
|
peers.sort_by(|&(_, _, ref v1), &(_, _, ref v2)| v1.cmp(v2));
|
||||||
trace!(target: "sync", "Syncing with peers: {} active, {} confirmed, {} total", self.active_peers.len(), peers.len(), self.peers.len());
|
trace!(target: "sync", "Syncing with peers: {} active, {} confirmed, {} total", self.active_peers.len(), peers.len(), self.peers.len());
|
||||||
@ -1881,7 +1892,7 @@ impl ChainSync {
|
|||||||
let mut count = (peers.len() as f64).powf(0.5).round() as usize;
|
let mut count = (peers.len() as f64).powf(0.5).round() as usize;
|
||||||
count = min(count, MAX_PEERS_PROPAGATION);
|
count = min(count, MAX_PEERS_PROPAGATION);
|
||||||
count = max(count, MIN_PEERS_PROPAGATION);
|
count = max(count, MIN_PEERS_PROPAGATION);
|
||||||
::rand::thread_rng().shuffle(&mut peers);
|
random::new().shuffle(&mut peers);
|
||||||
peers.truncate(count);
|
peers.truncate(count);
|
||||||
peers
|
peers
|
||||||
}
|
}
|
||||||
@ -1961,10 +1972,11 @@ impl ChainSync {
|
|||||||
let small = self.peers.len() < MIN_PEERS_PROPAGATION;
|
let small = self.peers.len() < MIN_PEERS_PROPAGATION;
|
||||||
let block_number = io.chain().chain_info().best_block_number;
|
let block_number = io.chain().chain_info().best_block_number;
|
||||||
|
|
||||||
|
let mut random = random::new();
|
||||||
let lucky_peers = {
|
let lucky_peers = {
|
||||||
let stats = &mut self.transactions_stats;
|
let stats = &mut self.transactions_stats;
|
||||||
self.peers.iter_mut()
|
self.peers.iter_mut()
|
||||||
.filter(|_| small || ::rand::random::<u32>() < fraction)
|
.filter(|_| small || random.next_u32() < fraction)
|
||||||
.take(MAX_PEERS_PROPAGATION)
|
.take(MAX_PEERS_PROPAGATION)
|
||||||
.filter_map(|(peer_id, mut peer_info)| {
|
.filter_map(|(peer_id, mut peer_info)| {
|
||||||
// Send all transactions
|
// Send all transactions
|
||||||
@ -2056,7 +2068,7 @@ impl ChainSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// called when block is imported to chain - propagates the blocks and updates transactions sent to peers
|
/// called when block is imported to chain - propagates the blocks and updates transactions sent to peers
|
||||||
pub fn chain_new_blocks(&mut self, io: &mut SyncIo, _imported: &[H256], invalid: &[H256], _enacted: &[H256], _retracted: &[H256], sealed: &[H256], proposed: &[Bytes]) {
|
pub fn chain_new_blocks(&mut self, io: &mut SyncIo, _imported: &[H256], invalid: &[H256], enacted: &[H256], _retracted: &[H256], sealed: &[H256], proposed: &[Bytes]) {
|
||||||
let queue_info = io.chain().queue_info();
|
let queue_info = io.chain().queue_info();
|
||||||
if !self.status().is_syncing(queue_info) || !sealed.is_empty() {
|
if !self.status().is_syncing(queue_info) || !sealed.is_empty() {
|
||||||
trace!(target: "sync", "Propagating blocks, state={:?}", self.state);
|
trace!(target: "sync", "Propagating blocks, state={:?}", self.state);
|
||||||
@ -2067,6 +2079,21 @@ impl ChainSync {
|
|||||||
trace!(target: "sync", "Bad blocks in the queue, restarting");
|
trace!(target: "sync", "Bad blocks in the queue, restarting");
|
||||||
self.restart(io);
|
self.restart(io);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !enacted.is_empty() {
|
||||||
|
// Select random peers to re-broadcast transactions to.
|
||||||
|
let mut random = random::new();
|
||||||
|
let len = self.peers.len();
|
||||||
|
let peers = random.gen_range(0, min(len, 3));
|
||||||
|
trace!(target: "sync", "Re-broadcasting transactions to {} random peers.", peers);
|
||||||
|
|
||||||
|
for _ in 0..peers {
|
||||||
|
let peer = random.gen_range(0, len);
|
||||||
|
self.peers.values_mut().nth(peer).map(|mut peer_info| {
|
||||||
|
peer_info.last_sent_transactions.clear()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when peer sends us new consensus packet
|
/// Called when peer sends us new consensus packet
|
||||||
|
@ -76,18 +76,28 @@ impl<'db> TrieMut for FatDBMut<'db> {
|
|||||||
self.raw.get(&key.sha3())
|
self.raw.get(&key.sha3())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<()> {
|
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
let hash = key.sha3();
|
let hash = key.sha3();
|
||||||
self.raw.insert(&hash, value)?;
|
let out = self.raw.insert(&hash, value)?;
|
||||||
let db = self.raw.db_mut();
|
let db = self.raw.db_mut();
|
||||||
db.emplace(Self::to_aux_key(&hash), DBValue::from_slice(key));
|
|
||||||
Ok(())
|
// don't insert if it doesn't exist.
|
||||||
|
if out.is_none() {
|
||||||
|
db.emplace(Self::to_aux_key(&hash), DBValue::from_slice(key));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, key: &[u8]) -> super::Result<()> {
|
fn remove(&mut self, key: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
let hash = key.sha3();
|
let hash = key.sha3();
|
||||||
self.raw.db_mut().remove(&Self::to_aux_key(&hash));
|
let out = self.raw.remove(&hash)?;
|
||||||
self.raw.remove(&hash)
|
|
||||||
|
// don't remove if it already exists.
|
||||||
|
if out.is_some() {
|
||||||
|
self.raw.db_mut().remove(&Self::to_aux_key(&hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,13 +119,13 @@ pub trait TrieMut {
|
|||||||
/// What is the value of the given key in this trie?
|
/// What is the value of the given key in this trie?
|
||||||
fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result<Option<DBValue>> where 'a: 'key;
|
fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result<Option<DBValue>> where 'a: 'key;
|
||||||
|
|
||||||
/// Insert a `key`/`value` pair into the trie. An `empty` value is equivalent to removing
|
/// Insert a `key`/`value` pair into the trie. An empty value is equivalent to removing
|
||||||
/// `key` from the trie.
|
/// `key` from the trie. Returns the old value associated with this key, if it existed.
|
||||||
fn insert(&mut self, key: &[u8], value: &[u8]) -> Result<()>;
|
fn insert(&mut self, key: &[u8], value: &[u8]) -> Result<Option<DBValue>>;
|
||||||
|
|
||||||
/// Remove a `key` from the trie. Equivalent to making it equal to the empty
|
/// Remove a `key` from the trie. Equivalent to making it equal to the empty
|
||||||
/// value.
|
/// value. Returns the old value associated with this key, if it existed.
|
||||||
fn remove(&mut self, key: &[u8]) -> Result<()>;
|
fn remove(&mut self, key: &[u8]) -> Result<Option<DBValue>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trie iterator that also supports random access.
|
/// A trie iterator that also supports random access.
|
||||||
|
@ -68,11 +68,11 @@ impl<'db> TrieMut for SecTrieDBMut<'db> {
|
|||||||
self.raw.get(&key.sha3())
|
self.raw.get(&key.sha3())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<()> {
|
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
self.raw.insert(&key.sha3(), value)
|
self.raw.insert(&key.sha3(), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, key: &[u8]) -> super::Result<()> {
|
fn remove(&mut self, key: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
self.raw.remove(&key.sha3())
|
self.raw.remove(&key.sha3())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,14 +452,16 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// insert a key, value pair into the trie, creating new nodes if necessary.
|
/// insert a key, value pair into the trie, creating new nodes if necessary.
|
||||||
fn insert_at(&mut self, handle: NodeHandle, partial: NibbleSlice, value: DBValue) -> super::Result<(StorageHandle, bool)> {
|
fn insert_at(&mut self, handle: NodeHandle, partial: NibbleSlice, value: DBValue, old_val: &mut Option<DBValue>)
|
||||||
|
-> super::Result<(StorageHandle, bool)>
|
||||||
|
{
|
||||||
let h = match handle {
|
let h = match handle {
|
||||||
NodeHandle::InMemory(h) => h,
|
NodeHandle::InMemory(h) => h,
|
||||||
NodeHandle::Hash(h) => self.cache(h)?,
|
NodeHandle::Hash(h) => self.cache(h)?,
|
||||||
};
|
};
|
||||||
let stored = self.storage.destroy(h);
|
let stored = self.storage.destroy(h);
|
||||||
let (new_stored, changed) = self.inspect(stored, move |trie, stored| {
|
let (new_stored, changed) = self.inspect(stored, move |trie, stored| {
|
||||||
trie.insert_inspector(stored, partial, value).map(|a| a.into_action())
|
trie.insert_inspector(stored, partial, value, old_val).map(|a| a.into_action())
|
||||||
})?.expect("Insertion never deletes.");
|
})?.expect("Insertion never deletes.");
|
||||||
|
|
||||||
Ok((self.storage.alloc(new_stored), changed))
|
Ok((self.storage.alloc(new_stored), changed))
|
||||||
@ -467,7 +469,9 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
|
|
||||||
/// the insertion inspector.
|
/// the insertion inspector.
|
||||||
#[cfg_attr(feature = "dev", allow(cyclomatic_complexity))]
|
#[cfg_attr(feature = "dev", allow(cyclomatic_complexity))]
|
||||||
fn insert_inspector(&mut self, node: Node, partial: NibbleSlice, value: DBValue) -> super::Result<InsertAction> {
|
fn insert_inspector(&mut self, node: Node, partial: NibbleSlice, value: DBValue, old_val: &mut Option<DBValue>)
|
||||||
|
-> super::Result<InsertAction>
|
||||||
|
{
|
||||||
trace!(target: "trie", "augmented (partial: {:?}, value: {:?})", partial, value.pretty());
|
trace!(target: "trie", "augmented (partial: {:?}, value: {:?})", partial, value.pretty());
|
||||||
|
|
||||||
Ok(match node {
|
Ok(match node {
|
||||||
@ -481,6 +485,8 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
if partial.is_empty() {
|
if partial.is_empty() {
|
||||||
let unchanged = stored_value.as_ref() == Some(&value);
|
let unchanged = stored_value.as_ref() == Some(&value);
|
||||||
let branch = Node::Branch(children, Some(value));
|
let branch = Node::Branch(children, Some(value));
|
||||||
|
*old_val = stored_value;
|
||||||
|
|
||||||
match unchanged {
|
match unchanged {
|
||||||
true => InsertAction::Restore(branch),
|
true => InsertAction::Restore(branch),
|
||||||
false => InsertAction::Replace(branch),
|
false => InsertAction::Replace(branch),
|
||||||
@ -490,7 +496,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
let partial = partial.mid(1);
|
let partial = partial.mid(1);
|
||||||
if let Some(child) = children[idx].take() {
|
if let Some(child) = children[idx].take() {
|
||||||
// original had something there. recurse down into it.
|
// original had something there. recurse down into it.
|
||||||
let (new_child, changed) = self.insert_at(child, partial, value)?;
|
let (new_child, changed) = self.insert_at(child, partial, value, old_val)?;
|
||||||
children[idx] = Some(new_child.into());
|
children[idx] = Some(new_child.into());
|
||||||
if !changed {
|
if !changed {
|
||||||
// the new node we composed didn't change. that means our branch is untouched too.
|
// the new node we composed didn't change. that means our branch is untouched too.
|
||||||
@ -511,7 +517,10 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
if cp == existing_key.len() && cp == partial.len() {
|
if cp == existing_key.len() && cp == partial.len() {
|
||||||
trace!(target: "trie", "equivalent-leaf: REPLACE");
|
trace!(target: "trie", "equivalent-leaf: REPLACE");
|
||||||
// equivalent leaf.
|
// equivalent leaf.
|
||||||
match stored_value == value {
|
let unchanged = stored_value == value;
|
||||||
|
*old_val = Some(stored_value);
|
||||||
|
|
||||||
|
match unchanged {
|
||||||
// unchanged. restore
|
// unchanged. restore
|
||||||
true => InsertAction::Restore(Node::Leaf(encoded.clone(), value)),
|
true => InsertAction::Restore(Node::Leaf(encoded.clone(), value)),
|
||||||
false => InsertAction::Replace(Node::Leaf(encoded.clone(), value)),
|
false => InsertAction::Replace(Node::Leaf(encoded.clone(), value)),
|
||||||
@ -533,7 +542,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// always replace because whatever we get out here is not the branch we started with.
|
// always replace because whatever we get out here is not the branch we started with.
|
||||||
let branch_action = self.insert_inspector(branch, partial, value)?.unwrap_node();
|
let branch_action = self.insert_inspector(branch, partial, value, old_val)?.unwrap_node();
|
||||||
InsertAction::Replace(branch_action)
|
InsertAction::Replace(branch_action)
|
||||||
} else if cp == existing_key.len() {
|
} else if cp == existing_key.len() {
|
||||||
trace!(target: "trie", "complete-prefix (cp={:?}): AUGMENT-AT-END", cp);
|
trace!(target: "trie", "complete-prefix (cp={:?}): AUGMENT-AT-END", cp);
|
||||||
@ -542,7 +551,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
// make a stub branch and an extension.
|
// make a stub branch and an extension.
|
||||||
let branch = Node::Branch(empty_children(), Some(stored_value));
|
let branch = Node::Branch(empty_children(), Some(stored_value));
|
||||||
// augment the new branch.
|
// augment the new branch.
|
||||||
let branch = self.insert_inspector(branch, partial.mid(cp), value)?.unwrap_node();
|
let branch = self.insert_inspector(branch, partial.mid(cp), value, old_val)?.unwrap_node();
|
||||||
|
|
||||||
// always replace since we took a leaf and made an extension.
|
// always replace since we took a leaf and made an extension.
|
||||||
let branch_handle = self.storage.alloc(Stored::New(branch)).into();
|
let branch_handle = self.storage.alloc(Stored::New(branch)).into();
|
||||||
@ -553,9 +562,10 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
// partially-shared prefix for an extension.
|
// partially-shared prefix for an extension.
|
||||||
// start by making a leaf.
|
// start by making a leaf.
|
||||||
let low = Node::Leaf(existing_key.mid(cp).encoded(true), stored_value);
|
let low = Node::Leaf(existing_key.mid(cp).encoded(true), stored_value);
|
||||||
|
|
||||||
// augment it. this will result in the Leaf -> cp == 0 routine,
|
// augment it. this will result in the Leaf -> cp == 0 routine,
|
||||||
// which creates a branch.
|
// which creates a branch.
|
||||||
let augmented_low = self.insert_inspector(low, partial.mid(cp), value)?.unwrap_node();
|
let augmented_low = self.insert_inspector(low, partial.mid(cp), value, old_val)?.unwrap_node();
|
||||||
|
|
||||||
// make an extension using it. this is a replacement.
|
// make an extension using it. this is a replacement.
|
||||||
InsertAction::Replace(Node::Extension(
|
InsertAction::Replace(Node::Extension(
|
||||||
@ -586,7 +596,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// continue inserting.
|
// continue inserting.
|
||||||
let branch_action = self.insert_inspector(Node::Branch(children, None), partial, value)?.unwrap_node();
|
let branch_action = self.insert_inspector(Node::Branch(children, None), partial, value, old_val)?.unwrap_node();
|
||||||
InsertAction::Replace(branch_action)
|
InsertAction::Replace(branch_action)
|
||||||
} else if cp == existing_key.len() {
|
} else if cp == existing_key.len() {
|
||||||
trace!(target: "trie", "complete-prefix (cp={:?}): AUGMENT-AT-END", cp);
|
trace!(target: "trie", "complete-prefix (cp={:?}): AUGMENT-AT-END", cp);
|
||||||
@ -594,7 +604,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
// fully-shared prefix.
|
// fully-shared prefix.
|
||||||
|
|
||||||
// insert into the child node.
|
// insert into the child node.
|
||||||
let (new_child, changed) = self.insert_at(child_branch, partial.mid(cp), value)?;
|
let (new_child, changed) = self.insert_at(child_branch, partial.mid(cp), value, old_val)?;
|
||||||
let new_ext = Node::Extension(existing_key.encoded(false), new_child.into());
|
let new_ext = Node::Extension(existing_key.encoded(false), new_child.into());
|
||||||
|
|
||||||
// if the child branch wasn't changed, meaning this extension remains the same.
|
// if the child branch wasn't changed, meaning this extension remains the same.
|
||||||
@ -608,7 +618,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
// partially-shared.
|
// partially-shared.
|
||||||
let low = Node::Extension(existing_key.mid(cp).encoded(false), child_branch);
|
let low = Node::Extension(existing_key.mid(cp).encoded(false), child_branch);
|
||||||
// augment the extension. this will take the cp == 0 path, creating a branch.
|
// augment the extension. this will take the cp == 0 path, creating a branch.
|
||||||
let augmented_low = self.insert_inspector(low, partial.mid(cp), value)?.unwrap_node();
|
let augmented_low = self.insert_inspector(low, partial.mid(cp), value, old_val)?.unwrap_node();
|
||||||
|
|
||||||
// always replace, since this extension is not the one we started with.
|
// always replace, since this extension is not the one we started with.
|
||||||
// this is known because the partial key is only the common prefix.
|
// this is known because the partial key is only the common prefix.
|
||||||
@ -622,7 +632,9 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a node from the trie based on key.
|
/// Remove a node from the trie based on key.
|
||||||
fn remove_at(&mut self, handle: NodeHandle, partial: NibbleSlice) -> super::Result<Option<(StorageHandle, bool)>> {
|
fn remove_at(&mut self, handle: NodeHandle, partial: NibbleSlice, old_val: &mut Option<DBValue>)
|
||||||
|
-> super::Result<Option<(StorageHandle, bool)>>
|
||||||
|
{
|
||||||
let stored = match handle {
|
let stored = match handle {
|
||||||
NodeHandle::InMemory(h) => self.storage.destroy(h),
|
NodeHandle::InMemory(h) => self.storage.destroy(h),
|
||||||
NodeHandle::Hash(h) => {
|
NodeHandle::Hash(h) => {
|
||||||
@ -631,17 +643,18 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let opt = self.inspect(stored, move |trie, node| trie.remove_inspector(node, partial))?;
|
let opt = self.inspect(stored, move |trie, node| trie.remove_inspector(node, partial, old_val))?;
|
||||||
|
|
||||||
Ok(opt.map(|(new, changed)| (self.storage.alloc(new), changed)))
|
Ok(opt.map(|(new, changed)| (self.storage.alloc(new), changed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the removal inspector
|
/// the removal inspector
|
||||||
fn remove_inspector(&mut self, node: Node, partial: NibbleSlice) -> super::Result<Action> {
|
fn remove_inspector(&mut self, node: Node, partial: NibbleSlice, old_val: &mut Option<DBValue>) -> super::Result<Action> {
|
||||||
Ok(match (node, partial.is_empty()) {
|
Ok(match (node, partial.is_empty()) {
|
||||||
(Node::Empty, _) => Action::Delete,
|
(Node::Empty, _) => Action::Delete,
|
||||||
(Node::Branch(c, None), true) => Action::Restore(Node::Branch(c, None)),
|
(Node::Branch(c, None), true) => Action::Restore(Node::Branch(c, None)),
|
||||||
(Node::Branch(children, _), true) => {
|
(Node::Branch(children, Some(val)), true) => {
|
||||||
|
*old_val = Some(val);
|
||||||
// always replace since we took the value out.
|
// always replace since we took the value out.
|
||||||
Action::Replace(self.fix(Node::Branch(children, None))?)
|
Action::Replace(self.fix(Node::Branch(children, None))?)
|
||||||
}
|
}
|
||||||
@ -649,7 +662,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
let idx = partial.at(0) as usize;
|
let idx = partial.at(0) as usize;
|
||||||
if let Some(child) = children[idx].take() {
|
if let Some(child) = children[idx].take() {
|
||||||
trace!(target: "trie", "removing value out of branch child, partial={:?}", partial);
|
trace!(target: "trie", "removing value out of branch child, partial={:?}", partial);
|
||||||
match self.remove_at(child, partial.mid(1))? {
|
match self.remove_at(child, partial.mid(1), old_val)? {
|
||||||
Some((new, changed)) => {
|
Some((new, changed)) => {
|
||||||
children[idx] = Some(new.into());
|
children[idx] = Some(new.into());
|
||||||
let branch = Node::Branch(children, value);
|
let branch = Node::Branch(children, value);
|
||||||
@ -675,6 +688,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
(Node::Leaf(encoded, value), _) => {
|
(Node::Leaf(encoded, value), _) => {
|
||||||
if NibbleSlice::from_encoded(&encoded).0 == partial {
|
if NibbleSlice::from_encoded(&encoded).0 == partial {
|
||||||
// this is the node we were looking for. Let's delete it.
|
// this is the node we were looking for. Let's delete it.
|
||||||
|
*old_val = Some(value);
|
||||||
Action::Delete
|
Action::Delete
|
||||||
} else {
|
} else {
|
||||||
// leaf the node alone.
|
// leaf the node alone.
|
||||||
@ -690,7 +704,7 @@ impl<'a> TrieDBMut<'a> {
|
|||||||
if cp == existing_len {
|
if cp == existing_len {
|
||||||
// try to remove from the child branch.
|
// try to remove from the child branch.
|
||||||
trace!(target: "trie", "removing from extension child, partial={:?}", partial);
|
trace!(target: "trie", "removing from extension child, partial={:?}", partial);
|
||||||
match self.remove_at(child_branch, partial.mid(cp))? {
|
match self.remove_at(child_branch, partial.mid(cp), old_val)? {
|
||||||
Some((new_child, changed)) => {
|
Some((new_child, changed)) => {
|
||||||
let new_child = new_child.into();
|
let new_child = new_child.into();
|
||||||
|
|
||||||
@ -907,28 +921,35 @@ impl<'a> TrieMut for TrieDBMut<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<()> {
|
fn insert(&mut self, key: &[u8], value: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
if value.is_empty() {
|
if value.is_empty() { return self.remove(key) }
|
||||||
return self.remove(key);
|
|
||||||
}
|
let mut old_val = None;
|
||||||
|
|
||||||
trace!(target: "trie", "insert: key={:?}, value={:?}", key.pretty(), value.pretty());
|
trace!(target: "trie", "insert: key={:?}, value={:?}", key.pretty(), value.pretty());
|
||||||
|
|
||||||
let root_handle = self.root_handle();
|
let root_handle = self.root_handle();
|
||||||
let (new_handle, changed) = self.insert_at(root_handle, NibbleSlice::new(key), DBValue::from_slice(value))?;
|
let (new_handle, changed) = self.insert_at(
|
||||||
|
root_handle,
|
||||||
|
NibbleSlice::new(key),
|
||||||
|
DBValue::from_slice(value),
|
||||||
|
&mut old_val,
|
||||||
|
)?;
|
||||||
|
|
||||||
trace!(target: "trie", "insert: altered trie={}", changed);
|
trace!(target: "trie", "insert: altered trie={}", changed);
|
||||||
self.root_handle = NodeHandle::InMemory(new_handle);
|
self.root_handle = NodeHandle::InMemory(new_handle);
|
||||||
|
|
||||||
Ok(())
|
Ok(old_val)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, key: &[u8]) -> super::Result<()> {
|
fn remove(&mut self, key: &[u8]) -> super::Result<Option<DBValue>> {
|
||||||
trace!(target: "trie", "remove: key={:?}", key.pretty());
|
trace!(target: "trie", "remove: key={:?}", key.pretty());
|
||||||
|
|
||||||
let root_handle = self.root_handle();
|
let root_handle = self.root_handle();
|
||||||
let key = NibbleSlice::new(key);
|
let key = NibbleSlice::new(key);
|
||||||
match self.remove_at(root_handle, key)? {
|
let mut old_val = None;
|
||||||
|
|
||||||
|
match self.remove_at(root_handle, key, &mut old_val)? {
|
||||||
Some((handle, changed)) => {
|
Some((handle, changed)) => {
|
||||||
trace!(target: "trie", "remove: altered trie={}", changed);
|
trace!(target: "trie", "remove: altered trie={}", changed);
|
||||||
self.root_handle = NodeHandle::InMemory(handle);
|
self.root_handle = NodeHandle::InMemory(handle);
|
||||||
@ -938,9 +959,9 @@ impl<'a> TrieMut for TrieDBMut<'a> {
|
|||||||
self.root_handle = NodeHandle::Hash(SHA3_NULL_RLP);
|
self.root_handle = NodeHandle::Hash(SHA3_NULL_RLP);
|
||||||
*self.root = SHA3_NULL_RLP;
|
*self.root = SHA3_NULL_RLP;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(old_val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1287,4 +1308,29 @@ mod tests {
|
|||||||
assert!(t.is_empty());
|
assert!(t.is_empty());
|
||||||
assert_eq!(*t.root(), SHA3_NULL_RLP);
|
assert_eq!(*t.root(), SHA3_NULL_RLP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_old_values() {
|
||||||
|
let mut seed = H256::new();
|
||||||
|
let x = StandardMap {
|
||||||
|
alphabet: Alphabet::Custom(b"@QWERTYUIOPASDFGHJKLZXCVBNM[/]^_".to_vec()),
|
||||||
|
min_key: 5,
|
||||||
|
journal_key: 0,
|
||||||
|
value_mode: ValueMode::Index,
|
||||||
|
count: 4,
|
||||||
|
}.make_with(&mut seed);
|
||||||
|
|
||||||
|
let mut db = MemoryDB::new();
|
||||||
|
let mut root = H256::new();
|
||||||
|
let mut t = TrieDBMut::new(&mut db, &mut root);
|
||||||
|
for &(ref key, ref value) in &x {
|
||||||
|
assert!(t.insert(key, value).unwrap().is_none());
|
||||||
|
assert_eq!(t.insert(key, value).unwrap(), Some(DBValue::from_slice(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in x {
|
||||||
|
assert_eq!(t.remove(&key).unwrap(), Some(DBValue::from_slice(&value)));
|
||||||
|
assert!(t.remove(&key).unwrap().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user