Persistent tracking of dapps (#4302)

* Tests for RPC

* Extracting dapp_id from Origin and x-parity-origin

* Separate type for DappId

* Persistent tracking of recent dapps

* Fixing tests

* Exposing dapp timestamps

* Fixing import to work on stable

* Fixing test again
This commit is contained in:
Tomasz Drwięga 2017-01-30 10:59:46 +01:00 committed by Gav Wood
parent 47e1c5e2f1
commit cf348dae60
15 changed files with 336 additions and 127 deletions

View File

@ -57,12 +57,19 @@ impl Endpoint for RpcEndpoint {
struct MetadataExtractor; struct MetadataExtractor;
impl HttpMetaExtractor<Metadata> for MetadataExtractor { impl HttpMetaExtractor<Metadata> for MetadataExtractor {
fn read_metadata(&self, request: &hyper::server::Request<hyper::net::HttpStream>) -> Metadata { fn read_metadata(&self, request: &hyper::server::Request<hyper::net::HttpStream>) -> Metadata {
let dapp_id = request.headers().get::<hyper::header::Referer>() let dapp_id = request.headers().get::<hyper::header::Origin>()
.and_then(|referer| hyper::Url::parse(referer).ok()) .map(|origin| format!("{}://{}", origin.scheme, origin.host))
.and_then(|url| { .or_else(|| {
url.path_segments() // fallback to custom header, but only if origin is null
.and_then(|mut split| split.next()) request.headers().get_raw("origin")
.map(|app_id| app_id.to_owned()) .and_then(|raw| raw.one())
.and_then(|raw| if raw == "null".as_bytes() {
request.headers().get_raw("x-parity-origin")
.and_then(|raw| raw.one())
.map(|raw| String::from_utf8_lossy(raw).into_owned())
} else {
None
})
}); });
Metadata { Metadata {
dapp_id: dapp_id, dapp_id: dapp_id,

View File

@ -19,6 +19,7 @@ use std::str;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use env_logger::LogBuilder; use env_logger::LogBuilder;
use ethcore_rpc::Metadata;
use jsonrpc_core::MetaIoHandler; use jsonrpc_core::MetaIoHandler;
use jsonrpc_core::reactor::RpcEventLoop; use jsonrpc_core::reactor::RpcEventLoop;
@ -58,7 +59,7 @@ impl Deref for ServerLoop {
} }
} }
pub fn init_server<F, B>(process: F, remote: Remote) -> (ServerLoop, Arc<FakeRegistrar>) where pub fn init_server<F, B>(process: F, io: MetaIoHandler<Metadata>, remote: Remote) -> (ServerLoop, Arc<FakeRegistrar>) where
F: FnOnce(ServerBuilder) -> ServerBuilder<B>, F: FnOnce(ServerBuilder) -> ServerBuilder<B>,
B: Fetch, B: Fetch,
{ {
@ -70,7 +71,7 @@ pub fn init_server<F, B>(process: F, remote: Remote) -> (ServerLoop, Arc<FakeReg
// TODO [ToDr] When https://github.com/ethcore/jsonrpc/issues/26 is resolved // TODO [ToDr] When https://github.com/ethcore/jsonrpc/issues/26 is resolved
// this additional EventLoop wouldn't be needed, we should be able to re-use remote. // this additional EventLoop wouldn't be needed, we should be able to re-use remote.
let event_loop = RpcEventLoop::spawn(); let event_loop = RpcEventLoop::spawn();
let handler = event_loop.handler(Arc::new(MetaIoHandler::default())); let handler = event_loop.handler(Arc::new(io));
let server = process(ServerBuilder::new( let server = process(ServerBuilder::new(
&dapps_path, registrar.clone(), remote, &dapps_path, registrar.clone(), remote,
)) ))
@ -100,12 +101,16 @@ pub fn serve_with_auth(user: &str, pass: &str) -> ServerLoop {
} }
} }
pub fn serve_with_rpc(io: MetaIoHandler<Metadata>) -> ServerLoop {
init_server(|builder| builder.allowed_hosts(None), io, Remote::new_sync()).0
}
pub fn serve_hosts(hosts: Option<Vec<String>>) -> ServerLoop { pub fn serve_hosts(hosts: Option<Vec<String>>) -> ServerLoop {
init_server(|builder| builder.allowed_hosts(hosts), Remote::new_sync()).0 init_server(|builder| builder.allowed_hosts(hosts), Default::default(), Remote::new_sync()).0
} }
pub fn serve_with_registrar() -> (ServerLoop, Arc<FakeRegistrar>) { pub fn serve_with_registrar() -> (ServerLoop, Arc<FakeRegistrar>) {
init_server(|builder| builder.allowed_hosts(None), Remote::new_sync()) init_server(|builder| builder.allowed_hosts(None), Default::default(), Remote::new_sync())
} }
pub fn serve_with_registrar_and_sync() -> (ServerLoop, Arc<FakeRegistrar>) { pub fn serve_with_registrar_and_sync() -> (ServerLoop, Arc<FakeRegistrar>) {
@ -113,7 +118,7 @@ pub fn serve_with_registrar_and_sync() -> (ServerLoop, Arc<FakeRegistrar>) {
builder builder
.sync_status(Arc::new(|| true)) .sync_status(Arc::new(|| true))
.allowed_hosts(None) .allowed_hosts(None)
}, Remote::new_sync()) }, Default::default(), Remote::new_sync())
} }
pub fn serve_with_registrar_and_fetch() -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) { pub fn serve_with_registrar_and_fetch() -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) {
@ -125,7 +130,7 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
let f = fetch.clone(); let f = fetch.clone();
let (server, reg) = init_server(move |builder| { let (server, reg) = init_server(move |builder| {
builder.allowed_hosts(None).fetch(f.clone()) builder.allowed_hosts(None).fetch(f.clone())
}, if multi_threaded { Remote::new_thread_per_future() } else { Remote::new_sync() }); }, Default::default(), if multi_threaded { Remote::new_thread_per_future() } else { Remote::new_sync() });
(server, fetch, reg) (server, fetch, reg)
} }
@ -138,13 +143,13 @@ pub fn serve_with_fetch(web_token: &'static str) -> (ServerLoop, FakeFetch) {
.allowed_hosts(None) .allowed_hosts(None)
.fetch(f.clone()) .fetch(f.clone())
.web_proxy_tokens(Arc::new(move |token| &token == web_token)) .web_proxy_tokens(Arc::new(move |token| &token == web_token))
}, Remote::new_sync()); }, Default::default(), Remote::new_sync());
(server, fetch) (server, fetch)
} }
pub fn serve() -> ServerLoop { pub fn serve() -> ServerLoop {
init_server(|builder| builder.allowed_hosts(None), Remote::new_sync()).0 init_server(|builder| builder.allowed_hosts(None), Default::default(), Remote::new_sync()).0
} }
pub fn request(server: ServerLoop, request: &str) -> http_client::Response { pub fn request(server: ServerLoop, request: &str) -> http_client::Response {

View File

@ -22,5 +22,6 @@ mod api;
mod authorization; mod authorization;
mod fetch; mod fetch;
mod redirection; mod redirection;
mod rpc;
mod validation; mod validation;

119
dapps/src/tests/rpc.rs Normal file
View File

@ -0,0 +1,119 @@
// Copyright 2015-2017 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/>.
use futures::{future, Future};
use ethcore_rpc::{Metadata, Origin};
use jsonrpc_core::{MetaIoHandler, Value};
use tests::helpers::{serve_with_rpc, request};
#[test]
fn should_serve_rpc() {
// given
let mut io = MetaIoHandler::new();
io.add_method("rpc_test", |_| {
Ok(Value::String("Hello World!".into()))
});
let server = serve_with_rpc(io);
// when
let req = r#"{"jsonrpc":"2.0","id":1,"method":"rpc_test","params":[]}"#;
let response = request(server, &format!(
"\
POST /rpc/ HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
",
req.as_bytes().len(),
req,
));
// then
response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body, "31\n{\"jsonrpc\":\"2.0\",\"result\":\"Hello World!\",\"id\":1}\n\n0\n\n".to_owned());
}
#[test]
fn should_extract_metadata() {
// given
let mut io = MetaIoHandler::new();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);
future::ok(Value::String("Hello World!".into())).boxed()
});
let server = serve_with_rpc(io);
// when
let req = r#"{"jsonrpc":"2.0","id":1,"method":"rpc_test","params":[]}"#;
let response = request(server, &format!(
"\
POST /rpc/ HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Origin: https://parity.io/\r\n\
X-Parity-Origin: https://this.should.be.ignored\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
",
req.as_bytes().len(),
req,
));
// then
response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body, "31\n{\"jsonrpc\":\"2.0\",\"result\":\"Hello World!\",\"id\":1}\n\n0\n\n".to_owned());
}
#[test]
fn should_extract_metadata_from_custom_header() {
// given
let mut io = MetaIoHandler::new();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);
future::ok(Value::String("Hello World!".into())).boxed()
});
let server = serve_with_rpc(io);
// when
let req = r#"{"jsonrpc":"2.0","id":1,"method":"rpc_test","params":[]}"#;
let response = request(server, &format!(
"\
POST /rpc/ HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Origin: null\r\n\
X-Parity-Origin: https://parity.io/\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
",
req.as_bytes().len(),
req,
));
// then
response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body, "31\n{\"jsonrpc\":\"2.0\",\"result\":\"Hello World!\",\"id\":1}\n\n0\n\n".to_owned());
}

