Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"contracts/analytics",
"contracts/fees",
"contracts/dex",
"contracts/common",
"contracts/compliance_registry",
"contracts/property-management",
"contracts/fractional",
Expand Down
25 changes: 25 additions & 0 deletions contracts/common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "propchain-common"
version = "1.0.0"
authors = ["PropChain Team <dev@propchain.io>"]
edition = "2021"
description = "Shared utilities for PropChain contracts (transaction-scoped caching)"
license = "MIT"
homepage = "https://propchain.io"
repository = "https://github.com/MettaChain/PropChain-contract"
publish = false

[dependencies]
ink = { workspace = true, default-features = false }
scale = { workspace = true, default-features = false }

[lib]
path = "src/lib.rs"

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
]
ink-as-dependency = []
144 changes: 144 additions & 0 deletions contracts/common/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,148 @@ mod tests {
cache.set(1, 100);
assert_eq!(cache.dirty_keys(), vec![1]);
}

// ── Issue #1011: extended coverage ──────────────────────────────────

#[test]
fn insert_retrieve_roundtrip_via_get_and_set() {
let mut cache: TransactionCache<u64, u128> = TransactionCache::new();

// Miss before insert.
assert!(cache.get(&1).is_none());

// Insert then retrieve.
cache.set(1, 1_000_000u128);
assert_eq!(cache.get(&1), Some(&1_000_000u128));

// Overwrite replaces the cached value.
cache.set(1, 2_000_000u128);
assert_eq!(cache.get(&1), Some(&2_000_000u128));

// Distinct keys stay independent.
cache.set(2, 42u128);
assert_eq!(cache.get(&1), Some(&2_000_000u128));
assert_eq!(cache.get(&2), Some(&42u128));
}

#[test]
fn get_or_insert_with_recomputes_fresh_value_after_invalidate() {
let mut cache: TransactionCache<u32, u32> = TransactionCache::new();

let v = *cache.get_or_insert_with(7, || 100);
assert_eq!(v, 100);

cache.invalidate(&7);
// The closure runs again and the new value wins.
let v = *cache.get_or_insert_with(7, || 200);
assert_eq!(v, 200);
assert_eq!(cache.len(), 1);
}

#[test]
fn computed_entries_are_clean_until_set() {
let mut cache: TransactionCache<u32, u32> = TransactionCache::new();
cache.get_or_insert_with(1, || 11);
cache.get_or_insert_with(2, || 22);
// Reads never dirty an entry — only `set` does.
assert!(cache.dirty_keys().is_empty());

cache.set(1, 111);
assert_eq!(cache.dirty_keys(), vec![1]);
}

#[test]
fn invalidate_removes_only_the_target_key() {
let mut cache: TransactionCache<u32, u32> = TransactionCache::new();
cache.set(1, 10);
cache.set(2, 20);
cache.set(3, 30);

cache.invalidate(&2);
assert!(cache.get(&2).is_none());
// Siblings survive.
assert_eq!(cache.get(&1), Some(&10));
assert_eq!(cache.get(&3), Some(&30));
assert_eq!(cache.len(), 2);
assert_eq!(cache.dirty_keys(), vec![1, 3]);
}

#[test]
fn invalidate_all_evicts_every_entry() {
let mut cache: TransactionCache<u32, u32> = TransactionCache::new();
cache.set(1, 10);
cache.get_or_insert_with(2, || 20);
cache.set(3, 30);
assert_eq!(cache.len(), 3);

cache.invalidate_all();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(cache.dirty_keys().is_empty());
assert!(cache.get(&1).is_none());
assert!(cache.get(&3).is_none());
}

#[test]
fn dirty_keys_are_ordered_across_multiple_keys() {
let mut cache: TransactionCache<u32, u32> = TransactionCache::new();

// Insert out of order; BTreeMap backing yields ascending key order.
cache.set(30, 300);
cache.set(10, 100);
cache.set(20, 200);
assert_eq!(cache.dirty_keys(), vec![10, 20, 30]);

// A later overwrite keeps the key in its sorted position once.
cache.set(20, 250);
assert_eq!(cache.dirty_keys(), vec![10, 20, 30]);
}

#[test]
fn len_and_is_empty_track_entries() {
let mut cache: TransactionCache<&'static str, u8> = TransactionCache::new();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);

cache.set("a", 1);
cache.set("b", 2);
assert!(!cache.is_empty());
assert_eq!(cache.len(), 2);

cache.invalidate(&"a");
assert_eq!(cache.len(), 1);

cache.invalidate_all();
assert!(cache.is_empty());
}

#[test]
fn scale_roundtrip_is_byte_identical_for_cached_value_types() {
use scale::{Decode, Encode};

// Representative cached value types: token amounts and id hashes.
let amount: u128 = 340_282_366_920_938_463_463u128;
let property_id: u64 = 123_456_789;
let code_hash: [u8; 32] = [0xA5; 32];

for value in [amount, property_id as u128, u128::MAX] {
let encoded = value.encode();
let decoded = u128::decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded, value);
// Re-encoding is byte-identical (format stability pin).
assert_eq!(decoded.encode(), encoded);
}

let hash_encoded = code_hash.encode();
let hash_decoded = <[u8; 32]>::decode(&mut &hash_encoded[..]).unwrap();
assert_eq!(hash_decoded, code_hash);
assert_eq!(hash_decoded.encode(), hash_encoded);

// Composite of both shapes round-trips too.
let entry = (property_id, amount, code_hash);
let entry_encoded = entry.encode();
let entry_decoded = <(u64, u128, [u8; 32])>::decode(&mut &entry_encoded[..]).unwrap();
assert_eq!(entry_decoded, entry);
assert_eq!(entry_decoded.encode(), entry_encoded);
}
}
9 changes: 9 additions & 0 deletions contracts/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#![cfg_attr(not(feature = "std"), no_std)]

//! Shared helpers for PropChain contracts.
//!
//! Currently hosts the intra-transaction caching abstraction
//! ([`cache::TransactionCache`]) used to avoid repeated storage reads
//! within a single message call.

pub mod cache;
6 changes: 6 additions & 0 deletions contracts/fees/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,4 +700,10 @@ mod propchain_fees {
self.calculate_fee(operation)
}
}

// Unit-test module lives in `tests.rs` (mirrors the bridge contract's
// `include!("tests.rs")` pattern). Named `fee_tests` because the
// `include!("errors.rs")` above already contributes a sibling `mod tests`.
#[cfg(test)]
include!("tests.rs");
}
Loading
Loading