iota_names/
registry.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use iota_types::{
7    base_types::{IotaAddress, ObjectID},
8    collection_types::VecMap,
9    dynamic_field::Field,
10    id::ID,
11    object::{MoveObject, Object},
12};
13use serde::{Deserialize, Serialize};
14
15use crate::{constants::IOTA_NAMES_LEAF_EXPIRATION_TIMESTAMP, error::IotaNamesError, name::Name};
16
17/// Rust version of the Move `iota::table::Table` type.
18#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
19pub struct Table {
20    pub id: ObjectID,
21    pub size: u64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Registry {
26    /// The `registry` table maps `Name` to `NameRecord`.
27    /// Added / replaced in the `add_record` function.
28    registry: Table,
29    /// The `reverse_registry` table maps `IotaAddress` to `Name`.
30    /// Updated in the `set_reverse_lookup` function.
31    reverse_registry: Table,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct RegistryEntry {
36    pub id: ObjectID,
37    pub name: Name,
38    pub name_record: NameRecord,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct ReverseRegistryEntry {
43    pub id: ObjectID,
44    pub address: IotaAddress,
45    pub name: Name,
46}
47
48/// A single record in the registry.
49#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
50pub struct NameRecord {
51    /// The ID of the registration NFT assigned to this record.
52    ///
53    /// The owner of the corresponding registration NFT has the rights to be
54    /// able to change and adjust the `target_address` of this name.
55    ///
56    /// It is possible that the ID changes if the record expires and is
57    /// purchased by someone else.
58    pub nft_id: ID,
59    /// Timestamp in milliseconds when the record expires.
60    pub expiration_timestamp_ms: u64,
61    /// The target address that this name points to.
62    pub target_address: Option<IotaAddress>,
63    /// Additional data which may be stored in a record.
64    pub data: VecMap<String, String>,
65}
66
67impl TryFrom<Object> for NameRecord {
68    type Error = IotaNamesError;
69
70    fn try_from(object: Object) -> Result<Self, IotaNamesError> {
71        object
72            .to_rust::<Field<Name, Self>>()
73            .map(|record| record.value)
74            .ok_or_else(|| IotaNamesError::MalformedObject(object.id()))
75    }
76}
77
78impl TryFrom<MoveObject> for NameRecord {
79    type Error = IotaNamesError;
80
81    fn try_from(object: MoveObject) -> Result<Self, IotaNamesError> {
82        object
83            .to_rust::<Field<Name, Self>>()
84            .map(|record| record.value)
85            .ok_or_else(|| IotaNamesError::MalformedObject(object.id()))
86    }
87}
88
89impl NameRecord {
90    /// Leaf records expire when their parent expires.
91    /// The `expiration_timestamp_ms` is set to `0` (on-chain) to indicate this.
92    pub fn is_leaf_record(&self) -> bool {
93        self.expiration_timestamp_ms == IOTA_NAMES_LEAF_EXPIRATION_TIMESTAMP
94    }
95
96    /// Validates that a `NameRecord` is a valid parent of a child `NameRecord`.
97    ///
98    /// WARNING: This only applies for `leaf` records.
99    pub fn is_valid_leaf_parent(&self, child: &NameRecord) -> bool {
100        self.nft_id == child.nft_id
101    }
102
103    /// Checks if a `node` name record has expired.
104    /// Expects the latest checkpoint's timestamp.
105    pub fn is_node_expired(&self, checkpoint_timestamp_ms: u64) -> bool {
106        self.expiration_timestamp_ms < checkpoint_timestamp_ms
107    }
108
109    /// Gets the expiration time as a [`SystemTime`].
110    pub fn expiration_time(&self) -> SystemTime {
111        UNIX_EPOCH + Duration::from_millis(self.expiration_timestamp_ms)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn expirations() {
121        let system_time: u64 = 100;
122
123        let mut name = NameRecord {
124            nft_id: iota_types::id::ID::new(ObjectID::random()),
125            data: VecMap { contents: vec![] },
126            target_address: Some(IotaAddress::random_for_testing_only()),
127            expiration_timestamp_ms: system_time + 10,
128        };
129
130        assert!(!name.is_node_expired(system_time));
131
132        name.expiration_timestamp_ms = system_time - 10;
133
134        assert!(name.is_node_expired(system_time));
135    }
136}