View File

@ -74,7 +74,18 @@ impl From<SSError> for Error {
} }
/// Dapp identifier /// Dapp identifier
pub type DappId = String; #[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct DappId(String);
impl From<DappId> for String {
fn from(id: DappId) -> String { id.0 }
}
impl From<String> for DappId {
fn from(id: String) -> DappId { DappId(id) }
}
impl<'a> From<&'a str> for DappId {
fn from(id: &'a str) -> DappId { DappId(id.to_owned()) }
}
fn transient_sstore() -> EthMultiStore { fn transient_sstore() -> EthMultiStore {
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed") EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")
@ -181,7 +192,7 @@ impl AccountProvider {
} }
/// Gets a list of dapps recently requesting accounts. /// Gets a list of dapps recently requesting accounts.
pub fn recent_dapps(&self) -> Result<Vec<DappId>, Error> { pub fn recent_dapps(&self) -> Result<HashMap<DappId, u64>, Error> {
Ok(self.dapps_settings.read().recent_dapps()) Ok(self.dapps_settings.read().recent_dapps())
} }
@ -405,7 +416,7 @@ impl AccountProvider {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{AccountProvider, Unlock}; use super::{AccountProvider, Unlock, DappId};
use std::time::Instant; use std::time::Instant;
use ethstore::ethkey::{Generator, Random}; use ethstore::ethkey::{Generator, Random};
@ -466,7 +477,7 @@ mod tests {
fn should_set_dapps_addresses() { fn should_set_dapps_addresses() {
// given // given
let ap = AccountProvider::transient_provider(); let ap = AccountProvider::transient_provider();
let app = "app1".to_owned(); let app = DappId("app1".into());
// set `AllAccounts` policy // set `AllAccounts` policy
ap.set_new_dapps_whitelist(None).unwrap(); ap.set_new_dapps_whitelist(None).unwrap();

View File

@ -17,11 +17,17 @@
//! Address Book and Dapps Settings Store //! Address Book and Dapps Settings Store
use std::{fs, fmt, hash, ops}; use std::{fs, fmt, hash, ops};
use std::collections::{HashMap, VecDeque}; use std::sync::atomic::{self, AtomicUsize};
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use ethstore::ethkey::Address; use ethstore::ethkey::Address;
use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings, NewDappsPolicy as JsonNewDappsPolicy}; use ethjson::misc::{
AccountMeta,
DappsSettings as JsonSettings,
DappsHistory as JsonDappsHistory,
NewDappsPolicy as JsonNewDappsPolicy,
};
use account_provider::DappId; use account_provider::DappId;
/// Disk-backed map from Address to String. Uses JSON. /// Disk-backed map from Address to String. Uses JSON.
@ -35,7 +41,7 @@ impl AddressBook {
let mut r = AddressBook { let mut r = AddressBook {
cache: DiskMap::new(path, "address_book.json".into()) cache: DiskMap::new(path, "address_book.json".into())
}; };
r.cache.revert(AccountMeta::read_address_map); r.cache.revert(AccountMeta::read);
r r
} }
@ -52,7 +58,7 @@ impl AddressBook {
} }
fn save(&self) { fn save(&self) {
self.cache.save(AccountMeta::write_address_map) self.cache.save(AccountMeta::write)
} }
/// Sets new name for given address. /// Sets new name for given address.
@ -134,7 +140,51 @@ impl From<NewDappsPolicy> for JsonNewDappsPolicy {
} }
} }
const MAX_RECENT_DAPPS: usize = 10; /// Transient dapps data
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct TransientDappsData {
/// Timestamp of last access
pub last_accessed: u64,
}
impl From<JsonDappsHistory> for TransientDappsData {
fn from(s: JsonDappsHistory) -> Self {
TransientDappsData {
last_accessed: s.last_accessed,
}
}
}
impl From<TransientDappsData> for JsonDappsHistory {
fn from(s: TransientDappsData) -> Self {
JsonDappsHistory {
last_accessed: s.last_accessed,
}
}
}
enum TimeProvider {
Clock,
Incremenetal(AtomicUsize)
}
impl TimeProvider {
fn get(&self) -> u64 {
match *self {
TimeProvider::Clock => {
::std::time::UNIX_EPOCH.elapsed()
.expect("Correct time is required to be set")
.as_secs()
},
TimeProvider::Incremenetal(ref time) => {
time.fetch_add(1, atomic::Ordering::SeqCst) as u64
},
}
}
}
const MAX_RECENT_DAPPS: usize = 50;
/// Disk-backed map from DappId to Settings. Uses JSON. /// Disk-backed map from DappId to Settings. Uses JSON.
pub struct DappsSettingsStore { pub struct DappsSettingsStore {
@ -142,8 +192,10 @@ pub struct DappsSettingsStore {
settings: DiskMap<DappId, DappsSettings>, settings: DiskMap<DappId, DappsSettings>,
/// New Dapps Policy /// New Dapps Policy
policy: DiskMap<String, NewDappsPolicy>, policy: DiskMap<String, NewDappsPolicy>,
/// Recently Accessed Dapps (transient) /// Transient Data of recently Accessed Dapps
recent: VecDeque<DappId>, history: DiskMap<DappId, TransientDappsData>,
/// Time
time: TimeProvider,
} }
impl DappsSettingsStore { impl DappsSettingsStore {
@ -152,10 +204,12 @@ impl DappsSettingsStore {
let mut r = DappsSettingsStore { let mut r = DappsSettingsStore {
settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()), settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()),
policy: DiskMap::new(path.clone(), "dapps_policy.json".into()), policy: DiskMap::new(path.clone(), "dapps_policy.json".into()),
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS), history: DiskMap::new(path.clone(), "dapps_history.json".into()),
time: TimeProvider::Clock,
}; };
r.settings.revert(JsonSettings::read_dapps_settings); r.settings.revert(JsonSettings::read);
r.policy.revert(JsonNewDappsPolicy::read_new_dapps_policy); r.policy.revert(JsonNewDappsPolicy::read);
r.history.revert(JsonDappsHistory::read);
r r
} }
@ -164,7 +218,8 @@ impl DappsSettingsStore {
DappsSettingsStore { DappsSettingsStore {
settings: DiskMap::transient(), settings: DiskMap::transient(),
policy: DiskMap::transient(), policy: DiskMap::transient(),
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS), history: DiskMap::transient(),
time: TimeProvider::Incremenetal(AtomicUsize::new(1)),
} }
} }
@ -178,24 +233,36 @@ impl DappsSettingsStore {
self.policy.get("default").cloned().unwrap_or(NewDappsPolicy::AllAccounts) self.policy.get("default").cloned().unwrap_or(NewDappsPolicy::AllAccounts)
} }
/// Returns recent dapps (in order of last request) /// Returns recent dapps with last accessed timestamp
pub fn recent_dapps(&self) -> Vec<DappId> { pub fn recent_dapps(&self) -> HashMap<DappId, u64> {
self.recent.iter().cloned().collect() self.history.iter().map(|(k, v)| (k.clone(), v.last_accessed)).collect()
} }
/// Marks recent dapp as used /// Marks recent dapp as used
pub fn mark_dapp_used(&mut self, dapp: DappId) { pub fn mark_dapp_used(&mut self, dapp: DappId) {
self.recent.retain(|id| id != &dapp); {
self.recent.push_front(dapp); let mut entry = self.history.entry(dapp).or_insert_with(|| Default::default());
while self.recent.len() > MAX_RECENT_DAPPS { entry.last_accessed = self.time.get();
self.recent.pop_back();
} }
// Clear extraneous entries
while self.history.len() > MAX_RECENT_DAPPS {
let min = self.history.iter()
.min_by_key(|&(_, ref v)| v.last_accessed)
.map(|(ref k, _)| k.clone())
.cloned();
match min {
Some(k) => self.history.remove(&k),
None => break,
};
}
self.history.save(JsonDappsHistory::write);
} }
/// Sets current new dapps policy /// Sets current new dapps policy
pub fn set_policy(&mut self, policy: NewDappsPolicy) { pub fn set_policy(&mut self, policy: NewDappsPolicy) {
self.policy.insert("default".into(), policy); self.policy.insert("default".into(), policy);
self.policy.save(JsonNewDappsPolicy::write_new_dapps_policy); self.policy.save(JsonNewDappsPolicy::write);
} }
/// Sets accounts for specific dapp. /// Sets accounts for specific dapp.
@ -204,7 +271,7 @@ impl DappsSettingsStore {
let mut settings = self.settings.entry(id).or_insert_with(DappsSettings::default); let mut settings = self.settings.entry(id).or_insert_with(DappsSettings::default);
settings.accounts = accounts; settings.accounts = accounts;
} }
self.settings.save(JsonSettings::write_dapps_settings); self.settings.save(JsonSettings::write);
} }
} }
@ -280,6 +347,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy}; use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy};
use account_provider::DappId;
use std::collections::HashMap; use std::collections::HashMap;
use ethjson::misc::AccountMeta; use ethjson::misc::AccountMeta;
use devtools::RandomTempPath; use devtools::RandomTempPath;
@ -333,18 +401,22 @@ mod tests {
} }
#[test] #[test]
fn should_maintain_a_list_of_recent_dapps() { fn should_maintain_a_map_of_recent_dapps() {
let mut store = DappsSettingsStore::transient(); let mut store = DappsSettingsStore::transient();
assert!(store.recent_dapps().is_empty(), "Initially recent dapps should be empty."); assert!(store.recent_dapps().is_empty(), "Initially recent dapps should be empty.");
store.mark_dapp_used("dapp1".into()); let dapp1: DappId = "dapp1".into();
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned()]); let dapp2: DappId = "dapp2".into();
store.mark_dapp_used(dapp1.clone());
let recent = store.recent_dapps();
assert_eq!(recent.len(), 1);
assert_eq!(recent.get(&dapp1), Some(&1));
store.mark_dapp_used("dapp2".into()); store.mark_dapp_used(dapp2.clone());
assert_eq!(store.recent_dapps(), vec!["dapp2".to_owned(), "dapp1".to_owned()]); let recent = store.recent_dapps();
assert_eq!(recent.len(), 2);
store.mark_dapp_used("dapp1".into()); assert_eq!(recent.get(&dapp1), Some(&1));
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned(), "dapp2".to_owned()]); assert_eq!(recent.get(&dapp2), Some(&2));
} }
#[test] #[test]

