use super::{Account, AccountBuilder};
use bytes::Bytes;
use futures::{
future::{err, ok},
Future,
};
use hashbrown::HashMap;
use interledger_btp::{BtpOpenSignupAccount, BtpOpenSignupStore, BtpStore};
use interledger_http::HttpStore;
use interledger_router::RouterStore;
use interledger_service::{Account as AccountTrait, AccountStore};
use parking_lot::{Mutex, RwLock};
use std::cmp::max;
use std::{
iter::{empty, once, FromIterator, IntoIterator},
str,
sync::Arc,
};
type BtpTokenAndUsername = (String, Option<String>);
#[derive(Clone)]
pub struct InMemoryStore {
accounts: Arc<RwLock<HashMap<u64, Account>>>,
routing_table: Arc<RwLock<HashMap<Bytes, u64>>>,
btp_auth: Arc<RwLock<HashMap<BtpTokenAndUsername, u64>>>,
http_auth: Arc<RwLock<HashMap<String, u64>>>,
next_account_id: Arc<Mutex<u64>>,
}
impl InMemoryStore {
pub fn new(accounts: impl IntoIterator<Item = AccountBuilder>) -> Self {
InMemoryStore::from_accounts(accounts.into_iter().map(|builder| builder.build()))
}
pub fn default() -> Self {
InMemoryStore::from_accounts(empty())
}
pub fn from_accounts(accounts: impl IntoIterator<Item = Account>) -> Self {
let mut next_account_id: u64 = 0;
let accounts = HashMap::from_iter(accounts.into_iter().map(|account| {
next_account_id = max(account.id(), next_account_id);
(account.id(), account)
}));
next_account_id += 1;
let routing_table: HashMap<Bytes, u64> =
HashMap::from_iter(accounts.iter().flat_map(|(account_id, account)| {
once((account.inner.ilp_address.clone(), *account_id)).chain(
account
.inner
.additional_routes
.iter()
.map(move |route| (route.clone(), *account_id)),
)
}));
let btp_auth = HashMap::from_iter(accounts.iter().filter_map(|(account_id, account)| {
if let Some(ref token) = account.inner.btp_incoming_token {
Some((
(
token.to_string(),
account.inner.btp_incoming_username.clone(),
),
*account_id,
))
} else {
None
}
}));
let http_auth = HashMap::from_iter(accounts.iter().filter_map(|(account_id, account)| {
if let Some(ref auth) = account.inner.http_incoming_authorization {
Some((auth.to_string(), *account_id))
} else {
None
}
}));
InMemoryStore {
accounts: Arc::new(RwLock::new(accounts)),
routing_table: Arc::new(RwLock::new(routing_table)),
btp_auth: Arc::new(RwLock::new(btp_auth)),
http_auth: Arc::new(RwLock::new(http_auth)),
next_account_id: Arc::new(Mutex::new(next_account_id)),
}
}
}
impl AccountStore for InMemoryStore {
type Account = Account;
fn get_accounts(
&self,
accounts_ids: Vec<u64>,
) -> Box<Future<Item = Vec<Account>, Error = ()> + Send> {
let accounts: Vec<Account> = accounts_ids
.iter()
.filter_map(|account_id| self.accounts.read().get(account_id).cloned())
.collect();
if accounts.len() == accounts_ids.len() {
Box::new(ok(accounts))
} else {
Box::new(err(()))
}
}
}
impl HttpStore for InMemoryStore {
type Account = Account;
fn get_account_from_http_auth(
&self,
auth_header: &str,
) -> Box<Future<Item = Account, Error = ()> + Send> {
if let Some(account_id) = self.http_auth.read().get(auth_header) {
Box::new(ok(self.accounts.read()[account_id].clone()))
} else {
Box::new(err(()))
}
}
}
impl RouterStore for InMemoryStore {
fn routing_table(&self) -> HashMap<Bytes, u64> {
self.routing_table.read().clone()
}
}
impl BtpStore for InMemoryStore {
type Account = Account;
fn get_account_from_btp_token(
&self,
token: &str,
username: Option<&str>,
) -> Box<Future<Item = Self::Account, Error = ()> + Send> {
if let Some(account_id) = self
.btp_auth
.read()
.get(&(token.to_string(), username.map(|s| s.to_string())))
{
Box::new(ok(self.accounts.read()[account_id].clone()))
} else {
Box::new(err(()))
}
}
}
impl BtpOpenSignupStore for InMemoryStore {
type Account = Account;
fn create_btp_account<'a>(
&self,
account: BtpOpenSignupAccount<'a>,
) -> Box<Future<Item = Self::Account, Error = ()> + Send> {
let account_id = {
let next_id: u64 = *self.next_account_id.lock();
*self.next_account_id.lock() += 1;
next_id
};
let account = AccountBuilder::new()
.id(account_id)
.ilp_address(account.ilp_address)
.btp_incoming_token(account.auth_token.to_string())
.btp_incoming_username(account.username.map(String::from))
.asset_code(account.asset_code.to_string())
.asset_scale(account.asset_scale)
.build();
(*self.accounts.write()).insert(account_id, account.clone());
(*self.routing_table.write()).insert(account.inner.ilp_address.clone(), account_id);
(*self.btp_auth.write()).insert(
(
account.inner.btp_incoming_token.clone().unwrap(),
account.inner.btp_incoming_username.clone(),
),
account_id,
);
Box::new(ok(account))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_accounts() {
let store = InMemoryStore::new(vec![
AccountBuilder::new().id(0),
AccountBuilder::new().id(1),
AccountBuilder::new().id(4),
]);
let accounts = store.get_accounts(vec![0, 4]).wait().unwrap();
assert_eq!(accounts[0].id(), 0);
assert_eq!(accounts[1].id(), 4);
assert!(store.get_accounts(vec![0, 5]).wait().is_err());
}
#[test]
fn query_by_http_auth() {
let account = AccountBuilder::new()
.http_incoming_authorization("Bearer test_token".to_string())
.build();
let store = InMemoryStore::from_accounts(vec![account]);
store
.get_account_from_http_auth("Bearer test_token")
.wait()
.unwrap();
assert!(store
.get_account_from_http_auth("Bearer bad_token")
.wait()
.is_err());
}
#[test]
fn query_by_btp() {
let account = AccountBuilder::new()
.btp_incoming_token("test_token".to_string())
.build();
let store = InMemoryStore::from_accounts(vec![account]);
store
.get_account_from_btp_token("test_token", None)
.wait()
.unwrap();
assert!(store
.get_account_from_btp_token("bad_token", None)
.wait()
.is_err());
}
#[test]
fn routing_table() {
let store = InMemoryStore::new(vec![
AccountBuilder::new()
.id(1)
.ilp_address(b"example.one")
.additional_routes(&[b"example.three"]),
AccountBuilder::new().id(2).ilp_address(b"example.two"),
]);
assert_eq!(
store.routing_table(),
HashMap::from_iter(vec![
(Bytes::from("example.one"), 1),
(Bytes::from("example.two"), 2),
(Bytes::from("example.three"), 1)
])
);
}
#[test]
fn open_btp_signup() {
let store = InMemoryStore::default();
let account = store
.create_btp_account(BtpOpenSignupAccount {
auth_token: "token",
username: None,
ilp_address: b"example.account",
asset_code: "XYZ",
asset_scale: 9,
})
.wait()
.unwrap();
assert_eq!(account.id(), 1);
}
}