use crate::{Result, StateKey, StateValue, XenithError};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, Default)]
pub struct KeyMetadata {
pub address: Option<[u8; 20]>,
pub slot: Option<[u8; 32]>,
}
#[async_trait]
pub trait StateStore: Send + Sync {
async fn get(&self, key: &StateKey) -> Result<Option<StateValue>>;
async fn set(&self, key: &StateKey, value: StateValue) -> Result<()>;
async fn delete(&self, key: &StateKey) -> Result<()>;
async fn list_prefix(&self, prefix: &str) -> Result<Vec<StateKey>>;
async fn get_metadata(&self, key: &StateKey) -> Result<Option<KeyMetadata>>;
async fn set_metadata(&self, key: &StateKey, meta: KeyMetadata) -> Result<()>;
}
#[derive(Clone, Default)]
pub struct InMemoryStore {
inner: Arc<Mutex<HashMap<String, StateValue>>>,
meta: Arc<Mutex<HashMap<String, KeyMetadata>>>,
}
#[async_trait]
impl StateStore for InMemoryStore {
async fn get(&self, key: &StateKey) -> Result<Option<StateValue>> {
let map = self
.inner
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
Ok(map.get(key.as_ref()).cloned())
}
async fn set(&self, key: &StateKey, value: StateValue) -> Result<()> {
let mut map = self
.inner
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
map.insert(key.as_ref().to_owned(), value);
Ok(())
}
async fn delete(&self, key: &StateKey) -> Result<()> {
let mut map = self
.inner
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
map.remove(key.as_ref());
Ok(())
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<StateKey>> {
let map = self
.inner
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
let mut keys: Vec<StateKey> = map
.keys()
.filter(|k| k.starts_with(prefix))
.map(|k| StateKey::from_raw(k.clone()))
.collect();
keys.sort_by(|a, b| a.as_ref().cmp(b.as_ref()));
Ok(keys)
}
async fn get_metadata(&self, key: &StateKey) -> Result<Option<KeyMetadata>> {
let map = self
.meta
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
Ok(map.get(key.as_ref()).cloned())
}
async fn set_metadata(&self, key: &StateKey, meta: KeyMetadata) -> Result<()> {
let mut map = self
.meta
.lock()
.map_err(|_| XenithError::StoreError("lock poisoned".into()))?;
map.insert(key.as_ref().to_owned(), meta);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ChainId;
use bytes::Bytes;
fn val(ts: u64) -> StateValue {
use crate::StateVersion;
StateValue {
data: Bytes::from_static(b"x"),
version: StateVersion {
timestamp_ms: ts,
sequence: 0,
source_chain: 1,
},
updated_at: 0,
source_chain: ChainId(1),
}
}
#[tokio::test]
async fn set_then_get_returns_value() {
let store = InMemoryStore::default();
let key = StateKey::new("proto", "pool", "0x1");
store.set(&key, val(1)).await.unwrap();
assert_eq!(store.get(&key).await.unwrap(), Some(val(1)));
}
#[tokio::test]
async fn get_missing_key_returns_none() {
let store = InMemoryStore::default();
let key = StateKey::new("proto", "pool", "missing");
assert_eq!(store.get(&key).await.unwrap(), None);
}
#[tokio::test]
async fn delete_removes_key() {
let store = InMemoryStore::default();
let key = StateKey::new("proto", "pool", "0x2");
store.set(&key, val(1)).await.unwrap();
store.delete(&key).await.unwrap();
assert_eq!(store.get(&key).await.unwrap(), None);
}
#[tokio::test]
async fn delete_missing_key_is_noop() {
let store = InMemoryStore::default();
let key = StateKey::new("proto", "pool", "ghost");
store.delete(&key).await.unwrap(); }
#[tokio::test]
async fn list_prefix_returns_matching_keys() {
let store = InMemoryStore::default();
let a = StateKey::new("uniswap", "pool", "0xaaa");
let b = StateKey::new("uniswap", "pool", "0xbbb");
let other = StateKey::new("aave", "reserve", "0xaaa");
store.set(&a, val(1)).await.unwrap();
store.set(&b, val(2)).await.unwrap();
store.set(&other, val(3)).await.unwrap();
let keys = store.list_prefix("uniswap").await.unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&a));
assert!(keys.contains(&b));
assert!(!keys.contains(&other));
}
#[tokio::test]
async fn list_prefix_empty_when_no_match() {
let store = InMemoryStore::default();
store
.set(&StateKey::new("aave", "x", "1"), val(1))
.await
.unwrap();
assert!(store.list_prefix("uniswap").await.unwrap().is_empty());
}
}