diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4c36dd4..9c51ea5 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,6 +17,7 @@ on: - ".github/workflows/shared-build-wasm.yml" - ".github/actions/**" - "**.rs" + - "**.move" - "**.toml" - "**.lock" - "bindings/**" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 3bdcfd9..962e2a1 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -16,6 +16,7 @@ on: - "**.ts" - "**.js" - "**.json" + - "**.move" concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/hierarchies-move/scripts/upgrade_hierarchies.sh b/hierarchies-move/scripts/upgrade_hierarchies.sh new file mode 100755 index 0000000..3aa1da0 --- /dev/null +++ b/hierarchies-move/scripts/upgrade_hierarchies.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Copyright 2020-2025 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +script_dir=$(cd "$(dirname $0)" && pwd) +package_dir=$script_dir/.. + +chain_id=$(iota client chain-identifier) +current_pkg_id=$(toml get "$package_dir/Move.lock" env | jq --raw-output "map(values | select(.[\"chain-id\"] == \"$chain_id\") .[\"latest-published-id\"]) | first") +upgrade_cap_id=$(iota client objects --json | jq --raw-output "map(select(.data.type == \"0x2::package::UpgradeCap\" and .data.content.fields.package == \"$current_pkg_id\")) | first | .data.objectId") + +iota client upgrade --upgrade-capability $upgrade_cap_id $package_dir \ No newline at end of file diff --git a/hierarchies-move/sources/accreditation.move b/hierarchies-move/sources/accreditation.move index d3570ed..fd7d3d8 100644 --- a/hierarchies-move/sources/accreditation.move +++ b/hierarchies-move/sources/accreditation.move @@ -68,6 +68,8 @@ public(package) fun is_property_allowed( let accreditation = &self.accreditations[idx_properties_to_attest]; let maybe_property = accreditation.properties.try_get(property_name); + idx_properties_to_attest = idx_properties_to_attest + 1; + if (maybe_property.is_none()) { continue }; @@ -78,7 +80,6 @@ public(package) fun is_property_allowed( ) { return true }; - idx_properties_to_attest = idx_properties_to_attest + 1; }; return false } diff --git a/hierarchies-move/tests/hierarchies_tests.move b/hierarchies-move/tests/hierarchies_tests.move index d799555..767b2db 100644 --- a/hierarchies-move/tests/hierarchies_tests.move +++ b/hierarchies-move/tests/hierarchies_tests.move @@ -15,7 +15,8 @@ use hierarchies::{ add_root_authority, revoke_root_authority, is_root_authority, - revoke_property + revoke_property, + validate_property }, property, property_name::new_property_name, @@ -1296,3 +1297,74 @@ fun test_validate_properties_fails_for_revoked_property() { clock.destroy_for_testing(); let _ = scenario.end(); } + +#[test] +fun test_validate_property_returns_false_when_attester_has_different_property() { + let alice = @0x1; + let mut scenario = test_scenario::begin(alice); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000); + + // Create a new federation + new_federation(scenario.ctx()); + scenario.next_tx(alice); + + let mut fed: Federation = scenario.take_shared(); + let root_cap: RootAuthorityCap = scenario.take_from_address(alice); + let accredit_cap: AccreditCap = scenario.take_from_address(alice); + + // Step 1: Add two properties to the federation: "role" and "foo" + let property_name_role = new_property_name(utf8(b"role")); + let property_value_example = new_property_value_number(1); + let mut allowed_values_role = vec_set::empty(); + allowed_values_role.insert(property_value_example); + + let property_name_foo = new_property_name(utf8(b"foo")); + let property_value_bar = new_property_value_number(2); + let mut allowed_values_foo = vec_set::empty(); + allowed_values_foo.insert(property_value_bar); + + let property_role = property::new_property( + property_name_role, + allowed_values_role, + false, + option::none(), + ); + let property_foo = property::new_property( + property_name_foo, + allowed_values_foo, + false, + option::none(), + ); + + fed.add_property(&root_cap, property_role, scenario.ctx()); + fed.add_property(&root_cap, property_foo, scenario.ctx()); + scenario.next_tx(alice); + + let bob_id = @0x2.to_id(); + let property_for_bob = property::new_property( + property_name_foo, + allowed_values_foo, + false, + option::none(), + ); + + fed.create_accreditation_to_attest( + &accredit_cap, + bob_id, + vector[property_for_bob], + &clock, + scenario.ctx(), + ); + + assert!(fed.validate_property(&bob_id, property_name_foo, property_value_bar, &clock), 0); + + assert!(!fed.validate_property(&bob_id, property_name_role, property_value_example, &clock), 1); + + // Cleanup + test_scenario::return_shared(fed); + test_scenario::return_to_address(alice, root_cap); + test_scenario::return_to_address(alice, accredit_cap); + clock.destroy_for_testing(); + let _ = scenario.end(); +} diff --git a/hierarchies-rs/hierarchies/src/client/full_client.rs b/hierarchies-rs/hierarchies/src/client/full_client.rs index a1510a6..9d71357 100644 --- a/hierarchies-rs/hierarchies/src/client/full_client.rs +++ b/hierarchies-rs/hierarchies/src/client/full_client.rs @@ -300,6 +300,10 @@ where fn network_name(&self) -> &NetworkName { self.read_client.network() } + + fn package_history(&self) -> Vec { + self.read_client.package_history() + } } impl CoreClient for HierarchiesClient diff --git a/hierarchies-rs/hierarchies/src/client/read_only.rs b/hierarchies-rs/hierarchies/src/client/read_only.rs index 395c1bf..41a7421 100644 --- a/hierarchies-rs/hierarchies/src/client/read_only.rs +++ b/hierarchies-rs/hierarchies/src/client/read_only.rs @@ -17,7 +17,6 @@ use iota_interaction::types::transaction::{ProgrammableTransaction, TransactionK use iota_interaction_ts::bindings::WasmIotaClient; use product_common::core_client::CoreClientReadOnly; use product_common::network_name::NetworkName; -use product_common::package_registry::Env; use serde::de::DeserializeOwned; use crate::client::error::ClientError; @@ -39,9 +38,8 @@ use crate::package; pub struct HierarchiesClientReadOnly { /// The underlying IOTA client adapter used for communication. client: IotaClientAdapter, - /// The [`ObjectID`] of the deployed Hierarchies package (smart contract). - /// All interactions go through this package ID. - hierarchies_package_id: ObjectID, + /// The history of the deployed Hierarchies package (smart contract). + package_history: Vec, /// The name of the network this client is connected to (e.g., "mainnet", "testnet"). network_name: NetworkName, chain_id: String, @@ -105,13 +103,17 @@ impl HierarchiesClientReadOnly { /// using the internal package registry. async fn new_internal(iota_client: IotaClientAdapter, network: NetworkName) -> Result { let chain_id = network.as_ref().to_string(); - let (network, hierarchies_pkg_id) = { + let (network, package_history) = { let package_registry = package::hierarchies_package_registry().await; - let package_id = package_registry.package_id(&network).ok_or_else(|| { - ClientError::Configuration(ConfigError::PackageNotFound { - network: network.to_string(), - }) - })?; + let package_history = package_registry + .history(&network) + .ok_or_else(|| { + ClientError::Configuration(ConfigError::PackageNotFound { + network: network.to_string(), + }) + })? + .to_vec(); + let network = match chain_id.as_str() { product_common::package_registry::MAINNET_CHAIN_ID => { NetworkName::try_from("iota").expect("valid network name") @@ -122,11 +124,11 @@ impl HierarchiesClientReadOnly { .unwrap_or(network), }; - (network, package_id) + (network, package_history) }; Ok(HierarchiesClientReadOnly { client: iota_client, - hierarchies_package_id: hierarchies_pkg_id, + package_history, network_name: network, chain_id, }) @@ -149,7 +151,7 @@ impl HierarchiesClientReadOnly { // Use the passed pkg_id to add a new env or override the information of an existing one. { let mut registry = package::hierarchies_package_registry_mut().await; - registry.insert_env_history(Env::new(network.as_ref()), vec![package_id]); + registry.insert_new_package_version(&network, package_id); } Self::new_internal(client, network).await @@ -325,7 +327,10 @@ impl HierarchiesClientReadOnly { #[async_trait::async_trait] impl CoreClientReadOnly for HierarchiesClientReadOnly { fn package_id(&self) -> ObjectID { - self.hierarchies_package_id + *self + .package_history + .last() + .expect("at least one package exists in history") } fn network_name(&self) -> &NetworkName { @@ -335,4 +340,8 @@ impl CoreClientReadOnly for HierarchiesClientReadOnly { fn client_adapter(&self) -> &IotaClientAdapter { &self.client } + + fn package_history(&self) -> Vec { + self.package_history.clone() + } } diff --git a/hierarchies-rs/hierarchies/src/core/operations.rs b/hierarchies-rs/hierarchies/src/core/operations.rs index 21372c1..6ec3b38 100644 --- a/hierarchies-rs/hierarchies/src/core/operations.rs +++ b/hierarchies-rs/hierarchies/src/core/operations.rs @@ -69,8 +69,10 @@ impl HierarchiesImpl { where C: CoreClientReadOnly + OptionalSync, { - let cap: RootAuthorityCap = client - .find_object_for_address(owner, |cap: &RootAuthorityCap| cap.federation_id == federation_id) + let cap = client + .find_object_for_address::(owner, |cap: &RootAuthorityCap| { + cap.federation_id == federation_id + }) .await .map_err(|e| CapabilityError::Rpc { source: e.into() })? .ok_or_else(|| CapabilityError::NotFound { diff --git a/hierarchies-rs/hierarchies/tests/e2e/client.rs b/hierarchies-rs/hierarchies/tests/e2e/client.rs index 0a415ed..6a355ed 100644 --- a/hierarchies-rs/hierarchies/tests/e2e/client.rs +++ b/hierarchies-rs/hierarchies/tests/e2e/client.rs @@ -71,6 +71,10 @@ impl CoreClientReadOnly for TestClient { fn client_adapter(&self) -> &IotaClientAdapter { self.client.client_adapter() } + + fn package_history(&self) -> Vec { + self.client.package_history() + } } impl CoreClient for TestClient {