Merge pull request #5778 from paritytech/unlock
A CLI flag to allow fast transaction signing when account is unlocked.
This commit is contained in:
commit
8d04dffe69
@ -26,7 +26,7 @@ use std::time::{Instant, Duration};
|
|||||||
use util::{RwLock};
|
use util::{RwLock};
|
||||||
use ethstore::{
|
use ethstore::{
|
||||||
SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
|
SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
|
||||||
random_string, SecretVaultRef, StoreAccountRef,
|
random_string, SecretVaultRef, StoreAccountRef, OpaqueSecret,
|
||||||
};
|
};
|
||||||
use ethstore::dir::MemoryDirectory;
|
use ethstore::dir::MemoryDirectory;
|
||||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||||
@ -36,10 +36,10 @@ pub use ethstore::ethkey::Signature;
|
|||||||
pub use ethstore::{Derivation, IndexDerivation, KeyFile};
|
pub use ethstore::{Derivation, IndexDerivation, KeyFile};
|
||||||
|
|
||||||
/// Type of unlock.
|
/// Type of unlock.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, PartialEq)]
|
||||||
enum Unlock {
|
enum Unlock {
|
||||||
/// If account is unlocked temporarily, it should be locked after first usage.
|
/// If account is unlocked temporarily, it should be locked after first usage.
|
||||||
Temp,
|
OneTime,
|
||||||
/// Account unlocked permantently can always sign message.
|
/// Account unlocked permantently can always sign message.
|
||||||
/// Use with caution.
|
/// Use with caution.
|
||||||
Perm,
|
Perm,
|
||||||
@ -116,8 +116,13 @@ type AccountToken = String;
|
|||||||
/// Account management.
|
/// Account management.
|
||||||
/// Responsible for unlocking accounts.
|
/// Responsible for unlocking accounts.
|
||||||
pub struct AccountProvider {
|
pub struct AccountProvider {
|
||||||
|
/// For performance reasons some methods can re-use unlocked secrets.
|
||||||
|
unlocked_secrets: RwLock<HashMap<StoreAccountRef, OpaqueSecret>>,
|
||||||
|
/// Unlocked account data.
|
||||||
unlocked: RwLock<HashMap<StoreAccountRef, AccountData>>,
|
unlocked: RwLock<HashMap<StoreAccountRef, AccountData>>,
|
||||||
|
/// Address book.
|
||||||
address_book: RwLock<AddressBook>,
|
address_book: RwLock<AddressBook>,
|
||||||
|
/// Dapps settings.
|
||||||
dapps_settings: RwLock<DappsSettingsStore>,
|
dapps_settings: RwLock<DappsSettingsStore>,
|
||||||
/// Accounts on disk
|
/// Accounts on disk
|
||||||
sstore: Box<SecretStore>,
|
sstore: Box<SecretStore>,
|
||||||
@ -125,6 +130,9 @@ pub struct AccountProvider {
|
|||||||
transient_sstore: EthMultiStore,
|
transient_sstore: EthMultiStore,
|
||||||
/// Accounts in hardware wallets.
|
/// Accounts in hardware wallets.
|
||||||
hardware_store: Option<HardwareWalletManager>,
|
hardware_store: Option<HardwareWalletManager>,
|
||||||
|
/// When unlocking account permanently we additionally keep a raw secret in memory
|
||||||
|
/// to increase the performance of transaction signing.
|
||||||
|
unlock_keep_secret: bool,
|
||||||
/// Disallowed accounts.
|
/// Disallowed accounts.
|
||||||
blacklisted_accounts: Vec<Address>,
|
blacklisted_accounts: Vec<Address>,
|
||||||
}
|
}
|
||||||
@ -135,6 +143,8 @@ pub struct AccountProviderSettings {
|
|||||||
pub enable_hardware_wallets: bool,
|
pub enable_hardware_wallets: bool,
|
||||||
/// Use the classic chain key on the hardware wallet.
|
/// Use the classic chain key on the hardware wallet.
|
||||||
pub hardware_wallet_classic_key: bool,
|
pub hardware_wallet_classic_key: bool,
|
||||||
|
/// Store raw account secret when unlocking the account permanently.
|
||||||
|
pub unlock_keep_secret: bool,
|
||||||
/// Disallowed accounts.
|
/// Disallowed accounts.
|
||||||
pub blacklisted_accounts: Vec<Address>,
|
pub blacklisted_accounts: Vec<Address>,
|
||||||
}
|
}
|
||||||
@ -144,6 +154,7 @@ impl Default for AccountProviderSettings {
|
|||||||
AccountProviderSettings {
|
AccountProviderSettings {
|
||||||
enable_hardware_wallets: false,
|
enable_hardware_wallets: false,
|
||||||
hardware_wallet_classic_key: false,
|
hardware_wallet_classic_key: false,
|
||||||
|
unlock_keep_secret: false,
|
||||||
blacklisted_accounts: vec![],
|
blacklisted_accounts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,12 +181,14 @@ impl AccountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AccountProvider {
|
AccountProvider {
|
||||||
|
unlocked_secrets: RwLock::new(HashMap::new()),
|
||||||
unlocked: RwLock::new(HashMap::new()),
|
unlocked: RwLock::new(HashMap::new()),
|
||||||
address_book: RwLock::new(address_book),
|
address_book: RwLock::new(address_book),
|
||||||
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
|
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
|
||||||
sstore: sstore,
|
sstore: sstore,
|
||||||
transient_sstore: transient_sstore(),
|
transient_sstore: transient_sstore(),
|
||||||
hardware_store: hardware_store,
|
hardware_store: hardware_store,
|
||||||
|
unlock_keep_secret: settings.unlock_keep_secret,
|
||||||
blacklisted_accounts: settings.blacklisted_accounts,
|
blacklisted_accounts: settings.blacklisted_accounts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,12 +196,14 @@ impl AccountProvider {
|
|||||||
/// Creates not disk backed provider.
|
/// Creates not disk backed provider.
|
||||||
pub fn transient_provider() -> Self {
|
pub fn transient_provider() -> Self {
|
||||||
AccountProvider {
|
AccountProvider {
|
||||||
|
unlocked_secrets: RwLock::new(HashMap::new()),
|
||||||
unlocked: RwLock::new(HashMap::new()),
|
unlocked: RwLock::new(HashMap::new()),
|
||||||
address_book: RwLock::new(AddressBook::transient()),
|
address_book: RwLock::new(AddressBook::transient()),
|
||||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||||
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
|
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
|
||||||
transient_sstore: transient_sstore(),
|
transient_sstore: transient_sstore(),
|
||||||
hardware_store: None,
|
hardware_store: None,
|
||||||
|
unlock_keep_secret: false,
|
||||||
blacklisted_accounts: vec![],
|
blacklisted_accounts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -537,10 +552,7 @@ impl AccountProvider {
|
|||||||
|
|
||||||
/// Helper method used for unlocking accounts.
|
/// Helper method used for unlocking accounts.
|
||||||
fn unlock_account(&self, address: Address, password: String, unlock: Unlock) -> Result<(), Error> {
|
fn unlock_account(&self, address: Address, password: String, unlock: Unlock) -> Result<(), Error> {
|
||||||
// verify password by signing dump message
|
|
||||||
// result may be discarded
|
|
||||||
let account = self.sstore.account_ref(&address)?;
|
let account = self.sstore.account_ref(&address)?;
|
||||||
let _ = self.sstore.sign(&account, &password, &Default::default())?;
|
|
||||||
|
|
||||||
// check if account is already unlocked pernamently, if it is, do nothing
|
// check if account is already unlocked pernamently, if it is, do nothing
|
||||||
let mut unlocked = self.unlocked.write();
|
let mut unlocked = self.unlocked.write();
|
||||||
@ -550,6 +562,16 @@ impl AccountProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.unlock_keep_secret && unlock != Unlock::OneTime {
|
||||||
|
// verify password and get the secret
|
||||||
|
let secret = self.sstore.raw_secret(&account, &password)?;
|
||||||
|
self.unlocked_secrets.write().insert(account.clone(), secret);
|
||||||
|
} else {
|
||||||
|
// verify password by signing dump message
|
||||||
|
// result may be discarded
|
||||||
|
let _ = self.sstore.sign(&account, &password, &Default::default())?;
|
||||||
|
}
|
||||||
|
|
||||||
let data = AccountData {
|
let data = AccountData {
|
||||||
unlock: unlock,
|
unlock: unlock,
|
||||||
password: password,
|
password: password,
|
||||||
@ -562,7 +584,7 @@ impl AccountProvider {
|
|||||||
fn password(&self, account: &StoreAccountRef) -> Result<String, SignError> {
|
fn password(&self, account: &StoreAccountRef) -> Result<String, SignError> {
|
||||||
let mut unlocked = self.unlocked.write();
|
let mut unlocked = self.unlocked.write();
|
||||||
let data = unlocked.get(account).ok_or(SignError::NotUnlocked)?.clone();
|
let data = unlocked.get(account).ok_or(SignError::NotUnlocked)?.clone();
|
||||||
if let Unlock::Temp = data.unlock {
|
if let Unlock::OneTime = data.unlock {
|
||||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||||
}
|
}
|
||||||
if let Unlock::Timed(ref end) = data.unlock {
|
if let Unlock::Timed(ref end) = data.unlock {
|
||||||
@ -581,7 +603,7 @@ impl AccountProvider {
|
|||||||
|
|
||||||
/// Unlocks account temporarily (for one signing).
|
/// Unlocks account temporarily (for one signing).
|
||||||
pub fn unlock_account_temporarily(&self, account: Address, password: String) -> Result<(), Error> {
|
pub fn unlock_account_temporarily(&self, account: Address, password: String) -> Result<(), Error> {
|
||||||
self.unlock_account(account, password, Unlock::Temp)
|
self.unlock_account(account, password, Unlock::OneTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unlocks account temporarily with a timeout.
|
/// Unlocks account temporarily with a timeout.
|
||||||
@ -592,17 +614,25 @@ impl AccountProvider {
|
|||||||
/// Checks if given account is unlocked
|
/// Checks if given account is unlocked
|
||||||
pub fn is_unlocked(&self, address: Address) -> bool {
|
pub fn is_unlocked(&self, address: Address) -> bool {
|
||||||
let unlocked = self.unlocked.read();
|
let unlocked = self.unlocked.read();
|
||||||
|
let unlocked_secrets = self.unlocked_secrets.read();
|
||||||
self.sstore.account_ref(&address)
|
self.sstore.account_ref(&address)
|
||||||
.map(|r| unlocked.get(&r).is_some())
|
.map(|r| unlocked.get(&r).is_some() || unlocked_secrets.get(&r).is_some())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signs the message. If password is not provided the account must be unlocked.
|
/// Signs the message. If password is not provided the account must be unlocked.
|
||||||
pub fn sign(&self, address: Address, password: Option<String>, message: Message) -> Result<Signature, SignError> {
|
pub fn sign(&self, address: Address, password: Option<String>, message: Message) -> Result<Signature, SignError> {
|
||||||
let account = self.sstore.account_ref(&address)?;
|
let account = self.sstore.account_ref(&address)?;
|
||||||
|
match self.unlocked_secrets.read().get(&account) {
|
||||||
|
Some(secret) => {
|
||||||
|
Ok(self.sstore.sign_with_secret(&secret, &message)?)
|
||||||
|
},
|
||||||
|
None => {
|
||||||
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
|
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
|
||||||
Ok(self.sstore.sign(&account, &password, &message)?)
|
Ok(self.sstore.sign(&account, &password, &message)?)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs message using the derived secret. If password is not provided the account must be unlocked.
|
/// Signs message using the derived secret. If password is not provided the account must be unlocked.
|
||||||
pub fn sign_derived(&self, address: &Address, password: Option<String>, derivation: Derivation, message: Message)
|
pub fn sign_derived(&self, address: &Address, password: Option<String>, derivation: Derivation, message: Message)
|
||||||
|
@ -25,3 +25,4 @@ pub use self::crypto::Crypto;
|
|||||||
pub use self::kdf::{Kdf, Pbkdf2, Scrypt, Prf};
|
pub use self::kdf::{Kdf, Pbkdf2, Scrypt, Prf};
|
||||||
pub use self::safe_account::SafeAccount;
|
pub use self::safe_account::SafeAccount;
|
||||||
pub use self::version::Version;
|
pub use self::version::Version;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
|
|||||||
use account::SafeAccount;
|
use account::SafeAccount;
|
||||||
use presale::PresaleWallet;
|
use presale::PresaleWallet;
|
||||||
use json::{self, Uuid, OpaqueKeyFile};
|
use json::{self, Uuid, OpaqueKeyFile};
|
||||||
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation};
|
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation, OpaqueSecret};
|
||||||
|
|
||||||
/// Accounts store.
|
/// Accounts store.
|
||||||
pub struct EthStore {
|
pub struct EthStore {
|
||||||
@ -140,6 +140,10 @@ impl SimpleSecretStore for EthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SecretStore for EthStore {
|
impl SecretStore for EthStore {
|
||||||
|
fn raw_secret(&self, account: &StoreAccountRef, password: &str) -> Result<OpaqueSecret, Error> {
|
||||||
|
Ok(OpaqueSecret(self.get(account)?.crypto.secret(password)?))
|
||||||
|
}
|
||||||
|
|
||||||
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error> {
|
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error> {
|
||||||
let json_wallet = json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))?;
|
let json_wallet = json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))?;
|
||||||
let wallet = PresaleWallet::from(json_wallet);
|
let wallet = PresaleWallet::from(json_wallet);
|
||||||
|
@ -66,3 +66,6 @@ pub use self::secret_store::{
|
|||||||
};
|
};
|
||||||
pub use self::random::random_string;
|
pub use self::random::random_string;
|
||||||
pub use self::parity_wordlist::random_phrase;
|
pub use self::parity_wordlist::random_phrase;
|
||||||
|
|
||||||
|
/// An opaque wrapper for secret.
|
||||||
|
pub struct OpaqueSecret(::ethkey::Secret);
|
||||||
|
@ -20,6 +20,7 @@ use ethkey::{Address, Message, Signature, Secret, Public};
|
|||||||
use Error;
|
use Error;
|
||||||
use json::{Uuid, OpaqueKeyFile};
|
use json::{Uuid, OpaqueKeyFile};
|
||||||
use bigint::hash::H256;
|
use bigint::hash::H256;
|
||||||
|
use OpaqueSecret;
|
||||||
|
|
||||||
/// Key directory reference
|
/// Key directory reference
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -88,6 +89,15 @@ pub trait SimpleSecretStore: Send + Sync {
|
|||||||
|
|
||||||
/// Secret Store API
|
/// Secret Store API
|
||||||
pub trait SecretStore: SimpleSecretStore {
|
pub trait SecretStore: SimpleSecretStore {
|
||||||
|
|
||||||
|
/// Returns a raw opaque Secret that can be later used to sign a message.
|
||||||
|
fn raw_secret(&self, account: &StoreAccountRef, password: &str) -> Result<OpaqueSecret, Error>;
|
||||||
|
|
||||||
|
/// Signs a message with raw secret.
|
||||||
|
fn sign_with_secret(&self, secret: &OpaqueSecret, message: &Message) -> Result<Signature, Error> {
|
||||||
|
Ok(::ethkey::sign(&secret.0, message)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Imports presale wallet
|
/// Imports presale wallet
|
||||||
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
|
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
|
||||||
/// Imports existing JSON wallet
|
/// Imports existing JSON wallet
|
||||||
|
@ -114,6 +114,8 @@ usage! {
|
|||||||
or |c: &Config| otry!(c.account).keys_iterations.clone(),
|
or |c: &Config| otry!(c.account).keys_iterations.clone(),
|
||||||
flag_no_hardware_wallets: bool = false,
|
flag_no_hardware_wallets: bool = false,
|
||||||
or |c: &Config| otry!(c.account).disable_hardware.clone(),
|
or |c: &Config| otry!(c.account).disable_hardware.clone(),
|
||||||
|
flag_fast_unlock: bool = false,
|
||||||
|
or |c: &Config| otry!(c.account).fast_unlock.clone(),
|
||||||
|
|
||||||
|
|
||||||
flag_force_ui: bool = false,
|
flag_force_ui: bool = false,
|
||||||
@ -424,6 +426,7 @@ struct Account {
|
|||||||
password: Option<Vec<String>>,
|
password: Option<Vec<String>>,
|
||||||
keys_iterations: Option<u32>,
|
keys_iterations: Option<u32>,
|
||||||
disable_hardware: Option<bool>,
|
disable_hardware: Option<bool>,
|
||||||
|
fast_unlock: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, RustcDecodable)]
|
#[derive(Default, Debug, PartialEq, RustcDecodable)]
|
||||||
@ -707,6 +710,7 @@ mod tests {
|
|||||||
flag_password: vec!["~/.safe/password.file".into()],
|
flag_password: vec!["~/.safe/password.file".into()],
|
||||||
flag_keys_iterations: 10240u32,
|
flag_keys_iterations: 10240u32,
|
||||||
flag_no_hardware_wallets: false,
|
flag_no_hardware_wallets: false,
|
||||||
|
flag_fast_unlock: false,
|
||||||
|
|
||||||
flag_force_ui: false,
|
flag_force_ui: false,
|
||||||
flag_no_ui: false,
|
flag_no_ui: false,
|
||||||
@ -927,6 +931,7 @@ mod tests {
|
|||||||
password: Some(vec!["passwdfile path".into()]),
|
password: Some(vec!["passwdfile path".into()]),
|
||||||
keys_iterations: None,
|
keys_iterations: None,
|
||||||
disable_hardware: None,
|
disable_hardware: None,
|
||||||
|
fast_unlock: None,
|
||||||
}),
|
}),
|
||||||
ui: Some(Ui {
|
ui: Some(Ui {
|
||||||
force: None,
|
force: None,
|
||||||
|
@ -99,6 +99,9 @@ Account Options:
|
|||||||
deriving key from the password (bigger is more
|
deriving key from the password (bigger is more
|
||||||
secure) (default: {flag_keys_iterations}).
|
secure) (default: {flag_keys_iterations}).
|
||||||
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets})
|
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets})
|
||||||
|
--fast-unlock Use drasticly faster unlocking mode. This setting causes
|
||||||
|
raw secrets to be stored unprotected in memory,
|
||||||
|
so use with care. (default: {flag_fast_unlock})
|
||||||
|
|
||||||
UI Options:
|
UI Options:
|
||||||
--force-ui Enable Trusted UI WebSocket endpoint,
|
--force-ui Enable Trusted UI WebSocket endpoint,
|
||||||
|
@ -497,6 +497,7 @@ impl Configuration {
|
|||||||
password_files: self.args.flag_password.clone(),
|
password_files: self.args.flag_password.clone(),
|
||||||
unlocked_accounts: to_addresses(&self.args.flag_unlock)?,
|
unlocked_accounts: to_addresses(&self.args.flag_unlock)?,
|
||||||
enable_hardware_wallets: !self.args.flag_no_hardware_wallets,
|
enable_hardware_wallets: !self.args.flag_no_hardware_wallets,
|
||||||
|
enable_fast_unlock: self.args.flag_fast_unlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
|
@ -180,6 +180,7 @@ pub struct AccountsConfig {
|
|||||||
pub password_files: Vec<String>,
|
pub password_files: Vec<String>,
|
||||||
pub unlocked_accounts: Vec<Address>,
|
pub unlocked_accounts: Vec<Address>,
|
||||||
pub enable_hardware_wallets: bool,
|
pub enable_hardware_wallets: bool,
|
||||||
|
pub enable_fast_unlock: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AccountsConfig {
|
impl Default for AccountsConfig {
|
||||||
@ -190,6 +191,7 @@ impl Default for AccountsConfig {
|
|||||||
password_files: Vec::new(),
|
password_files: Vec::new(),
|
||||||
unlocked_accounts: Vec::new(),
|
unlocked_accounts: Vec::new(),
|
||||||
enable_hardware_wallets: true,
|
enable_hardware_wallets: true,
|
||||||
|
enable_fast_unlock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -777,6 +777,7 @@ fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str,
|
|||||||
let account_settings = AccountProviderSettings {
|
let account_settings = AccountProviderSettings {
|
||||||
enable_hardware_wallets: cfg.enable_hardware_wallets,
|
enable_hardware_wallets: cfg.enable_hardware_wallets,
|
||||||
hardware_wallet_classic_key: spec == &SpecType::Classic,
|
hardware_wallet_classic_key: spec == &SpecType::Classic,
|
||||||
|
unlock_keep_secret: cfg.enable_fast_unlock,
|
||||||
blacklisted_accounts: match *spec {
|
blacklisted_accounts: match *spec {
|
||||||
SpecType::Morden | SpecType::Ropsten | SpecType::Kovan | SpecType::Dev => vec![],
|
SpecType::Morden | SpecType::Ropsten | SpecType::Kovan | SpecType::Dev => vec![],
|
||||||
_ => vec![
|
_ => vec![
|
||||||
|
Loading…
Reference in New Issue
Block a user