diff --git a/contracts/src/utils/structs/enumerable_set/mod.rs b/contracts/src/utils/structs/enumerable_set/mod.rs index e343b030a..fdfcee260 100644 --- a/contracts/src/utils/structs/enumerable_set/mod.rs +++ b/contracts/src/utils/structs/enumerable_set/mod.rs @@ -1,14 +1,58 @@ //! Smart contract for managing sets. +//! +//! Sets have the following properties: +//! +//! * Elements are added, removed, and checked for existence in constant time +//! (O(1)). +//! * Elements are enumerated in O(n). No guarantees are made on the ordering. +//! * Set can be cleared (all elements removed) in O(n). +//! +//! [`EnumerableSet`] provides a generic implementation of sets that can store +//! various data types including [`alloy_primitives::Address`], +//! [`alloy_primitives::B256`], [`alloy_primitives::U8`], +//! [`alloy_primitives::U16`], [`alloy_primitives::U32`], +//! [`alloy_primitives::U64`], [`alloy_primitives::U128`], and [`U256`]. +//! +//! # Usage Example +//! +//! ```rust,ignore +//! use alloy_primitives::Address; +//! use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet; +//! use stylus_sdk::prelude::*; +//! +//! #[storage] +//! pub struct MyContract { +//! members: EnumerableSet
, +//! } +//! +//! impl MyContract { +//! pub fn add_member(&mut self, member: Address) -> bool { +//! self.members.add(member) +//! } +//! +//! pub fn remove_member(&mut self, member: Address) -> bool { +//! self.members.remove(member) +//! } +//! +//! pub fn is_member(&self, member: Address) -> bool { +//! self.members.contains(member) +//! } +//! } +//! ``` +//! +//! # Custom Storage Types +//! +//! You can implement [`EnumerableSet`] for your own storage types by +//! implementing the [`Element`] and [`Accessor`] traits (see [`element`] +//! module). This allows you to create sets of custom data structures that +//! integrate seamlessly with Stylus storage. +//! +//! **Note**: [`stylus_sdk::storage::StorageBytes`] and +//! [`stylus_sdk::storage::StorageString`] cannot currently be implemented due +//! to current Stylus SDK limitations, but this might change in the future. pub mod element; -/// Sets have the following properties: -/// -/// * Elements are added, removed, and checked for existence in constant -/// time (O(1)). -/// * Elements are enumerated in O(n). No guarantees are made on the -/// ordering. -/// * Set can be cleared (all elements removed) in O(n). use alloc::{vec, vec::Vec}; use alloy_primitives::{uint, U256}; diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 4ce942940..fba1ced28 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -20,3 +20,4 @@ ** xref:uups-proxy.adoc[UUPS Proxy] * xref:utilities.adoc[Utilities] +** xref:enumerable-set-custom.adoc[Custom EnumerableSet Implementation] diff --git a/docs/modules/ROOT/pages/enumerable-set-custom.adoc b/docs/modules/ROOT/pages/enumerable-set-custom.adoc new file mode 100644 index 000000000..6fa6f0a92 --- /dev/null +++ b/docs/modules/ROOT/pages/enumerable-set-custom.adoc @@ -0,0 +1,389 @@ += Custom EnumerableSet Implementation + +EnumerableSet is a powerful data structure that allows you to store and manage sets of various data types efficiently. While OpenZeppelin Stylus Contracts provides built-in support for common types like `Address`, `U256`, and others, you can also implement EnumerableSet for your own custom storage types. + +== Overview + +The EnumerableSet implementation is generic and can work with any type that implements the required traits. The key traits that enable this functionality are: + +* `Element`: Associates a type with its storage representation +* `Accessor`: Provides methods to get and set values in storage + +== Built-in Support + +EnumerableSet comes with built-in support for the following types: + +* `Address` - Ethereum addresses +* `B256` - 32-byte fixed arrays (commonly used for hashes) +* `U8`, `U16`, `U32`, `U64`, `U128`, `U256` - Unsigned integers of various sizes + +== Basic Usage + +Here's how to use EnumerableSet with built-in types: + +[source,rust] +---- +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet; +use stylus_sdk::prelude::*; + +#[storage] +pub struct MyContract { + admins: EnumerableSet
, + token_ids: EnumerableSet, +} + +#[public] +impl MyContract { + /// Add an admin to the set + pub fn add_admin(&mut self, admin: Address) -> bool { + self.admins.add(admin) + } + + /// Remove an admin from the set + pub fn remove_admin(&mut self, admin: Address) -> bool { + self.admins.remove(admin) + } + + /// Check if an address is an admin + pub fn is_admin(&self, admin: Address) -> bool { + self.admins.contains(admin) + } + + /// Get the number of admins + pub fn admin_count(&self) -> U256 { + self.admins.length() + } + + /// Get all admins as a vector + pub fn get_all_admins(&self) -> Vec
{ + self.admins.values() + } +} +---- + +== Implementing Custom Types + +To use EnumerableSet with your own custom types, you need to implement the `Element` and `Accessor` traits. Here's a step-by-step guide: + +=== Step 1: Define Your Custom Type + +First, create your custom type. It must implement `Copy` and `StorageKey`: + +[source,rust] +---- +use alloy_primitives::U256; +use stylus_sdk::storage::{StorageKey, StorageType, SimpleStorageType}; +use stylus_sdk::prelude::*; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct UserId(pub U256); + +// Implement StorageKey for your type +impl StorageKey for UserId { + const SLOT: U256 = U256::ZERO; + + fn to_key(&self) -> U256 { + self.0 + } +} +---- + +=== Step 2: Create a Storage Wrapper + +Create a storage wrapper for your type: + +[source,rust] +---- +use stylus_sdk::storage::{StorageU256, Erase}; + +#[storage] +pub struct StorageUserId { + inner: StorageU256, +} + +impl StorageType for StorageUserId { + type Wraps<'a> = UserId where Self: 'a; + type WrapsMut<'a> = UserId where Self: 'a; + + fn load<'a>(&'a self) -> Self::Wraps<'a> { + UserId(self.inner.get()) + } + + fn store<'a>(&'a mut self, value: Self::WrapsMut<'a>) { + self.inner.set(value.0); + } +} + +impl<'a> SimpleStorageType<'a> for StorageUserId { + type Wraps = UserId; +} + +impl Erase for StorageUserId { + fn erase(&mut self) { + self.inner.erase(); + } +} +---- + +=== Step 3: Implement the Required Traits + +Now implement the `Element` and `Accessor` traits: + +[source,rust] +---- +use openzeppelin_stylus::utils::structs::enumerable_set::element::{Element, Accessor}; + +impl Element for UserId { + type StorageElement = StorageUserId; +} + +impl Accessor for StorageUserId { + type Wraps = UserId; + + fn get(&self) -> Self::Wraps { + UserId(self.inner.get()) + } + + fn set(&mut self, value: Self::Wraps) { + self.inner.set(value.0); + } +} +---- + +=== Step 4: Use Your Custom EnumerableSet + +Now you can use your custom type with EnumerableSet: + +[source,rust] +---- +use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet; + +#[storage] +pub struct UserManager { + active_users: EnumerableSet, +} + +#[public] +impl UserManager { + pub fn add_user(&mut self, user_id: UserId) -> bool { + self.active_users.add(user_id) + } + + pub fn remove_user(&mut self, user_id: UserId) -> bool { + self.active_users.remove(user_id) + } + + pub fn is_active_user(&self, user_id: UserId) -> bool { + self.active_users.contains(user_id) + } + + pub fn active_user_count(&self) -> U256 { + self.active_users.length() + } + + pub fn get_all_active_users(&self) -> Vec { + self.active_users.values() + } + + pub fn clear_all_users(&mut self) { + self.active_users.clear() + } +} +---- + +== Complete Example: Custom Product Type + +Here's a complete example implementing a custom `Product` type for an e-commerce contract: + +[source,rust] +---- +use alloy_primitives::U256; +use openzeppelin_stylus::utils::structs::enumerable_set::{EnumerableSet, element::{Element, Accessor}}; +use stylus_sdk::{ + prelude::*, + storage::{StorageKey, StorageType, SimpleStorageType, StorageU256, Erase}, +}; + +// Custom Product type +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ProductId(pub U256); + +impl StorageKey for ProductId { + const SLOT: U256 = U256::ZERO; + + fn to_key(&self) -> U256 { + self.0 + } +} + +// Storage wrapper for ProductId +#[storage] +pub struct StorageProductId { + inner: StorageU256, +} + +impl StorageType for StorageProductId { + type Wraps<'a> = ProductId where Self: 'a; + type WrapsMut<'a> = ProductId where Self: 'a; + + fn load<'a>(&'a self) -> Self::Wraps<'a> { + ProductId(self.inner.get()) + } + + fn store<'a>(&'a mut self, value: Self::WrapsMut<'a>) { + self.inner.set(value.0); + } +} + +impl<'a> SimpleStorageType<'a> for StorageProductId { + type Wraps = ProductId; +} + +impl Erase for StorageProductId { + fn erase(&mut self) { + self.inner.erase(); + } +} + +// Implement required traits +impl Element for ProductId { + type StorageElement = StorageProductId; +} + +impl Accessor for StorageProductId { + type Wraps = ProductId; + + fn get(&self) -> Self::Wraps { + ProductId(self.inner.get()) + } + + fn set(&mut self, value: Self::Wraps) { + self.inner.set(value.0); + } +} + +// Main contract using the custom EnumerableSet +#[entrypoint] +#[storage] +pub struct ECommerceContract { + available_products: EnumerableSet, + featured_products: EnumerableSet, +} + +#[public] +impl ECommerceContract { + #[constructor] + pub fn constructor(&mut self) {} + + /// Add a product to available products + pub fn add_product(&mut self, product_id: ProductId) -> bool { + self.available_products.add(product_id) + } + + /// Remove a product from available products + pub fn remove_product(&mut self, product_id: ProductId) -> bool { + self.available_products.remove(product_id) + } + + /// Check if a product is available + pub fn is_product_available(&self, product_id: ProductId) -> bool { + self.available_products.contains(product_id) + } + + /// Get total number of available products + pub fn total_products(&self) -> U256 { + self.available_products.length() + } + + /// Feature a product (add to featured list) + pub fn feature_product(&mut self, product_id: ProductId) -> bool { + if self.available_products.contains(product_id) { + self.featured_products.add(product_id) + } else { + false + } + } + + /// Get all featured products + pub fn get_featured_products(&self) -> Vec { + self.featured_products.values() + } + + /// Clear all featured products + pub fn clear_featured_products(&mut self) { + self.featured_products.clear() + } +} +---- + +== Limitations + +=== StorageBytes and StorageString + +Currently, `StorageBytes` and `StorageString` cannot be implemented with EnumerableSet due to limitations in the current Stylus SDK. This restriction exists because these types don't conform to the required trait bounds. This limitation might be resolved in future versions of the Stylus SDK. + +=== Gas Considerations + +* Adding and removing elements are O(1) operations +* Checking membership is O(1) +* Enumerating all elements is O(n) +* Clearing the set is O(n) + +Consider these performance characteristics when designing your contract's functionality. + +== Best Practices + +1. **Use appropriate types**: Choose the smallest type that can represent your data to minimize storage costs. + +2. **Consider enumeration**: If you frequently need to iterate over all elements, make sure the set size remains reasonable. + +3. **Batch operations**: When possible, batch multiple add/remove operations to save gas. + +4. **Access control**: Implement proper access control for functions that modify sets, as these operations directly affect contract state. + +5. **Event emission**: Consider emitting events when set contents change to facilitate off-chain monitoring. + +== Testing Your Implementation + +Always thoroughly test your custom EnumerableSet implementation: + +[source,rust] +---- +#[cfg(test)] +mod tests { + use super::*; + use stylus_sdk::prelude::TopLevelStorage; + + unsafe impl TopLevelStorage for ECommerceContract {} + + #[test] + fn test_custom_enumerable_set() { + let mut contract = ECommerceContract::default(); + + let product1 = ProductId(U256::from(1)); + let product2 = ProductId(U256::from(2)); + + // Test adding products + assert!(contract.add_product(product1)); + assert!(contract.add_product(product2)); + + // Test duplicate addition + assert!(!contract.add_product(product1)); + + // Test membership + assert!(contract.is_product_available(product1)); + assert!(contract.is_product_available(product2)); + + // Test count + assert_eq!(contract.total_products(), U256::from(2)); + + // Test removal + assert!(contract.remove_product(product1)); + assert!(!contract.is_product_available(product1)); + assert_eq!(contract.total_products(), U256::from(1)); + } +} +---- + +This testing approach ensures your implementation works correctly and maintains the expected EnumerableSet invariants. diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 13c8c9925..fe7d26441 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -49,3 +49,5 @@ Contracts for Stylus provides these libraries for enhanced data structure manage - https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/bitmap/index.html[`BitMaps`]: Store packed booleans in storage. - https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/checkpoints/index.html[`Checkpoints`]: Checkpoint values with built-in lookups. - https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/enumerable_set/index.html[`EnumerableSets`]: Contract for managing sets of many primitive types. + +For detailed guidance on implementing EnumerableSet with custom storage types, see xref:enumerable-set-custom.adoc[Custom EnumerableSet Implementation]. diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 000000000..a0343cfea --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,297 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@openzeppelin/docs-utils': + specifier: ^0.1.2 + version: 0.1.6 + +packages: + + '@frangio/servbot@0.3.0-1': + resolution: {integrity: sha512-eKXRqt8Zh3aqtVYoyayuLiktcW6vnYCuwlcqg91cv3HSdM5foTmIECJEJiwI+GBmSLY37mzczpt/ZfvYqzrQWQ==} + engines: {node: '>=12.x', pnpm: 10.x} + + '@openzeppelin/docs-utils@0.1.6': + resolution: {integrity: sha512-cVLtDPrCdVgnLV9QRK9D1jrTB8ezQ8tCLTM4g6PHe9TIK3DbO6lSizLF98DhncK2bk6uodOLRT3LO1WtNzei1Q==} + hasBin: true + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-port-reachable@3.1.0: + resolution: {integrity: sha512-vjc0SSRNZ32s9SbZBzGaiP6YVB+xglLShhgZD/FHMZUXBvQWaV9CtzgeVhjccFJrI6RAMV+LX7NYxueW/A8W5A==} + engines: {node: '>=8'} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + +snapshots: + + '@frangio/servbot@0.3.0-1': {} + + '@openzeppelin/docs-utils@0.1.6': + dependencies: + '@frangio/servbot': 0.3.0-1 + chalk: 3.0.0 + chokidar: 3.6.0 + env-paths: 2.2.1 + find-up: 4.1.0 + is-port-reachable: 3.1.0 + js-yaml: 3.14.1 + lodash.startcase: 4.4.0 + minimist: 1.2.8 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + env-paths@2.2.1: {} + + esprima@4.0.1: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + has-flag@4.0.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-port-reachable@3.1.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + minimist@1.2.8: {} + + normalize-path@3.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@4.0.0: {} + + picomatch@2.3.1: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + sprintf-js@1.0.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0