diff --git a/sdk/src/client/api/block_builder/input_selection/transition.rs b/sdk/src/client/api/block_builder/input_selection/transition.rs index 58beed6813..bbfa9f644f 100644 --- a/sdk/src/client/api/block_builder/input_selection/transition.rs +++ b/sdk/src/client/api/block_builder/input_selection/transition.rs @@ -55,6 +55,7 @@ impl InputSelection { let features = input.features().iter().filter(|feature| !feature.is_sender()).cloned(); let mut builder = AccountOutputBuilder::from(input) + .with_amount_or_minimum(input.amount(), self.protocol_parameters.storage_score_parameters()) .with_account_id(account_id) .with_foundry_counter(u32::max(highest_foundry_serial_number, input.foundry_counter())) .with_features(features); @@ -102,6 +103,7 @@ impl InputSelection { let features = input.features().iter().filter(|feature| !feature.is_sender()).cloned(); let output = NftOutputBuilder::from(input) + .with_amount_or_minimum(input.amount(), self.protocol_parameters.storage_score_parameters()) .with_nft_id(nft_id) .with_features(features) .finish_output()?; @@ -139,7 +141,9 @@ impl InputSelection { return Ok(None); } - let output = FoundryOutputBuilder::from(input).finish_output()?; + let output = FoundryOutputBuilder::from(input) + .with_amount_or_minimum(input.amount(), self.protocol_parameters.storage_score_parameters()) + .finish_output()?; log::debug!("Automatic transition of {output_id:?}/{foundry_id:?}"); diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 40f174656d..9f5559ceb3 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -73,6 +73,11 @@ impl AccountOutputBuilder { Self::new(OutputBuilderAmount::Amount(amount), account_id) } + /// Creates an [`AccountOutputBuilder`] with a provided amount, unless it is below the minimum. + pub fn new_with_amount_or_minimum(amount: u64, account_id: AccountId, params: StorageScoreParameters) -> Self { + Self::new(OutputBuilderAmount::AmountOrMinimum(amount, params), account_id) + } + /// Creates an [`AccountOutputBuilder`] with provided storage score parameters. /// The amount will be set to the minimum required amount of the resulting output. pub fn new_with_minimum_amount(params: StorageScoreParameters, account_id: AccountId) -> Self { @@ -98,6 +103,13 @@ impl AccountOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = OutputBuilderAmount::AmountOrMinimum(amount, params); + self + } + /// Sets the amount to the minimum required amount. #[inline(always)] pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { @@ -246,6 +258,7 @@ impl AccountOutputBuilder { output.amount = match self.amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }; diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 7f94894b86..f1063d8798 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -105,6 +105,11 @@ impl AnchorOutputBuilder { Self::new(OutputBuilderAmount::Amount(amount), anchor_id) } + /// Creates an [`AnchorOutputBuilder`] with a provided amount, unless it is below the minimum. + pub fn new_with_amount_or_minimum(amount: u64, anchor_id: AnchorId, params: StorageScoreParameters) -> Self { + Self::new(OutputBuilderAmount::AmountOrMinimum(amount, params), anchor_id) + } + /// Creates an [`AnchorOutputBuilder`] with provided storage score parameters. /// The amount will be set to the minimum required amount of the resulting output. #[inline(always)] @@ -131,6 +136,13 @@ impl AnchorOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = OutputBuilderAmount::AmountOrMinimum(amount, params); + self + } + /// Sets the amount to the minimum required amount. #[inline(always)] pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { @@ -277,6 +289,7 @@ impl AnchorOutputBuilder { output.amount = match self.amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }; diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 4b5de1a79e..39e784048c 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -38,6 +38,11 @@ impl BasicOutputBuilder { Self::new(OutputBuilderAmount::Amount(amount)) } + /// Creates a [`BasicOutputBuilder`] with a provided amount, unless it is below the minimum. + pub fn new_with_amount_or_minimum(amount: u64, params: StorageScoreParameters) -> Self { + Self::new(OutputBuilderAmount::AmountOrMinimum(amount, params)) + } + /// Creates an [`BasicOutputBuilder`] with provided storage score parameters. /// The amount will be set to the minimum required amount of the resulting output. #[inline(always)] @@ -61,6 +66,13 @@ impl BasicOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = OutputBuilderAmount::AmountOrMinimum(amount, params); + self + } + /// Sets the amount to the minimum required amount. #[inline(always)] pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { @@ -182,6 +194,7 @@ impl BasicOutputBuilder { self } } + OutputBuilderAmount::AmountOrMinimum(_, _) => self, OutputBuilderAmount::MinimumAmount(_) => self, }) } @@ -211,6 +224,7 @@ impl BasicOutputBuilder { output.amount = match self.amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }; diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index 26bfea1274..245ea04b84 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -41,13 +41,20 @@ impl DelegationId { } } +// TODO maybe can be removed as part of https://github.com/iotaledger/iota-sdk/issues/1938 +#[derive(Copy, Clone)] +pub enum DelegatedAmount { + Amount(u64), + MinimumAmount(StorageScoreParameters), +} + /// Builder for a [`DelegationOutput`]. #[derive(Clone)] #[must_use] pub struct DelegationOutputBuilder { // TODO https://github.com/iotaledger/iota-sdk/issues/1938 amount: Option, - delegated_amount: OutputBuilderAmount, + delegated_amount: DelegatedAmount, delegation_id: DelegationId, validator_address: AccountAddress, start_epoch: EpochIndex, @@ -59,7 +66,19 @@ impl DelegationOutputBuilder { /// Creates a [`DelegationOutputBuilder`] with a provided amount. /// Will set the delegated amount field to match. pub fn new_with_amount(amount: u64, delegation_id: DelegationId, validator_address: AccountAddress) -> Self { - Self::new(OutputBuilderAmount::Amount(amount), delegation_id, validator_address) + Self::new(DelegatedAmount::Amount(amount), delegation_id, validator_address) + } + + /// Creates a [`DelegationOutputBuilder`] with a provided amount, unless it is below the minimum. + /// Will set the delegated amount field to match. + pub fn new_with_amount_or_minimum( + amount: u64, + delegation_id: DelegationId, + validator_address: AccountAddress, + params: StorageScoreParameters, + ) -> Self { + Self::new(DelegatedAmount::Amount(amount), delegation_id, validator_address) + .with_amount_or_minimum(amount, params) } /// Creates a [`DelegationOutputBuilder`] with provided storage score parameters. @@ -69,18 +88,10 @@ impl DelegationOutputBuilder { delegation_id: DelegationId, validator_address: AccountAddress, ) -> Self { - Self::new( - OutputBuilderAmount::MinimumAmount(params), - delegation_id, - validator_address, - ) + Self::new(DelegatedAmount::MinimumAmount(params), delegation_id, validator_address) } - fn new( - delegated_amount: OutputBuilderAmount, - delegation_id: DelegationId, - validator_address: AccountAddress, - ) -> Self { + fn new(delegated_amount: DelegatedAmount, delegation_id: DelegationId, validator_address: AccountAddress) -> Self { Self { amount: None, delegated_amount, @@ -98,9 +109,16 @@ impl DelegationOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = Some(OutputBuilderAmount::AmountOrMinimum(amount, params)); + self + } + /// Sets the amount to the minimum required amount. pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { - if matches!(self.delegated_amount, OutputBuilderAmount::MinimumAmount(_)) { + if matches!(self.delegated_amount, DelegatedAmount::MinimumAmount(_)) { self.amount = None; } else { self.amount = Some(OutputBuilderAmount::MinimumAmount(params)); @@ -181,21 +199,22 @@ impl DelegationOutputBuilder { }; match self.delegated_amount { - OutputBuilderAmount::Amount(amount) => { + DelegatedAmount::Amount(amount) => { output.delegated_amount = amount; output.amount = self.amount.map_or(amount, |builder_amount| match builder_amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }); } - OutputBuilderAmount::MinimumAmount(params) => { + DelegatedAmount::MinimumAmount(params) => { let min = output.minimum_amount(params); output.delegated_amount = min; - output.amount = if let Some(OutputBuilderAmount::Amount(amount)) = self.amount { - amount - } else { - min - }; + output.amount = self.amount.map_or(min, |builder_amount| match builder_amount { + OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), + OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), + }); } } @@ -212,7 +231,7 @@ impl From<&DelegationOutput> for DelegationOutputBuilder { fn from(output: &DelegationOutput) -> Self { Self { amount: Some(OutputBuilderAmount::Amount(output.amount)), - delegated_amount: OutputBuilderAmount::Amount(output.delegated_amount), + delegated_amount: DelegatedAmount::Amount(output.delegated_amount), delegation_id: output.delegation_id, validator_address: *output.validator_address.as_account(), start_epoch: output.start_epoch, diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index cb72eb4fda..1b6fb06a25 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -97,6 +97,20 @@ impl FoundryOutputBuilder { Self::new(OutputBuilderAmount::Amount(amount), serial_number, token_scheme) } + /// Creates a [`FoundryOutputBuilder`] with a provided amount, unless it is below the minimum. + pub fn new_with_amount_or_minimum( + amount: u64, + serial_number: u32, + token_scheme: TokenScheme, + params: StorageScoreParameters, + ) -> Self { + Self::new( + OutputBuilderAmount::AmountOrMinimum(amount, params), + serial_number, + token_scheme, + ) + } + /// Creates a [`FoundryOutputBuilder`] with provided storage score parameters. /// The amount will be set to the minimum required amount of the resulting output. pub fn new_with_minimum_amount( @@ -125,6 +139,13 @@ impl FoundryOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = OutputBuilderAmount::AmountOrMinimum(amount, params); + self + } + /// Sets the amount to the minimum required amount. #[inline(always)] pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { @@ -265,6 +286,7 @@ impl FoundryOutputBuilder { output.amount = match self.amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }; diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index 15c70edc7f..d9ac7fe6c7 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -74,6 +74,7 @@ pub const OUTPUT_INDEX_RANGE: RangeInclusive = 0..=OUTPUT_INDEX_MAX; // [0. #[derive(Copy, Clone)] pub enum OutputBuilderAmount { Amount(u64), + AmountOrMinimum(u64, StorageScoreParameters), MinimumAmount(StorageScoreParameters), } diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 2bf6e318e1..73e31b0d56 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -72,6 +72,11 @@ impl NftOutputBuilder { Self::new(OutputBuilderAmount::Amount(amount), nft_id) } + /// Creates an [`NftOutputBuilder`] with a provided amount, unless it is below the minimum. + pub fn new_with_amount_or_minimum(amount: u64, nft_id: NftId, params: StorageScoreParameters) -> Self { + Self::new(OutputBuilderAmount::AmountOrMinimum(amount, params), nft_id) + } + /// Creates an [`NftOutputBuilder`] with provided storage score parameters. /// The amount will be set to the minimum required amount of the resulting output. pub fn new_with_minimum_amount(params: StorageScoreParameters, nft_id: NftId) -> Self { @@ -96,6 +101,13 @@ impl NftOutputBuilder { self } + /// Sets the amount to the provided value, unless it is below the minimum. + #[inline(always)] + pub fn with_amount_or_minimum(mut self, amount: u64, params: StorageScoreParameters) -> Self { + self.amount = OutputBuilderAmount::AmountOrMinimum(amount, params); + self + } + /// Sets the amount to the minimum required amount. #[inline(always)] pub fn with_minimum_amount(mut self, params: StorageScoreParameters) -> Self { @@ -245,6 +257,7 @@ impl NftOutputBuilder { self } } + OutputBuilderAmount::AmountOrMinimum(_, _) => self, OutputBuilderAmount::MinimumAmount(_) => self, }) } @@ -275,6 +288,7 @@ impl NftOutputBuilder { output.amount = match self.amount { OutputBuilderAmount::Amount(amount) => amount, + OutputBuilderAmount::AmountOrMinimum(amount, params) => output.minimum_amount(params).max(amount), OutputBuilderAmount::MinimumAmount(params) => output.minimum_amount(params), }; diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index a18126ccbd..71989a3b20 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -2142,3 +2142,115 @@ fn implicit_account_transition() { // One remainder Mana assert_eq!(selected.transaction.outputs()[0].mana(), 1); } + +#[test] +fn auto_transition_account_less_than_min() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let small_amount = 5; + + let inputs = build_inputs( + [( + Account { + amount: small_amount, + account_id: account_id_1, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + issuer: None, + }, + None, + )], + Some(SLOT_INDEX), + ); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap_err(); + + let min_amount = AccountOutputBuilder::from(inputs[0].output.as_account()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + + assert_eq!( + selected, + Error::InsufficientAmount { + found: small_amount, + required: min_amount + }, + ); +} + +#[test] +fn auto_transition_account_less_than_min_additional() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let small_amount = 5; + + let inputs = build_inputs( + [ + ( + Account { + amount: small_amount, + account_id: account_id_1, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + issuer: None, + }, + None, + ), + ( + Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + native_token: None, + sdruc: None, + timelock: None, + expiration: None, + }, + None, + ), + ], + Some(SLOT_INDEX), + ); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + let min_amount = AccountOutputBuilder::from(inputs[0].output.as_account()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + let account_output = selected + .transaction + .outputs() + .iter() + .filter_map(Output::as_account_opt) + .find(|o| o.account_id() == &account_id_1) + .unwrap(); + assert_eq!(account_output.amount(), min_amount); +} diff --git a/sdk/tests/client/input_selection/foundry_outputs.rs b/sdk/tests/client/input_selection/foundry_outputs.rs index 2f64dc8b17..6969fac375 100644 --- a/sdk/tests/client/input_selection/foundry_outputs.rs +++ b/sdk/tests/client/input_selection/foundry_outputs.rs @@ -11,8 +11,8 @@ use iota_sdk::{ types::block::{ address::{AccountAddress, Address}, output::{ - unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, FoundryId, Output, - SimpleTokenScheme, TokenId, + unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, FoundryId, FoundryOutputBuilder, + Output, SimpleTokenScheme, TokenId, }, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, @@ -1226,3 +1226,157 @@ fn melt_and_burn_native_tokens() { } }); } + +#[test] +fn auto_transition_foundry_less_than_min() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + let foundry_id = FoundryId::build(&AccountAddress::from(account_id), 1, SimpleTokenScheme::KIND); + let token_id = TokenId::from(foundry_id); + + let small_amount_foundry = 5; + let small_amount_account = 10; + + let mut inputs = build_inputs( + [( + Foundry { + amount: small_amount_foundry, + account_id, + serial_number: 1, + token_scheme: SimpleTokenScheme::new(1000, 0, 1000).unwrap(), + native_token: Some((&token_id.to_string(), 1000)), + }, + None, + )], + Some(SLOT_INDEX), + ); + let account_output = AccountOutputBuilder::new_with_amount(small_amount_account, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_foundry_counter(1) + .finish_output() + .unwrap(); + inputs.push(InputSigningData { + output: account_output, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap_err(); + + let min_amount = FoundryOutputBuilder::from(inputs[0].output.as_foundry()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount() + + AccountOutputBuilder::from(inputs[1].output.as_account()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + + assert_eq!( + selected, + Error::InsufficientAmount { + found: small_amount_foundry + small_amount_account, + required: min_amount + }, + ); +} + +#[test] +fn auto_transition_foundry_less_than_min_additional() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + let foundry_id = FoundryId::build(&AccountAddress::from(account_id), 1, SimpleTokenScheme::KIND); + let token_id = TokenId::from(foundry_id); + + let small_amount = 5; + + let mut inputs = build_inputs( + [ + ( + Foundry { + amount: small_amount, + account_id, + serial_number: 1, + token_scheme: SimpleTokenScheme::new(1000, 0, 1000).unwrap(), + native_token: Some((&token_id.to_string(), 1000)), + }, + None, + ), + ( + Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + native_token: None, + sdruc: None, + timelock: None, + expiration: None, + }, + None, + ), + ], + Some(SLOT_INDEX), + ); + let account_output = AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_foundry_counter(1) + .finish_output() + .unwrap(); + inputs.push(InputSigningData { + output: account_output, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + let min_amount_foundry = FoundryOutputBuilder::from(inputs[0].output.as_foundry()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + let foundry_output = selected + .transaction + .outputs() + .iter() + .filter_map(Output::as_foundry_opt) + .find(|o| o.id() == foundry_id) + .unwrap(); + let account_output = selected + .transaction + .outputs() + .iter() + .filter_map(Output::as_account_opt) + .find(|o| o.account_id() == &account_id) + .unwrap(); + assert_eq!(foundry_output.amount(), min_amount_foundry); + assert_eq!(account_output.amount(), 1_000_000); +} diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index 5e3cc3f1ea..59e9ea7154 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -1427,3 +1427,119 @@ fn changed_immutable_metadata() { ))) if nft_id == nft_id_1 )); } + +#[test] +fn auto_transition_nft_less_than_min() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let nft_id_1 = NftId::from_str(NFT_ID_1).unwrap(); + + let small_amount = 5; + + let inputs = build_inputs( + [( + Nft { + amount: small_amount, + nft_id: nft_id_1, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + issuer: None, + sdruc: None, + expiration: None, + }, + None, + )], + Some(SLOT_INDEX), + ); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap_err(); + + let min_amount = NftOutputBuilder::from(inputs[0].output.as_nft()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + + assert_eq!( + selected, + Error::InsufficientAmount { + found: small_amount, + required: min_amount + }, + ); +} + +#[test] +fn auto_transition_nft_less_than_min_additional() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let nft_id_1 = NftId::from_str(NFT_ID_1).unwrap(); + + let small_amount = 5; + + let inputs = build_inputs( + [ + ( + Nft { + amount: small_amount, + nft_id: nft_id_1, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + issuer: None, + sdruc: None, + expiration: None, + }, + None, + ), + ( + Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + sender: None, + native_token: None, + sdruc: None, + timelock: None, + expiration: None, + }, + None, + ), + ], + Some(SLOT_INDEX), + ); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .with_required_inputs([*inputs[0].output_id()]) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + let min_amount = NftOutputBuilder::from(inputs[0].output.as_nft()) + .with_minimum_amount(protocol_parameters.storage_score_parameters()) + .finish_output() + .unwrap() + .amount(); + let nft_output = selected + .transaction + .outputs() + .iter() + .filter_map(Output::as_nft_opt) + .find(|o| o.nft_id() == &nft_id_1) + .unwrap(); + assert_eq!(nft_output.amount(), min_amount); +}