multiversx_sc/storage/mappers/address_to_id_mapper.rs
1use core::marker::PhantomData;
2
3use super::{
4 StorageMapper, StorageMapperFromAddress,
5 source::{CurrentStorage, StorageAddress},
6};
7use crate::{
8 api::{ErrorApiImpl, StorageMapperApi},
9 storage::{StorageKey, storage_clear, storage_set},
10 types::{ManagedAddress, ManagedType},
11};
12
13const ID_SUFFIX: &str = "addrId";
14const ADDRESS_SUFFIX: &str = "addr";
15const LAST_ID_SUFFIX: &str = "lastId";
16
17const UNKNOWN_ADDR_ERR_MSG: &str = "Unknown address";
18
19pub type AddressId = u64;
20pub const NULL_ID: AddressId = 0;
21
22/// A specialized bidirectional mapper between addresses and auto-incrementing numeric IDs.
23///
24/// # Storage Layout
25///
26/// The `AddressToIdMapper` maintains bidirectional mappings with sequential ID assignment:
27///
28/// 1. **Address to ID mapping**:
29/// - `base_key + "addr" + encoded_address` → assigned ID (u64)
30///
31/// 2. **ID to address mapping**:
32/// - `base_key + "addrId" + id` → address
33///
34/// 3. **ID counter**:
35/// - `base_key + "lastId"` → highest assigned ID (for generating new IDs)
36///
37/// # ID Assignment
38///
39/// - IDs start from 1 and increment sequentially
40/// - `NULL_ID` (0) represents "no ID assigned" or "not found"
41/// - Once an ID is assigned, it remains associated with that address until explicitly removed
42/// - Removed IDs are not reused (IDs only increment, never decrement)
43///
44/// # Main Operations
45///
46/// - **Auto-insert**: `get_id_or_insert(address)` - Gets ID or assigns new one if not exists. O(1).
47/// - **Strict insert**: `insert_new(address)` - Assigns ID only if address is new, errors if exists. O(1).
48/// - **Lookup ID**: `get_id(address)` - Returns ID for address (0 if not found). O(1).
49/// - **Lookup Address**: `get_address(id)` - Returns address for ID. O(1).
50/// - **Contains**: `contains_id(id)` - Checks if ID is assigned. O(1).
51/// - **Remove by ID**: `remove_by_id(id)` - Removes mapping by ID, returns address. O(1).
52/// - **Remove by Address**: `remove_by_address(address)` - Removes mapping by address, returns ID. O(1).
53/// - **Counter**: `get_last_id()` - Returns the highest assigned ID.
54///
55/// # Trade-offs
56///
57/// - **Pros**: Sequential IDs are compact and predictable; auto-increment simplifies ID management;
58/// O(1) bidirectional lookup; no duplicate addresses.
59/// - **Cons**: IDs are never reused (gaps after removal); ID overflow at u64::MAX (unlikely but possible);
60/// slightly less flexible than generic `BiDiMapper`.
61///
62/// # Comparison with BiDiMapper
63///
64/// - **AddressToIdMapper**: Specialized for addresses; auto-incrementing IDs; single type for IDs (u64)
65/// - **BiDiMapper**: Generic; manual ID/value assignment; supports any types for both sides
66///
67/// # Use Cases
68///
69/// - User registration systems (address → user ID)
70/// - Whitelist/participant management with sequential numbering
71/// - Address indexing for efficient iteration
72/// - Mapping external addresses to internal compact IDs
73/// - Any scenario where addresses need numeric identifiers
74///
75/// # Example
76///
77/// ```rust
78/// # use multiversx_sc::storage::mappers::{StorageMapper, AddressToIdMapper};
79/// # use multiversx_sc::types::ManagedAddress;
80/// # fn example<SA: multiversx_sc::api::StorageMapperApi>(
81/// # addr1: ManagedAddress<SA>,
82/// # addr2: ManagedAddress<SA>,
83/// # addr3: ManagedAddress<SA>
84/// # ) {
85/// # let mapper = AddressToIdMapper::<SA>::new(
86/// # multiversx_sc::storage::StorageKey::new(&b"users"[..])
87/// # );
88/// // Auto-assign IDs (get or create)
89/// let id1 = mapper.get_id_or_insert(&addr1); // Returns 1
90/// let id2 = mapper.get_id_or_insert(&addr2); // Returns 2
91/// let id1_again = mapper.get_id_or_insert(&addr1); // Returns 1 (existing)
92///
93/// assert_eq!(id1, 1);
94/// assert_eq!(id2, 2);
95/// assert_eq!(id1_again, 1);
96/// assert_eq!(mapper.get_last_id(), 2);
97///
98/// // Strict insert (errors if address already exists)
99/// let id3 = mapper.insert_new(&addr3); // Returns 3
100/// assert_eq!(id3, 3);
101/// // mapper.insert_new(&addr1); // Would error: "Address already registered"
102///
103/// // Bidirectional lookup
104/// assert_eq!(mapper.get_id(&addr1), 1);
105/// assert_eq!(mapper.get_address(1), Some(addr1.clone()));
106///
107/// // Check existence
108/// assert!(mapper.contains_id(2));
109/// assert_eq!(mapper.get_id(&addr2), 2); // Returns 2
110///
111/// // Non-zero lookup (errors if not found)
112/// let id = mapper.get_id_non_zero(&addr1); // Returns 1
113/// assert_eq!(id, 1);
114/// // mapper.get_id_non_zero(&unknown_addr); // Would error: "Unknown address"
115///
116/// // Remove by address
117/// let removed_id = mapper.remove_by_address(&addr2);
118/// assert_eq!(removed_id, 2);
119/// assert_eq!(mapper.get_id(&addr2), 0); // Now returns NULL_ID
120/// assert!(!mapper.contains_id(2));
121///
122/// // Remove by ID
123/// let removed_addr = mapper.remove_by_id(1);
124/// assert_eq!(removed_addr, Some(addr1.clone()));
125/// assert_eq!(mapper.get_id(&addr1), 0);
126///
127/// // Note: next inserted address gets ID 4, not 2 (IDs never reused)
128/// # }
129/// ```
130pub struct AddressToIdMapper<SA, A = CurrentStorage>
131where
132 SA: StorageMapperApi,
133 A: StorageAddress<SA>,
134{
135 _phantom_api: PhantomData<SA>,
136 address: A,
137 base_key: StorageKey<SA>,
138}
139
140impl<SA> StorageMapper<SA> for AddressToIdMapper<SA>
141where
142 SA: StorageMapperApi,
143{
144 fn new(base_key: StorageKey<SA>) -> Self {
145 AddressToIdMapper {
146 _phantom_api: PhantomData,
147 address: CurrentStorage,
148 base_key,
149 }
150 }
151}
152
153impl<SA> StorageMapperFromAddress<SA> for AddressToIdMapper<SA, ManagedAddress<SA>>
154where
155 SA: StorageMapperApi,
156{
157 fn new_from_address(address: ManagedAddress<SA>, base_key: StorageKey<SA>) -> Self {
158 AddressToIdMapper {
159 _phantom_api: PhantomData,
160 address,
161 base_key,
162 }
163 }
164}
165
166impl<SA, A> AddressToIdMapper<SA, A>
167where
168 SA: StorageMapperApi,
169 A: StorageAddress<SA>,
170{
171 pub fn contains_id(&self, id: AddressId) -> bool {
172 let key = self.id_to_address_key(id);
173 self.address.address_storage_get_len(key.as_ref()) != 0
174 }
175
176 pub fn get_id(&self, address: &ManagedAddress<SA>) -> AddressId {
177 let key = self.address_to_id_key(address);
178 self.address.address_storage_get(key.as_ref())
179 }
180
181 pub fn get_id_non_zero(&self, address: &ManagedAddress<SA>) -> AddressId {
182 let id = self.get_id(address);
183 if id == NULL_ID {
184 SA::error_api_impl().signal_error(UNKNOWN_ADDR_ERR_MSG.as_bytes());
185 }
186
187 id
188 }
189
190 pub fn get_address(&self, id: AddressId) -> Option<ManagedAddress<SA>> {
191 let key = self.id_to_address_key(id);
192 if self.address.address_storage_get_len(key.as_ref()) == 0 {
193 return None;
194 }
195
196 let addr = self.address.address_storage_get(key.as_ref());
197 Some(addr)
198 }
199
200 fn id_to_address_key(&self, id: AddressId) -> StorageKey<SA> {
201 let mut item_key = self.base_key.clone();
202 item_key.append_bytes(ID_SUFFIX.as_bytes());
203 item_key.append_item(&id);
204
205 item_key
206 }
207
208 fn address_to_id_key(&self, address: &ManagedAddress<SA>) -> StorageKey<SA> {
209 let mut item_key = self.base_key.clone();
210 item_key.append_bytes(ADDRESS_SUFFIX.as_bytes());
211 item_key.append_item(address);
212
213 item_key
214 }
215
216 fn last_id_key(&self) -> StorageKey<SA> {
217 let mut item_key = self.base_key.clone();
218 item_key.append_bytes(LAST_ID_SUFFIX.as_bytes());
219
220 item_key
221 }
222
223 pub fn get_last_id(&self) -> AddressId {
224 self.address
225 .address_storage_get(self.last_id_key().as_ref())
226 }
227}
228
229impl<SA> AddressToIdMapper<SA, CurrentStorage>
230where
231 SA: StorageMapperApi,
232{
233 pub fn get_id_or_insert(&self, address: &ManagedAddress<SA>) -> AddressId {
234 let current_id = self
235 .address
236 .address_storage_get(self.address_to_id_key(address).as_ref());
237 if current_id != 0 {
238 return current_id;
239 }
240
241 self.insert_address(address)
242 }
243
244 pub fn insert_new(&self, address: &ManagedAddress<SA>) -> AddressId {
245 let existing_id = self.get_id(address);
246 if existing_id != NULL_ID {
247 SA::error_api_impl().signal_error(b"Address already registered");
248 }
249
250 self.insert_address(address)
251 }
252
253 pub fn remove_by_id(&self, id: AddressId) -> Option<ManagedAddress<SA>> {
254 let address = self.get_address(id)?;
255 self.remove_entry(id, &address);
256
257 Some(address)
258 }
259
260 pub fn remove_by_address(&self, address: &ManagedAddress<SA>) -> AddressId {
261 let current_id = self.get_id(address);
262 if current_id != NULL_ID {
263 self.remove_entry(current_id, address);
264 }
265
266 current_id
267 }
268
269 fn insert_address(&self, address: &ManagedAddress<SA>) -> AddressId {
270 let new_id = self.get_last_id() + 1;
271 storage_set(self.address_to_id_key(address).as_ref(), &new_id);
272 storage_set(self.id_to_address_key(new_id).as_ref(), address);
273
274 self.set_last_id(new_id);
275
276 new_id
277 }
278
279 fn set_last_id(&self, last_id: AddressId) {
280 if last_id == 0 {
281 SA::error_api_impl().signal_error(b"ID Overflow");
282 }
283
284 storage_set(self.last_id_key().as_ref(), &last_id);
285 }
286
287 fn remove_entry(&self, id: AddressId, address: &ManagedAddress<SA>) {
288 storage_clear(self.address_to_id_key(address).as_ref());
289 storage_clear(self.id_to_address_key(id).as_ref());
290 }
291}