diff --git a/cli/src/command/account.rs b/cli/src/command/account.rs index 5ea180908c..6c4ec0f01a 100644 --- a/cli/src/command/account.rs +++ b/cli/src/command/account.rs @@ -966,6 +966,9 @@ async fn print_address(account: &Account, address: &Bip44Address) -> Result<(), Output::Delegation(_) => { // TODO do we want to log them? } + Output::Anchor(_) => { + // TODO do we want to log them? + } } let unlock_conditions = output_data .output diff --git a/sdk/src/client/api/block_builder/input_selection/mod.rs b/sdk/src/client/api/block_builder/input_selection/mod.rs index 4baa80fc55..e32ba19027 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -81,6 +81,7 @@ impl InputSelection { AccountTransition::State, ))), Address::Nft(nft_address) => Ok(Some(Requirement::Nft(*nft_address.nft_id()))), + Address::Anchor(_) => todo!(), } } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs index 9fc5f5267f..ab3d29a08a 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs @@ -48,6 +48,7 @@ impl InputSelection { Err(e) => Err(e), } } + Address::Anchor(_) => todo!(), } } } diff --git a/sdk/src/client/secret/ledger_nano.rs b/sdk/src/client/secret/ledger_nano.rs index 409ab571a0..232697d534 100644 --- a/sdk/src/client/secret/ledger_nano.rs +++ b/sdk/src/client/secret/ledger_nano.rs @@ -544,6 +544,7 @@ fn merge_unlocks( merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?)); } Address::Nft(_nft) => merged_unlocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)), + Address::Anchor(_) => todo!(), }, None => { // We can only sign ed25519 addresses and block_indexes needs to contain the account or nft diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index 67c58865e8..8213da048e 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -532,6 +532,7 @@ where blocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?)); } Address::Nft(_nft) => blocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)), + Address::Anchor(_) => todo!(), }, None => { // We can only sign ed25519 addresses and block_indexes needs to contain the account or nft diff --git a/sdk/src/client/utils.rs b/sdk/src/client/utils.rs index bc0b8ec97a..e1da240f6d 100644 --- a/sdk/src/client/utils.rs +++ b/sdk/src/client/utils.rs @@ -31,6 +31,7 @@ pub fn bech32_to_hex(bech32: impl ConvertTo) -> Result { Address::Ed25519(ed) => ed.to_string(), Address::Account(account) => account.to_string(), Address::Nft(nft) => nft.to_string(), + Address::Anchor(anchor) => anchor.to_string(), }) } diff --git a/sdk/src/types/block/address/anchor.rs b/sdk/src/types/block/address/anchor.rs new file mode 100644 index 0000000000..ffbd15c8b6 --- /dev/null +++ b/sdk/src/types/block/address/anchor.rs @@ -0,0 +1,91 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::str::FromStr; + +use derive_more::{AsRef, Deref, From}; + +use crate::types::block::{output::AnchorId, Error}; + +/// An anchor address. +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, packable::Packable)] +#[as_ref(forward)] +pub struct AnchorAddress(AnchorId); + +impl AnchorAddress { + /// The [`Address`](crate::types::block::address::Address) kind of an [`AnchorAddress`]. + /// TODO + pub const KIND: u8 = 255; + /// The length of an [`AnchorAddress`]. + pub const LENGTH: usize = AnchorId::LENGTH; + + /// Creates a new [`AnchorAddress`]. + #[inline(always)] + pub fn new(id: AnchorId) -> Self { + Self::from(id) + } + + /// Returns the [`AnchorId`] of an [`AnchorAddress`]. + #[inline(always)] + pub fn anchor_id(&self) -> &AnchorId { + &self.0 + } + + /// Consumes an [`AnchorAddress`] and returns its [`AnchorId`]. + #[inline(always)] + pub fn into_anchor_id(self) -> AnchorId { + self.0 + } +} + +impl FromStr for AnchorAddress { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self::new(AnchorId::from_str(s)?)) + } +} + +impl core::fmt::Display for AnchorAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl core::fmt::Debug for AnchorAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "AnchorAddress({self})") + } +} + +#[cfg(feature = "serde")] +mod dto { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + struct AnchorAddressDto { + #[serde(rename = "type")] + kind: u8, + anchor_id: AnchorId, + } + + impl From<&AnchorAddress> for AnchorAddressDto { + fn from(value: &AnchorAddress) -> Self { + Self { + kind: AnchorAddress::KIND, + anchor_id: value.0, + } + } + } + + impl From for AnchorAddress { + fn from(value: AnchorAddressDto) -> Self { + Self(value.anchor_id) + } + } + + impl_serde_typed_dto!(AnchorAddress, AnchorAddressDto, "anchor address"); +} diff --git a/sdk/src/types/block/address/mod.rs b/sdk/src/types/block/address/mod.rs index a870d399b7..9d5ab8c761 100644 --- a/sdk/src/types/block/address/mod.rs +++ b/sdk/src/types/block/address/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod account; +mod anchor; mod bech32; mod ed25519; mod nft; @@ -10,6 +11,7 @@ use derive_more::From; pub use self::{ account::AccountAddress, + anchor::AnchorAddress, bech32::{Bech32Address, Hrp}, ed25519::Ed25519Address, nft::NftAddress, @@ -37,6 +39,9 @@ pub enum Address { /// An NFT address. #[packable(tag = NftAddress::KIND)] Nft(NftAddress), + /// An anchor address. + #[packable(tag = AnchorAddress::KIND)] + Anchor(AnchorAddress), } impl core::fmt::Debug for Address { @@ -45,6 +50,7 @@ impl core::fmt::Debug for Address { Self::Ed25519(address) => address.fmt(f), Self::Account(address) => address.fmt(f), Self::Nft(address) => address.fmt(f), + Self::Anchor(address) => address.fmt(f), } } } @@ -56,6 +62,7 @@ impl Address { Self::Ed25519(_) => Ed25519Address::KIND, Self::Account(_) => AccountAddress::KIND, Self::Nft(_) => NftAddress::KIND, + Self::Anchor(_) => AnchorAddress::KIND, } } @@ -104,6 +111,21 @@ impl Address { } } + /// Checks whether the address is an [`AnchorAddress`]. + pub fn is_anchor(&self) -> bool { + matches!(self, Self::Anchor(_)) + } + + /// Gets the address as an actual [`AnchorAddress`]. + /// PANIC: do not call on a non-anchor address. + pub fn as_anchor(&self) -> &AnchorAddress { + if let Self::Anchor(address) = self { + address + } else { + panic!("as_anchor called on a non-anchor address"); + } + } + /// Tries to create an [`Address`] from a bech32 encoded string. pub fn try_from_bech32(address: impl AsRef) -> Result { Bech32Address::try_from_str(address).map(|res| res.inner) diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index 0ac9c990b0..b7cc02e0b0 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -16,7 +16,7 @@ use crate::types::block::{ output::{ feature::{BlockIssuerKeyCount, FeatureCount}, unlock_condition::UnlockConditionCount, - AccountId, ChainId, MetadataFeatureLength, NativeTokenCount, NftId, OutputIndex, StateMetadataLength, + AccountId, AnchorId, ChainId, MetadataFeatureLength, NativeTokenCount, NftId, OutputIndex, StateMetadataLength, TagFeatureLength, }, payload::{ContextInputCount, InputCount, OutputCount, TagLength, TaggedDataLength}, @@ -150,7 +150,9 @@ pub enum Error { }, BlockIssuerKeysNotUniqueSorted, RemainingBytesAfterBlock, + // TODO remove? SelfControlledAccountOutput(AccountId), + SelfControlledAnchorOutput(AnchorId), SelfDepositNft(NftId), SignaturePublicKeyMismatch { expected: String, @@ -346,6 +348,9 @@ impl fmt::Display for Error { Self::SelfControlledAccountOutput(account_id) => { write!(f, "self controlled account output, account ID {account_id}") } + Self::SelfControlledAnchorOutput(anchor_id) => { + write!(f, "self controlled anchor output, anchor ID {anchor_id}") + } Self::SelfDepositNft(nft_id) => { write!(f, "self deposit nft output, NFT ID {nft_id}") } diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs new file mode 100644 index 0000000000..7b8263d9bf --- /dev/null +++ b/sdk/src/types/block/output/anchor.rs @@ -0,0 +1,923 @@ +// Copyright 2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use alloc::{collections::BTreeSet, vec::Vec}; + +use hashbrown::HashMap; +use packable::{ + bounded::BoundedU16, + error::{UnpackError, UnpackErrorExt}, + packer::Packer, + prefix::BoxedSlicePrefix, + unpacker::Unpacker, + Packable, +}; + +use crate::types::{ + block::{ + address::{Address, AnchorAddress}, + output::{ + feature::{verify_allowed_features, Feature, FeatureFlags, Features}, + unlock_condition::{ + verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions, + }, + verify_output_amount_min, verify_output_amount_packable, verify_output_amount_supply, ChainId, NativeToken, + NativeTokens, Output, OutputBuilderAmount, OutputId, Rent, RentStructure, StateTransitionError, + StateTransitionVerifier, + }, + protocol::ProtocolParameters, + semantic::{TransactionFailureReason, ValidationContext}, + unlock::Unlock, + Error, + }, + ValidationParams, +}; + +impl_id!(pub AnchorId, 32, "Unique identifier of an anchor, which is the BLAKE2b-256 hash of the Output ID that created it."); + +#[cfg(feature = "serde")] +string_serde_impl!(AnchorId); + +impl From<&OutputId> for AnchorId { + fn from(output_id: &OutputId) -> Self { + Self::from(output_id.hash()) + } +} + +impl AnchorId { + /// + pub fn or_from_output_id(self, output_id: &OutputId) -> Self { + if self.is_null() { Self::from(output_id) } else { self } + } +} + +impl From for Address { + fn from(value: AnchorId) -> Self { + Self::Anchor(AnchorAddress::new(value)) + } +} + +/// Types of anchor transition. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum AnchorTransition { + /// State transition. + State, + /// Governance transition. + Governance, +} + +impl AnchorTransition { + /// Checks whether the anchor transition is a state one. + pub fn is_state(&self) -> bool { + matches!(self, Self::State) + } + + /// Checks whether the anchor transition is a governance one. + pub fn is_governance(&self) -> bool { + matches!(self, Self::Governance) + } +} + +impl core::fmt::Display for AnchorTransition { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::State => write!(f, "state"), + Self::Governance => write!(f, "governance"), + } + } +} + +/// +#[derive(Clone)] +#[must_use] +pub struct AnchorOutputBuilder { + amount: OutputBuilderAmount, + mana: u64, + native_tokens: BTreeSet, + anchor_id: AnchorId, + state_index: Option, + state_metadata: Vec, + unlock_conditions: BTreeSet, + features: BTreeSet, + immutable_features: BTreeSet, +} + +impl AnchorOutputBuilder { + /// Creates an [`AnchorOutputBuilder`] with a provided amount. + pub fn new_with_amount(amount: u64, anchor_id: AnchorId) -> Self { + Self::new(OutputBuilderAmount::Amount(amount), anchor_id) + } + + /// Creates an [`AnchorOutputBuilder`] with a provided rent structure. + /// The amount will be set to the minimum storage deposit. + pub fn new_with_minimum_storage_deposit(rent_structure: RentStructure, anchor_id: AnchorId) -> Self { + Self::new(OutputBuilderAmount::MinimumStorageDeposit(rent_structure), anchor_id) + } + + fn new(amount: OutputBuilderAmount, anchor_id: AnchorId) -> Self { + Self { + amount, + mana: Default::default(), + native_tokens: BTreeSet::new(), + anchor_id, + state_index: None, + state_metadata: Vec::new(), + unlock_conditions: BTreeSet::new(), + features: BTreeSet::new(), + immutable_features: BTreeSet::new(), + } + } + + /// Sets the amount to the provided value. + #[inline(always)] + pub fn with_amount(mut self, amount: u64) -> Self { + self.amount = OutputBuilderAmount::Amount(amount); + self + } + + /// Sets the amount to the minimum storage deposit. + #[inline(always)] + pub fn with_minimum_storage_deposit(mut self, rent_structure: RentStructure) -> Self { + self.amount = OutputBuilderAmount::MinimumStorageDeposit(rent_structure); + self + } + + /// Sets the mana to the provided value. + #[inline(always)] + pub fn with_mana(mut self, mana: u64) -> Self { + self.mana = mana; + self + } + + /// + #[inline(always)] + pub fn add_native_token(mut self, native_token: NativeToken) -> Self { + self.native_tokens.insert(native_token); + self + } + + /// + #[inline(always)] + pub fn with_native_tokens(mut self, native_tokens: impl IntoIterator) -> Self { + self.native_tokens = native_tokens.into_iter().collect(); + self + } + + /// Sets the anchor ID to the provided value. + #[inline(always)] + pub fn with_anchor_id(mut self, anchor_id: AnchorId) -> Self { + self.anchor_id = anchor_id; + self + } + + /// + #[inline(always)] + pub fn with_state_index(mut self, state_index: impl Into>) -> Self { + self.state_index = state_index.into(); + self + } + + /// + #[inline(always)] + pub fn with_state_metadata(mut self, state_metadata: impl Into>) -> Self { + self.state_metadata = state_metadata.into(); + self + } + + /// Adds an [`UnlockCondition`] to the builder, if one does not already exist of that type. + #[inline(always)] + pub fn add_unlock_condition(mut self, unlock_condition: impl Into) -> Self { + self.unlock_conditions.insert(unlock_condition.into()); + self + } + + /// Sets the [`UnlockConditions`]s in the builder, overwriting any existing values. + #[inline(always)] + pub fn with_unlock_conditions( + mut self, + unlock_conditions: impl IntoIterator>, + ) -> Self { + self.unlock_conditions = unlock_conditions.into_iter().map(Into::into).collect(); + self + } + + /// Replaces an [`UnlockCondition`] of the builder with a new one, or adds it. + pub fn replace_unlock_condition(mut self, unlock_condition: impl Into) -> Self { + self.unlock_conditions.replace(unlock_condition.into()); + self + } + + /// Clears all [`UnlockConditions`]s from the builder. + #[inline(always)] + pub fn clear_unlock_conditions(mut self) -> Self { + self.unlock_conditions.clear(); + self + } + + /// Adds a [`Feature`] to the builder, if one does not already exist of that type. + #[inline(always)] + pub fn add_feature(mut self, feature: impl Into) -> Self { + self.features.insert(feature.into()); + self + } + + /// Sets the [`Feature`]s in the builder, overwriting any existing values. + #[inline(always)] + pub fn with_features(mut self, features: impl IntoIterator>) -> Self { + self.features = features.into_iter().map(Into::into).collect(); + self + } + + /// Replaces a [`Feature`] of the builder with a new one, or adds it. + pub fn replace_feature(mut self, feature: impl Into) -> Self { + self.features.replace(feature.into()); + self + } + + /// Clears all [`Feature`]s from the builder. + #[inline(always)] + pub fn clear_features(mut self) -> Self { + self.features.clear(); + self + } + + /// Adds an immutable [`Feature`] to the builder, if one does not already exist of that type. + #[inline(always)] + pub fn add_immutable_feature(mut self, immutable_feature: impl Into) -> Self { + self.immutable_features.insert(immutable_feature.into()); + self + } + + /// Sets the immutable [`Feature`]s in the builder, overwriting any existing values. + #[inline(always)] + pub fn with_immutable_features(mut self, immutable_features: impl IntoIterator>) -> Self { + self.immutable_features = immutable_features.into_iter().map(Into::into).collect(); + self + } + + /// Replaces an immutable [`Feature`] of the builder with a new one, or adds it. + pub fn replace_immutable_feature(mut self, immutable_feature: impl Into) -> Self { + self.immutable_features.replace(immutable_feature.into()); + self + } + + /// Clears all immutable [`Feature`]s from the builder. + #[inline(always)] + pub fn clear_immutable_features(mut self) -> Self { + self.immutable_features.clear(); + self + } + + /// + pub fn finish(self) -> Result { + let state_index = self.state_index.unwrap_or(0); + + let state_metadata = self + .state_metadata + .into_boxed_slice() + .try_into() + .map_err(Error::InvalidStateMetadataLength)?; + + verify_index_counter(&self.anchor_id, state_index)?; + + let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?; + + verify_unlock_conditions(&unlock_conditions, &self.anchor_id)?; + + let features = Features::from_set(self.features)?; + + verify_allowed_features(&features, AnchorOutput::ALLOWED_FEATURES)?; + + let immutable_features = Features::from_set(self.immutable_features)?; + + verify_allowed_features(&immutable_features, AnchorOutput::ALLOWED_IMMUTABLE_FEATURES)?; + + let mut output = AnchorOutput { + amount: 1, + mana: self.mana, + native_tokens: NativeTokens::from_set(self.native_tokens)?, + anchor_id: self.anchor_id, + state_index, + state_metadata, + unlock_conditions, + features, + immutable_features, + }; + + output.amount = match self.amount { + OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => { + Output::Anchor(output.clone()).rent_cost(rent_structure) + } + }; + + verify_output_amount_min(output.amount)?; + + Ok(output) + } + + /// + pub fn finish_with_params<'a>(self, params: impl Into> + Send) -> Result { + let output = self.finish()?; + + if let Some(token_supply) = params.into().token_supply() { + verify_output_amount_supply(output.amount, token_supply)?; + } + + Ok(output) + } + + /// Finishes the [`AnchorOutputBuilder`] into an [`Output`]. + pub fn finish_output<'a>(self, params: impl Into> + Send) -> Result { + Ok(Output::Anchor(self.finish_with_params(params)?)) + } +} + +impl From<&AnchorOutput> for AnchorOutputBuilder { + fn from(output: &AnchorOutput) -> Self { + Self { + amount: OutputBuilderAmount::Amount(output.amount), + mana: output.mana, + native_tokens: output.native_tokens.iter().copied().collect(), + anchor_id: output.anchor_id, + state_index: Some(output.state_index), + state_metadata: output.state_metadata.to_vec(), + unlock_conditions: output.unlock_conditions.iter().cloned().collect(), + features: output.features.iter().cloned().collect(), + immutable_features: output.immutable_features.iter().cloned().collect(), + } + } +} + +pub(crate) type StateMetadataLength = BoundedU16<0, { AnchorOutput::STATE_METADATA_LENGTH_MAX }>; + +/// Describes an anchor in the ledger that can be controlled by the state and governance controllers. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AnchorOutput { + // Amount of IOTA coins held by the output. + amount: u64, + mana: u64, + // Native tokens held by the output. + native_tokens: NativeTokens, + // Unique identifier of the anchor. + anchor_id: AnchorId, + // A counter that must increase by 1 every time the anchor is state transitioned. + state_index: u32, + // Metadata that can only be changed by the state controller. + state_metadata: BoxedSlicePrefix, + unlock_conditions: UnlockConditions, + // + features: Features, + // + immutable_features: Features, +} + +impl AnchorOutput { + /// The [`Output`](crate::types::block::output::Output) kind of an [`AnchorOutput`]. + /// TODO + pub const KIND: u8 = 255; + /// Maximum possible length in bytes of the state metadata. + pub const STATE_METADATA_LENGTH_MAX: u16 = 8192; + /// The set of allowed [`UnlockCondition`]s for an [`AnchorOutput`]. + pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = + UnlockConditionFlags::STATE_CONTROLLER_ADDRESS.union(UnlockConditionFlags::GOVERNOR_ADDRESS); + /// The set of allowed [`Feature`]s for an [`AnchorOutput`]. + pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER.union(FeatureFlags::METADATA); + /// The set of allowed immutable [`Feature`]s for an [`AnchorOutput`]. + pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::ISSUER.union(FeatureFlags::METADATA); + + /// Creates a new [`AnchorOutputBuilder`] with a provided amount. + #[inline(always)] + pub fn build_with_amount(amount: u64, anchor_id: AnchorId) -> AnchorOutputBuilder { + AnchorOutputBuilder::new_with_amount(amount, anchor_id) + } + + /// Creates a new [`AnchorOutputBuilder`] with a provided rent structure. + /// The amount will be set to the minimum storage deposit. + #[inline(always)] + pub fn build_with_minimum_storage_deposit( + rent_structure: RentStructure, + anchor_id: AnchorId, + ) -> AnchorOutputBuilder { + AnchorOutputBuilder::new_with_minimum_storage_deposit(rent_structure, anchor_id) + } + + /// + #[inline(always)] + pub fn amount(&self) -> u64 { + self.amount + } + + #[inline(always)] + pub fn mana(&self) -> u64 { + self.mana + } + + /// + #[inline(always)] + pub fn native_tokens(&self) -> &NativeTokens { + &self.native_tokens + } + + /// + #[inline(always)] + pub fn anchor_id(&self) -> &AnchorId { + &self.anchor_id + } + + /// Returns the anchor ID if not null, or creates it from the output ID. + #[inline(always)] + pub fn anchor_id_non_null(&self, output_id: &OutputId) -> AnchorId { + self.anchor_id.or_from_output_id(output_id) + } + + /// + #[inline(always)] + pub fn state_index(&self) -> u32 { + self.state_index + } + + /// + #[inline(always)] + pub fn state_metadata(&self) -> &[u8] { + &self.state_metadata + } + + /// + #[inline(always)] + pub fn unlock_conditions(&self) -> &UnlockConditions { + &self.unlock_conditions + } + + /// + #[inline(always)] + pub fn features(&self) -> &Features { + &self.features + } + + /// + #[inline(always)] + pub fn immutable_features(&self) -> &Features { + &self.immutable_features + } + + /// + #[inline(always)] + pub fn state_controller_address(&self) -> &Address { + // An AnchorOutput must have a StateControllerAddressUnlockCondition. + self.unlock_conditions + .state_controller_address() + .map(|unlock_condition| unlock_condition.address()) + .unwrap() + } + + /// + #[inline(always)] + pub fn governor_address(&self) -> &Address { + // An AnchorOutput must have a GovernorAddressUnlockCondition. + self.unlock_conditions + .governor_address() + .map(|unlock_condition| unlock_condition.address()) + .unwrap() + } + + /// + #[inline(always)] + pub fn chain_id(&self) -> ChainId { + ChainId::Anchor(self.anchor_id) + } + + /// Returns the anchor address for this output. + pub fn anchor_address(&self, output_id: &OutputId) -> AnchorAddress { + AnchorAddress::new(self.anchor_id_non_null(output_id)) + } + + /// + pub fn unlock( + &self, + output_id: &OutputId, + unlock: &Unlock, + inputs: &[(&OutputId, &Output)], + context: &mut ValidationContext<'_>, + ) -> Result<(), TransactionFailureReason> { + let anchor_id = if self.anchor_id().is_null() { + AnchorId::from(output_id) + } else { + *self.anchor_id() + }; + let next_state = context.output_chains.get(&ChainId::from(anchor_id)); + + match next_state { + Some(Output::Anchor(next_state)) => { + if self.state_index() == next_state.state_index() { + self.governor_address().unlock(unlock, inputs, context)?; + } else { + self.state_controller_address().unlock(unlock, inputs, context)?; + // Only a state transition can be used to consider the anchor address for output unlocks and + // sender/issuer validations. + context + .unlocked_addresses + .insert(Address::from(AnchorAddress::from(anchor_id))); + } + } + None => self.governor_address().unlock(unlock, inputs, context)?, + // The next state can only be an anchor output since it is identified by an anchor chain identifier. + Some(_) => unreachable!(), + }; + + Ok(()) + } + + // Transition, just without full ValidationContext + pub(crate) fn transition_inner( + current_state: &Self, + next_state: &Self, + _input_chains: &HashMap, + _outputs: &[Output], + ) -> Result<(), StateTransitionError> { + if current_state.immutable_features != next_state.immutable_features { + return Err(StateTransitionError::MutatedImmutableField); + } + + if next_state.state_index == current_state.state_index + 1 { + // State transition. + if current_state.state_controller_address() != next_state.state_controller_address() + || current_state.governor_address() != next_state.governor_address() + || current_state.features.metadata() != next_state.features.metadata() + { + return Err(StateTransitionError::MutatedFieldWithoutRights); + } + } else if next_state.state_index == current_state.state_index { + // Governance transition. + if current_state.amount != next_state.amount + || current_state.native_tokens != next_state.native_tokens + || current_state.state_metadata != next_state.state_metadata + { + return Err(StateTransitionError::MutatedFieldWithoutRights); + } + } else { + return Err(StateTransitionError::UnsupportedStateIndexOperation { + current_state: current_state.state_index, + next_state: next_state.state_index, + }); + } + + Ok(()) + } +} + +impl StateTransitionVerifier for AnchorOutput { + fn creation(next_state: &Self, context: &ValidationContext<'_>) -> Result<(), StateTransitionError> { + if !next_state.anchor_id.is_null() { + return Err(StateTransitionError::NonZeroCreatedId); + } + + if let Some(issuer) = next_state.immutable_features().issuer() { + if !context.unlocked_addresses.contains(issuer.address()) { + return Err(StateTransitionError::IssuerNotUnlocked); + } + } + + Ok(()) + } + + fn transition( + current_state: &Self, + next_state: &Self, + context: &ValidationContext<'_>, + ) -> Result<(), StateTransitionError> { + Self::transition_inner( + current_state, + next_state, + &context.input_chains, + context.essence.outputs(), + ) + } + + fn destruction(_current_state: &Self, _context: &ValidationContext<'_>) -> Result<(), StateTransitionError> { + Ok(()) + } +} + +impl Packable for AnchorOutput { + type UnpackError = Error; + type UnpackVisitor = ProtocolParameters; + + fn pack(&self, packer: &mut P) -> Result<(), P::Error> { + self.amount.pack(packer)?; + self.mana.pack(packer)?; + self.native_tokens.pack(packer)?; + self.anchor_id.pack(packer)?; + self.state_index.pack(packer)?; + self.state_metadata.pack(packer)?; + self.unlock_conditions.pack(packer)?; + self.features.pack(packer)?; + self.immutable_features.pack(packer)?; + + Ok(()) + } + + fn unpack( + unpacker: &mut U, + visitor: &Self::UnpackVisitor, + ) -> Result> { + let amount = u64::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + + verify_output_amount_packable::(&amount, visitor).map_err(UnpackError::Packable)?; + + let mana = u64::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + + let native_tokens = NativeTokens::unpack::<_, VERIFY>(unpacker, &())?; + let anchor_id = AnchorId::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + let state_index = u32::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + let state_metadata = BoxedSlicePrefix::::unpack::<_, VERIFY>(unpacker, &()) + .map_packable_err(|err| Error::InvalidStateMetadataLength(err.into_prefix_err().into()))?; + + if VERIFY { + verify_index_counter(&anchor_id, state_index).map_err(UnpackError::Packable)?; + } + + let unlock_conditions = UnlockConditions::unpack::<_, VERIFY>(unpacker, visitor)?; + + if VERIFY { + verify_unlock_conditions(&unlock_conditions, &anchor_id).map_err(UnpackError::Packable)?; + } + + let features = Features::unpack::<_, VERIFY>(unpacker, &())?; + + if VERIFY { + verify_allowed_features(&features, Self::ALLOWED_FEATURES).map_err(UnpackError::Packable)?; + } + + let immutable_features = Features::unpack::<_, VERIFY>(unpacker, &())?; + + if VERIFY { + verify_allowed_features(&immutable_features, Self::ALLOWED_IMMUTABLE_FEATURES) + .map_err(UnpackError::Packable)?; + } + + Ok(Self { + amount, + mana, + native_tokens, + anchor_id, + state_index, + state_metadata, + unlock_conditions, + features, + immutable_features, + }) + } +} + +#[inline] +fn verify_index_counter(anchor_id: &AnchorId, state_index: u32) -> Result<(), Error> { + if anchor_id.is_null() && state_index != 0 { + Err(Error::NonZeroStateIndexOrFoundryCounter) + } else { + Ok(()) + } +} + +fn verify_unlock_conditions(unlock_conditions: &UnlockConditions, anchor_id: &AnchorId) -> Result<(), Error> { + if let Some(unlock_condition) = unlock_conditions.state_controller_address() { + if let Address::Anchor(anchor_address) = unlock_condition.address() { + if anchor_address.anchor_id() == anchor_id { + return Err(Error::SelfControlledAnchorOutput(*anchor_id)); + } + } + } else { + return Err(Error::MissingStateControllerUnlockCondition); + } + + if let Some(unlock_condition) = unlock_conditions.governor_address() { + if let Address::Anchor(anchor_address) = unlock_condition.address() { + if anchor_address.anchor_id() == anchor_id { + return Err(Error::SelfControlledAnchorOutput(*anchor_id)); + } + } + } else { + return Err(Error::MissingGovernorUnlockCondition); + } + + verify_allowed_unlock_conditions(unlock_conditions, AnchorOutput::ALLOWED_UNLOCK_CONDITIONS) +} + +#[cfg(feature = "serde")] +pub(crate) mod dto { + use alloc::boxed::Box; + + use serde::{Deserialize, Serialize}; + + use super::*; + use crate::{ + types::{ + block::{output::unlock_condition::dto::UnlockConditionDto, Error}, + TryFromDto, + }, + utils::serde::{prefix_hex_bytes, string}, + }; + + /// Describes an anchor in the ledger that can be controlled by the state and governance controllers. + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct AnchorOutputDto { + #[serde(rename = "type")] + pub kind: u8, + #[serde(with = "string")] + pub amount: u64, + #[serde(with = "string")] + pub mana: u64, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub native_tokens: Vec, + pub anchor_id: AnchorId, + pub state_index: u32, + #[serde(skip_serializing_if = "<[_]>::is_empty", default, with = "prefix_hex_bytes")] + pub state_metadata: Box<[u8]>, + pub unlock_conditions: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub features: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub immutable_features: Vec, + } + + impl From<&AnchorOutput> for AnchorOutputDto { + fn from(value: &AnchorOutput) -> Self { + Self { + kind: AnchorOutput::KIND, + amount: value.amount(), + mana: value.mana(), + native_tokens: value.native_tokens().to_vec(), + anchor_id: *value.anchor_id(), + state_index: value.state_index(), + state_metadata: value.state_metadata().into(), + unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(), + features: value.features().to_vec(), + immutable_features: value.immutable_features().to_vec(), + } + } + } + + impl TryFromDto for AnchorOutput { + type Dto = AnchorOutputDto; + type Error = Error; + + fn try_from_dto_with_params_inner(dto: Self::Dto, params: ValidationParams<'_>) -> Result { + let mut builder = AnchorOutputBuilder::new_with_amount(dto.amount, dto.anchor_id) + .with_mana(dto.mana) + .with_state_index(dto.state_index) + .with_native_tokens(dto.native_tokens) + .with_features(dto.features) + .with_immutable_features(dto.immutable_features) + .with_state_metadata(dto.state_metadata); + + for u in dto.unlock_conditions { + builder = builder.add_unlock_condition(UnlockCondition::try_from_dto_with_params(u, ¶ms)?); + } + + builder.finish_with_params(params) + } + } + + impl AnchorOutput { + #[allow(clippy::too_many_arguments)] + pub fn try_from_dtos<'a>( + amount: OutputBuilderAmount, + mana: u64, + native_tokens: Option>, + anchor_id: &AnchorId, + state_index: Option, + state_metadata: Option>, + unlock_conditions: Vec, + features: Option>, + immutable_features: Option>, + params: impl Into> + Send, + ) -> Result { + let params = params.into(); + let mut builder = match amount { + OutputBuilderAmount::Amount(amount) => AnchorOutputBuilder::new_with_amount(amount, *anchor_id), + OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => { + AnchorOutputBuilder::new_with_minimum_storage_deposit(rent_structure, *anchor_id) + } + } + .with_mana(mana); + + if let Some(native_tokens) = native_tokens { + builder = builder.with_native_tokens(native_tokens); + } + + if let Some(state_index) = state_index { + builder = builder.with_state_index(state_index); + } + + if let Some(state_metadata) = state_metadata { + builder = builder.with_state_metadata(state_metadata); + } + + let unlock_conditions = unlock_conditions + .into_iter() + .map(|u| UnlockCondition::try_from_dto_with_params(u, ¶ms)) + .collect::, Error>>()?; + builder = builder.with_unlock_conditions(unlock_conditions); + + if let Some(features) = features { + builder = builder.with_features(features); + } + + if let Some(immutable_features) = immutable_features { + builder = builder.with_immutable_features(immutable_features); + } + + builder.finish_with_params(params) + } + } +} + +// #[cfg(test)] +// mod tests { + +// use super::*; +// use crate::types::{ +// block::{ +// output::{dto::OutputDto, FoundryId, SimpleTokenScheme, TokenId}, +// protocol::protocol_parameters, +// rand::{ +// address::rand_anchor_address, +// output::{ +// feature::rand_allowed_features, +// rand_account_output, rand_anchor_id, +// unlock_condition::{ +// rand_governor_address_unlock_condition_different_from, +// rand_state_controller_address_unlock_condition_different_from, +// }, +// }, +// }, +// }, +// TryFromDto, +// }; + +// #[test] +// fn to_from_dto() { +// let protocol_parameters = protocol_parameters(); +// let output = rand_account_output(protocol_parameters.token_supply()); +// let dto = OutputDto::Account((&output).into()); +// let output_unver = Output::try_from_dto(dto.clone()).unwrap(); +// assert_eq!(&output, output_unver.as_account()); +// let output_ver = Output::try_from_dto_with_params(dto, &protocol_parameters).unwrap(); +// assert_eq!(&output, output_ver.as_account()); + +// let output_split = AnchorOutput::try_from_dtos( +// OutputBuilderAmount::Amount(output.amount()), +// output.mana(), +// Some(output.native_tokens().to_vec()), +// output.anchor_id(), +// output.state_index().into(), +// output.state_metadata().to_owned().into(), +// output.unlock_conditions().iter().map(Into::into).collect(), +// Some(output.features().to_vec()), +// Some(output.immutable_features().to_vec()), +// &protocol_parameters, +// ) +// .unwrap(); +// assert_eq!(output, output_split); + +// let anchor_id = rand_anchor_id(); +// let foundry_id = FoundryId::build(&rand_anchor_address(), 0, SimpleTokenScheme::KIND); +// let gov_address = rand_governor_address_unlock_condition_different_from(&anchor_id); +// let state_address = rand_state_controller_address_unlock_condition_different_from(&anchor_id); + +// let test_split_dto = |builder: AnchorOutputBuilder| { +// let output_split = AnchorOutput::try_from_dtos( +// builder.amount, +// builder.mana, +// Some(builder.native_tokens.iter().copied().collect()), +// &builder.anchor_id, +// builder.state_index, +// builder.state_metadata.to_owned().into(), +// builder.unlock_conditions.iter().map(Into::into).collect(), +// Some(builder.features.iter().cloned().collect()), +// Some(builder.immutable_features.iter().cloned().collect()), +// &protocol_parameters, +// ) +// .unwrap(); +// assert_eq!(builder.finish_with_params(&protocol_parameters).unwrap(), output_split); +// }; + +// let builder = AnchorOutput::build_with_amount(100, anchor_id) +// .add_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) +// .add_unlock_condition(gov_address) +// .add_unlock_condition(state_address) +// .with_features(rand_allowed_features(AnchorOutput::ALLOWED_FEATURES)) +// .with_immutable_features(rand_allowed_features(AnchorOutput::ALLOWED_IMMUTABLE_FEATURES)); +// test_split_dto(builder); + +// let builder = AnchorOutput::build_with_minimum_storage_deposit(protocol_parameters.rent_structure(), +// anchor_id) .add_native_token(NativeToken::new(TokenId::from(foundry_id), 1000).unwrap()) +// .add_unlock_condition(gov_address) +// .add_unlock_condition(state_address) +// .with_features(rand_allowed_features(AnchorOutput::ALLOWED_FEATURES)) +// .with_immutable_features(rand_allowed_features(AnchorOutput::ALLOWED_IMMUTABLE_FEATURES)); +// test_split_dto(builder); +// } +// } diff --git a/sdk/src/types/block/output/chain_id.rs b/sdk/src/types/block/output/chain_id.rs index a3159a0ae3..b5eb24debc 100644 --- a/sdk/src/types/block/output/chain_id.rs +++ b/sdk/src/types/block/output/chain_id.rs @@ -3,7 +3,7 @@ use derive_more::From; -use crate::types::block::output::{AccountId, DelegationId, FoundryId, NftId, OutputId}; +use crate::types::block::output::{AccountId, AnchorId, DelegationId, FoundryId, NftId, OutputId}; /// #[derive(Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd, From)] @@ -17,6 +17,8 @@ pub enum ChainId { Nft(NftId), /// Delegation(DelegationId), + /// + Anchor(AnchorId), } impl core::fmt::Debug for ChainId { @@ -27,6 +29,7 @@ impl core::fmt::Debug for ChainId { Self::Foundry(id) => formatter.field(id), Self::Nft(id) => formatter.field(id), Self::Delegation(id) => formatter.field(id), + Self::Anchor(id) => formatter.field(id), }; formatter.finish() } @@ -40,6 +43,7 @@ impl ChainId { Self::Foundry(id) => id.is_null(), Self::Nft(id) => id.is_null(), Self::Delegation(id) => id.is_null(), + Self::Anchor(id) => id.is_null(), } } @@ -54,6 +58,7 @@ impl ChainId { Self::Foundry(_) => self, Self::Nft(_) => Self::Nft(NftId::from(output_id)), Self::Delegation(_) => Self::Delegation(DelegationId::from(output_id)), + Self::Anchor(_) => Self::Anchor(AnchorId::from(output_id)), } } } @@ -65,6 +70,7 @@ impl core::fmt::Display for ChainId { Self::Foundry(id) => write!(f, "{id}"), Self::Nft(id) => write!(f, "{id}"), Self::Delegation(id) => write!(f, "{id}"), + Self::Anchor(id) => write!(f, "{id}"), } } } diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index c9829eb5a0..33f293d56e 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod anchor; mod chain_id; mod delegation; mod inputs_commitment; @@ -43,6 +44,7 @@ pub(crate) use self::{ }; pub use self::{ account::{AccountId, AccountOutput, AccountOutputBuilder, AccountTransition}, + anchor::{AnchorId, AnchorOutput, AnchorTransition}, basic::{BasicOutput, BasicOutputBuilder}, chain_id::ChainId, delegation::{DelegationId, DelegationOutput, DelegationOutputBuilder}, @@ -123,6 +125,8 @@ pub enum Output { Nft(NftOutput), /// A delegation output. Delegation(DelegationOutput), + /// An anchor output. + Anchor(AnchorOutput), } impl core::fmt::Debug for Output { @@ -133,6 +137,7 @@ impl core::fmt::Debug for Output { Self::Foundry(output) => output.fmt(f), Self::Nft(output) => output.fmt(f), Self::Delegation(output) => output.fmt(f), + Self::Anchor(output) => output.fmt(f), } } } @@ -149,6 +154,7 @@ impl Output { Self::Foundry(_) => FoundryOutput::KIND, Self::Nft(_) => NftOutput::KIND, Self::Delegation(_) => DelegationOutput::KIND, + Self::Anchor(_) => AnchorOutput::KIND, } } @@ -160,6 +166,7 @@ impl Output { Self::Foundry(_) => "Foundry", Self::Nft(_) => "Nft", Self::Delegation(_) => "Delegation", + Self::Anchor(_) => "Anchor", } } @@ -171,6 +178,7 @@ impl Output { Self::Foundry(output) => output.amount(), Self::Nft(output) => output.amount(), Self::Delegation(output) => output.amount(), + Self::Anchor(output) => output.amount(), } } @@ -182,6 +190,7 @@ impl Output { Self::Foundry(output) => Some(output.native_tokens()), Self::Nft(output) => Some(output.native_tokens()), Self::Delegation(_) => None, + Self::Anchor(output) => Some(output.native_tokens()), } } @@ -193,6 +202,7 @@ impl Output { Self::Foundry(output) => Some(output.unlock_conditions()), Self::Nft(output) => Some(output.unlock_conditions()), Self::Delegation(output) => Some(output.unlock_conditions()), + Self::Anchor(output) => Some(output.unlock_conditions()), } } @@ -204,6 +214,7 @@ impl Output { Self::Foundry(output) => Some(output.features()), Self::Nft(output) => Some(output.features()), Self::Delegation(_) => None, + Self::Anchor(output) => Some(output.features()), } } @@ -215,6 +226,7 @@ impl Output { Self::Foundry(output) => Some(output.immutable_features()), Self::Nft(output) => Some(output.immutable_features()), Self::Delegation(_) => None, + Self::Anchor(output) => Some(output.immutable_features()), } } @@ -226,6 +238,7 @@ impl Output { Self::Foundry(output) => Some(output.chain_id()), Self::Nft(output) => Some(output.chain_id()), Self::Delegation(_) => None, + Self::Anchor(output) => Some(output.chain_id()), } } @@ -304,6 +317,21 @@ impl Output { } } + /// Checks whether the output is a [`AnchorOutput`]. + pub fn is_anchor(&self) -> bool { + matches!(self, Self::Anchor(_)) + } + + /// Gets the output as an actual [`AnchorOutput`]. + /// NOTE: Will panic if the output is not a [`AnchorOutput`]. + pub fn as_anchor(&self) -> &AnchorOutput { + if let Self::Anchor(output) = self { + output + } else { + panic!("invalid downcast of non-AnchorOutput"); + } + } + /// Returns the address that is required to unlock this [`Output`] and the account or nft address that gets /// unlocked by it, if it's an account or nft. /// If no `account_transition` has been provided, assumes a state transition. @@ -338,6 +366,17 @@ impl Output { *output.unlock_conditions().locked_address(output.address(), slot_index), None, )), + Self::Anchor(output) => { + if account_transition.unwrap_or(AccountTransition::State) == AccountTransition::State { + // Account address is only unlocked if it's a state transition + Ok(( + *output.state_controller_address(), + Some(Address::Anchor(output.anchor_address(output_id))), + )) + } else { + Ok((*output.governor_address(), None)) + } + } } } @@ -448,6 +487,10 @@ impl Packable for Output { DelegationOutput::KIND.pack(packer)?; output.pack(packer) } + Self::Anchor(output) => { + AnchorOutput::KIND.pack(packer)?; + output.pack(packer) + } }?; Ok(()) @@ -463,6 +506,7 @@ impl Packable for Output { FoundryOutput::KIND => Self::from(FoundryOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?), NftOutput::KIND => Self::from(NftOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?), DelegationOutput::KIND => Self::from(DelegationOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?), + AnchorOutput::KIND => Self::from(AnchorOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?), k => return Err(Error::InvalidOutputKind(k)).map_err(UnpackError::Packable), }) } @@ -526,8 +570,8 @@ pub mod dto { use super::*; pub use super::{ - account::dto::AccountOutputDto, basic::dto::BasicOutputDto, delegation::dto::DelegationOutputDto, - foundry::dto::FoundryOutputDto, nft::dto::NftOutputDto, + account::dto::AccountOutputDto, anchor::dto::AnchorOutputDto, basic::dto::BasicOutputDto, + delegation::dto::DelegationOutputDto, foundry::dto::FoundryOutputDto, nft::dto::NftOutputDto, }; use crate::types::{block::Error, TryFromDto, ValidationParams}; @@ -539,6 +583,7 @@ pub mod dto { Foundry(FoundryOutputDto), Nft(NftOutputDto), Delegation(DelegationOutputDto), + Anchor(AnchorOutputDto), } impl From<&Output> for OutputDto { @@ -549,6 +594,7 @@ pub mod dto { Output::Foundry(o) => Self::Foundry(o.into()), Output::Nft(o) => Self::Nft(o.into()), Output::Delegation(o) => Self::Delegation(o.into()), + Output::Anchor(o) => Self::Anchor(o.into()), } } } @@ -566,6 +612,7 @@ pub mod dto { OutputDto::Delegation(o) => { Self::Delegation(DelegationOutput::try_from_dto_with_params_inner(o, params)?) } + OutputDto::Anchor(o) => Self::Anchor(AnchorOutput::try_from_dto_with_params_inner(o, params)?), }) } } @@ -600,6 +647,10 @@ pub mod dto { serde::de::Error::custom(format!("cannot deserialize delegation output: {e}")) })?) } + AnchorOutput::KIND => Self::Anchor( + AnchorOutputDto::deserialize(value) + .map_err(|e| serde::de::Error::custom(format!("cannot deserialize anchor output: {e}")))?, + ), _ => return Err(serde::de::Error::custom("invalid output type")), }, ) @@ -619,6 +670,7 @@ pub mod dto { T4(&'a FoundryOutputDto), T5(&'a NftOutputDto), T6(&'a DelegationOutputDto), + T7(&'a AnchorOutputDto), } #[derive(Serialize)] struct TypedOutput<'a> { @@ -641,6 +693,9 @@ pub mod dto { Self::Delegation(o) => TypedOutput { output: OutputDto_::T6(o), }, + Self::Anchor(o) => TypedOutput { + output: OutputDto_::T7(o), + }, }; output.serialize(serializer) } diff --git a/sdk/src/types/block/payload/transaction/essence/regular.rs b/sdk/src/types/block/payload/transaction/essence/regular.rs index c21dfb0ddd..7f688c7526 100644 --- a/sdk/src/types/block/payload/transaction/essence/regular.rs +++ b/sdk/src/types/block/payload/transaction/essence/regular.rs @@ -368,6 +368,7 @@ fn verify_outputs(outputs: &[Output], visitor: &ProtocolPara Output::Foundry(output) => (output.amount(), Some(output.native_tokens()), Some(output.chain_id())), Output::Nft(output) => (output.amount(), Some(output.native_tokens()), Some(output.chain_id())), Output::Delegation(output) => (output.amount(), None, Some(output.chain_id())), + Output::Anchor(output) => (output.amount(), None, Some(output.chain_id())), }; amount_sum = amount_sum diff --git a/sdk/src/types/block/semantic.rs b/sdk/src/types/block/semantic.rs index b98b916bcb..057bdae549 100644 --- a/sdk/src/types/block/semantic.rs +++ b/sdk/src/types/block/semantic.rs @@ -268,6 +268,7 @@ pub fn semantic_validation( None, output.unlock_conditions(), ), + Output::Anchor(_) => todo!(), }; if let Err(conflict) = conflict { @@ -325,6 +326,7 @@ pub fn semantic_validation( Output::Foundry(output) => (output.amount(), Some(output.native_tokens()), Some(output.features())), Output::Nft(output) => (output.amount(), Some(output.native_tokens()), Some(output.features())), Output::Delegation(output) => (output.amount(), None, None), + Output::Anchor(_) => todo!(), }; if let Some(sender) = features.and_then(|f| f.sender()) {