Skip to main content

iota_types/
transaction_deny_rules.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt;
5
6use iota_sdk_move_types::iota_framework::linked_table::{LinkedTable, Node};
7pub use iota_sdk_move_types::iota_framework::transaction_deny_rules::{
8    TransactionDenyRules, TransactionDenyRulesInnerV1,
9};
10use iota_sdk_types::{DenyRuleSet, Identifier, Version};
11use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr};
12use serde::{Serialize, de::DeserializeOwned};
13
14use crate::{
15    IOTA_FRAMEWORK_ADDRESS, IOTA_TRANSACTION_DENY_RULES_OBJECT_ID, MoveTypeTagTrait,
16    dynamic_field::get_dynamic_field_from_store,
17    error::{IotaError, IotaResult},
18    storage::ObjectStore,
19};
20
21pub const TRANSACTION_DENY_RULES_MODULE_NAME: &IdentStr = ident_str!("transaction_deny_rules");
22pub const TRANSACTION_DENY_RULES_MODULE: Identifier =
23    Identifier::from_static("transaction_deny_rules");
24pub const TRANSACTION_DENY_RULES_UPDATE_FUNCTION_NAME: Identifier =
25    Identifier::from_static("update");
26pub const TRANSACTION_DENY_RULES_CREATE_FUNCTION_NAME: Identifier =
27    Identifier::from_static("create");
28pub const RESOLVED_IOTA_TRANSACTION_DENY_RULES: (&AccountAddress, &IdentStr, &IdentStr) = (
29    &IOTA_FRAMEWORK_ADDRESS,
30    ident_str!("transaction_deny_rules"),
31    ident_str!("TransactionDenyRules"),
32);
33
34/// The initial shared version of the `TransactionDenyRules` object. Returns
35/// `None` while the `TransactionDenyRulesCreate` end-of-epoch transaction has
36/// not created the object yet.
37///
38/// # Panics
39///
40/// Panics if the object exists but is not shared, which would mean the
41/// system invariant on the reserved object is broken.
42pub fn get_transaction_deny_rules_obj_initial_shared_version(
43    object_store: &dyn ObjectStore,
44) -> IotaResult<Option<Version>> {
45    Ok(object_store
46        .try_get_object(&IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)?
47        .map(|obj| {
48            obj.owner
49                .into_opt_shared()
50                .expect("TransactionDenyRules object must be shared")
51        }))
52}
53
54pub const TRANSACTION_DENY_RULES_INNER_V1: u64 = 1;
55
56/// The full deny rule state read back from the `TransactionDenyRules` object.
57/// Returns `None` while the object has not been created yet.
58///
59/// Each deny list is reconstructed by walking its `LinkedTable` from `head`
60/// through `node.next` with derived-id child reads. Any node can rebuild the
61/// state from its object store alone. Validators seed enforcement and the
62/// mirrored on-chain state from this at epoch start.
63pub fn get_transaction_deny_rules(
64    object_store: &dyn ObjectStore,
65) -> IotaResult<Option<DenyRuleSet>> {
66    let Some(object) = object_store.try_get_object(&IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)? else {
67        return Ok(None);
68    };
69    let iota_sdk_types::ObjectData::Struct(move_object) = &object.data else {
70        return Err(IotaError::ObjectDeserialization {
71            error: "TransactionDenyRules object must be a Move object".to_string(),
72        });
73    };
74    let rules: TransactionDenyRules = bcs::from_bytes(move_object.contents()).map_err(|err| {
75        IotaError::ObjectDeserialization {
76            error: format!("failed to decode TransactionDenyRules: {err}"),
77        }
78    })?;
79    if rules.inner.version != TRANSACTION_DENY_RULES_INNER_V1 {
80        return Err(IotaError::ObjectDeserialization {
81            error: format!(
82                "unsupported TransactionDenyRules inner version {}",
83                rules.inner.version
84            ),
85        });
86    }
87    let inner: TransactionDenyRulesInnerV1 =
88        get_dynamic_field_from_store(object_store, rules.inner.id.id.bytes, &rules.inner.version)?;
89
90    Ok(Some(DenyRuleSet {
91        denied_addresses: walk_linked_table(object_store, &inner.denied_addresses)?
92            .into_iter()
93            .collect(),
94        denied_objects: walk_linked_table(object_store, &inner.denied_objects)?
95            .into_iter()
96            .map(|id| id.bytes)
97            .collect(),
98        denied_packages: walk_linked_table(object_store, &inner.denied_packages)?
99            .into_iter()
100            .map(|id| id.bytes)
101            .collect(),
102        package_publish_disabled: inner.package_publish_disabled,
103        package_upgrade_disabled: inner.package_upgrade_disabled,
104        shared_object_disabled: inner.shared_object_disabled,
105        user_transaction_disabled: inner.user_transaction_disabled,
106        receiving_objects_disabled: inner.receiving_objects_disabled,
107        move_authenticator_disabled: inner.move_authenticator_disabled,
108    }))
109}
110
111/// Collects a `LinkedTable`'s keys in list order by following the `next`
112/// links. Each entry costs one derived-id child read.
113fn walk_linked_table<K>(
114    object_store: &dyn ObjectStore,
115    table: &LinkedTable<K, bool>,
116) -> IotaResult<Vec<K>>
117where
118    K: MoveTypeTagTrait + Serialize + DeserializeOwned + Clone + fmt::Debug,
119{
120    let table_id = table.id.id.bytes;
121    let mut keys = Vec::with_capacity(table.size as usize);
122    let mut next = table.head.clone();
123    while let Some(key) = next {
124        // A cycle in the links would otherwise never terminate. The walk
125        // visits at most `size` entries.
126        if keys.len() as u64 == table.size {
127            return Err(IotaError::ObjectDeserialization {
128                error: format!(
129                    "LinkedTable {} has more linked entries than its size {}",
130                    table_id, table.size
131                ),
132            });
133        }
134        let node: Node<K, bool> = get_dynamic_field_from_store(object_store, table_id, &key)?;
135        keys.push(key);
136        next = node.next;
137    }
138    if keys.len() as u64 != table.size {
139        return Err(IotaError::ObjectDeserialization {
140            error: format!(
141                "LinkedTable {} links {} entries but its size is {}",
142                table_id,
143                keys.len(),
144                table.size
145            ),
146        });
147    }
148    Ok(keys)
149}