View File

@ -22,7 +22,13 @@ use {json, SafeAccount, Error};
use json::Uuid; use json::Uuid;
use super::KeyDirectory; use super::KeyDirectory;
const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json", "dapps_policy.json"]; const IGNORED_FILES: &'static [&'static str] = &[
"thumbs.db",
"address_book.json",
"dapps_policy.json",
"dapps_accounts.json",
"dapps_history.json",
];
#[cfg(not(windows))] #[cfg(not(windows))]
fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> { fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> {

View File

@ -29,9 +29,9 @@ macro_rules! impl_hash {
#[derive(Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)] #[derive(Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)]
pub struct $name(pub $inner); pub struct $name(pub $inner);
impl Into<$inner> for $name { impl From<$name> for $inner {
fn into(self) -> $inner { fn from(other: $name) -> $inner {
self.0 other.0
} }
} }

View File

@ -16,14 +16,10 @@
//! Misc deserialization. //! Misc deserialization.
use std::io::{Read, Write};
use std::collections::HashMap;
use serde_json;
use util;
use hash; use hash;
/// Collected account metadata /// Collected account metadata
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AccountMeta { pub struct AccountMeta {
/// The name of the account. /// The name of the account.
pub name: String, pub name: String,
@ -33,26 +29,4 @@ pub struct AccountMeta {
pub uuid: Option<String>, pub uuid: Option<String>,
} }
impl Default for AccountMeta { impl_serialization!(hash::Address => AccountMeta);
fn default() -> Self {
AccountMeta {
name: String::new(),
meta: "{}".to_owned(),
uuid: None,
}
}
}
impl AccountMeta {
/// Read a hash map of Address -> AccountMeta.
pub fn read_address_map<R>(reader: R) -> Result<HashMap<util::Address, AccountMeta>, serde_json::Error> where R: Read {
serde_json::from_reader(reader).map(|ok: HashMap<hash::Address, AccountMeta>|
ok.into_iter().map(|(a, m)| (a.into(), m)).collect()
)
}
/// Write a hash map of Address -> AccountMeta.
pub fn write_address_map<W>(m: &HashMap<util::Address, AccountMeta>, writer: &mut W) -> Result<(), serde_json::Error> where W: Write {
serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m)).collect::<HashMap<hash::Address, _>>())
}
}

View File

@ -16,13 +16,8 @@
//! Dapps settings de/serialization. //! Dapps settings de/serialization.
use std::io;
use std::collections::HashMap;
use serde_json;
use hash; use hash;
type DappId = String;
/// Settings for specific dapp. /// Settings for specific dapp.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DappsSettings { pub struct DappsSettings {
@ -30,25 +25,16 @@ pub struct DappsSettings {
pub accounts: Vec<hash::Address>, pub accounts: Vec<hash::Address>,
} }
impl DappsSettings { impl_serialization!(String => DappsSettings);
/// Read a hash map of DappId -> DappsSettings
pub fn read_dapps_settings<R, S>(reader: R) -> Result<HashMap<DappId, S>, serde_json::Error> where /// History for specific dapp.
R: io::Read, #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
S: From<DappsSettings> + Clone, pub struct DappsHistory {
{ /// Last accessed timestamp
serde_json::from_reader(reader).map(|ok: HashMap<DappId, DappsSettings>| pub last_accessed: u64,
ok.into_iter().map(|(a, m)| (a.into(), m.into())).collect()
)
} }
/// Write a hash map of DappId -> DappsSettings impl_serialization!(String => DappsHistory);
pub fn write_dapps_settings<W, S>(m: &HashMap<DappId, S>, writer: &mut W) -> Result<(), serde_json::Error> where
W: io::Write,
S: Into<DappsSettings> + Clone,
{
serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m.clone().into())).collect::<HashMap<DappId, DappsSettings>>())
}
}
/// Accounts policy for new dapps. /// Accounts policy for new dapps.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -59,22 +45,4 @@ pub enum NewDappsPolicy {
Whitelist(Vec<hash::Address>), Whitelist(Vec<hash::Address>),
} }
impl NewDappsPolicy { impl_serialization!(String => NewDappsPolicy);
/// Read a hash map of `String -> NewDappsPolicy`
pub fn read_new_dapps_policy<R, S>(reader: R) -> Result<HashMap<String, S>, serde_json::Error> where
R: io::Read,
S: From<NewDappsPolicy> + Clone,
{
serde_json::from_reader(reader).map(|ok: HashMap<String, NewDappsPolicy>|
ok.into_iter().map(|(a, m)| (a.into(), m.into())).collect()
)
}
/// Write a hash map of `String -> NewDappsPolicy`
pub fn write_new_dapps_policy<W, S>(m: &HashMap<String, S>, writer: &mut W) -> Result<(), serde_json::Error> where
W: io::Write,
S: Into<NewDappsPolicy> + Clone,
{
serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m.clone().into())).collect::<HashMap<String, NewDappsPolicy>>())
}
}

