diff --git a/bindings/core/src/method/utils.rs b/bindings/core/src/method/utils.rs index 706111a7d2..eeae9f517a 100644 --- a/bindings/core/src/method/utils.rs +++ b/bindings/core/src/method/utils.rs @@ -19,7 +19,7 @@ use iota_sdk::{ unlock::Unlock, BlockDto, }, - utils::serde::{mana_rewards, string}, + utils::serde::{option_mana_rewards, string}, }; use serde::{Deserialize, Serialize}; @@ -179,8 +179,8 @@ pub enum UtilsMethod { transaction: TransactionDto, inputs: Vec, unlocks: Option>, - #[serde(default, with = "mana_rewards")] - mana_rewards: BTreeMap, + #[serde(default, with = "option_mana_rewards")] + mana_rewards: Option>, protocol_parameters: ProtocolParameters, }, /// Applies mana decay to the given mana. diff --git a/sdk/src/client/api/block_builder/transaction.rs b/sdk/src/client/api/block_builder/transaction.rs index a0db364fd2..7fa90e945e 100644 --- a/sdk/src/client/api/block_builder/transaction.rs +++ b/sdk/src/client/api/block_builder/transaction.rs @@ -31,7 +31,7 @@ const REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH: usize = 1 + 2; pub fn verify_semantic( input_signing_data: &[InputSigningData], transaction_payload: &SignedTransactionPayload, - mana_rewards: BTreeMap, + mana_rewards: impl Into>>, protocol_parameters: ProtocolParameters, ) -> Result<(), TransactionFailureReason> { let inputs = input_signing_data @@ -43,7 +43,7 @@ pub fn verify_semantic( transaction_payload.transaction(), &inputs, Some(transaction_payload.unlocks()), - mana_rewards, + mana_rewards.into(), protocol_parameters, ); diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index 4bbf203458..d0400af283 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -30,7 +30,7 @@ pub struct SemanticValidationContext<'a> { pub(crate) unlocks: Option<&'a [Unlock]>, pub(crate) input_amount: u64, pub(crate) input_mana: u64, - pub(crate) mana_rewards: BTreeMap, + pub(crate) mana_rewards: Option>, pub(crate) commitment_context_input: Option, pub(crate) reward_context_inputs: HashMap, pub(crate) input_native_tokens: BTreeMap, @@ -52,7 +52,7 @@ impl<'a> SemanticValidationContext<'a> { transaction: &'a Transaction, inputs: &'a [(&'a OutputId, &'a Output)], unlocks: Option<&'a [Unlock]>, - mana_rewards: BTreeMap, + mana_rewards: Option>, protocol_parameters: ProtocolParameters, ) -> Self { let transaction_id = transaction.id(); @@ -239,8 +239,9 @@ impl<'a> SemanticValidationContext<'a> { ) .ok_or(TransactionFailureReason::ManaOverflow)?; - if let Some(mana_rewards) = self.mana_rewards.get(*output_id) { - self.input_mana + if let Some(mana_rewards) = self.mana_rewards.as_ref().and_then(|r| r.get(*output_id)) { + self.input_mana = self + .input_mana .checked_add(*mana_rewards) .ok_or(TransactionFailureReason::ManaOverflow)?; } @@ -407,7 +408,7 @@ impl<'a> SemanticValidationContext<'a> { if !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) { return Err(TransactionFailureReason::CapabilitiesManaBurningNotAllowed); } - } else { + } else if self.mana_rewards.is_some() || self.reward_context_inputs.is_empty() { return Err(TransactionFailureReason::InputOutputManaMismatch); } } diff --git a/sdk/src/types/block/semantic/state_transition.rs b/sdk/src/types/block/semantic/state_transition.rs index 17f4b56e4e..e420fc7f3c 100644 --- a/sdk/src/types/block/semantic/state_transition.rs +++ b/sdk/src/types/block/semantic/state_transition.rs @@ -247,7 +247,10 @@ impl StateTransitionVerifier for AccountOutput { if staking_input.end_epoch() >= future_bounded_epoch { return Err(TransactionFailureReason::StakingFeatureRemovedBeforeUnbonding); - } else if !context.mana_rewards.contains_key(current_output_id) + } else if context + .mana_rewards + .as_ref() + .is_some_and(|r| !r.contains_key(current_output_id)) || !context.reward_context_inputs.contains_key(current_output_id) { return Err(TransactionFailureReason::StakingRewardClaimingInvalid); @@ -280,7 +283,10 @@ impl StateTransitionVerifier for AccountOutput { && (staking_input.start_epoch() != past_bounded_epoch || staking_input.end_epoch() < past_bounded_epoch + context.protocol_parameters.staking_unbonding_period - || !context.mana_rewards.contains_key(current_output_id) + || context + .mana_rewards + .as_ref() + .is_some_and(|r| !r.contains_key(current_output_id)) || !context.reward_context_inputs.contains_key(current_output_id)) { return Err(TransactionFailureReason::StakingRewardClaimingInvalid); @@ -321,7 +327,10 @@ impl StateTransitionVerifier for AccountOutput { if staking.end_epoch() >= future_bounded_epoch { return Err(TransactionFailureReason::StakingFeatureRemovedBeforeUnbonding); - } else if !context.mana_rewards.contains_key(output_id) + } else if context + .mana_rewards + .as_ref() + .is_some_and(|r| !r.contains_key(output_id)) || !context.reward_context_inputs.contains_key(output_id) { return Err(TransactionFailureReason::StakingRewardClaimingInvalid); @@ -571,7 +580,12 @@ impl StateTransitionVerifier for DelegationOutput { _current_state: &Self, context: &SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { - if !context.mana_rewards.contains_key(output_id) || !context.reward_context_inputs.contains_key(output_id) { + if context + .mana_rewards + .as_ref() + .is_some_and(|r| !r.contains_key(output_id)) + || !context.reward_context_inputs.contains_key(output_id) + { return Err(TransactionFailureReason::DelegationRewardInputMissing); } diff --git a/sdk/src/utils/serde.rs b/sdk/src/utils/serde.rs index 7137e5e262..6235b76869 100644 --- a/sdk/src/utils/serde.rs +++ b/sdk/src/utils/serde.rs @@ -275,3 +275,32 @@ pub mod mana_rewards { .collect::, _>>() } } + +#[cfg(feature = "client")] +pub mod option_mana_rewards { + use alloc::collections::BTreeMap; + + use serde::{Deserialize, Deserializer}; + + use crate::types::block::output::OutputId; + + pub fn serialize( + mana_rewards: &Option>, + s: S, + ) -> Result { + match mana_rewards { + Some(map) => super::mana_rewards::serialize(map, s), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + Option::>::deserialize(d)? + .map(|map| { + map.into_iter() + .map(|(k, v)| Ok((k, v.parse().map_err(serde::de::Error::custom)?))) + .collect::, _>>() + }) + .transpose() + } +} diff --git a/sdk/tests/client/input_selection/delegation_outputs.rs b/sdk/tests/client/input_selection/delegation_outputs.rs index ad48472b28..e5d94b1d49 100644 --- a/sdk/tests/client/input_selection/delegation_outputs.rs +++ b/sdk/tests/client/input_selection/delegation_outputs.rs @@ -86,6 +86,33 @@ fn remainder_needed_for_mana() { .select() .unwrap(); + let inputs = inputs + .iter() + .map(|input| (input.output_id(), &input.output)) + .collect::>(); + + // validating without rewards + iota_sdk::types::block::semantic::SemanticValidationContext::new( + &selected.transaction, + &inputs, + None, + None, + protocol_parameters.clone(), + ) + .validate() + .unwrap(); + + // validating with rewards + iota_sdk::types::block::semantic::SemanticValidationContext::new( + &selected.transaction, + &inputs, + None, + Some(std::collections::BTreeMap::from([(delegation_output_id, mana_rewards)])), + protocol_parameters.clone(), + ) + .validate() + .unwrap(); + assert_eq!(selected.inputs_data.len(), 2); assert_eq!(selected.transaction.outputs().len(), 2); assert!(selected.transaction.outputs().contains(&outputs[0]));