multiversx_sc/storage/mappers/whitelist_mapper.rs
1use core::marker::PhantomData;
2
3use multiversx_sc_codec::{TopDecode, TopEncode};
4
5use super::{
6 SingleValueMapper, StorageMapper, StorageMapperFromAddress,
7 source::{CurrentStorage, StorageAddress},
8};
9use crate::{
10 api::{ErrorApiImpl, StorageMapperApi},
11 codec::NestedEncode,
12 storage::StorageKey,
13 types::ManagedAddress,
14};
15
16type FlagMapper<SA, A> = SingleValueMapper<SA, bool, A>;
17
18const ITEM_NOT_WHITELISTED_ERR_MSG: &str = "Item not whitelisted";
19
20/// A non-iterable whitelist mapper optimized for fast membership testing.
21///
22/// # Storage Layout
23///
24/// The `WhitelistMapper` uses a simple direct key approach with boolean flags:
25///
26/// - `base_key + encoded_item` → `true` (if whitelisted) or empty (if not whitelisted)
27///
28/// Each whitelisted item requires exactly one storage key, making this extremely space-efficient.
29/// Uses `SingleValueMapper<bool>` internally for each item.
30///
31/// # Main Operations
32///
33/// - **Add**: `add(item)` - Adds item to whitelist. O(1) with one storage write.
34/// - **Remove**: `remove(item)` - Removes item from whitelist. O(1) with storage clear.
35/// - **Check**: `contains(item)` - Tests membership. O(1) with one storage read.
36/// - **Require**: `require_whitelisted(item)` - Checks membership or errors. O(1).
37///
38/// # Key Characteristics
39///
40/// - **Non-iterable**: Cannot iterate over whitelisted items (use `SetMapper` if iteration needed)
41/// - **Space-efficient**: One storage key per item, minimal overhead
42/// - **Fast lookups**: Direct key-based membership testing
43/// - **Boolean logic**: Uses presence/absence, since true = 1, false = empty
44///
45/// # Trade-offs
46///
47/// - **Pros**: Extremely efficient for membership testing; minimal storage overhead; simple and fast.
48/// - **Cons**: No iteration capability; no built-in counting; cannot list all whitelisted items;
49/// no bulk operations.
50///
51/// # Comparison with SetMapper/UnorderedSetMapper
52///
53/// - **WhitelistMapper**: Non-iterable; most space-efficient; fastest lookups
54/// - **SetMapper**: Iterable; maintains insertion order; higher overhead
55/// - **UnorderedSetMapper**: Iterable; no order guarantees; moderate overhead
56///
57/// # Use Cases
58///
59/// - Simple whitelists where iteration is not needed
60/// - Permission systems (allowed addresses, tokens, etc.)
61/// - Feature flags or capability checking
62/// - Any membership testing where space efficiency is critical
63/// - Large whitelists where iteration would be prohibitively expensive
64///
65/// # Example
66///
67/// ```rust
68/// # use multiversx_sc::storage::mappers::{StorageMapper, WhitelistMapper};
69/// # use multiversx_sc::types::ManagedAddress;
70/// # fn example<SA: multiversx_sc::api::StorageMapperApi>(
71/// # admin: ManagedAddress<SA>,
72/// # user1: ManagedAddress<SA>,
73/// # user2: ManagedAddress<SA>
74/// # ) {
75/// # let whitelist = WhitelistMapper::<SA, ManagedAddress<SA>>::new(
76/// # multiversx_sc::storage::StorageKey::new(&b"allowed_users"[..])
77/// # );
78/// // Add addresses to whitelist
79/// whitelist.add(&admin);
80/// whitelist.add(&user1);
81///
82/// // Check membership
83/// assert!(whitelist.contains(&admin));
84/// assert!(whitelist.contains(&user1));
85/// assert!(!whitelist.contains(&user2));
86///
87/// // Require membership (errors if not whitelisted)
88/// whitelist.require_whitelisted(&admin); // OK
89/// // whitelist.require_whitelisted(&user2); // Would error: "Item not whitelisted"
90///
91/// // Remove from whitelist
92/// whitelist.remove(&user1);
93/// assert!(!whitelist.contains(&user1));
94///
95/// // Use in access control
96/// fn admin_only_function<SA: multiversx_sc::api::StorageMapperApi>(
97/// whitelist: &WhitelistMapper<SA, ManagedAddress<SA>>,
98/// caller: &ManagedAddress<SA>
99/// ) {
100/// whitelist.require_whitelisted(caller);
101/// // Function logic here...
102/// }
103/// # }
104/// ```
105///
106/// # Token Whitelist Example
107///
108/// ```rust
109/// # use multiversx_sc::storage::mappers::{StorageMapper, WhitelistMapper};
110/// # use multiversx_sc::types::TokenIdentifier;
111/// # fn token_example<SA: multiversx_sc::api::StorageMapperApi>(
112/// # token1: TokenIdentifier<SA>,
113/// # token2: TokenIdentifier<SA>
114/// # ) {
115/// # let allowed_tokens = WhitelistMapper::<SA, TokenIdentifier<SA>>::new(
116/// # multiversx_sc::storage::StorageKey::new(&b"allowed_tokens"[..])
117/// # );
118/// // Whitelist specific tokens
119/// allowed_tokens.add(&token1);
120///
121/// // Validate token in payment
122/// if allowed_tokens.contains(&token1) {
123/// // Process payment
124/// } else {
125/// // Reject payment
126/// }
127/// # }
128/// ```
129pub struct WhitelistMapper<SA, T, A = CurrentStorage>
130where
131 SA: StorageMapperApi,
132 A: StorageAddress<SA>,
133 T: NestedEncode + 'static,
134{
135 address: A,
136 base_key: StorageKey<SA>,
137 _phantom: PhantomData<T>,
138}
139
140impl<SA, T> StorageMapper<SA> for WhitelistMapper<SA, T, CurrentStorage>
141where
142 SA: StorageMapperApi,
143 T: NestedEncode + 'static,
144{
145 fn new(base_key: StorageKey<SA>) -> Self {
146 Self {
147 address: CurrentStorage,
148 base_key,
149 _phantom: PhantomData,
150 }
151 }
152}
153
154impl<SA, T> StorageMapperFromAddress<SA> for WhitelistMapper<SA, T, ManagedAddress<SA>>
155where
156 SA: StorageMapperApi,
157 T: NestedEncode + 'static,
158{
159 fn new_from_address(address: ManagedAddress<SA>, base_key: StorageKey<SA>) -> Self {
160 Self {
161 address,
162 base_key,
163 _phantom: PhantomData,
164 }
165 }
166}
167
168impl<SA, T> WhitelistMapper<SA, T, ManagedAddress<SA>>
169where
170 SA: StorageMapperApi,
171 T: TopDecode + TopEncode + NestedEncode + 'static,
172{
173 pub fn contains(&self, item: &T) -> bool {
174 let mapper = self.build_mapper_for_item(item);
175 !mapper.is_empty()
176 }
177
178 pub fn require_whitelisted(&self, item: &T) {
179 if !self.contains(item) {
180 SA::error_api_impl().signal_error(ITEM_NOT_WHITELISTED_ERR_MSG.as_bytes());
181 }
182 }
183
184 fn build_mapper_for_item(&self, item: &T) -> FlagMapper<SA, ManagedAddress<SA>> {
185 let mut key = self.base_key.clone();
186 key.append_item(item);
187
188 FlagMapper::<SA, ManagedAddress<SA>>::new_from_address(self.address.clone(), key)
189 }
190}
191
192impl<SA, T> WhitelistMapper<SA, T, CurrentStorage>
193where
194 SA: StorageMapperApi,
195 T: TopDecode + TopEncode + NestedEncode + 'static,
196{
197 pub fn contains(&self, item: &T) -> bool {
198 let mapper = self.build_mapper_for_item(item);
199 !mapper.is_empty()
200 }
201
202 pub fn require_whitelisted(&self, item: &T) {
203 if !self.contains(item) {
204 SA::error_api_impl().signal_error(ITEM_NOT_WHITELISTED_ERR_MSG.as_bytes());
205 }
206 }
207
208 pub fn add(&self, item: &T) {
209 let mapper = self.build_mapper_for_item(item);
210 mapper.set(true);
211 }
212
213 pub fn remove(&self, item: &T) {
214 let mapper = self.build_mapper_for_item(item);
215 mapper.clear();
216 }
217
218 fn build_mapper_for_item(&self, item: &T) -> FlagMapper<SA, CurrentStorage> {
219 let mut key = self.base_key.clone();
220 key.append_item(item);
221
222 FlagMapper::<SA, CurrentStorage>::new(key)
223 }
224}