View File

@ -16,8 +16,39 @@
//! Misc deserialization. //! Misc deserialization.
macro_rules! impl_serialization {
($key: ty => $name: ty) => {
impl $name {
/// Read a hash map of DappId -> $name
pub fn read<R, S, D>(reader: R) -> Result<::std::collections::HashMap<D, S>, ::serde_json::Error> where
R: ::std::io::Read,
D: From<$key> + ::std::hash::Hash + Eq,
S: From<$name> + Clone,
{
::serde_json::from_reader(reader).map(|ok: ::std::collections::HashMap<$key, $name>|
ok.into_iter().map(|(a, m)| (a.into(), m.into())).collect()
)
}
/// Write a hash map of DappId -> $name
pub fn write<W, S, D>(m: &::std::collections::HashMap<D, S>, writer: &mut W) -> Result<(), ::serde_json::Error> where
W: ::std::io::Write,
D: Into<$key> + ::std::hash::Hash + Eq + Clone,
S: Into<$name> + Clone,
{
::serde_json::to_writer(
writer,
&m.iter()
.map(|(a, m)| (a.clone().into(), m.clone().into()))
.collect::<::std::collections::HashMap<$key, $name>>()
)
}
}
}
}
mod account_meta; mod account_meta;
mod dapps_settings; mod dapps_settings;
pub use self::dapps_settings::{DappsSettings, NewDappsPolicy}; pub use self::dapps_settings::{DappsSettings, DappsHistory, NewDappsPolicy};
pub use self::account_meta::AccountMeta; pub use self::account_meta::AccountMeta;

