Skip to main content

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