Merge branch 'master' into rotating-key
Conflicts: ethcore/src/account_provider/mod.rs rpc/src/v1/types/mod.rs.in
This commit is contained in:
468
ethcore/src/account_provider/mod.rs
Normal file
468
ethcore/src/account_provider/mod.rs
Normal file
@@ -0,0 +1,468 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Account management.
|
||||
|
||||
mod stores;
|
||||
|
||||
use self::stores::{AddressBook, DappsSettingsStore};
|
||||
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Instant, Duration};
|
||||
use util::{Mutex, RwLock, Itertools};
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, SafeAccount, EthStore, EthMultiStore, random_string};
|
||||
use ethstore::dir::{KeyDirectory};
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
pub use ethstore::ethkey::Signature;
|
||||
|
||||
/// Type of unlock.
|
||||
#[derive(Clone)]
|
||||
enum Unlock {
|
||||
/// If account is unlocked temporarily, it should be locked after first usage.
|
||||
Temp,
|
||||
/// Account unlocked permantently can always sign message.
|
||||
/// Use with caution.
|
||||
Perm,
|
||||
/// Account unlocked with a timeout
|
||||
Timed(Instant),
|
||||
}
|
||||
|
||||
/// Data associated with account.
|
||||
#[derive(Clone)]
|
||||
struct AccountData {
|
||||
unlock: Unlock,
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// `AccountProvider` errors.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Returned when account is not unlocked.
|
||||
NotUnlocked,
|
||||
/// Returned when signing fails.
|
||||
SStore(SSError),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Error::NotUnlocked => write!(f, "Account is locked"),
|
||||
Error::SStore(ref e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SSError> for Error {
|
||||
fn from(e: SSError) -> Self {
|
||||
Error::SStore(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NullDir {
|
||||
accounts: RwLock<HashMap<Address, Vec<SafeAccount>>>,
|
||||
}
|
||||
|
||||
impl KeyDirectory for NullDir {
|
||||
fn load(&self) -> Result<Vec<SafeAccount>, SSError> {
|
||||
Ok(self.accounts.read().values().cloned().flatten().collect())
|
||||
}
|
||||
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, SSError> {
|
||||
let mut lock = self.accounts.write();
|
||||
let mut accounts = lock.entry(account.address.clone()).or_insert_with(Vec::new);
|
||||
// If the filename is the same we just need to replace the entry
|
||||
accounts.retain(|acc| acc.filename != account.filename);
|
||||
accounts.push(account.clone());
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), SSError> {
|
||||
let mut accounts = self.accounts.write();
|
||||
let is_empty = if let Some(mut accounts) = accounts.get_mut(&account.address) {
|
||||
if let Some(position) = accounts.iter().position(|acc| acc == account) {
|
||||
accounts.remove(position);
|
||||
}
|
||||
accounts.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if is_empty {
|
||||
accounts.remove(&account.address);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dapp identifier
|
||||
pub type DappId = String;
|
||||
|
||||
fn transient_sstore() -> EthMultiStore {
|
||||
EthMultiStore::open(Box::new(NullDir::default())).expect("NullDir load always succeeds; qed")
|
||||
}
|
||||
|
||||
type AccountToken = String;
|
||||
|
||||
/// Account management.
|
||||
/// Responsible for unlocking accounts.
|
||||
pub struct AccountProvider {
|
||||
address_book: Mutex<AddressBook>,
|
||||
unlocked: Mutex<HashMap<Address, AccountData>>,
|
||||
dapps_settings: RwLock<DappsSettingsStore>,
|
||||
/// Accounts on disk
|
||||
sstore: Box<SecretStore>,
|
||||
/// Accounts unlocked with rolling tokens
|
||||
transient_sstore: EthMultiStore,
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
/// Creates new account provider.
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates not disk backed provider.
|
||||
pub fn transient_provider() -> Self {
|
||||
AccountProvider {
|
||||
address_book: Mutex::new(AddressBook::transient()),
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||
sstore: Box::new(EthStore::open(Box::new(NullDir::default())).expect("NullDir load always succeeds; qed")),
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new random account.
|
||||
pub fn new_account(&self, password: &str) -> Result<Address, Error> {
|
||||
self.new_account_and_public(password).map(|d| d.0)
|
||||
}
|
||||
|
||||
/// Creates new random account and returns address and public key
|
||||
pub fn new_account_and_public(&self, password: &str) -> Result<(Address, Public), Error> {
|
||||
let acc = Random.generate().expect("secp context has generation capabilities; qed");
|
||||
let public = acc.public().clone();
|
||||
let secret = acc.secret().clone();
|
||||
let address = try!(self.sstore.insert_account(secret, password));
|
||||
Ok((address, public))
|
||||
}
|
||||
|
||||
/// Inserts new account into underlying store.
|
||||
/// Does not unlock account!
|
||||
pub fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
let address = try!(self.sstore.insert_account(secret, password));
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_presale(&self, presale_json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let address = try!(self.sstore.import_presale(presale_json, password));
|
||||
Ok(Address::from(address).into())
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let address = try!(self.sstore.import_wallet(json, password));
|
||||
Ok(Address::from(address).into())
|
||||
}
|
||||
|
||||
/// Returns addresses of all accounts.
|
||||
pub fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
let accounts = try!(self.sstore.accounts());
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// Gets addresses visile for dapp.
|
||||
pub fn dapps_addresses(&self, dapp: DappId) -> Result<Vec<Address>, Error> {
|
||||
let accounts = self.dapps_settings.read().get();
|
||||
Ok(accounts.get(&dapp).map(|settings| settings.accounts.clone()).unwrap_or_else(Vec::new))
|
||||
}
|
||||
|
||||
/// Sets addresses visile for dapp.
|
||||
pub fn set_dapps_addresses(&self, dapp: DappId, addresses: Vec<Address>) -> Result<(), Error> {
|
||||
self.dapps_settings.write().set_accounts(dapp, addresses);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn addresses_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
Ok(self.address_book.read().get())
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn set_address_name(&self, account: Address, name: String) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().set_name(account, name))
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn set_address_meta(&self, account: Address, meta: String) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().set_meta(account, meta))
|
||||
}
|
||||
|
||||
/// Removes and address from the addressbook
|
||||
pub fn remove_address(&self, addr: Address) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().remove(addr))
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
let r: HashMap<Address, AccountMeta> = try!(self.sstore.accounts())
|
||||
.into_iter()
|
||||
.map(|a| (a.clone(), self.account_meta(a).ok().unwrap_or_default()))
|
||||
.collect();
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn account_meta(&self, account: Address) -> Result<AccountMeta, Error> {
|
||||
Ok(AccountMeta {
|
||||
name: try!(self.sstore.name(&account)),
|
||||
meta: try!(self.sstore.meta(&account)),
|
||||
uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a UUID
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn set_account_name(&self, account: Address, name: String) -> Result<(), Error> {
|
||||
try!(self.sstore.set_name(&account, name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn set_account_meta(&self, account: Address, meta: String) -> Result<(), Error> {
|
||||
try!(self.sstore.set_meta(&account, meta));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the password for `account` is `password`. `false` if not.
|
||||
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
|
||||
self.sstore.test_password(account, password)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Permanently removes an account.
|
||||
pub fn kill_account(&self, account: &Address, password: &str) -> Result<(), Error> {
|
||||
try!(self.sstore.remove_account(account, &password));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Changes the password of `account` from `password` to `new_password`. Fails if incorrect `password` given.
|
||||
pub fn change_password(&self, account: &Address, password: String, new_password: String) -> Result<(), Error> {
|
||||
self.sstore.change_password(account, &password, &new_password).map_err(Error::SStore)
|
||||
}
|
||||
|
||||
/// Helper method used for unlocking accounts.
|
||||
fn unlock_account(&self, account: Address, password: String, unlock: Unlock) -> Result<(), Error> {
|
||||
// verify password by signing dump message
|
||||
// result may be discarded
|
||||
let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
|
||||
|
||||
// check if account is already unlocked pernamently, if it is, do nothing
|
||||
let mut unlocked = self.unlocked.lock();
|
||||
if let Some(data) = unlocked.get(&account) {
|
||||
if let Unlock::Perm = data.unlock {
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let data = AccountData {
|
||||
unlock: unlock,
|
||||
password: password,
|
||||
};
|
||||
|
||||
unlocked.insert(account, data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn password(&self, account: &Address) -> Result<String, Error> {
|
||||
let mut unlocked = self.unlocked.lock();
|
||||
let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone();
|
||||
if let Unlock::Temp = data.unlock {
|
||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||
}
|
||||
if let Unlock::Timed(ref end) = data.unlock {
|
||||
if Instant::now() > *end {
|
||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||
return Err(Error::NotUnlocked);
|
||||
}
|
||||
}
|
||||
Ok(data.password.clone())
|
||||
}
|
||||
|
||||
/// Unlocks account permanently.
|
||||
pub fn unlock_account_permanently(&self, account: Address, password: String) -> Result<(), Error> {
|
||||
self.unlock_account(account, password, Unlock::Perm)
|
||||
}
|
||||
|
||||
/// Unlocks account temporarily (for one signing).
|
||||
pub fn unlock_account_temporarily(&self, account: Address, password: String) -> Result<(), Error> {
|
||||
self.unlock_account(account, password, Unlock::Temp)
|
||||
}
|
||||
|
||||
/// Unlocks account temporarily with a timeout.
|
||||
pub fn unlock_account_timed(&self, account: Address, password: String, duration_ms: u32) -> Result<(), Error> {
|
||||
self.unlock_account(account, password, Unlock::Timed(Instant::now() + Duration::from_millis(duration_ms as u64)))
|
||||
}
|
||||
|
||||
/// Checks if given account is unlocked
|
||||
pub fn is_unlocked(&self, account: Address) -> bool {
|
||||
let unlocked = self.unlocked.lock();
|
||||
unlocked.get(&account).is_some()
|
||||
}
|
||||
|
||||
/// Signs the message. If password is not provided the account must be unlocked.
|
||||
pub fn sign(&self, account: Address, password: Option<String>, message: Message) -> Result<Signature, Error> {
|
||||
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
|
||||
Ok(try!(self.sstore.sign(&account, &password, &message)))
|
||||
}
|
||||
|
||||
/// Signs given message with supplied token. Returns a token to use in next signing within this session.
|
||||
pub fn sign_with_token(&self, account: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), Error> {
|
||||
let is_std_password = try!(self.sstore.test_password(&account, &token));
|
||||
|
||||
let new_token = random_string(16);
|
||||
let signature = if is_std_password {
|
||||
// Insert to transient store
|
||||
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
|
||||
// sign
|
||||
try!(self.sstore.sign(&account, &token, &message))
|
||||
} else {
|
||||
// check transient store
|
||||
try!(self.transient_sstore.change_password(&account, &token, &new_token));
|
||||
// and sign
|
||||
try!(self.transient_sstore.sign(&account, &new_token, &message))
|
||||
};
|
||||
|
||||
Ok((signature, new_token))
|
||||
}
|
||||
|
||||
/// Decrypts a message with given token. Returns a token to use in next operation for this account.
|
||||
pub fn decrypt_with_token(&self, account: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
|
||||
-> Result<(Vec<u8>, AccountToken), Error>
|
||||
{
|
||||
let is_std_password = try!(self.sstore.test_password(&account, &token));
|
||||
|
||||
let new_token = random_string(16);
|
||||
let message = if is_std_password {
|
||||
// Insert to transient store
|
||||
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
|
||||
// decrypt
|
||||
try!(self.sstore.decrypt(&account, &token, shared_mac, message))
|
||||
} else {
|
||||
// check transient store
|
||||
try!(self.transient_sstore.change_password(&account, &token, &new_token));
|
||||
// and decrypt
|
||||
try!(self.transient_sstore.decrypt(&account, &token, shared_mac, message))
|
||||
};
|
||||
|
||||
Ok((message, new_token))
|
||||
}
|
||||
|
||||
/// Decrypts a message. If password is not provided the account must be unlocked.
|
||||
pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
|
||||
Ok(try!(self.sstore.decrypt(&account, &password, shared_mac, message)))
|
||||
}
|
||||
|
||||
/// Returns the underlying `SecretStore` reference if one exists.
|
||||
pub fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> {
|
||||
self.sstore.list_geth_accounts(testnet).into_iter().map(|a| Address::from(a).into()).collect()
|
||||
}
|
||||
|
||||
/// Returns the underlying `SecretStore` reference if one exists.
|
||||
pub fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> {
|
||||
self.sstore.import_geth_accounts(desired, testnet).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AccountProvider, Unlock};
|
||||
use std::time::Instant;
|
||||
use ethstore::ethkey::{Generator, Random};
|
||||
|
||||
#[test]
|
||||
fn unlock_account_temp() {
|
||||
let kp = Random.generate().unwrap();
|
||||
let ap = AccountProvider::transient_provider();
|
||||
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
|
||||
assert!(ap.unlock_account_temporarily(kp.address(), "test1".into()).is_err());
|
||||
assert!(ap.unlock_account_temporarily(kp.address(), "test".into()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlock_account_perm() {
|
||||
let kp = Random.generate().unwrap();
|
||||
let ap = AccountProvider::transient_provider();
|
||||
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
|
||||
assert!(ap.unlock_account_permanently(kp.address(), "test1".into()).is_err());
|
||||
assert!(ap.unlock_account_permanently(kp.address(), "test".into()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
assert!(ap.unlock_account_temporarily(kp.address(), "test".into()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlock_account_timer() {
|
||||
let kp = Random.generate().unwrap();
|
||||
let ap = AccountProvider::transient_provider();
|
||||
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err());
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
ap.unlocked.lock().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_sign_and_return_token() {
|
||||
// given
|
||||
let kp = Random.generate().unwrap();
|
||||
let ap = AccountProvider::transient_provider();
|
||||
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
|
||||
|
||||
// when
|
||||
let (_signature, token) = ap.sign_with_token(kp.address(), "test".into(), Default::default()).unwrap();
|
||||
|
||||
// then
|
||||
ap.sign_with_token(kp.address(), token.clone(), Default::default())
|
||||
.expect("First usage of token should be correct.");
|
||||
assert!(ap.sign_with_token(kp.address(), token, Default::default()).is_err(), "Second usage of the same token should fail.");
|
||||
}
|
||||
|
||||
fn should_set_dapps_addresses() {
|
||||
// given
|
||||
let ap = AccountProvider::transient_provider();
|
||||
let app = "app1".to_owned();
|
||||
|
||||
// when
|
||||
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]);
|
||||
}
|
||||
}
|
||||
271
ethcore/src/account_provider/stores.rs
Normal file
271
ethcore/src/account_provider/stores.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Address Book and Dapps Settings Store
|
||||
|
||||
use std::{fs, fmt, hash, ops};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ethstore::ethkey::Address;
|
||||
use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings};
|
||||
use account_provider::DappId;
|
||||
|
||||
/// Disk-backed map from Address to String. Uses JSON.
|
||||
pub struct AddressBook {
|
||||
cache: DiskMap<Address, AccountMeta>,
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
/// Creates new address book at given directory.
|
||||
pub fn new(path: String) -> Self {
|
||||
let mut r = AddressBook {
|
||||
cache: DiskMap::new(path, "address_book.json".into())
|
||||
};
|
||||
r.cache.revert(AccountMeta::read_address_map);
|
||||
r
|
||||
}
|
||||
|
||||
/// Creates transient address book (no changes are saved to disk).
|
||||
pub fn transient() -> Self {
|
||||
AddressBook {
|
||||
cache: DiskMap::transient()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the address book.
|
||||
pub fn get(&self) -> HashMap<Address, AccountMeta> {
|
||||
self.cache.clone()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
self.cache.save(AccountMeta::write_address_map)
|
||||
}
|
||||
|
||||
/// Sets new name for given address.
|
||||
pub fn set_name(&mut self, a: Address, name: String) {
|
||||
{
|
||||
let mut x = self.cache.entry(a)
|
||||
.or_insert_with(|| AccountMeta {name: Default::default(), meta: "{}".to_owned(), uuid: None});
|
||||
x.name = name;
|
||||
}
|
||||
self.save();
|
||||
}
|
||||
|
||||
/// Sets new meta for given address.
|
||||
pub fn set_meta(&mut self, a: Address, meta: String) {
|
||||
{
|
||||
let mut x = self.cache.entry(a)
|
||||
.or_insert_with(|| AccountMeta {name: "Anonymous".to_owned(), meta: Default::default(), uuid: None});
|
||||
x.meta = meta;
|
||||
}
|
||||
self.save();
|
||||
}
|
||||
|
||||
/// Removes an entry
|
||||
pub fn remove(&mut self, a: Address) {
|
||||
self.cache.remove(&a);
|
||||
self.save();
|
||||
}
|
||||
}
|
||||
|
||||
/// Dapps user settings
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct DappsSettings {
|
||||
/// A list of visible accounts
|
||||
pub accounts: Vec<Address>,
|
||||
}
|
||||
|
||||
impl From<JsonSettings> for DappsSettings {
|
||||
fn from(s: JsonSettings) -> Self {
|
||||
DappsSettings {
|
||||
accounts: s.accounts.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DappsSettings> for JsonSettings {
|
||||
fn from(s: DappsSettings) -> Self {
|
||||
JsonSettings {
|
||||
accounts: s.accounts.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Disk-backed map from DappId to Settings. Uses JSON.
|
||||
pub struct DappsSettingsStore {
|
||||
cache: DiskMap<DappId, DappsSettings>,
|
||||
}
|
||||
|
||||
impl DappsSettingsStore {
|
||||
/// Creates new store at given directory path.
|
||||
pub fn new(path: String) -> Self {
|
||||
let mut r = DappsSettingsStore {
|
||||
cache: DiskMap::new(path, "dapps_accounts.json".into())
|
||||
};
|
||||
r.cache.revert(JsonSettings::read_dapps_settings);
|
||||
r
|
||||
}
|
||||
|
||||
/// Creates transient store (no changes are saved to disk).
|
||||
pub fn transient() -> Self {
|
||||
DappsSettingsStore {
|
||||
cache: DiskMap::transient()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get copy of the dapps settings
|
||||
pub fn get(&self) -> HashMap<DappId, DappsSettings> {
|
||||
self.cache.clone()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
self.cache.save(JsonSettings::write_dapps_settings)
|
||||
}
|
||||
|
||||
pub fn set_accounts(&mut self, id: DappId, accounts: Vec<Address>) {
|
||||
{
|
||||
let mut settings = self.cache.entry(id).or_insert_with(DappsSettings::default);
|
||||
settings.accounts = accounts;
|
||||
}
|
||||
self.save();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disk-serializable HashMap
|
||||
#[derive(Debug)]
|
||||
struct DiskMap<K: hash::Hash + Eq, V> {
|
||||
path: PathBuf,
|
||||
cache: HashMap<K, V>,
|
||||
transient: bool,
|
||||
}
|
||||
|
||||
impl<K: hash::Hash + Eq, V> ops::Deref for DiskMap<K, V> {
|
||||
type Target = HashMap<K, V>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cache
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: hash::Hash + Eq, V> ops::DerefMut for DiskMap<K, V> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cache
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
|
||||
pub fn new(path: String, file_name: String) -> Self {
|
||||
trace!(target: "diskmap", "new({})", path);
|
||||
let mut path: PathBuf = path.into();
|
||||
path.push(file_name);
|
||||
trace!(target: "diskmap", "path={:?}", path);
|
||||
DiskMap {
|
||||
path: path,
|
||||
cache: HashMap::new(),
|
||||
transient: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transient() -> Self {
|
||||
let mut map = DiskMap::new(Default::default(), "diskmap.json".into());
|
||||
map.transient = true;
|
||||
map
|
||||
}
|
||||
|
||||
fn revert<F, E>(&mut self, read: F) where
|
||||
F: Fn(fs::File) -> Result<HashMap<K, V>, E>,
|
||||
E: fmt::Display,
|
||||
{
|
||||
if self.transient { return; }
|
||||
trace!(target: "diskmap", "revert {:?}", self.path);
|
||||
let _ = fs::File::open(self.path.clone())
|
||||
.map_err(|e| trace!(target: "diskmap", "Couldn't open disk map: {}", e))
|
||||
.and_then(|f| read(f).map_err(|e| warn!(target: "diskmap", "Couldn't read disk map: {}", e)))
|
||||
.and_then(|m| {
|
||||
self.cache = m;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn save<F, E>(&self, write: F) where
|
||||
F: Fn(&HashMap<K, V>, &mut fs::File) -> Result<(), E>,
|
||||
E: fmt::Display,
|
||||
{
|
||||
if self.transient { return; }
|
||||
trace!(target: "diskmap", "save {:?}", self.path);
|
||||
let _ = fs::File::create(self.path.clone())
|
||||
.map_err(|e| warn!(target: "diskmap", "Couldn't open disk map for writing: {}", e))
|
||||
.and_then(|mut f| {
|
||||
write(&self.cache, &mut f).map_err(|e| warn!(target: "diskmap", "Couldn't write to disk map: {}", e))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AddressBook, DappsSettingsStore, DappsSettings};
|
||||
use std::collections::HashMap;
|
||||
use ethjson::misc::AccountMeta;
|
||||
use devtools::RandomTempPath;
|
||||
|
||||
#[test]
|
||||
fn should_save_and_reload_address_book() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = AddressBook::new(path.clone());
|
||||
b.set_name(1.into(), "One".to_owned());
|
||||
b.set_meta(1.into(), "{1:1}".to_owned());
|
||||
let b = AddressBook::new(path);
|
||||
assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_save_and_reload_dapps_settings() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = DappsSettingsStore::new(path.clone());
|
||||
|
||||
// when
|
||||
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
|
||||
|
||||
// then
|
||||
let b = DappsSettingsStore::new(path);
|
||||
assert_eq!(b.get(), hash_map![
|
||||
"dappOne".into() => DappsSettings {
|
||||
accounts: vec![1.into(), 2.into()],
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_address() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = AddressBook::new(path.clone());
|
||||
|
||||
b.set_name(1.into(), "One".to_owned());
|
||||
b.set_name(2.into(), "Two".to_owned());
|
||||
b.set_name(3.into(), "Three".to_owned());
|
||||
b.remove(2.into());
|
||||
|
||||
let b = AddressBook::new(path);
|
||||
assert_eq!(b.get(), hash_map![
|
||||
1.into() => AccountMeta{name: "One".to_owned(), meta: "{}".to_owned(), uuid: None},
|
||||
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user