use std::collections::HashMap;
use chia_protocol::{Bytes32, CoinSpend};
use crate::error::ChainSourceError;
use crate::lineage::SingletonLineage;
use crate::provider::{ProviderId, ProviderInfo, ProviderKind};
use crate::record::CoinRecord;
use crate::source::{ChainSource, ChainSourceProvider};
#[derive(Debug, Default, Clone)]
pub struct MockChainSource {
coins: HashMap<Bytes32, CoinRecord>,
spends: HashMap<Bytes32, CoinSpend>,
lineages: HashMap<Bytes32, SingletonLineage>,
timestamps: HashMap<u32, u64>,
peak: Option<u32>,
forced_error: Option<ChainSourceError>,
}
impl MockChainSource {
pub fn new() -> Self {
Self::default()
}
pub fn with_coin(mut self, coin_id: Bytes32, record: CoinRecord) -> Self {
self.coins.insert(coin_id, record);
self
}
pub fn with_spend(mut self, coin_id: Bytes32, spend: CoinSpend) -> Self {
self.spends.insert(coin_id, spend);
self
}
pub fn with_lineage(mut self, launcher_id: Bytes32, lineage: SingletonLineage) -> Self {
self.lineages.insert(launcher_id, lineage);
self
}
pub fn with_timestamp(mut self, height: u32, timestamp: u64) -> Self {
self.timestamps.insert(height, timestamp);
self
}
pub fn with_peak(mut self, height: u32) -> Self {
self.peak = Some(height);
self
}
pub fn fail_with(mut self, error: ChainSourceError) -> Self {
self.forced_error = Some(error);
self
}
fn guard(&self) -> Result<(), ChainSourceError> {
match &self.forced_error {
Some(error) => Err(error.clone()),
None => Ok(()),
}
}
}
impl ChainSource for MockChainSource {
type Error = ChainSourceError;
fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
self.guard()?;
Ok(self.coins.get(&coin_id).cloned())
}
fn coin_records_by_puzzle_hash(
&self,
puzzle_hash: Bytes32,
include_spent: bool,
) -> Result<Vec<CoinRecord>, Self::Error> {
self.guard()?;
Ok(self
.coins
.values()
.filter(|record| record.coin.puzzle_hash == puzzle_hash)
.filter(|record| include_spent || !record.is_spent())
.cloned()
.collect())
}
fn coin_records_by_parent(
&self,
parent_coin_id: Bytes32,
) -> Result<Vec<CoinRecord>, Self::Error> {
self.guard()?;
Ok(self
.coins
.values()
.filter(|record| record.coin.parent_coin_info == parent_coin_id)
.cloned()
.collect())
}
fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
self.guard()?;
Ok(self.spends.get(&coin_id).cloned())
}
fn resolve_singleton_lineage(
&self,
launcher_id: Bytes32,
) -> Result<Option<SingletonLineage>, Self::Error> {
self.guard()?;
Ok(self.lineages.get(&launcher_id).cloned())
}
fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
self.guard()?;
Ok(self.peak)
}
fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error> {
self.guard()?;
Ok(self.timestamps.get(&height).copied())
}
}
impl ChainSourceProvider for MockChainSource {
fn provider_info(&self) -> ProviderInfo {
ProviderInfo {
id: ProviderId(std::borrow::Cow::Borrowed("mock")),
kind: ProviderKind::Custom,
priority: i32::MAX,
trustless: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chia_protocol::{Coin, Program};
fn coin(parent: [u8; 32], puzzle_hash: [u8; 32]) -> Coin {
Coin::new(Bytes32::new(parent), Bytes32::new(puzzle_hash), 1)
}
fn record(coin: Coin, spent_height: Option<u32>) -> CoinRecord {
CoinRecord {
coin,
confirmed_height: Some(1),
spent_height,
timestamp: None,
coinbase: false,
}
}
#[test]
fn reads_loaded_coins_spends_lineages_and_context() {
let parent = coin([0x01; 32], [0x22; 32]);
let child = coin(parent.coin_id().into(), [0x33; 32]);
let launcher = Bytes32::new([0x09; 32]);
let source = MockChainSource::new()
.with_coin(parent.coin_id(), record(parent, None))
.with_coin(child.coin_id(), record(child, None))
.with_spend(
parent.coin_id(),
CoinSpend::new(parent, Program::from(vec![1]), Program::from(vec![0x80])),
)
.with_lineage(launcher, SingletonLineage::single(launcher))
.with_timestamp(7, 1_700_000_000)
.with_peak(7);
assert_eq!(
source.coin_record(parent.coin_id()),
Ok(Some(record(parent, None)))
);
assert!(source.coin_spend(parent.coin_id()).unwrap().is_some());
assert_eq!(source.peak_height(), Ok(Some(7)));
assert_eq!(source.block_timestamp(7), Ok(Some(1_700_000_000)));
assert_eq!(source.block_timestamp(8), Ok(None));
assert_eq!(
source.resolve_singleton_lineage(launcher),
Ok(Some(SingletonLineage::single(launcher)))
);
assert!(source.parent_spend(child.coin_id()).unwrap().is_some());
assert_eq!(source.parent_spend(Bytes32::new([0xFF; 32])), Ok(None));
}
#[test]
fn puzzle_hash_and_parent_queries_filter_correctly() {
let ph = [0x22u8; 32];
let unspent = coin([0x01; 32], ph);
let spent = coin([0x02; 32], ph);
let child = coin(unspent.coin_id().into(), [0x44; 32]);
let source = MockChainSource::new()
.with_coin(unspent.coin_id(), record(unspent, None))
.with_coin(spent.coin_id(), record(spent, Some(9)))
.with_coin(child.coin_id(), record(child, None));
assert_eq!(
source
.coin_records_by_puzzle_hash(Bytes32::new(ph), false)
.unwrap()
.len(),
1
);
assert_eq!(
source
.coin_records_by_puzzle_hash(Bytes32::new(ph), true)
.unwrap()
.len(),
2
);
let by_parent = source.coin_records_by_parent(unspent.coin_id()).unwrap();
assert_eq!(by_parent.len(), 1);
assert_eq!(by_parent[0].coin, child);
}
#[test]
fn forced_error_fails_every_read_closed() {
let source = MockChainSource::new().fail_with(ChainSourceError::Timeout);
let id = Bytes32::new([0x01; 32]);
assert_eq!(source.coin_record(id), Err(ChainSourceError::Timeout));
assert!(source.coin_records_by_puzzle_hash(id, true).is_err());
assert!(source.coin_records_by_parent(id).is_err());
assert!(source.coin_spend(id).is_err());
assert!(source.parent_spend(id).is_err());
assert!(source.resolve_singleton_lineage(id).is_err());
assert!(source.peak_height().is_err());
assert!(source.block_timestamp(1).is_err());
}
}