View File

@ -196,12 +196,12 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
.map(|accounts| accounts.map(into_vec)) .map(|accounts| accounts.map(into_vec))
} }
fn recent_dapps(&self) -> Result<Vec<DappId>, Error> { fn recent_dapps(&self) -> Result<BTreeMap<DappId, u64>, Error> {
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
store.recent_dapps() store.recent_dapps()
.map_err(|e| errors::account("Couldn't get recent dapps.", e)) .map_err(|e| errors::account("Couldn't get recent dapps.", e))
.map(into_vec) .map(|map| map.into_iter().map(|(k, v)| (k.into(), v)).collect())
} }
fn import_geth_accounts(&self, addresses: Vec<RpcH160>) -> Result<Vec<RpcH160>, Error> { fn import_geth_accounts(&self, addresses: Vec<RpcH160>) -> Result<Vec<RpcH160>, Error> {

View File

@ -173,7 +173,7 @@ fn rpc_parity_recent_dapps() {
// then // then
let request = r#"{"jsonrpc": "2.0", "method": "parity_listRecentDapps","params":[], "id": 1}"#; let request = r#"{"jsonrpc": "2.0", "method": "parity_listRecentDapps","params":[], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":["dapp1"],"id":1}"#; let response = r#"{"jsonrpc":"2.0","result":{"dapp1":1},"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
} }

View File

@ -92,9 +92,10 @@ build_rpc_trait! {
#[rpc(name = "parity_getNewDappsWhitelist")] #[rpc(name = "parity_getNewDappsWhitelist")]
fn new_dapps_whitelist(&self) -> Result<Option<Vec<H160>>, Error>; fn new_dapps_whitelist(&self) -> Result<Option<Vec<H160>>, Error>;
/// Sets accounts exposed for particular dapp. /// Returns identified dapps that recently used RPC
/// Includes last usage timestamp.
#[rpc(name = "parity_listRecentDapps")] #[rpc(name = "parity_listRecentDapps")]
fn recent_dapps(&self) -> Result<Vec<DappId>, Error>; fn recent_dapps(&self) -> Result<BTreeMap<DappId, u64>, Error>;
/// Imports a number of Geth accounts, with the list provided as the argument. /// Imports a number of Geth accounts, with the list provided as the argument.
#[rpc(name = "parity_importGethAccounts")] #[rpc(name = "parity_importGethAccounts")]

View File

@ -16,8 +16,10 @@
//! Dapp Id type //! Dapp Id type
use ethcore::account_provider::DappId as EthDappId;
/// Dapplication Internal Id /// Dapplication Internal Id
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct DappId(pub String); pub struct DappId(pub String);
impl Into<String> for DappId { impl Into<String> for DappId {
@ -32,6 +34,18 @@ impl From<String> for DappId {
} }
} }
impl From<EthDappId> for DappId {
fn from(id: EthDappId) -> Self {
DappId(id.into())
}
}
impl Into<EthDappId> for DappId {
fn into(self) -> EthDappId {
Into::<String>::into(self).into()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {