diff --git a/Cargo.lock b/Cargo.lock index daabb8d75..c616ec0c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,6 +1422,21 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "bounded-collections" +version = "3.5.1" +dependencies = [ + "assert_matches", + "borsh", + "derive_more 2.1.1", + "hex", + "rstest", + "schemars 0.8.22", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "bs58" version = "0.4.0" @@ -1905,12 +1920,12 @@ name = "contract-interface" version = "3.5.1" dependencies = [ "borsh", + "bounded-collections", "bs58 0.5.1", "derive_more 2.1.1", "hex", "insta", "near-sdk", - "non-empty-collections", "rstest", "schemars 0.8.22", "serde", @@ -5254,6 +5269,7 @@ dependencies = [ "anyhow", "assert_matches", "borsh", + "bounded-collections", "cargo-near-build", "contract-history", "contract-interface", @@ -5274,7 +5290,6 @@ dependencies = [ "near-account-id 2.5.0", "near-sdk", "near-workspaces", - "non-empty-collections", "rand 0.8.5", "rand_core 0.6.4", "rstest", @@ -5335,6 +5350,7 @@ dependencies = [ "backon", "base64 0.22.1", "borsh", + "bounded-collections", "bs58 0.5.1", "built", "bytes", @@ -5369,7 +5385,6 @@ dependencies = [ "near-sdk", "near-time 2.10.6", "node-types", - "non-empty-collections", "num_enum", "pprof", "prometheus", @@ -7227,20 +7242,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "non-empty-collections" -version = "3.5.1" -dependencies = [ - "assert_matches", - "borsh", - "derive_more 2.1.1", - "rstest", - "schemars 0.8.22", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "ntapi" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index 6eb04e6d9..2153e8c2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ exclude = ["libs/nearcore"] members = [ "crates/attestation", "crates/backup-cli", + "crates/bounded-collections", "crates/ckd-example-cli", "crates/contract", "crates/contract-history", @@ -15,7 +16,6 @@ members = [ "crates/mpc-attestation", "crates/node", "crates/node-types", - "crates/non-empty-collections", "crates/primitives", "crates/tee-authority", "crates/test-migration-contract", @@ -33,6 +33,7 @@ license = "MIT" [workspace.dependencies] #workspace members attestation = { path = "crates/attestation" } +bounded-collections = { path = "crates/bounded-collections" } contract-history = { path = "crates/contract-history" } contract-interface = { path = "crates/contract-interface" } foreign-chain-inspector = { path = "crates/foreign-chain-inspector" } @@ -44,7 +45,6 @@ mpc-node = { path = "crates/node" } mpc-primitives = { path = "crates/primitives", features = ["abi"] } mpc-tls = { path = "crates/tls" } node-types = { path = "crates/node-types" } -non-empty-collections = { path = "crates/non-empty-collections" } tee-authority = { path = "crates/tee-authority" } test-utils = { path = "crates/test-utils" } utilities = { path = "crates/utilities" } diff --git a/crates/non-empty-collections/Cargo.toml b/crates/bounded-collections/Cargo.toml similarity index 90% rename from crates/non-empty-collections/Cargo.toml rename to crates/bounded-collections/Cargo.toml index e759b1d15..7278f05c6 100644 --- a/crates/non-empty-collections/Cargo.toml +++ b/crates/bounded-collections/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "non-empty-collections" +name = "bounded-collections" version.workspace = true edition.workspace = true license.workspace = true @@ -10,6 +10,7 @@ abi = ["borsh/unstable__schema", "schemars"] [dependencies] borsh = { workspace = true } derive_more = { workspace = true } +hex = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } diff --git a/crates/bounded-collections/src/bounded_vec.rs b/crates/bounded-collections/src/bounded_vec.rs new file mode 100644 index 000000000..1fc79ebf8 --- /dev/null +++ b/crates/bounded-collections/src/bounded_vec.rs @@ -0,0 +1,1300 @@ +use std::{ + convert::{TryFrom, TryInto}, + slice::{Iter, IterMut}, + vec, +}; + +use thiserror::Error; + +/// Vec bounded with minimal (L - lower bound) and maximal (U - upper bound) items quantity. +/// +/// By default the witness type is [`witnesses::NonEmpty`], which requires `L > 0`. +/// For a possibly-empty bounded vector (where `L = 0`), use [`EmptyBoundedVec`] instead. +/// +/// # Type Parameters +/// +/// * `W` - witness type to prove vector ranges and shape of interface accordingly +#[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)] +pub struct BoundedVec> { + inner: Vec, + witness: W, +} + +/// BoundedVec errors +#[derive(Error, PartialEq, Eq, Debug, Clone)] +pub enum BoundedVecOutOfBounds { + /// Items quantity is less than L (lower bound) + #[error("Lower bound violation: got {got} (expected >= {lower_bound})")] + LowerBoundError { + /// L (lower bound) + lower_bound: usize, + /// provided value + got: usize, + }, + /// Items quantity is more than U (upper bound) + #[error("Upper bound violation: got {got} (expected <= {upper_bound})")] + UpperBoundError { + /// U (upper bound) + upper_bound: usize, + /// provided value + got: usize, + }, +} + +/// Module for type witnesses used to prove vector bounds at compile time +pub mod witnesses { + /// Compile-time proof of valid bounds. Must be constructed with same bounds to instantiate `BoundedVec`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct NonEmpty( + (), // private field to prevent direct construction. + ); + + /// Possibly empty vector with upper bound. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct PossiblyEmpty( + (), // private field to prevent direct construction. + ); + + /// Type a compile-time proof of valid bounds + pub const fn non_empty() -> NonEmpty { + const { + if L == 0 { + panic!("L must be greater than 0") + } + if L > U { + panic!("L must be less than or equal to U") + } + + NonEmpty::(()) + } + } + + /// Type a compile-time proof for possibly empty vector with upper bound + pub const fn possibly_empty() -> PossiblyEmpty { + const { PossiblyEmpty::(()) } + } +} + +impl BoundedVec> { + /// Creates new [`BoundedVec`] or returns error if items count is out of bounds + /// + /// # Parameters + /// + /// * `items` - vector of items within bounds + /// + /// # Errors + /// + /// * `UpperBoundError` - if `items` len is more than U (upper bound) + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use bounded_collections::witnesses; + /// let data: BoundedVec<_, 0, 8, witnesses::PossiblyEmpty<8>> = + /// BoundedVec::<_, 0, 8, witnesses::PossiblyEmpty<8>>::from_vec(vec![1u8, 2]).unwrap(); + /// ``` + pub fn from_vec(items: Vec) -> Result { + let witness = witnesses::possibly_empty::(); + let len = items.len(); + if len > U { + Err(BoundedVecOutOfBounds::UpperBoundError { + upper_bound: U, + got: len, + }) + } else { + Ok(BoundedVec { + inner: items, + witness, + }) + } + } + + /// Returns the first element of the vector, or [`None`] if it is empty + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use bounded_collections::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.first(), Some(&1u8)); + /// ``` + pub fn first(&self) -> Option<&T> { + self.inner.first() + } + + /// Returns `true` if the vector contains no elements + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use bounded_collections::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.is_empty(), false); + /// ``` + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Returns the last element of the vector, or [`None`] if it is empty + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use bounded_collections::witnesses; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.last(), Some(&2u8)); + /// ``` + pub fn last(&self) -> Option<&T> { + self.inner.last() + } +} + +/// Methods which works for all witnesses +impl BoundedVec { + /// Returns an underlying [`Vec`] + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.into_vec(), vec![1u8,2]); + /// ``` + pub fn into_vec(self) -> Vec { + self.inner + } + + /// Extracts a slice containing the entire vector. + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(data.as_slice(), &[1u8,2]); + /// ``` + pub fn as_slice(&self) -> &[T] { + self.inner.as_slice() + } + + /// Returns a reference for an element at index or `None` if out of bounds + /// + /// # Example + /// + /// ``` + /// use bounded_collections::BoundedVec; + /// let data: BoundedVec = [1u8,2].into(); + /// let elem = *data.get(1).unwrap(); + /// assert_eq!(elem, 2); + /// ``` + pub fn get(&self, index: usize) -> Option<&T> { + self.inner.get(index) + } + + /// Returns the number of elements in the vector + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec = vec![1u8,2].try_into().unwrap(); + /// assert_eq!(data.len(), 2); + /// ``` + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns an iterator + pub fn iter(&self) -> Iter { + self.inner.iter() + } + + /// Returns an iterator that allows to modify each value + pub fn iter_mut(&mut self) -> IterMut { + self.inner.iter_mut() + } +} + +impl BoundedVec> { + /// Creates new BoundedVec or returns error if items count is out of bounds + /// + /// # Parameters + /// + /// * `items` - vector of items within bounds + /// + /// # Errors + /// + /// * `LowerBoundError` - if `items` len is less than L (lower bound) + /// * `UpperBoundError` - if `items` len is more than U (upper bound) + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use bounded_collections::witnesses; + /// let data: BoundedVec<_, 2, 8, witnesses::NonEmpty<2, 8>> = + /// BoundedVec::<_, 2, 8, witnesses::NonEmpty<2, 8>>::from_vec(vec![1u8, 2]).unwrap(); + /// ``` + pub fn from_vec(items: Vec) -> Result { + let witness = witnesses::non_empty::(); + let len = items.len(); + if len < L { + Err(BoundedVecOutOfBounds::LowerBoundError { + lower_bound: L, + got: len, + }) + } else if len > U { + Err(BoundedVecOutOfBounds::UpperBoundError { + upper_bound: U, + got: len, + }) + } else { + Ok(BoundedVec { + inner: items, + witness, + }) + } + } + + /// Returns the first element of non-empty Vec + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(*data.first(), 1); + /// ``` + pub fn first(&self) -> &T { + self.inner.first().unwrap() + } + + /// Returns the last element of non-empty Vec + /// + /// # Example + /// ``` + /// use bounded_collections::BoundedVec; + /// use std::convert::TryInto; + /// + /// let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + /// assert_eq!(*data.last(), 2); + /// ``` + pub fn last(&self) -> &T { + self.inner.last().unwrap() + } + + /// Returns the last and all the rest of the elements + pub fn split_last(&self) -> (&T, &[T]) { + self.inner.split_last().unwrap() + } + + /// Return a new BoundedVec with indices included + pub fn enumerated(self) -> BoundedVec<(usize, T), L, U, witnesses::NonEmpty> { + BoundedVec { + inner: self.inner.into_iter().enumerate().collect(), + witness: self.witness, + } + } +} + +impl BoundedVec +where + Witness: Copy, +{ + /// Create a new `BoundedVec` by consuming `self` and mapping each element. + /// + /// This is useful as it keeps the knowledge that the length is >= L, <= U, + /// even through the old `BoundedVec` is consumed and turned into an iterator. + /// + /// # Example + /// + /// ``` + /// use bounded_collections::BoundedVec; + /// let data: BoundedVec = [1u8,2].into(); + /// let data = data.mapped(|x|x*2); + /// assert_eq!(data, [2u8,4].into()); + /// ``` + pub fn mapped(self, map_fn: F) -> BoundedVec + where + F: FnMut(T) -> N, + { + BoundedVec { + inner: self.inner.into_iter().map(map_fn).collect::>(), + witness: self.witness, + } + } + + /// Create a new `BoundedVec` by mapping references to the elements of self + /// + /// This is useful as it keeps the knowledge that the length is >= L, <= U, + /// will still hold for new `BoundedVec` + /// + /// # Example + /// + /// ``` + /// use bounded_collections::BoundedVec; + /// let data: BoundedVec = [1u8,2].into(); + /// let data = data.mapped_ref(|x|x*2); + /// assert_eq!(data, [2u8,4].into()); + /// ``` + pub fn mapped_ref(&self, map_fn: F) -> BoundedVec + where + F: FnMut(&T) -> N, + { + BoundedVec { + inner: self.inner.iter().map(map_fn).collect::>(), + witness: self.witness, + } + } + + /// Create a new `BoundedVec` by consuming `self` and mapping each element + /// to a `Result`. + /// + /// This is useful as it keeps the knowledge that the length is preserved + /// even through the old `BoundedVec` is consumed and turned into an iterator. + /// + /// As this method consumes self, returning an error means that this + /// vec is dropped. I.e. this method behaves roughly like using a + /// chain of `into_iter()`, `map`, `collect::,E>>` and + /// then converting the `Vec` back to a `Vec1`. + /// + /// + /// # Errors + /// + /// Once any call to `map_fn` returns a error that error is directly + /// returned by this method. + /// + /// # Example + /// + /// ``` + /// use bounded_collections::BoundedVec; + /// let data: BoundedVec = [1u8,2].into(); + /// let data: Result, _> = data.try_mapped(|x| Err("failed")); + /// assert_eq!(data, Err("failed")); + /// ``` + pub fn try_mapped(self, mut map_fn: F) -> Result, E> + where + F: FnMut(T) -> Result, + { + let out = self + .inner + .into_iter() + .map(&mut map_fn) + .collect::, E>>()?; + + Ok(BoundedVec { + inner: out, + witness: self.witness, + }) + } + + /// Create a new `BoundedVec` by mapping references of `self` elements + /// to a `Result`. + /// + /// This is useful as it keeps the knowledge that the length is preserved + /// even through the old `BoundedVec` is consumed and turned into an iterator. + /// + /// # Errors + /// + /// Once any call to `map_fn` returns a error that error is directly + /// returned by this method. + /// + /// # Example + /// + /// ``` + /// use bounded_collections::BoundedVec; + /// let data: BoundedVec = [1u8,2].into(); + /// let data: Result, _> = data.try_mapped_ref(|x| Err("failed")); + /// assert_eq!(data, Err("failed")); + /// ``` + pub fn try_mapped_ref(&self, mut map_fn: F) -> Result, E> + where + F: FnMut(&T) -> Result, + { + let out = self + .inner + .iter() + .map(&mut map_fn) + .collect::, E>>()?; + + Ok(BoundedVec { + inner: out, + witness: self.witness, + }) + } +} + +/// A non-empty Vec with no effective upper-bound on its length +pub type NonEmptyVec = BoundedVec>; + +/// Possibly empty Vec with upper-bound on its length +pub type EmptyBoundedVec = BoundedVec>; + +/// Non-empty Vec with bounded length +pub type NonEmptyBoundedVec = + BoundedVec>; + +impl TryFrom> + for BoundedVec> +{ + type Error = BoundedVecOutOfBounds; + + fn try_from(value: Vec) -> Result { + Self::from_vec(value) + } +} + +impl TryFrom> for BoundedVec> { + type Error = BoundedVecOutOfBounds; + + fn try_from(value: Vec) -> Result { + Self::from_vec(value) + } +} + +// when feature(const_evaluatable_checked) is stable cover all array sizes (L..=U) +impl From<[T; L]> + for BoundedVec> +{ + fn from(arr: [T; L]) -> Self { + BoundedVec { + inner: arr.into(), + witness: witnesses::non_empty(), + } + } +} + +impl From>> + for Vec +{ + fn from(v: BoundedVec>) -> Self { + v.inner + } +} + +impl From>> for Vec { + fn from(v: BoundedVec>) -> Self { + v.inner + } +} + +impl IntoIterator for BoundedVec { + type Item = T; + type IntoIter = vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a BoundedVec { + type Item = &'a T; + type IntoIter = core::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.inner.iter() + } +} + +impl<'a, T, const L: usize, const U: usize, W> IntoIterator for &'a mut BoundedVec { + type Item = &'a mut T; + type IntoIter = core::slice::IterMut<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.inner.iter_mut() + } +} + +impl AsRef> for BoundedVec { + fn as_ref(&self) -> &Vec { + &self.inner + } +} + +impl AsRef<[T]> for BoundedVec { + fn as_ref(&self) -> &[T] { + self.inner.as_slice() + } +} + +/// `AsRef<[T; N]>` is only available when `L == U == N`, i.e. the vector has +/// a fixed length known at compile time. +/// +/// ``` +/// use bounded_collections::BoundedVec; +/// let data: BoundedVec = [1u8, 2, 3].into(); +/// let arr: &[u8; 3] = data.as_ref(); +/// assert_eq!(arr, &[1, 2, 3]); +/// ``` +/// +/// Does not compile when L != U (variable-length vec): +/// ```compile_fail,E0277 +/// use bounded_collections::BoundedVec; +/// let data: BoundedVec = vec![1u8, 2].try_into().unwrap(); +/// let _: &[u8; 2] = data.as_ref(); +/// ``` +/// +/// Does not compile when N differs from L and U: +/// ```compile_fail,E0277 +/// use bounded_collections::BoundedVec; +/// let data: BoundedVec = [1u8, 2, 3].into(); +/// let _: &[u8; 4] = data.as_ref(); +/// ``` +impl AsRef<[T; N]> for BoundedVec> { + fn as_ref(&self) -> &[T; N] { + self.inner.as_slice().try_into().expect( + "When L == U == N, the length is guaranteed to be exactly N, so the conversion to a fixed-size array is infallible", + ) + } +} + +/// [`Option>`] to [`Vec`] +pub trait OptBoundedVecToVec { + /// [`Option>`] to [`Vec`] + fn to_vec(self) -> Vec; +} + +impl OptBoundedVecToVec + for Option>> +{ + fn to_vec(self) -> Vec { + self.map(|bv| bv.into()).unwrap_or_default() + } +} + +mod borsh_impl { + use super::*; + use borsh::{BorshDeserialize, BorshSerialize}; + + impl BorshSerialize + for BoundedVec + { + fn serialize(&self, writer: &mut Writer) -> std::io::Result<()> { + self.inner.serialize(writer) + } + } + + impl BorshDeserialize + for BoundedVec> + { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let inner = Vec::::deserialize_reader(reader)?; + Self::from_vec(inner) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + } + } + + impl BorshDeserialize + for BoundedVec> + { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let inner = Vec::::deserialize_reader(reader)?; + Self::from_vec(inner) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + } + } + + #[cfg(feature = "abi")] + mod schema { + use super::*; + use borsh::BorshSchema; + use borsh::schema::{Declaration, Definition, add_definition}; + use std::collections::BTreeMap; + + impl BorshSchema for BoundedVec { + fn declaration() -> Declaration { + format!("BoundedVec<{}, {}, {}>", T::declaration(), L, U) + } + + fn add_definitions_recursively(definitions: &mut BTreeMap) { + let definition = Definition::Sequence { + length_width: Definition::DEFAULT_LENGTH_WIDTH, + length_range: (L as u64)..=(U as u64), + elements: T::declaration(), + }; + add_definition(Self::declaration(), definition, definitions); + T::add_definitions_recursively(definitions); + } + } + } +} + +mod serde_impl { + use super::*; + use serde::{Deserialize, Serialize}; + + // direct impl to unify serde in one place instead of doing attribute on declaration and deserialize here + impl Serialize for BoundedVec { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner.serialize(serializer) + } + } + + impl<'de, T: Deserialize<'de>, const L: usize, const U: usize> Deserialize<'de> + for BoundedVec + { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let inner = Vec::::deserialize(deserializer)?; + BoundedVec::::from_vec(inner).map_err(serde::de::Error::custom) + } + } + + impl<'de, T: Deserialize<'de>, const U: usize> Deserialize<'de> for EmptyBoundedVec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let inner = Vec::::deserialize(deserializer)?; + EmptyBoundedVec::from_vec(inner).map_err(serde::de::Error::custom) + } + } + + #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] + mod schema { + use super::*; + use schemars::JsonSchema; + + impl JsonSchema for BoundedVec { + fn schema_name() -> String { + format!("BoundedVec_{}_Min{}_Max{}", T::schema_name(), L, U) + } + + fn json_schema( + generator: &mut schemars::r#gen::SchemaGenerator, + ) -> schemars::schema::Schema { + let mut schema = >::json_schema(generator); + if let schemars::schema::Schema::Object(ref mut obj) = schema { + if let Some(ref mut array) = obj.array { + array.min_items = u32::try_from(L).ok(); + array.max_items = u32::try_from(U).ok(); + } + } + schema + } + } + } +} + +/// Serde helper for serializing/deserializing `BoundedVec` as a hex string. +/// +/// Use with `#[serde(with = "bounded_collections::hex_serde")]` on fields +/// whose type is `BoundedVec`. +/// +/// When the `abi` feature is enabled, pair with +/// `#[schemars(with = "bounded_collections::hex_serde::HexString")]` +/// to generate a string schema with hex length constraints. +/// +/// # Example +/// ```ignore +/// use bounded_collections::BoundedVec; +/// +/// #[derive(serde::Serialize, serde::Deserialize)] +/// #[cfg_attr(feature = "abi", derive(schemars::JsonSchema))] +/// struct MyStruct { +/// #[serde(with = "bounded_collections::hex_serde")] +/// #[cfg_attr(feature = "abi", schemars(with = "bounded_collections::hex_serde::HexString<1, 64>"))] +/// data: BoundedVec, +/// } +/// ``` +pub mod hex_serde { + use super::*; + use serde::Deserialize; + + #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] + const HEX_PATTERN: &str = "^[0-9a-fA-F]*$"; + + pub fn serialize( + value: &BoundedVec, + serializer: S, + ) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&hex::encode(value.as_slice())) + } + + pub fn deserialize<'de, D, const L: usize, const U: usize>( + deserializer: D, + ) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let hex_str = String::deserialize(deserializer)?; + let bytes: Vec = hex::decode(&hex_str).map_err(serde::de::Error::custom)?; + bytes.try_into().map_err(serde::de::Error::custom) + } + + /// Marker type for JSON schema generation of hex-encoded `BoundedVec`. + /// + /// Use with `#[schemars(with = "bounded_collections::hex_serde::HexString")]` + /// alongside `#[serde(with = "bounded_collections::hex_serde")]`. + #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] + pub struct HexString; + + #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] + impl schemars::JsonSchema for HexString { + fn schema_name() -> String { + format!("HexString_Min{}_Max{}", L, U) + } + + fn json_schema( + _generator: &mut schemars::r#gen::SchemaGenerator, + ) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + string: Some(Box::new(schemars::schema::StringValidation { + min_length: Some((L * 2) as u32), + max_length: Some((U * 2) as u32), + pattern: Some(HEX_PATTERN.to_string()), + })), + ..Default::default() + } + .into() + } + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use core::convert::TryInto; + + use super::*; + + #[test] + fn from_vec_succeeds_within_bounds() { + // Given + let items = vec![1u8, 2]; + // When + let result = BoundedVec::::from_vec(items); + // Then + assert_matches!(result, Ok(_)); + } + + #[test] + fn from_vec_fails_below_lower_bound() { + // Given + let empty: Vec = vec![]; + // When + let result = BoundedVec::::from_vec(empty); + // Then + assert_eq!( + result.unwrap_err(), + BoundedVecOutOfBounds::LowerBoundError { + lower_bound: 2, + got: 0, + } + ); + } + + #[test] + fn from_vec_fails_when_length_less_than_lower_bound() { + // Given + let items = vec![1u8, 2]; + // When + let result = BoundedVec::::from_vec(items); + // Then + assert_eq!( + result.unwrap_err(), + BoundedVecOutOfBounds::LowerBoundError { + lower_bound: 3, + got: 2, + } + ); + } + + #[test] + fn from_vec_fails_above_upper_bound() { + // Given + let items = vec![1u8, 2, 3]; + // When + let result = BoundedVec::::from_vec(items); + // Then + assert_eq!( + result.unwrap_err(), + BoundedVecOutOfBounds::UpperBoundError { + upper_bound: 2, + got: 3, + } + ); + } + + #[test] + fn empty_bounded_from_vec_succeeds_within_bounds() { + // Given + let items = vec![1u8, 2]; + // When + let result = EmptyBoundedVec::::from_vec(items); + // Then + assert_matches!(result, Ok(_)); + } + + #[test] + fn empty_bounded_from_vec_accepts_empty() { + // Given + let items: Vec = vec![]; + // When + let result = EmptyBoundedVec::::from_vec(items); + // Then + assert_eq!( + result, + Ok(BoundedVec { + inner: vec![], + witness: witnesses::possibly_empty() + }) + ); + } + + #[test] + fn empty_bounded_from_vec_fails_above_upper_bound() { + // Given + let items = vec![1u8, 2, 3]; + // When + let result = EmptyBoundedVec::::from_vec(items); + // Then + assert_eq!( + result.unwrap_err(), + BoundedVecOutOfBounds::UpperBoundError { + upper_bound: 2, + got: 3, + } + ); + } + + #[test] + fn is_empty_returns_false_for_non_empty_vec() { + // Given + let data: EmptyBoundedVec<_, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert!(!data.is_empty()); + } + + #[test] + fn as_slice_returns_slice_of_elements() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.as_slice(), &[1u8, 2]); + } + + #[test] + fn len_returns_element_count() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.len(), 2); + } + + #[test] + fn first_returns_first_element() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.first(), &1u8); + } + + #[test] + fn last_returns_last_element() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.last(), &2u8); + } + + #[test] + fn empty_bounded_first_returns_some_when_non_empty() { + // Given + let data: EmptyBoundedVec<_, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.first(), Some(&1u8)); + } + + #[test] + fn empty_bounded_last_returns_some_when_non_empty() { + // Given + let data: EmptyBoundedVec<_, 8> = vec![1u8, 2].try_into().unwrap(); + // When / Then + assert_eq!(data.last(), Some(&2u8)); + } + + #[test] + fn mapped_applies_function_to_all_elements() { + // Given + let data: BoundedVec = [1u8, 2].into(); + // When + let result = data.mapped(|x| x * 2); + // Then + assert_eq!(result, [2u8, 4].into()); + } + + #[test] + fn mapped_ref_applies_function_to_all_elements() { + // Given + let data: BoundedVec = [1u8, 2].into(); + // When + let result = data.mapped_ref(|x| x * 2); + // Then + assert_eq!(result, [2u8, 4].into()); + } + + #[test] + fn get_returns_element_at_valid_index() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When + let elem = data.get(1); + // Then + assert_eq!(elem, Some(&2u8)); + } + + #[test] + fn get_returns_none_for_out_of_bounds_index() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When + let elem = data.get(3); + // Then + assert_eq!(elem, None); + } + + #[test] + fn try_mapped_succeeds_when_all_elements_succeed() { + // Given + let data: BoundedVec = [1u8, 2].into(); + // When + let result = data.try_mapped(|x| 100u8.checked_div(x).ok_or("error")); + // Then + assert_eq!(result, Ok([100u8, 50].into())); + } + + #[test] + fn try_mapped_fails_when_any_element_fails() { + // Given + let data: BoundedVec = [0u8, 2].into(); + // When + let result = data.try_mapped(|x| 100u8.checked_div(x).ok_or("error")); + // Then + assert_eq!(result, Err("error")); + } + + #[test] + fn try_mapped_ref_succeeds_when_all_elements_succeed() { + // Given + let data: BoundedVec = [1u8, 2].into(); + // When + let result = data.try_mapped_ref(|x| 100u8.checked_div(*x).ok_or("error")); + // Then + assert_eq!(result, Ok([100u8, 50].into())); + } + + #[test] + fn try_mapped_ref_fails_when_any_element_fails() { + // Given + let data: BoundedVec = [0u8, 2].into(); + // When + let result = data.try_mapped_ref(|x| 100u8.checked_div(*x).ok_or("error")); + // Then + assert_eq!(result, Err("error")); + } + + #[test] + fn split_last_returns_last_and_rest() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When + let (last, rest) = data.split_last(); + // Then + assert_eq!(last, &2u8); + assert_eq!(rest, &[1u8]); + } + + #[test] + fn split_last_on_single_element_returns_empty_rest() { + // Given + let data: BoundedVec<_, 1, 8> = vec![1u8].try_into().unwrap(); + // When + let (last, rest) = data.split_last(); + // Then + assert_eq!(last, &1u8); + assert!(rest.is_empty()); + } + + #[test] + fn enumerated_pairs_elements_with_indices() { + // Given + let data: BoundedVec<_, 2, 8> = vec![1u8, 2].try_into().unwrap(); + // When + let result = data.enumerated(); + // Then + let expected: BoundedVec<_, 2, 8> = vec![(0, 1u8), (1, 2)].try_into().unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn into_iter_yields_owned_elements() { + // Given + let vec = vec![1u8, 2]; + let data: BoundedVec<_, 2, 8> = vec.clone().try_into().unwrap(); + // When + let collected: Vec = data.into_iter().collect(); + // Then + assert_eq!(collected, vec); + } + + #[test] + fn iter_yields_references() { + // Given + let vec = vec![1u8, 2]; + let data: BoundedVec<_, 2, 8> = vec.clone().try_into().unwrap(); + // When + let collected: Vec<&u8> = data.iter().collect(); + // Then + assert_eq!(collected, vec.iter().collect::>()); + } + + #[test] + fn iter_mut_yields_mutable_references() { + // Given + let mut vec = vec![1u8, 2]; + let mut data: BoundedVec<_, 2, 8> = vec.clone().try_into().unwrap(); + // When + let collected: Vec<&mut u8> = data.iter_mut().collect(); + // Then + assert_eq!(collected, vec.iter_mut().collect::>()); + } +} + +#[cfg(test)] +mod serde_tests { + use assert_matches::assert_matches; + + use super::*; + + #[test] + fn deserialize_non_empty_vec_succeeds() { + // Given + let json = "[1, 2]"; + // When + let result = serde_json::from_str::>(json).unwrap(); + // Then + assert_eq!(result.as_slice(), &[1, 2]); + } + + #[test] + fn deserialize_non_empty_vec_rejects_empty_array() { + // Given + let json = "[]"; + // When + let result = serde_json::from_str::>(json); + // Then + assert_matches!(result, Err(_)); + } + + #[test] + fn deserialize_empty_bounded_vec_accepts_empty_array() { + // Given + let json = "[]"; + // When + let result = serde_json::from_str::>(json); + // Then + assert_matches!(result, Ok(_)); + } +} + +#[cfg(test)] +mod hex_serde_tests { + use assert_matches::assert_matches; + + use super::*; + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] + struct Wrapper { + #[serde(with = "hex_serde")] + data: BoundedVec, + } + + #[test] + fn roundtrip() { + let original = Wrapper { + data: vec![0xAB, 0xCD, 0xEF].try_into().unwrap(), + }; + let json = serde_json::to_string(&original).unwrap(); + assert_eq!(json, r#"{"data":"abcdef"}"#); + let deserialized: Wrapper = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn rejects_invalid_hex() { + let json = r#"{"data":"zzzz"}"#; + assert_matches!(serde_json::from_str::(json), Err(_)); + } + + #[test] + fn rejects_out_of_bounds() { + // 1 byte is below lower bound of 2 + let json = r#"{"data":"ab"}"#; + assert_matches!(serde_json::from_str::(json), Err(_)); + + // 5 bytes exceeds upper bound of 4 + let json = r#"{"data":"abcdef0102"}"#; + assert_matches!(serde_json::from_str::(json), Err(_)); + } +} + +#[cfg(test)] +mod borsh_tests { + use super::*; + use borsh::BorshDeserialize; + + #[test] + fn borsh_roundtrip_preserves_non_empty_vec() { + // Given + let original: BoundedVec = vec![1u8, 2, 3].try_into().unwrap(); + // When + let bytes = borsh::to_vec(&original).unwrap(); + let deserialized: BoundedVec = BorshDeserialize::try_from_slice(&bytes).unwrap(); + // Then + assert_eq!(deserialized, original); + } + + #[test] + fn borsh_roundtrip_preserves_empty_bounded_vec() { + // Given + let original: EmptyBoundedVec = vec![1u8, 2].try_into().unwrap(); + // When + let bytes = borsh::to_vec(&original).unwrap(); + let deserialized: EmptyBoundedVec = + BorshDeserialize::try_from_slice(&bytes).unwrap(); + // Then + assert_eq!(deserialized, original); + } + + #[test] + fn borsh_deserialize_rejects_too_few_elements() { + // Given + let empty_bytes = borsh::to_vec(&Vec::::new()).unwrap(); + // When + let result: Result, _> = + BorshDeserialize::try_from_slice(&empty_bytes); + // Then + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn borsh_deserialize_rejects_too_many_elements() { + // Given + let too_many_bytes = borsh::to_vec(&vec![1u8, 2, 3, 4, 5]).unwrap(); + // When + let result: Result, _> = + BorshDeserialize::try_from_slice(&too_many_bytes); + // Then + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn borsh_deserialize_empty_bounded_rejects_too_many() { + // Given + let too_many_bytes = borsh::to_vec(&vec![1u8, 2, 3]).unwrap(); + // When + let result: Result, _> = + BorshDeserialize::try_from_slice(&too_many_bytes); + // Then + assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn borsh_deserialize_empty_bounded_accepts_empty() { + // Given + let empty_bytes = borsh::to_vec(&Vec::::new()).unwrap(); + // When + let result: EmptyBoundedVec = + BorshDeserialize::try_from_slice(&empty_bytes).unwrap(); + // Then + assert!(result.is_empty()); + } +} + +#[cfg(all(test, feature = "abi"))] +mod borsh_schema_tests { + use super::*; + use borsh::{ + BorshSchema, + schema::{BorshSchemaContainer, Definition}, + }; + + #[test] + fn schema_declaration_includes_bounds() { + // Given / When + let decl = BoundedVec::::declaration(); + // Then + assert_eq!(decl, "BoundedVec"); + } + + #[test] + fn schema_declaration_empty_bounded_starts_at_zero() { + // Given / When + let decl = EmptyBoundedVec::::declaration(); + // Then + assert_eq!(decl, "BoundedVec"); + } + + #[test] + fn schema_encodes_length_range() { + // Given + let schema = BorshSchemaContainer::for_type::>(); + // When + let def = schema.get_definition("BoundedVec").unwrap(); + // Then + match def { + Definition::Sequence { + length_width, + length_range, + elements, + } => { + assert_eq!(*length_width, Definition::DEFAULT_LENGTH_WIDTH); + assert_eq!(*length_range, 2..=8); + assert_eq!(elements, "u8"); + } + other => panic!("expected Sequence, got {:?}", other), + } + } + + #[test] + fn schema_empty_bounded_range_starts_at_zero() { + // Given + let schema = BorshSchemaContainer::for_type::>(); + // When + let def = schema.get_definition("BoundedVec").unwrap(); + // Then + match def { + Definition::Sequence { length_range, .. } => { + assert_eq!(*length_range, 0..=4); + } + other => panic!("expected Sequence, got {:?}", other), + } + } + + #[test] + fn schema_validates_successfully() { + // Given + let schema = BorshSchemaContainer::for_type::>(); + // When / Then + assert_eq!(Ok(()), schema.validate()); + } +} diff --git a/crates/non-empty-collections/src/btreemap.rs b/crates/bounded-collections/src/btreemap.rs similarity index 100% rename from crates/non-empty-collections/src/btreemap.rs rename to crates/bounded-collections/src/btreemap.rs diff --git a/crates/non-empty-collections/src/btreeset.rs b/crates/bounded-collections/src/btreeset.rs similarity index 100% rename from crates/non-empty-collections/src/btreeset.rs rename to crates/bounded-collections/src/btreeset.rs diff --git a/crates/bounded-collections/src/lib.rs b/crates/bounded-collections/src/lib.rs new file mode 100644 index 000000000..96ffed446 --- /dev/null +++ b/crates/bounded-collections/src/lib.rs @@ -0,0 +1,10 @@ +mod bounded_vec; +mod btreemap; +mod btreeset; + +pub use bounded_vec::{ + BoundedVec, BoundedVecOutOfBounds, EmptyBoundedVec, NonEmptyBoundedVec, NonEmptyVec, + OptBoundedVecToVec, hex_serde, witnesses, +}; +pub use btreemap::{EmptyMapError, NonEmptyBTreeMap}; +pub use btreeset::{EmptySetError, NonEmptyBTreeSet}; diff --git a/crates/contract-interface/Cargo.toml b/crates/contract-interface/Cargo.toml index c89c9cee0..14459ca5e 100644 --- a/crates/contract-interface/Cargo.toml +++ b/crates/contract-interface/Cargo.toml @@ -5,13 +5,13 @@ license = { workspace = true } edition = { workspace = true } [features] -abi = ["borsh/unstable__schema", "schemars", "non-empty-collections/abi"] +abi = ["borsh/unstable__schema", "schemars", "bounded-collections/abi"] [dependencies] borsh = { workspace = true } +bounded-collections = { workspace = true } bs58 = { workspace = true } derive_more = { workspace = true } -non-empty-collections = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } sha2 = { workspace = true } diff --git a/crates/contract-interface/src/types/foreign_chain.rs b/crates/contract-interface/src/types/foreign_chain.rs index 48fcecf7f..859a6c773 100644 --- a/crates/contract-interface/src/types/foreign_chain.rs +++ b/crates/contract-interface/src/types/foreign_chain.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use non_empty_collections::NonEmptyBTreeSet; +use bounded_collections::NonEmptyBTreeSet; use serde::{Deserialize, Serialize}; use serde_with::{hex::Hex, serde_as}; use sha2::Digest; diff --git a/crates/contract/Cargo.toml b/crates/contract/Cargo.toml index b55d201b2..8cd225251 100644 --- a/crates/contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -51,6 +51,7 @@ __abi-generate = ["near-sdk/__abi-generate"] anyhow = { workspace = true } assert_matches = { workspace = true } borsh = { workspace = true } +bounded-collections = { workspace = true } contract-interface = { workspace = true } curve25519-dalek = { workspace = true } derive_more = { workspace = true, features = ["from", "deref"] } @@ -70,7 +71,6 @@ near-account-id = { workspace = true, features = [ "serde", ] } near-sdk = { workspace = true } -non-empty-collections = { workspace = true } rand = { workspace = true, optional = true } schemars = { workspace = true } serde = { workspace = true } diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 5671a0121..dfe057073 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -2058,6 +2058,7 @@ mod tests { }; use crate::tee::tee_state::NodeId; use assert_matches::assert_matches; + use bounded_collections::NonEmptyBTreeSet; use contract_interface::types::{ BitcoinExtractedValue, BitcoinExtractor, BitcoinRpcRequest, ExtractedValue, ForeignTxSignPayloadV1, @@ -2077,7 +2078,6 @@ mod tests { }; use mpc_primitives::hash::{Hash32, Image}; use near_sdk::{test_utils::VMContextBuilder, testing_env, NearToken, VMContext}; - use non_empty_collections::NonEmptyBTreeSet; use primitives::key_state::{AttemptId, KeyForDomain}; use rand::seq::SliceRandom; use rand::SeedableRng; diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 960c2b302..5b70d1291 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -16,6 +16,7 @@ axum = { workspace = true } backon = { workspace = true } base64 = { workspace = true } borsh = { workspace = true } +bounded-collections = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } clap = { workspace = true } @@ -48,7 +49,6 @@ near-o11y = { workspace = true } near-sdk = { workspace = true } near-time = { workspace = true } node-types = { workspace = true } -non-empty-collections = { workspace = true } num_enum = { workspace = true } pprof = { workspace = true } prometheus = { workspace = true } diff --git a/crates/node/src/config/foreign_chains.rs b/crates/node/src/config/foreign_chains.rs index fdfe87d5e..c4f1b44b6 100644 --- a/crates/node/src/config/foreign_chains.rs +++ b/crates/node/src/config/foreign_chains.rs @@ -2,8 +2,8 @@ use std::collections::BTreeSet; use std::{borrow::Cow, collections::BTreeMap}; use anyhow::Context; +use bounded_collections::{NonEmptyBTreeMap, NonEmptyBTreeSet}; use contract_interface::types as dtos; -use non_empty_collections::{NonEmptyBTreeMap, NonEmptyBTreeSet}; use serde::{Deserialize, Serialize}; mod abstract_chain; diff --git a/crates/node/src/config/foreign_chains/abstract_chain.rs b/crates/node/src/config/foreign_chains/abstract_chain.rs index 7801c23e4..c3d3b5b37 100644 --- a/crates/node/src/config/foreign_chains/abstract_chain.rs +++ b/crates/node/src/config/foreign_chains/abstract_chain.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use non_empty_collections::NonEmptyBTreeMap; +use bounded_collections::NonEmptyBTreeMap; use serde::{Deserialize, Serialize}; use crate::config::foreign_chains::auth; diff --git a/crates/node/src/config/foreign_chains/bitcoin.rs b/crates/node/src/config/foreign_chains/bitcoin.rs index aee4688cb..f19109249 100644 --- a/crates/node/src/config/foreign_chains/bitcoin.rs +++ b/crates/node/src/config/foreign_chains/bitcoin.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use non_empty_collections::NonEmptyBTreeMap; +use bounded_collections::NonEmptyBTreeMap; use serde::{Deserialize, Serialize}; use crate::config::foreign_chains::auth; diff --git a/crates/node/src/config/foreign_chains/ethereum.rs b/crates/node/src/config/foreign_chains/ethereum.rs index ed5a8a87e..134ea14c6 100644 --- a/crates/node/src/config/foreign_chains/ethereum.rs +++ b/crates/node/src/config/foreign_chains/ethereum.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use non_empty_collections::NonEmptyBTreeMap; +use bounded_collections::NonEmptyBTreeMap; use serde::{Deserialize, Serialize}; use crate::config::foreign_chains::auth; diff --git a/crates/node/src/config/foreign_chains/solana.rs b/crates/node/src/config/foreign_chains/solana.rs index 4a1b3590c..be5ae127d 100644 --- a/crates/node/src/config/foreign_chains/solana.rs +++ b/crates/node/src/config/foreign_chains/solana.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use non_empty_collections::NonEmptyBTreeMap; +use bounded_collections::NonEmptyBTreeMap; use serde::{Deserialize, Serialize}; use crate::config::foreign_chains::auth; diff --git a/crates/node/src/config/foreign_chains/starknet.rs b/crates/node/src/config/foreign_chains/starknet.rs index 4597c28a5..02f2b4ec9 100644 --- a/crates/node/src/config/foreign_chains/starknet.rs +++ b/crates/node/src/config/foreign_chains/starknet.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use non_empty_collections::NonEmptyBTreeMap; +use bounded_collections::NonEmptyBTreeMap; use serde::{Deserialize, Serialize}; use crate::config::foreign_chains::auth; diff --git a/crates/node/src/providers/verify_foreign_tx/sign.rs b/crates/node/src/providers/verify_foreign_tx/sign.rs index 1fa861f6f..a4d46b8d6 100644 --- a/crates/node/src/providers/verify_foreign_tx/sign.rs +++ b/crates/node/src/providers/verify_foreign_tx/sign.rs @@ -360,7 +360,7 @@ mod tests { }; use crate::indexer::MockReadForeignChainPolicy; use assert_matches::assert_matches; - use non_empty_collections::NonEmptyBTreeSet; + use bounded_collections::NonEmptyBTreeSet; use std::collections::BTreeMap; fn bitcoin_request() -> dtos::ForeignChainRpcRequest { @@ -372,7 +372,7 @@ mod tests { } fn bitcoin_foreign_chains_config() -> ForeignChainsConfig { - let providers = non_empty_collections::NonEmptyBTreeMap::new( + let providers = bounded_collections::NonEmptyBTreeMap::new( "public".to_string(), BitcoinProviderConfig { rpc_url: "https://blockstream.info/api".to_string(), diff --git a/crates/node/src/tests/foreign_chain_policy.rs b/crates/node/src/tests/foreign_chain_policy.rs index cc1f85893..142385c72 100644 --- a/crates/node/src/tests/foreign_chain_policy.rs +++ b/crates/node/src/tests/foreign_chain_policy.rs @@ -26,7 +26,7 @@ async fn foreign_chain_policy_auto_vote_on_startup__should_apply_local_policy() DEFAULT_BLOCK_TIME, ); - let providers = non_empty_collections::NonEmptyBTreeMap::new( + let providers = bounded_collections::NonEmptyBTreeMap::new( "public".to_string(), SolanaProviderConfig { rpc_url: "https://rpc.public.example.com".to_string(), diff --git a/crates/non-empty-collections/src/lib.rs b/crates/non-empty-collections/src/lib.rs deleted file mode 100644 index cfc79c68e..000000000 --- a/crates/non-empty-collections/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod btreemap; -mod btreeset; - -pub use btreemap::{EmptyMapError, NonEmptyBTreeMap}; -pub use btreeset::{EmptySetError, NonEmptyBTreeSet};