iota_names/
registry.rs

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