diff --git a/bindings/core/src/method_handler/wallet.rs b/bindings/core/src/method_handler/wallet.rs index 2e69c06e15..17979f26e6 100644 --- a/bindings/core/src/method_handler/wallet.rs +++ b/bindings/core/src/method_handler/wallet.rs @@ -417,7 +417,6 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM &wallet.client().get_protocol_parameters().await?, )?, None, - None, ) .await?; Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) @@ -438,7 +437,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM &wallet.client().get_protocol_parameters().await?, )?; let transaction = wallet - .submit_and_store_transaction(signed_transaction_data, None, None) + .submit_and_store_transaction(signed_transaction_data, None) .await?; Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) } diff --git a/bindings/nodejs/examples/how_tos/account_output/send-amount.ts b/bindings/nodejs/examples/how_tos/account_output/send-amount.ts index 18a094b266..bf046dc034 100644 --- a/bindings/nodejs/examples/how_tos/account_output/send-amount.ts +++ b/bindings/nodejs/examples/how_tos/account_output/send-amount.ts @@ -71,7 +71,7 @@ async function run() { }, ]; const options = { - mandatoryInputs: [input], + requiredInputs: [input], allowMicroAmount: false, }; const transaction = await wallet.sendWithParams(params, options); diff --git a/bindings/nodejs/lib/types/wallet/transaction-options.ts b/bindings/nodejs/lib/types/wallet/transaction-options.ts index 3f88d2b72d..5ce1e5b2a9 100644 --- a/bindings/nodejs/lib/types/wallet/transaction-options.ts +++ b/bindings/nodejs/lib/types/wallet/transaction-options.ts @@ -1,7 +1,13 @@ // Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { AccountAddress, AccountId, Bech32Address, OutputId } from '../block'; +import { + AccountAddress, + AccountId, + Bech32Address, + ContextInput, + OutputId, +} from '../block'; import { TaggedDataPayload } from '../block/payload/tagged'; import { Burn } from '../client'; import { u256, HexEncodedString, NumericString, u64 } from '../utils'; @@ -13,20 +19,24 @@ export interface TransactionOptions { remainderValueStrategy?: RemainderValueStrategy; /** An optional tagged data payload. */ taggedDataPayload?: TaggedDataPayload; - /** - * Custom inputs that should be used for the transaction. - * If custom inputs are provided, only those are used. - * If also other additional inputs should be used, `mandatoryInputs` should be used instead. - */ - customInputs?: OutputId[]; + /** Transaction context inputs to include. */ + contextInputs?: ContextInput[]; /** Inputs that must be used for the transaction. */ - mandatoryInputs?: OutputId[]; + requiredInputs?: OutputId[]; /** Specifies what needs to be burned during input selection. */ burn?: Burn; /** Optional note, that is only stored locally. */ note?: string; /** Whether to allow sending a micro amount. */ allowMicroAmount?: boolean; + /** Whether to allow the selection of additional inputs for this transaction. */ + allowAdditionalInputSelection?: boolean; + /** Transaction capabilities. */ + capabilities?: HexEncodedString; + /** Mana allotments for the transaction. */ + manaAllotments?: { [account_id: AccountId]: u64 }; + /** Optional block issuer to which the transaction will have required mana allotted. */ + issuerId?: AccountId; } /** The possible remainder value strategies. */ diff --git a/bindings/python/examples/how_tos/account_output/send_amount.py b/bindings/python/examples/how_tos/account_output/send_amount.py index d844cadbd3..d41e4edee8 100644 --- a/bindings/python/examples/how_tos/account_output/send_amount.py +++ b/bindings/python/examples/how_tos/account_output/send_amount.py @@ -40,7 +40,7 @@ amount=1000000, )] options = { - 'mandatoryInputs': inputs, + 'requiredInputs': inputs, } transaction = wallet.send_with_params(params, options) wallet.wait_for_transaction_acceptance( diff --git a/bindings/python/iota_sdk/types/transaction_options.py b/bindings/python/iota_sdk/types/transaction_options.py index c15b1ba006..9ae1002a2a 100644 --- a/bindings/python/iota_sdk/types/transaction_options.py +++ b/bindings/python/iota_sdk/types/transaction_options.py @@ -1,11 +1,12 @@ -# Copyright 2023 IOTA Stiftung +# Copyright 2024 IOTA Stiftung # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Optional, List, Union from dataclasses import dataclass from iota_sdk.types.burn import Burn -from iota_sdk.types.common import json +from iota_sdk.types.common import HexStr, json +from iota_sdk.types.context_input import ContextInput from iota_sdk.types.output_id import OutputId from iota_sdk.types.payload import TaggedDataPayload @@ -68,26 +69,38 @@ class TransactionOptions: Attributes: remainder_value_strategy: The strategy applied for base coin remainders. tagged_data_payload: An optional tagged data payload. - custom_inputs: If custom inputs are provided only those are used. If also other additional inputs should be used, `mandatory_inputs` should be used instead. - mandatory_inputs: Inputs that must be used for the transaction. + context_inputs: Transaction context inputs to include. + required_inputs: Inputs that must be used for the transaction. burn: Specifies what needs to be burned during input selection. note: A string attached to the transaction. allow_micro_amount: Whether to allow sending a micro amount. + allow_additional_input_selection: Whether to allow the selection of additional inputs for this transaction. + capabilities: Transaction capabilities. + mana_allotments: Mana allotments for the transaction. + issuer_id: Optional block issuer to which the transaction will have required mana allotted. """ def __init__(self, remainder_value_strategy: Optional[Union[RemainderValueStrategy, RemainderValueStrategyCustomAddress]] = None, tagged_data_payload: Optional[TaggedDataPayload] = None, - custom_inputs: Optional[List[OutputId]] = None, - mandatory_inputs: Optional[List[OutputId]] = None, + context_inputs: Optional[List[ContextInput]] = None, + required_inputs: Optional[List[OutputId]] = None, burn: Optional[Burn] = None, note: Optional[str] = None, - allow_micro_amount: Optional[bool] = None): + allow_micro_amount: Optional[bool] = None, + allow_additional_input_selection: Optional[bool] = None, + capabilities: Optional[HexStr] = None, + mana_allotments: Optional[dict[HexStr, int]] = None, + issuer_id: Optional[HexStr] = None): """Initialize transaction options. """ self.remainder_value_strategy = remainder_value_strategy self.tagged_data_payload = tagged_data_payload - self.custom_inputs = custom_inputs - self.mandatory_inputs = mandatory_inputs + self.context_inputs = context_inputs + self.required_inputs = required_inputs self.burn = burn self.note = note self.allow_micro_amount = allow_micro_amount + self.allow_additional_input_selection = allow_additional_input_selection + self.capabilities = capabilities + self.mana_allotments = mana_allotments + self.issuer_id = issuer_id diff --git a/sdk/examples/how_tos/account_output/send_amount.rs b/sdk/examples/how_tos/account_output/send_amount.rs index 801d8b07ca..9388d31f71 100644 --- a/sdk/examples/how_tos/account_output/send_amount.rs +++ b/sdk/examples/how_tos/account_output/send_amount.rs @@ -1,4 +1,4 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! In this example we use an account as wallet. @@ -66,7 +66,7 @@ async fn main() -> Result<()> { 1_000_000, "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu", TransactionOptions { - mandatory_inputs: Some(vec![input]), + required_inputs: [input].into(), ..Default::default() }, ) diff --git a/sdk/examples/wallet/offline_signing/3_send_transaction.rs b/sdk/examples/wallet/offline_signing/3_send_transaction.rs index 8a49165ef3..aaaef1c8a1 100644 --- a/sdk/examples/wallet/offline_signing/3_send_transaction.rs +++ b/sdk/examples/wallet/offline_signing/3_send_transaction.rs @@ -42,7 +42,7 @@ async fn main() -> Result<()> { // Sends offline signed transaction online. let transaction = wallet - .submit_and_store_transaction(signed_transaction_data, None, None) + .submit_and_store_transaction(signed_transaction_data, None) .await?; wait_for_inclusion(&transaction.transaction_id, &wallet).await?; diff --git a/sdk/src/client/api/block_builder/input_selection/error.rs b/sdk/src/client/api/block_builder/input_selection/error.rs index 409b606802..bd0c302a87 100644 --- a/sdk/src/client/api/block_builder/input_selection/error.rs +++ b/sdk/src/client/api/block_builder/input_selection/error.rs @@ -1,4 +1,4 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! Error handling for input selection. @@ -14,6 +14,8 @@ use crate::types::block::output::{ChainId, OutputId, TokenId}; #[derive(Debug, Eq, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum Error { + #[error("additional inputs required for {0:?}, but additional input selection is disabled")] + AdditionalInputsRequired(Requirement), /// Block error. #[error("{0}")] Block(#[from] crate::types::block::Error), @@ -30,6 +32,14 @@ pub enum Error { /// The required amount. required: u64, }, + /// Insufficient mana provided. + #[error("insufficient mana: found {found}, required {required}")] + InsufficientMana { + /// The amount found. + found: u64, + /// The required amount. + required: u64, + }, /// Insufficient native token amount provided. #[error("insufficient native token amount: found {found}, required {required}")] InsufficientNativeTokenAmount { 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 617f7b8314..5a827c72d7 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! Input selection for transactions @@ -9,6 +9,7 @@ pub(crate) mod remainder; pub(crate) mod requirement; pub(crate) mod transition; +use alloc::collections::BTreeMap; use core::ops::Deref; use std::collections::{HashMap, HashSet}; @@ -17,17 +18,25 @@ use packable::PackableExt; use self::requirement::account::is_account_with_id; pub use self::{burn::Burn, error::Error, requirement::Requirement}; use crate::{ - client::{api::types::RemainderData, secret::types::InputSigningData}, + client::{ + api::{PreparedTransactionData, RemainderData}, + secret::types::InputSigningData, + }, types::block::{ address::{AccountAddress, Address, NftAddress}, - input::INPUT_COUNT_RANGE, + context_input::ContextInput, + input::{Input, UtxoInput, INPUT_COUNT_RANGE}, mana::ManaAllotment, output::{ - AccountOutput, ChainId, FoundryOutput, NativeTokensBuilder, NftOutput, Output, OutputId, OUTPUT_COUNT_RANGE, + AccountId, AccountOutput, AccountOutputBuilder, AnchorOutputBuilder, BasicOutputBuilder, FoundryOutput, + NativeTokensBuilder, NftOutput, NftOutputBuilder, Output, OutputId, OUTPUT_COUNT_RANGE, + }, + payload::{ + signed_transaction::{Transaction, TransactionCapabilities}, + TaggedDataPayload, }, - payload::signed_transaction::TransactionCapabilities, protocol::{CommittableAgeRange, ProtocolParameters}, - slot::SlotIndex, + slot::{SlotCommitmentId, SlotIndex}, }, }; @@ -38,80 +47,114 @@ pub struct InputSelection { required_inputs: HashSet, forbidden_inputs: HashSet, selected_inputs: Vec, - outputs: Vec, + context_inputs: HashSet, + provided_outputs: Vec, + added_outputs: Vec, addresses: HashSet
, burn: Option, - remainder_address: Option
, - protocol_parameters: ProtocolParameters, - slot_index: SlotIndex, + remainders: Remainders, + creation_slot: SlotIndex, + latest_slot_commitment_id: SlotCommitmentId, requirements: Vec, - automatically_transitioned: HashSet, - mana_allotments: u64, + min_mana_allotment: Option, + mana_allotments: BTreeMap, mana_rewards: HashMap, + payload: Option, + allow_additional_input_selection: bool, + transaction_capabilities: TransactionCapabilities, + protocol_parameters: ProtocolParameters, } -/// Result of the input selection algorithm. -#[derive(Clone, Debug)] -pub struct Selected { - /// Selected inputs. - pub inputs: Vec, - /// Provided and created outputs. - pub outputs: Vec, - /// Remainder outputs information. - pub remainders: Vec, - /// Mana rewards by input. - pub mana_rewards: HashMap, +/// Account and RMC for automatic mana allotment +#[derive(Copy, Clone, Debug)] +pub(crate) struct MinManaAllotment { + issuer_id: AccountId, + reference_mana_cost: u64, + allotment_debt: u64, } -impl InputSelection { - fn required_account_nft_addresses(&self, input: &InputSigningData) -> Result, Error> { - let required_address = input - .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? - .expect("expiration unlockable outputs already filtered out"); - - let required_address = if let Address::Restricted(restricted) = &required_address { - restricted.address() - } else { - &required_address - }; +#[derive(Clone, Debug, Default)] +pub(crate) struct Remainders { + address: Option
, + data: Vec, + storage_deposit_returns: Vec, + added_mana: u64, +} - match required_address { - Address::Account(account_address) => Ok(Some(Requirement::Account(*account_address.account_id()))), - Address::Nft(nft_address) => Ok(Some(Requirement::Nft(*nft_address.nft_id()))), - _ => Ok(None), - } - } +impl InputSelection { + /// Creates a new [`InputSelection`]. + pub fn new( + available_inputs: impl IntoIterator, + outputs: impl IntoIterator, + addresses: impl IntoIterator, + creation_slot_index: impl Into, + latest_slot_commitment_id: SlotCommitmentId, + protocol_parameters: ProtocolParameters, + ) -> Self { + let available_inputs = available_inputs.into_iter().collect::>(); - fn select_input(&mut self, input: InputSigningData) -> Result<(), Error> { - log::debug!("Selecting input {:?}", input.output_id()); + let mut addresses = HashSet::from_iter(addresses.into_iter().map(|a| { + // Get a potential Ed25519 address directly since we're only interested in that + #[allow(clippy::option_if_let_else)] // clippy's suggestion requires a clone + if let Some(address) = a.backing_ed25519() { + Address::Ed25519(*address) + } else { + a + } + })); - if let Some(output) = self.transition_input(&input)? { - // No need to check for `outputs_requirements` because - // - the sender feature doesn't need to be verified as it has been removed - // - the issuer feature doesn't need to be verified as the chain is not new - // - input doesn't need to be checked for as we just transitioned it - // - foundry account requirement should have been met already by a prior `required_account_nft_addresses` - self.outputs.push(output); - } + addresses.extend(available_inputs.iter().filter_map(|input| match &input.output { + Output::Account(output) => Some(Address::Account(AccountAddress::from( + output.account_id_non_null(input.output_id()), + ))), + Output::Nft(output) => Some(Address::Nft(NftAddress::from( + output.nft_id_non_null(input.output_id()), + ))), + _ => None, + })); - if let Some(requirement) = self.required_account_nft_addresses(&input)? { - log::debug!("Adding {requirement:?} from input {:?}", input.output_id()); - self.requirements.push(requirement); + Self { + available_inputs, + required_inputs: HashSet::new(), + forbidden_inputs: HashSet::new(), + selected_inputs: Vec::new(), + context_inputs: HashSet::new(), + provided_outputs: outputs.into_iter().collect(), + added_outputs: Vec::new(), + addresses, + burn: None, + remainders: Default::default(), + creation_slot: creation_slot_index.into(), + latest_slot_commitment_id, + requirements: Vec::new(), + min_mana_allotment: None, + mana_allotments: Default::default(), + mana_rewards: Default::default(), + allow_additional_input_selection: true, + transaction_capabilities: Default::default(), + payload: None, + protocol_parameters, } - - self.selected_inputs.push(input); - - Ok(()) } fn init(&mut self) -> Result<(), Error> { - // Adds an initial mana requirement. - self.requirements.push(Requirement::Mana); - // Adds an initial amount requirement. - self.requirements.push(Requirement::Amount); - // Adds an initial native tokens requirement. - self.requirements.push(Requirement::NativeTokens); + // If automatic min mana allotment is enabled, we need to initialize the allotment debt. + if let Some(MinManaAllotment { + issuer_id, + allotment_debt, + .. + }) = self.min_mana_allotment.as_mut() + { + // Add initial debt from any passed-in allotments + *allotment_debt = self.mana_allotments.get(issuer_id).copied().unwrap_or_default(); + } + // Add initial requirements + self.requirements.extend([ + Requirement::Mana, + Requirement::ContextInputs, + Requirement::Amount, + Requirement::NativeTokens, + ]); // Removes forbidden inputs from available inputs. self.available_inputs @@ -153,64 +196,176 @@ impl InputSelection { Ok(()) } - /// Creates a new [`InputSelection`]. - pub fn new( - available_inputs: impl Into>, - outputs: impl Into>, - addresses: impl IntoIterator, - slot_index: impl Into, - protocol_parameters: ProtocolParameters, - ) -> Self { - let available_inputs = available_inputs.into(); + /// Selects inputs that meet the requirements of the outputs to satisfy the semantic validation of the overall + /// transaction. Also creates a remainder output and chain transition outputs if required. + pub fn select(mut self) -> Result { + if !OUTPUT_COUNT_RANGE.contains(&(self.provided_outputs.len() as u16)) { + // If burn or mana allotments are provided, outputs will be added later, in the other cases it will just + // create remainder outputs. + if !(self.provided_outputs.is_empty() + && (self.burn.is_some() || !self.mana_allotments.is_empty() || !self.required_inputs.is_empty())) + { + return Err(Error::InvalidOutputCount(self.provided_outputs.len())); + } + } - let mut addresses = HashSet::from_iter(addresses.into_iter().map(|a| { - // Get a potential Ed25519 address directly since we're only interested in that - #[allow(clippy::option_if_let_else)] // clippy's suggestion requires a clone - if let Some(address) = a.backing_ed25519() { - Address::Ed25519(*address) - } else { - a + self.filter_inputs(); + + if self.available_inputs.is_empty() { + return Err(Error::NoAvailableInputsProvided); + } + + // Creates the initial state, selected inputs and requirements, based on the provided outputs. + self.init()?; + + // Process all the requirements until there are no more. + while let Some(requirement) = self.requirements.pop() { + // Fulfill the requirement. + let inputs = self.fulfill_requirement(&requirement)?; + + if !self.allow_additional_input_selection && !inputs.is_empty() { + return Err(Error::AdditionalInputsRequired(requirement)); } - })); - addresses.extend(available_inputs.iter().filter_map(|input| match &input.output { - Output::Account(output) => Some(Address::Account(AccountAddress::from( - output.account_id_non_null(input.output_id()), - ))), - Output::Nft(output) => Some(Address::Nft(NftAddress::from( - output.nft_id_non_null(input.output_id()), - ))), - _ => None, - })); + // Select suggested inputs. + for input in inputs { + self.select_input(input)?; + } + } - Self { - available_inputs, - required_inputs: HashSet::new(), - forbidden_inputs: HashSet::new(), - selected_inputs: Vec::new(), - outputs: outputs.into(), - addresses, - burn: None, - remainder_address: None, - protocol_parameters, - // Should be set from a commitment context input - slot_index: slot_index.into(), - requirements: Vec::new(), - automatically_transitioned: HashSet::new(), - mana_allotments: 0, - mana_rewards: Default::default(), + // If there is no min allotment calculation, then we should update the remainders as the last step + if self.min_mana_allotment.is_none() { + self.update_remainders()?; + } + + if !INPUT_COUNT_RANGE.contains(&(self.selected_inputs.len() as u16)) { + return Err(Error::InvalidInputCount(self.selected_inputs.len())); } + + if self.remainders.added_mana > 0 { + let remainder_address = self + .get_remainder_address()? + .ok_or(Error::MissingInputWithEd25519Address)? + .0; + let added_mana = self.remainders.added_mana; + if let Some(output) = self.get_output_for_added_mana(&remainder_address) { + log::debug!("Adding {added_mana} excess input mana to output with address {remainder_address}"); + let new_mana = output.mana() + added_mana; + *output = match output { + Output::Basic(b) => BasicOutputBuilder::from(&*b).with_mana(new_mana).finish_output()?, + Output::Account(a) => AccountOutputBuilder::from(&*a).with_mana(new_mana).finish_output()?, + Output::Anchor(a) => AnchorOutputBuilder::from(&*a).with_mana(new_mana).finish_output()?, + Output::Nft(n) => NftOutputBuilder::from(&*n).with_mana(new_mana).finish_output()?, + _ => unreachable!(), + }; + } + } + + let outputs = self + .provided_outputs + .into_iter() + .chain(self.added_outputs) + .chain(self.remainders.storage_deposit_returns) + .chain(self.remainders.data.iter().map(|r| r.output.clone())) + .collect::>(); + + // Check again, because more outputs may have been added. + if !OUTPUT_COUNT_RANGE.contains(&(outputs.len() as u16)) { + return Err(Error::InvalidOutputCount(outputs.len())); + } + + Self::validate_transitions(&self.selected_inputs, &outputs)?; + + for output_id in self.mana_rewards.keys() { + if !self.selected_inputs.iter().any(|i| output_id == i.output_id()) { + return Err(Error::ExtraManaRewards(*output_id)); + } + } + + let inputs_data = Self::sort_input_signing_data( + self.selected_inputs, + self.creation_slot, + self.protocol_parameters.committable_age_range(), + )?; + + let mut inputs: Vec = Vec::new(); + + for input in &inputs_data { + inputs.push(Input::Utxo(UtxoInput::from(*input.output_id()))); + } + + let mana_allotments = self + .mana_allotments + .into_iter() + .map(|(account_id, mana)| ManaAllotment::new(account_id, mana)) + .collect::, _>>()?; + + // Build transaction + + let mut builder = Transaction::builder(self.protocol_parameters.network_id()) + .with_inputs(inputs) + .with_outputs(outputs) + .with_mana_allotments(mana_allotments) + .with_context_inputs(self.context_inputs) + .with_creation_slot(self.creation_slot) + .with_capabilities(self.transaction_capabilities); + + if let Some(payload) = self.payload { + builder = builder.with_payload(payload); + } + + let transaction = builder.finish_with_params(&self.protocol_parameters)?; + + Ok(PreparedTransactionData { + transaction, + inputs_data, + remainders: self.remainders.data, + mana_rewards: self.mana_rewards.into_iter().collect(), + }) + } + + fn select_input(&mut self, input: InputSigningData) -> Result<(), Error> { + log::debug!("Selecting input {:?}", input.output_id()); + + if let Some(output) = self.transition_input(&input)? { + // No need to check for `outputs_requirements` because + // - the sender feature doesn't need to be verified as it has been removed + // - the issuer feature doesn't need to be verified as the chain is not new + // - input doesn't need to be checked for as we just transitioned it + // - foundry account requirement should have been met already by a prior `required_account_nft_addresses` + self.added_outputs.push(output); + } + + if let Some(requirement) = self.required_account_nft_addresses(&input)? { + log::debug!("Adding {requirement:?} from input {:?}", input.output_id()); + self.requirements.push(requirement); + } + + self.selected_inputs.push(input); + + // New inputs/outputs may need context inputs + if !self.requirements.contains(&Requirement::ContextInputs) { + self.requirements.push(Requirement::ContextInputs); + } + + Ok(()) } /// Sets the required inputs of an [`InputSelection`]. - pub fn with_required_inputs(mut self, inputs: impl Into>) -> Self { - self.required_inputs = inputs.into(); + pub fn with_required_inputs(mut self, inputs: impl IntoIterator) -> Self { + self.required_inputs = inputs.into_iter().collect(); self } /// Sets the forbidden inputs of an [`InputSelection`]. - pub fn with_forbidden_inputs(mut self, inputs: HashSet) -> Self { - self.forbidden_inputs = inputs; + pub fn with_forbidden_inputs(mut self, inputs: impl IntoIterator) -> Self { + self.forbidden_inputs = inputs.into_iter().collect(); + self + } + + /// Sets the context inputs of an [`InputSelection`]. + pub fn with_context_inputs(mut self, context_inputs: impl IntoIterator) -> Self { + self.context_inputs = context_inputs.into_iter().collect(); self } @@ -222,13 +377,13 @@ impl InputSelection { /// Sets the remainder address of an [`InputSelection`]. pub fn with_remainder_address(mut self, address: impl Into>) -> Self { - self.remainder_address = address.into(); + self.remainders.address = address.into(); self } - /// Sets the mana allotments sum of an [`InputSelection`]. - pub fn with_mana_allotments<'a>(mut self, mana_allotments: impl Iterator) -> Self { - self.mana_allotments = mana_allotments.map(ManaAllotment::mana).sum(); + /// Sets the mana allotments of an [`InputSelection`]. + pub fn with_mana_allotments(mut self, mana_allotments: impl IntoIterator) -> Self { + self.mana_allotments = mana_allotments.into_iter().collect(); self } @@ -244,6 +399,69 @@ impl InputSelection { self } + /// Add a transaction data payload. + pub fn with_payload(mut self, payload: impl Into>) -> Self { + self.payload = payload.into(); + self + } + + /// Specifies an account to which the minimum required mana allotment will be added. + pub fn with_min_mana_allotment(mut self, account_id: AccountId, reference_mana_cost: u64) -> Self { + self.min_mana_allotment.replace(MinManaAllotment { + issuer_id: account_id, + reference_mana_cost, + allotment_debt: 0, + }); + self + } + + /// Disables selecting additional inputs. + pub fn disable_additional_input_selection(mut self) -> Self { + self.allow_additional_input_selection = false; + self + } + + /// Sets the transaction capabilities. + pub fn with_transaction_capabilities( + mut self, + transaction_capabilities: impl Into, + ) -> Self { + self.transaction_capabilities = transaction_capabilities.into(); + self + } + + pub(crate) fn all_outputs(&self) -> impl Iterator { + self.non_remainder_outputs() + .chain(self.remainders.data.iter().map(|r| &r.output)) + .chain(&self.remainders.storage_deposit_returns) + } + + pub(crate) fn non_remainder_outputs(&self) -> impl Iterator { + self.provided_outputs.iter().chain(&self.added_outputs) + } + + fn required_account_nft_addresses(&self, input: &InputSigningData) -> Result, Error> { + let required_address = input + .output + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + )? + .expect("expiration unlockable outputs already filtered out"); + + let required_address = if let Address::Restricted(restricted) = &required_address { + restricted.address() + } else { + &required_address + }; + + match required_address { + Address::Account(account_address) => Ok(Some(Requirement::Account(*account_address.account_id()))), + Address::Nft(nft_address) => Ok(Some(Requirement::Nft(*nft_address.nft_id()))), + _ => Ok(None), + } + } + fn filter_inputs(&mut self) { self.available_inputs.retain(|input| { // TODO what about other kinds? @@ -267,14 +485,20 @@ impl InputSelection { // PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out. let unlock_conditions = input.output.unlock_conditions().unwrap(); - if unlock_conditions.is_timelocked(self.slot_index, self.protocol_parameters.min_committable_age()) { + if unlock_conditions.is_timelocked( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.min_committable_age(), + ) { return false; } let required_address = input .output // Account transition is irrelevant here as we keep accounts anyway. - .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + ) // PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out. .unwrap(); @@ -306,7 +530,7 @@ impl InputSelection { // Inputs need to be sorted before signing, because the reference unlock conditions can only reference a lower index pub(crate) fn sort_input_signing_data( mut inputs: Vec, - slot_index: SlotIndex, + commitment_slot_index: SlotIndex, committable_age_range: CommittableAgeRange, ) -> Result, Error> { // initially sort by output to make it deterministic @@ -318,7 +542,7 @@ impl InputSelection { inputs.into_iter().partition(|input_signing_data| { let required_address = input_signing_data .output - .required_address(slot_index, committable_age_range) + .required_address(commitment_slot_index, committable_age_range) // PANIC: safe to unwrap as non basic/account/foundry/nft outputs are already filtered out. .unwrap() .expect("expiration unlockable outputs already filtered out"); @@ -329,7 +553,7 @@ impl InputSelection { for input in account_nft_address_inputs { let required_address = input .output - .required_address(slot_index, committable_age_range)? + .required_address(commitment_slot_index, committable_age_range)? .expect("expiration unlockable outputs already filtered out"); match sorted_inputs @@ -373,7 +597,7 @@ impl InputSelection { match sorted_inputs.iter().position(|input_signing_data| { let required_address = input_signing_data .output - .required_address(slot_index, committable_age_range) + .required_address(commitment_slot_index, committable_age_range) // PANIC: safe to unwrap as non basic/alias/foundry/nft outputs are already filtered .unwrap() .expect("expiration unlockable outputs already filtered out"); @@ -398,74 +622,7 @@ impl InputSelection { Ok(sorted_inputs) } - /// Selects inputs that meet the requirements of the outputs to satisfy the semantic validation of the overall - /// transaction. Also creates a remainder output and chain transition outputs if required. - pub fn select(mut self) -> Result { - if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) { - // If burn or mana allotments are provided, outputs will be added later, in the other cases it will just - // create remainder outputs. - if !(self.outputs.is_empty() - && (self.burn.is_some() || self.mana_allotments != 0 || !self.required_inputs.is_empty())) - { - return Err(Error::InvalidOutputCount(self.outputs.len())); - } - } - - self.filter_inputs(); - - if self.available_inputs.is_empty() { - return Err(Error::NoAvailableInputsProvided); - } - - // Creates the initial state, selected inputs and requirements, based on the provided outputs. - self.init()?; - - // Process all the requirements until there are no more. - while let Some(requirement) = self.requirements.pop() { - // Fulfill the requirement. - let inputs = self.fulfill_requirement(requirement)?; - - // Select suggested inputs. - for input in inputs { - self.select_input(input)?; - } - } - - if !INPUT_COUNT_RANGE.contains(&(self.selected_inputs.len() as u16)) { - return Err(Error::InvalidInputCount(self.selected_inputs.len())); - } - - let (storage_deposit_returns, remainders) = self.storage_deposit_returns_and_remainders()?; - - self.outputs.extend(storage_deposit_returns); - self.outputs.extend(remainders.iter().map(|r| r.output.clone())); - - // Check again, because more outputs may have been added. - if !OUTPUT_COUNT_RANGE.contains(&(self.outputs.len() as u16)) { - return Err(Error::InvalidOutputCount(self.outputs.len())); - } - - self.validate_transitions()?; - - for output_id in self.mana_rewards.keys() { - if !self.selected_inputs.iter().any(|i| output_id == i.output_id()) { - return Err(Error::ExtraManaRewards(*output_id)); - } - } - - Ok(Selected { - inputs: Self::sort_input_signing_data( - self.selected_inputs, - self.slot_index, - self.protocol_parameters.committable_age_range(), - )?, - outputs: self.outputs, - remainders, - mana_rewards: self.mana_rewards, - }) - } - - fn validate_transitions(&self) -> Result<(), Error> { + fn validate_transitions(inputs: &[InputSigningData], outputs: &[Output]) -> Result<(), Error> { let mut input_native_tokens_builder = NativeTokensBuilder::new(); let mut output_native_tokens_builder = NativeTokensBuilder::new(); let mut input_accounts = Vec::new(); @@ -473,7 +630,7 @@ impl InputSelection { let mut input_foundries = Vec::new(); let mut input_nfts = Vec::new(); - for input in &self.selected_inputs { + for input in inputs { if let Some(native_token) = input.output.native_token() { input_native_tokens_builder.add_native_token(*native_token)?; } @@ -497,14 +654,14 @@ impl InputSelection { } } - for output in self.outputs.iter() { + for output in outputs { if let Some(native_token) = output.native_token() { output_native_tokens_builder.add_native_token(*native_token)?; } } // Validate utxo chain transitions - for output in self.outputs.iter() { + for output in outputs { match output { Output::Account(account_output) => { // Null id outputs are just minted and can't be a transition @@ -523,7 +680,7 @@ impl InputSelection { account, account_output, &input_chains_foundries, - &self.outputs, + outputs, ) { log::debug!("validate_transitions error {err:?}"); return Err(Error::UnfulfillableRequirement(Requirement::Account( diff --git a/sdk/src/client/api/block_builder/input_selection/remainder.rs b/sdk/src/client/api/block_builder/input_selection/remainder.rs index 119dcd3171..0d2651c394 100644 --- a/sdk/src/client/api/block_builder/input_selection/remainder.rs +++ b/sdk/src/client/api/block_builder/input_selection/remainder.rs @@ -1,33 +1,49 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use alloc::collections::BTreeMap; +use std::collections::HashMap; + use crypto::keys::bip44::Bip44; -use super::{ - requirement::native_tokens::{get_minted_and_melted_native_tokens, get_native_tokens, get_native_tokens_diff}, - Error, InputSelection, -}; +use super::{Error, InputSelection}; use crate::{ - client::api::RemainderData, + client::api::{ + input_selection::requirement::native_tokens::{get_native_tokens, get_native_tokens_diff}, + RemainderData, + }, types::block::{ address::{Address, Ed25519Address}, output::{ - unlock_condition::AddressUnlockCondition, AccountOutputBuilder, BasicOutputBuilder, NativeTokens, - NativeTokensBuilder, NftOutputBuilder, Output, StorageScoreParameters, + unlock_condition::AddressUnlockCondition, AccountOutput, AnchorOutput, BasicOutput, BasicOutputBuilder, + NativeTokens, NativeTokensBuilder, NftOutput, Output, StorageScoreParameters, }, Error as BlockError, }, }; impl InputSelection { - // Gets the remainder address from configuration of finds one from the inputs. - fn get_remainder_address(&self) -> Result)>, Error> { - if let Some(remainder_address) = &self.remainder_address { + /// Updates the remainders, overwriting old values. + pub(crate) fn update_remainders(&mut self) -> Result<(), Error> { + let (storage_deposit_returns, remainders) = self.storage_deposit_returns_and_remainders()?; + + self.remainders.storage_deposit_returns = storage_deposit_returns; + self.remainders.data = remainders; + + Ok(()) + } + + /// Gets the remainder address from configuration of finds one from the inputs. + pub(crate) fn get_remainder_address(&self) -> Result)>, Error> { + if let Some(remainder_address) = &self.remainders.address { // Search in inputs for the Bip44 chain for the remainder address, so the ledger can regenerate it for input in self.available_inputs.iter().chain(self.selected_inputs.iter()) { let required_address = input .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + )? .expect("expiration unlockable outputs already filtered out"); if &required_address == remainder_address { @@ -40,22 +56,24 @@ impl InputSelection { for input in &self.selected_inputs { let required_address = input .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + )? .expect("expiration unlockable outputs already filtered out"); - if required_address.is_ed25519_backed() { - return Ok(Some((required_address, input.chain))); + if let Some(&required_address) = required_address.backing_ed25519() { + return Ok(Some((required_address.into(), input.chain))); } } Ok(None) } - pub(crate) fn remainder_amount(&self) -> Result<(u64, bool), Error> { + pub(crate) fn remainder_amount(&self) -> Result<(u64, bool, bool), Error> { let mut input_native_tokens = get_native_tokens(self.selected_inputs.iter().map(|input| &input.output))?; - let mut output_native_tokens = get_native_tokens(self.outputs.iter())?; - let (minted_native_tokens, melted_native_tokens) = - get_minted_and_melted_native_tokens(&self.selected_inputs, self.outputs.as_slice())?; + let mut output_native_tokens = get_native_tokens(self.non_remainder_outputs())?; + let (minted_native_tokens, melted_native_tokens) = self.get_minted_and_melted_native_tokens()?; input_native_tokens.merge(minted_native_tokens)?; output_native_tokens.merge(melted_native_tokens)?; @@ -66,7 +84,7 @@ impl InputSelection { let native_tokens_diff = get_native_tokens_diff(&input_native_tokens, &output_native_tokens)?; - required_remainder_amount(native_tokens_diff, self.protocol_parameters.storage_score_parameters()) + self.required_remainder_amount(native_tokens_diff) } pub(crate) fn storage_deposit_returns_and_remainders( @@ -93,9 +111,8 @@ impl InputSelection { } let mut input_native_tokens = get_native_tokens(self.selected_inputs.iter().map(|input| &input.output))?; - let mut output_native_tokens = get_native_tokens(self.outputs.iter())?; - let (minted_native_tokens, melted_native_tokens) = - get_minted_and_melted_native_tokens(&self.selected_inputs, &self.outputs)?; + let mut output_native_tokens = get_native_tokens(self.non_remainder_outputs())?; + let (minted_native_tokens, melted_native_tokens) = self.get_minted_and_melted_native_tokens()?; input_native_tokens.merge(minted_native_tokens)?; output_native_tokens.merge(melted_native_tokens)?; @@ -106,7 +123,7 @@ impl InputSelection { let native_tokens_diff = get_native_tokens_diff(&input_native_tokens, &output_native_tokens)?; - let (input_mana, output_mana) = self.mana_sums()?; + let (input_mana, output_mana) = self.mana_sums(false)?; if input_amount == output_amount && input_mana == output_mana && native_tokens_diff.is_none() { log::debug!("No remainder required"); @@ -116,46 +133,27 @@ impl InputSelection { let amount_diff = input_amount .checked_sub(output_amount) .ok_or(BlockError::ConsumedAmountOverflow)?; - let mana_diff = input_mana + let mut mana_diff = input_mana .checked_sub(output_mana) .ok_or(BlockError::ConsumedManaOverflow)?; - // If there is only a mana remainder, try to fit it in an automatically transitioned output. - if input_amount == output_amount && input_mana != output_mana && native_tokens_diff.is_none() { - let filter = |output: &Output| { - output - .chain_id() - .as_ref() - .map(|chain_id| self.automatically_transitioned.contains(chain_id)) - .unwrap_or(false) - // Foundries can't hold mana so they are not considered here. - && !output.is_foundry() - }; - let index = self - .outputs - .iter() - .position(|output| filter(output) && output.is_account()) - .or_else(|| self.outputs.iter().position(filter)); - - if let Some(index) = index { - self.outputs[index] = match &self.outputs[index] { - Output::Account(output) => AccountOutputBuilder::from(output) - .with_mana(output.mana() + mana_diff) - .finish_output()?, - Output::Nft(output) => NftOutputBuilder::from(output) - .with_mana(output.mana() + mana_diff) - .finish_output()?, - _ => panic!("only account, nft can be automatically created and can hold mana"), - }; - - return Ok((storage_deposit_returns, Vec::new())); + let (remainder_address, chain) = self + .get_remainder_address()? + .ok_or(Error::MissingInputWithEd25519Address)?; + + // If there is a mana remainder, try to fit it in an existing output + if input_mana > output_mana { + if self.output_for_added_mana_exists(&remainder_address) { + log::debug!("Allocating {mana_diff} excess input mana for output with address {remainder_address}"); + self.remainders.added_mana = std::mem::take(&mut mana_diff); + // If we have no other remainders, we are done + if input_amount == output_amount && native_tokens_diff.is_none() { + log::debug!("No more remainder required"); + return Ok((storage_deposit_returns, Vec::new())); + } } } - let Some((remainder_address, chain)) = self.get_remainder_address()? else { - return Err(Error::MissingInputWithEd25519Address); - }; - let remainder_outputs = create_remainder_outputs( amount_diff, mana_diff, @@ -167,33 +165,85 @@ impl InputSelection { Ok((storage_deposit_returns, remainder_outputs)) } -} -/// Calculates the required amount for required remainder outputs (multiple outputs are required if multiple native -/// tokens are remaining) and returns if there are native tokens as remainder. -pub(crate) fn required_remainder_amount( - remainder_native_tokens: Option, - storage_score_parameters: StorageScoreParameters, -) -> Result<(u64, bool), Error> { - let native_tokens_remainder = remainder_native_tokens.is_some(); + fn output_for_added_mana_exists(&self, remainder_address: &Address) -> bool { + // Find the first value that matches the remainder address + self.added_outputs.iter().any(|o| { + (o.is_basic() || o.is_account() || o.is_anchor() || o.is_nft()) + && o.unlock_conditions() + .map_or(true, |uc| uc.expiration().is_none() && uc.timelock().is_none()) + && matches!(o.required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + ), Ok(Some(address)) if &address == remainder_address) + }) + } - let remainder_builder = BasicOutputBuilder::new_with_minimum_amount(storage_score_parameters).add_unlock_condition( - AddressUnlockCondition::new(Address::from(Ed25519Address::from([0; 32]))), - ); + pub(crate) fn get_output_for_added_mana(&mut self, remainder_address: &Address) -> Option<&mut Output> { + // Establish the order in which we want to pick an output + let sort_order = HashMap::from([ + (AccountOutput::KIND, 1), + (BasicOutput::KIND, 2), + (NftOutput::KIND, 3), + (AnchorOutput::KIND, 4), + ]); + // Remove those that do not have an ordering and sort + let ordered_outputs = self + .added_outputs + .iter_mut() + .filter(|o| { + o.unlock_conditions() + .map_or(true, |uc| uc.expiration().is_none() && uc.timelock().is_none()) + }) + .filter_map(|o| sort_order.get(&o.kind()).map(|order| (*order, o))) + .collect::>(); + // Find the first value that matches the remainder address + ordered_outputs.into_values().find(|o| { + matches!(o.required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + ), Ok(Some(address)) if &address == remainder_address) + }) + } - let remainder_amount = if let Some(native_tokens) = remainder_native_tokens { - let nt_remainder_amount = remainder_builder - .with_native_token(*native_tokens.first().unwrap()) - .finish_output()? - .amount(); - // Amount can be just multiplied, because all remainder outputs with a native token have the same storage - // cost. - nt_remainder_amount * native_tokens.len() as u64 - } else { - remainder_builder.finish_output()?.amount() - }; - - Ok((remainder_amount, native_tokens_remainder)) + /// Calculates the required amount for required remainder outputs (multiple outputs are required if multiple native + /// tokens are remaining) and returns if there are native tokens as remainder. + pub(crate) fn required_remainder_amount( + &self, + remainder_native_tokens: Option, + ) -> Result<(u64, bool, bool), Error> { + let native_tokens_remainder = remainder_native_tokens.is_some(); + + let remainder_builder = + BasicOutputBuilder::new_with_minimum_amount(self.protocol_parameters.storage_score_parameters()) + .add_unlock_condition(AddressUnlockCondition::new(Address::from(Ed25519Address::from( + [0; 32], + )))); + + let remainder_amount = if let Some(native_tokens) = remainder_native_tokens { + let nt_remainder_amount = remainder_builder + .with_native_token(*native_tokens.first().unwrap()) + .finish_output()? + .amount(); + // Amount can be just multiplied, because all remainder outputs with a native token have the same storage + // cost. + nt_remainder_amount * native_tokens.len() as u64 + } else { + remainder_builder.finish_output()?.amount() + }; + + let (selected_mana, required_mana) = self.mana_sums(false)?; + + let remainder_address = self.get_remainder_address()?.map(|v| v.0); + + // Mana can potentially be added to an appropriate existing output instead of a new remainder output + let mana_remainder = selected_mana > required_mana + && remainder_address.map_or(true, |remainder_address| { + !self.output_for_added_mana_exists(&remainder_address) + }); + + Ok((remainder_amount, native_tokens_remainder, mana_remainder)) + } } fn create_remainder_outputs( diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs b/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs index 30fd2357e9..9a45039fb6 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/amount.rs @@ -5,13 +5,13 @@ use std::collections::{HashMap, HashSet}; use super::{native_tokens::get_native_tokens, Error, InputSelection, Requirement}; use crate::{ - client::{api::input_selection::remainder::required_remainder_amount, secret::types::InputSigningData}, + client::secret::types::InputSigningData, types::block::{ address::Address, input::INPUT_COUNT_MAX, output::{ unlock_condition::StorageDepositReturnUnlockCondition, AccountOutputBuilder, FoundryOutputBuilder, - MinimumOutputAmount, NftOutputBuilder, Output, OutputId, StorageScoreParameters, TokenId, + MinimumOutputAmount, NftOutputBuilder, Output, OutputId, TokenId, }, slot::SlotIndex, }, @@ -45,12 +45,12 @@ impl InputSelection { for selected_input in &self.selected_inputs { inputs_sum += selected_input.output.amount(); - if let Some(sdruc) = sdruc_not_expired(&selected_input.output, self.slot_index) { + if let Some(sdruc) = sdruc_not_expired(&selected_input.output, self.creation_slot) { *inputs_sdr.entry(sdruc.return_address().clone()).or_default() += sdruc.amount(); } } - for output in &self.outputs { + for output in self.non_remainder_outputs() { outputs_sum += output.amount(); if let Output::Basic(output) = output { @@ -83,8 +83,6 @@ struct AmountSelection { remainder_amount: u64, native_tokens_remainder: bool, mana_remainder: bool, - slot_index: SlotIndex, - storage_score_parameters: StorageScoreParameters, selected_native_tokens: HashSet, } @@ -97,9 +95,7 @@ impl AmountSelection { .iter() .filter_map(|i| i.output.native_token().map(|n| *n.token_id())), ); - let (remainder_amount, native_tokens_remainder) = input_selection.remainder_amount()?; - - let (selected_mana, required_mana) = input_selection.mana_sums()?; + let (remainder_amount, native_tokens_remainder, mana_remainder) = input_selection.remainder_amount()?; Ok(Self { newly_selected_inputs: HashMap::new(), @@ -109,9 +105,7 @@ impl AmountSelection { outputs_sdr, remainder_amount, native_tokens_remainder, - mana_remainder: selected_mana > required_mana, - slot_index: input_selection.slot_index, - storage_score_parameters: input_selection.protocol_parameters.storage_score_parameters(), + mana_remainder, selected_native_tokens, }) } @@ -135,13 +129,17 @@ impl AmountSelection { } } - fn fulfil<'a>(&mut self, inputs: impl Iterator) -> Result { + fn fulfil<'a>( + &mut self, + input_selection: &InputSelection, + inputs: impl Iterator, + ) -> Result { for input in inputs { if self.newly_selected_inputs.contains_key(input.output_id()) { continue; } - if let Some(sdruc) = sdruc_not_expired(&input.output, self.slot_index) { + if let Some(sdruc) = sdruc_not_expired(&input.output, input_selection.creation_slot) { // Skip if no additional amount is made available if input.output.amount() == sdruc.amount() { continue; @@ -167,12 +165,14 @@ impl AmountSelection { if input.output.native_token().is_some() { // Recalculate the remaining amount, as a new native token may require a new remainder output. - let (remainder_amount, native_tokens_remainder) = self.remainder_amount()?; + let (remainder_amount, native_tokens_remainder, mana_remainder) = + self.remainder_amount(input_selection)?; log::debug!( "Calculated new remainder_amount: {remainder_amount}, native_tokens_remainder: {native_tokens_remainder}" ); self.remainder_amount = remainder_amount; self.native_tokens_remainder = native_tokens_remainder; + self.mana_remainder = mana_remainder; } if self.missing_amount() == 0 { @@ -183,11 +183,11 @@ impl AmountSelection { Ok(false) } - pub(crate) fn remainder_amount(&self) -> Result<(u64, bool), Error> { + pub(crate) fn remainder_amount(&self, input_selection: &InputSelection) -> Result<(u64, bool, bool), Error> { let input_native_tokens = get_native_tokens(self.newly_selected_inputs.values().map(|input| &input.output))?.finish()?; - required_remainder_amount(Some(input_native_tokens), self.storage_score_parameters) + input_selection.required_remainder_amount(Some(input_native_tokens)) } fn into_newly_selected_inputs(self) -> Vec { @@ -203,42 +203,42 @@ impl InputSelection { ) -> Result { // No native token, expired SDRUC. let inputs = base_inputs.clone().filter(|input| { - input.output.native_token().is_none() && sdruc_not_expired(&input.output, self.slot_index).is_none() + input.output.native_token().is_none() && sdruc_not_expired(&input.output, self.creation_slot).is_none() }); - if amount_selection.fulfil(inputs)? { + if amount_selection.fulfil(self, inputs)? { return Ok(true); } // No native token, unexpired SDRUC. let inputs = base_inputs.clone().filter(|input| { - input.output.native_token().is_none() && sdruc_not_expired(&input.output, self.slot_index).is_some() + input.output.native_token().is_none() && sdruc_not_expired(&input.output, self.creation_slot).is_some() }); - if amount_selection.fulfil(inputs)? { + if amount_selection.fulfil(self, inputs)? { return Ok(true); } // Native token, expired SDRUC. let inputs = base_inputs.clone().filter(|input| { - input.output.native_token().is_some() && sdruc_not_expired(&input.output, self.slot_index).is_none() + input.output.native_token().is_some() && sdruc_not_expired(&input.output, self.creation_slot).is_none() }); - if amount_selection.fulfil(inputs)? { + if amount_selection.fulfil(self, inputs)? { return Ok(true); } // Native token, unexpired SDRUC. let inputs = base_inputs.clone().filter(|input| { - input.output.native_token().is_some() && sdruc_not_expired(&input.output, self.slot_index).is_some() + input.output.native_token().is_some() && sdruc_not_expired(&input.output, self.creation_slot).is_some() }); - if amount_selection.fulfil(inputs)? { + if amount_selection.fulfil(self, inputs)? { return Ok(true); } // Everything else. - if amount_selection.fulfil(base_inputs)? { + if amount_selection.fulfil(self, base_inputs)? { return Ok(true); } @@ -247,15 +247,7 @@ impl InputSelection { fn reduce_funds_of_chains(&mut self, amount_selection: &mut AmountSelection) -> Result<(), Error> { // Only consider automatically transitioned outputs. - let outputs = self.outputs.iter_mut().filter(|output| { - output - .chain_id() - .as_ref() - .map(|chain_id| self.automatically_transitioned.contains(chain_id)) - .unwrap_or(false) - }); - - for output in outputs { + for output in self.added_outputs.iter_mut() { let diff = amount_selection.missing_amount(); let amount = output.amount(); let minimum_amount = output.minimum_amount(self.protocol_parameters.storage_score_parameters()); @@ -378,7 +370,7 @@ impl InputSelection { .unlock_conditions() .locked_address( output.address(), - self.slot_index, + self.creation_slot, self.protocol_parameters.committable_age_range(), ) .expect("slot index was provided") @@ -399,7 +391,7 @@ impl InputSelection { .unlock_conditions() .locked_address( output.address(), - self.slot_index, + self.creation_slot, self.protocol_parameters.committable_age_range(), ) .expect("slot index was provided") @@ -425,7 +417,7 @@ impl InputSelection { .peekable(); if inputs.peek().is_some() { - amount_selection.fulfil(inputs)?; + amount_selection.fulfil(self, inputs)?; log::debug!( "Outputs {:?} selected to fulfill the amount requirement", diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/context_inputs.rs b/sdk/src/client/api/block_builder/input_selection/requirement/context_inputs.rs new file mode 100644 index 0000000000..ed95a7b027 --- /dev/null +++ b/sdk/src/client/api/block_builder/input_selection/requirement/context_inputs.rs @@ -0,0 +1,106 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::{Error, InputSelection}; +use crate::{ + client::secret::types::InputSigningData, + types::block::{ + context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput, RewardContextInput}, + output::{AccountId, DelegationOutputBuilder, Output}, + }, +}; + +impl InputSelection { + pub(crate) fn fulfill_context_inputs_requirement(&mut self) -> Result, Error> { + let mut needs_commitment_context = false; + + for (idx, input) in self.selected_inputs.iter().enumerate() { + match &input.output { + // Transitioning an issuer account requires a BlockIssuanceCreditContextInput. + Output::Account(account) => { + if account.features().block_issuer().is_some() { + log::debug!("Adding block issuance context input for transitioned account output"); + self.context_inputs.insert( + BlockIssuanceCreditContextInput::from(account.account_id_non_null(input.output_id())) + .into(), + ); + } + } + // Transitioning an implicit account requires a BlockIssuanceCreditContextInput. + Output::Basic(basic) => { + if basic.is_implicit_account() { + log::debug!("Adding block issuance context input for transitioned implicit account output"); + self.context_inputs + .insert(BlockIssuanceCreditContextInput::from(AccountId::from(input.output_id())).into()); + } + } + _ => (), + } + + // Inputs with timelock or expiration unlock condition require a CommitmentContextInput + if input + .output + .unlock_conditions() + .map_or(false, |u| u.iter().any(|u| u.is_timelock() || u.is_expiration())) + { + log::debug!("Adding commitment context input for timelocked or expiring output"); + needs_commitment_context = true; + } + + if self.mana_rewards.get(input.output_id()).is_some() { + log::debug!("Adding reward and commitment context input for output claiming mana rewards"); + self.context_inputs.insert(RewardContextInput::new(idx as _)?.into()); + needs_commitment_context = true; + } + } + for output in self + .provided_outputs + .iter_mut() + .chain(&mut self.added_outputs) + .filter(|o| o.is_delegation()) + { + // Created delegations have their start epoch set, and delayed delegations have their end set + if output.as_delegation().delegation_id().is_null() { + let start_epoch = self + .protocol_parameters + .delegation_start_epoch(self.latest_slot_commitment_id); + log::debug!("Setting created delegation start epoch to {start_epoch}"); + *output = DelegationOutputBuilder::from(output.as_delegation()) + .with_start_epoch(start_epoch) + .finish_output()?; + } else { + let end_epoch = self + .protocol_parameters + .delegation_end_epoch(self.latest_slot_commitment_id); + log::debug!("Setting delayed delegation end epoch to {end_epoch}"); + *output = DelegationOutputBuilder::from(output.as_delegation()) + .with_end_epoch(end_epoch) + .finish_output()?; + } + log::debug!("Adding commitment context input for delegation output"); + needs_commitment_context = true; + } + // BlockIssuanceCreditContextInput requires a CommitmentContextInput. + if self + .context_inputs + .iter() + .any(|c| c.kind() == BlockIssuanceCreditContextInput::KIND) + { + // TODO https://github.com/iotaledger/iota-sdk/issues/1740 + log::debug!("Adding commitment context input for output with block issuance credit context input"); + needs_commitment_context = true; + } + + if needs_commitment_context + && !self + .context_inputs + .iter() + .any(|c| c.kind() == CommitmentContextInput::KIND) + { + // TODO https://github.com/iotaledger/iota-sdk/issues/1740 + self.context_inputs + .insert(CommitmentContextInput::new(self.latest_slot_commitment_id).into()); + } + Ok(Vec::new()) + } +} diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs b/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs index cf466f84ff..5862af536c 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/ed25519.rs @@ -9,7 +9,10 @@ impl InputSelection { fn selected_unlocks_ed25519_address(&self, input: &InputSigningData, address: &Address) -> bool { let required_address = input .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + ) // PANIC: safe to unwrap as outputs with no address have been filtered out already. .unwrap() .expect("expiration unlockable outputs already filtered out"); @@ -22,7 +25,10 @@ impl InputSelection { fn available_has_ed25519_address(&self, input: &InputSigningData, address: &Address) -> bool { let required_address = input .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range()) + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + ) // PANIC: safe to unwrap as outputs with no address have been filtered out already. .unwrap() .expect("expiration unlockable outputs already filtered out"); diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs index 4d65e6e1ec..95e4ae81f0 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs @@ -1,12 +1,244 @@ // Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; + use super::{Error, InputSelection}; -use crate::client::secret::types::InputSigningData; +use crate::{ + client::{ + api::input_selection::{MinManaAllotment, Requirement}, + secret::types::InputSigningData, + }, + types::block::{ + address::Address, + input::{Input, UtxoInput}, + mana::ManaAllotment, + output::{AccountOutputBuilder, Output}, + payload::{signed_transaction::Transaction, SignedTransactionPayload}, + signature::Ed25519Signature, + unlock::{AccountUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, Unlocks}, + Error as BlockError, + }, +}; impl InputSelection { pub(crate) fn fulfill_mana_requirement(&mut self) -> Result, Error> { - let (mut selected_mana, required_mana) = self.mana_sums()?; + let Some(MinManaAllotment { + issuer_id, + reference_mana_cost, + .. + }) = self.min_mana_allotment + else { + // If there is no min allotment calculation needed, just check mana + return self.get_inputs_for_mana_balance(); + }; + + if !self.selected_inputs.is_empty() && self.all_outputs().next().is_some() { + self.selected_inputs = Self::sort_input_signing_data( + std::mem::take(&mut self.selected_inputs), + self.creation_slot, + self.protocol_parameters.committable_age_range(), + )?; + + let inputs = self + .selected_inputs + .iter() + .map(|i| Input::Utxo(UtxoInput::from(*i.output_id()))); + + let outputs = self.all_outputs().cloned(); + + let mut builder = Transaction::builder(self.protocol_parameters.network_id()) + .with_inputs(inputs) + .with_outputs(outputs); + + if let Some(payload) = &self.payload { + builder = builder.with_payload(payload.clone()); + } + + // Add the empty allotment so the work score includes it + self.mana_allotments.entry(issuer_id).or_default(); + + // If the transaction fails to build, just keep going in case another requirement helps + let transaction = builder + .with_context_inputs(self.context_inputs.clone()) + .with_mana_allotments( + self.mana_allotments + .iter() + .map(|(&account_id, &mana)| ManaAllotment { account_id, mana }), + ) + .finish_with_params(&self.protocol_parameters)?; + + let signed_transaction = SignedTransactionPayload::new(transaction, self.null_transaction_unlocks()?)?; + + let block_work_score = self.protocol_parameters.work_score(&signed_transaction) + + self.protocol_parameters.work_score_parameters().block(); + + let required_allotment_mana = block_work_score as u64 * reference_mana_cost; + + let MinManaAllotment { + issuer_id, + allotment_debt, + .. + } = self + .min_mana_allotment + .as_mut() + .ok_or(Error::UnfulfillableRequirement(Requirement::Mana))?; + + // Add the required allotment to the issuing allotment + if required_allotment_mana > self.mana_allotments[issuer_id] { + log::debug!("Allotting at least {required_allotment_mana} mana to account ID {issuer_id}"); + let additional_allotment = required_allotment_mana - self.mana_allotments[&issuer_id]; + log::debug!("{additional_allotment} additional mana required to meet minimum allotment"); + // Unwrap: safe because we always add the record above + *self.mana_allotments.get_mut(issuer_id).unwrap() = required_allotment_mana; + log::debug!("Adding {additional_allotment} to allotment debt {allotment_debt}"); + *allotment_debt += additional_allotment; + } else { + log::debug!("Setting allotment debt to {}", self.mana_allotments[issuer_id]); + *allotment_debt = self.mana_allotments[issuer_id]; + } + + self.reduce_account_output()?; + } else { + if !self.requirements.contains(&Requirement::Mana) { + self.requirements.push(Requirement::Mana); + } + } + + // Remainders can only be calculated when the input mana is >= the output mana + let (input_mana, output_mana) = self.mana_sums(false)?; + if input_mana >= output_mana { + self.update_remainders()?; + } + + let additional_inputs = self.get_inputs_for_mana_balance()?; + // If we needed more inputs to cover the additional allotment mana + // then update remainders and re-run this requirement + if !additional_inputs.is_empty() { + if !self.requirements.contains(&Requirement::Mana) { + self.requirements.push(Requirement::Mana); + } + return Ok(additional_inputs); + } + + Ok(Vec::new()) + } + + pub(crate) fn reduce_account_output(&mut self) -> Result<(), Error> { + let MinManaAllotment { + issuer_id, + allotment_debt, + .. + } = self + .min_mana_allotment + .as_mut() + .ok_or(Error::UnfulfillableRequirement(Requirement::Mana))?; + if let Some(output) = self + .provided_outputs + .iter_mut() + .chain(&mut self.added_outputs) + .filter(|o| o.is_account() && o.mana() != 0) + .find(|o| o.as_account().account_id() == issuer_id) + { + log::debug!( + "Reducing account mana of {} by {} for allotment", + output.as_account().account_id(), + allotment_debt + ); + let output_mana = output.mana(); + *output = AccountOutputBuilder::from(output.as_account()) + .with_mana(output_mana.saturating_sub(*allotment_debt)) + .finish_output()?; + *allotment_debt = allotment_debt.saturating_sub(output_mana); + log::debug!("Allotment debt after reduction: {}", allotment_debt); + } + Ok(()) + } + + pub(crate) fn null_transaction_unlocks(&self) -> Result { + let mut blocks = Vec::new(); + let mut block_indexes = HashMap::::new(); + + // Assuming inputs_data is ordered by address type + for (current_block_index, input) in self.selected_inputs.iter().enumerate() { + // Get the address that is required to unlock the input + let required_address = input + .output + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + )? + .expect("expiration deadzone"); + + // Convert restricted and implicit addresses to Ed25519 address, so they're the same entry in + // `block_indexes`. + let required_address = match required_address { + Address::ImplicitAccountCreation(implicit) => Address::Ed25519(*implicit.ed25519_address()), + Address::Restricted(restricted) => restricted.address().clone(), + _ => required_address, + }; + + // Check if we already added an [Unlock] for this address + match block_indexes.get(&required_address) { + // If we already have an [Unlock] for this address, add a [Unlock] based on the address type + Some(block_index) => match required_address { + Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => { + blocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?)); + } + Address::Account(_) => blocks.push(Unlock::Account(AccountUnlock::new(*block_index as u16)?)), + Address::Nft(_) => blocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)), + _ => Err(BlockError::UnsupportedAddressKind(required_address.kind()))?, + }, + None => { + // We can only sign ed25519 addresses and block_indexes needs to contain the account or nft + // address already at this point, because the reference index needs to be lower + // than the current block index + match &required_address { + Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => {} + _ => Err(Error::MissingInputWithEd25519Address)?, + } + + let block = SignatureUnlock::new( + Ed25519Signature::from_bytes( + [0; Ed25519Signature::PUBLIC_KEY_LENGTH], + [0; Ed25519Signature::SIGNATURE_LENGTH], + ) + .into(), + ) + .into(); + blocks.push(block); + + // Add the ed25519 address to the block_indexes, so it gets referenced if further inputs have + // the same address in their unlock condition + block_indexes.insert(required_address.clone(), current_block_index); + } + } + + // When we have an account or Nft output, we will add their account or nft address to block_indexes, + // because they can be used to unlock outputs via [Unlock::Account] or [Unlock::Nft], + // that have the corresponding account or nft address in their unlock condition + match &input.output { + Output::Account(account_output) => block_indexes.insert( + Address::Account(account_output.account_address(input.output_id())), + current_block_index, + ), + Output::Nft(nft_output) => block_indexes.insert( + Address::Nft(nft_output.nft_address(input.output_id())), + current_block_index, + ), + _ => None, + }; + } + + Ok(Unlocks::new(blocks)?) + } + + pub(crate) fn get_inputs_for_mana_balance(&mut self) -> Result, Error> { + let (mut selected_mana, required_mana) = self.mana_sums(true)?; + + log::debug!("Mana requirement selected mana: {selected_mana}, required mana: {required_mana}"); if selected_mana >= required_mana { log::debug!("Mana requirement already fulfilled"); @@ -16,30 +248,44 @@ impl InputSelection { // TODO we should do as for the amount and have preferences on which inputs to pick. while let Some(input) = self.available_inputs.pop() { - selected_mana += input.output.mana(); + selected_mana += self.total_mana(&input)?; inputs.push(input); if selected_mana >= required_mana { break; } } + if selected_mana < required_mana { + return Err(Error::InsufficientMana { + found: selected_mana, + required: required_mana, + }); + } Ok(inputs) } } - pub(crate) fn mana_sums(&self) -> Result<(u64, u64), Error> { - let required_mana = self.outputs.iter().map(|o| o.mana()).sum::() + self.mana_allotments; + pub(crate) fn mana_sums(&self, include_remainders: bool) -> Result<(u64, u64), Error> { + let required_mana = if include_remainders { + self.all_outputs().map(|o| o.mana()).sum::() + self.remainders.added_mana + } else { + self.non_remainder_outputs().map(|o| o.mana()).sum::() + } + self.mana_allotments.values().sum::(); let mut selected_mana = 0; for input in &self.selected_inputs { - selected_mana += self.mana_rewards.get(input.output_id()).copied().unwrap_or_default(); - selected_mana += input.output.available_mana( - &self.protocol_parameters, - input.output_id().transaction_id().slot_index(), - self.slot_index, - )?; + selected_mana += self.total_mana(input)?; } Ok((selected_mana, required_mana)) } + + fn total_mana(&self, input: &InputSigningData) -> Result { + Ok(self.mana_rewards.get(input.output_id()).copied().unwrap_or_default() + + input.output.available_mana( + &self.protocol_parameters, + input.output_id().transaction_id().slot_index(), + self.creation_slot, + )?) + } } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs b/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs index 99b60580de..ac2751ae56 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/mod.rs @@ -1,8 +1,9 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 pub(crate) mod account; pub(crate) mod amount; +pub(crate) mod context_inputs; pub(crate) mod delegation; pub(crate) mod ed25519; pub(crate) mod foundry; @@ -48,25 +49,28 @@ pub enum Requirement { Amount, /// Mana requirement. Mana, + /// Context inputs requirement. + ContextInputs, } impl InputSelection { /// Fulfills a requirement by selecting the appropriate available inputs. /// Returns the selected inputs and an optional new requirement. - pub(crate) fn fulfill_requirement(&mut self, requirement: Requirement) -> Result, Error> { + pub(crate) fn fulfill_requirement(&mut self, requirement: &Requirement) -> Result, Error> { log::debug!("Fulfilling requirement {requirement:?}"); match requirement { - Requirement::Sender(address) => self.fulfill_sender_requirement(&address), - Requirement::Issuer(address) => self.fulfill_issuer_requirement(&address), - Requirement::Ed25519(address) => self.fulfill_ed25519_requirement(&address), - Requirement::Foundry(foundry_id) => self.fulfill_foundry_requirement(foundry_id), - Requirement::Account(account_id) => self.fulfill_account_requirement(account_id), - Requirement::Nft(nft_id) => self.fulfill_nft_requirement(nft_id), - Requirement::Delegation(delegation_id) => self.fulfill_delegation_requirement(delegation_id), + Requirement::Sender(address) => self.fulfill_sender_requirement(address), + Requirement::Issuer(address) => self.fulfill_issuer_requirement(address), + Requirement::Ed25519(address) => self.fulfill_ed25519_requirement(address), + Requirement::Foundry(foundry_id) => self.fulfill_foundry_requirement(*foundry_id), + Requirement::Account(account_id) => self.fulfill_account_requirement(*account_id), + Requirement::Nft(nft_id) => self.fulfill_nft_requirement(*nft_id), + Requirement::Delegation(delegation_id) => self.fulfill_delegation_requirement(*delegation_id), Requirement::NativeTokens => self.fulfill_native_tokens_requirement(), Requirement::Amount => self.fulfill_amount_requirement(), Requirement::Mana => self.fulfill_mana_requirement(), + Requirement::ContextInputs => self.fulfill_context_inputs_requirement(), } } @@ -74,7 +78,7 @@ impl InputSelection { pub(crate) fn outputs_requirements(&mut self) { let inputs = self.available_inputs.iter().chain(self.selected_inputs.iter()); - for output in &self.outputs { + for output in self.provided_outputs.iter().chain(&self.added_outputs) { let is_created = match output { // Add an account requirement if the account output is transitioning and then required in the inputs. Output::Account(account_output) => { @@ -161,8 +165,7 @@ impl InputSelection { if let Some(burn) = self.burn.as_ref() { for account_id in &burn.accounts { if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_account_with_id_non_null(output, account_id)) { return Err(Error::BurnAndTransition(ChainId::from(*account_id))); @@ -174,7 +177,10 @@ impl InputSelection { } for foundry_id in &burn.foundries { - if self.outputs.iter().any(|output| is_foundry_with_id(output, foundry_id)) { + if self + .non_remainder_outputs() + .any(|output| is_foundry_with_id(output, foundry_id)) + { return Err(Error::BurnAndTransition(ChainId::from(*foundry_id))); } @@ -185,8 +191,7 @@ impl InputSelection { for nft_id in &burn.nfts { if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_nft_with_id_non_null(output, nft_id)) { return Err(Error::BurnAndTransition(ChainId::from(*nft_id))); @@ -199,8 +204,7 @@ impl InputSelection { for delegation_id in &burn.delegations { if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_delegation_with_id_non_null(output, delegation_id)) { return Err(Error::BurnAndTransition(ChainId::from(*delegation_id))); diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/native_tokens.rs b/sdk/src/client/api/block_builder/input_selection/requirement/native_tokens.rs index e355d74dbd..aa0fef119e 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/native_tokens.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/native_tokens.rs @@ -23,66 +23,6 @@ pub(crate) fn get_native_tokens<'a>(outputs: impl Iterator) - Ok(required_native_tokens) } -pub(crate) fn get_minted_and_melted_native_tokens( - inputs: &[InputSigningData], - outputs: &[Output], -) -> Result<(NativeTokensBuilder, NativeTokensBuilder), Error> { - let mut minted_native_tokens = NativeTokensBuilder::new(); - let mut melted_native_tokens = NativeTokensBuilder::new(); - - for output in outputs { - if let Output::Foundry(output_foundry) = output { - let TokenScheme::Simple(output_foundry_simple_ts) = output_foundry.token_scheme(); - let mut initial_creation = true; - - for input in inputs { - if let Output::Foundry(input_foundry) = &input.output { - let token_id = output_foundry.token_id(); - - if output_foundry.id() == input_foundry.id() { - initial_creation = false; - let TokenScheme::Simple(input_foundry_simple_ts) = input_foundry.token_scheme(); - - match output_foundry_simple_ts - .circulating_supply() - .cmp(&input_foundry_simple_ts.circulating_supply()) - { - Ordering::Greater => { - let minted_native_token_amount = output_foundry_simple_ts.circulating_supply() - - input_foundry_simple_ts.circulating_supply(); - - minted_native_tokens - .add_native_token(NativeToken::new(token_id, minted_native_token_amount)?)?; - } - Ordering::Less => { - let melted_native_token_amount = input_foundry_simple_ts.circulating_supply() - - output_foundry_simple_ts.circulating_supply(); - - melted_native_tokens - .add_native_token(NativeToken::new(token_id, melted_native_token_amount)?)?; - } - Ordering::Equal => {} - } - } - } - } - - // If we created the foundry with this transaction, then we need to add the circulating supply as minted - // tokens - if initial_creation { - let circulating_supply = output_foundry_simple_ts.circulating_supply(); - - if !circulating_supply.is_zero() { - minted_native_tokens - .add_native_token(NativeToken::new(output_foundry.token_id(), circulating_supply)?)?; - } - } - } - } - - Ok((minted_native_tokens, melted_native_tokens)) -} - // TODO only handles one side pub(crate) fn get_native_tokens_diff( inputs: &NativeTokensBuilder, @@ -113,9 +53,8 @@ pub(crate) fn get_native_tokens_diff( impl InputSelection { pub(crate) fn fulfill_native_tokens_requirement(&mut self) -> Result, Error> { let mut input_native_tokens = get_native_tokens(self.selected_inputs.iter().map(|input| &input.output))?; - let mut output_native_tokens = get_native_tokens(self.outputs.iter())?; - let (minted_native_tokens, melted_native_tokens) = - get_minted_and_melted_native_tokens(&self.selected_inputs, &self.outputs)?; + let mut output_native_tokens = get_native_tokens(self.non_remainder_outputs())?; + let (minted_native_tokens, melted_native_tokens) = self.get_minted_and_melted_native_tokens()?; input_native_tokens.merge(minted_native_tokens)?; output_native_tokens.merge(melted_native_tokens)?; @@ -181,4 +120,63 @@ impl InputSelection { Ok(Vec::new()) } } + + pub(crate) fn get_minted_and_melted_native_tokens( + &self, + ) -> Result<(NativeTokensBuilder, NativeTokensBuilder), Error> { + let mut minted_native_tokens = NativeTokensBuilder::new(); + let mut melted_native_tokens = NativeTokensBuilder::new(); + + for output in self.non_remainder_outputs() { + if let Output::Foundry(output_foundry) = output { + let TokenScheme::Simple(output_foundry_simple_ts) = output_foundry.token_scheme(); + let mut initial_creation = true; + + for input in &self.selected_inputs { + if let Output::Foundry(input_foundry) = &input.output { + let token_id = output_foundry.token_id(); + + if output_foundry.id() == input_foundry.id() { + initial_creation = false; + let TokenScheme::Simple(input_foundry_simple_ts) = input_foundry.token_scheme(); + + match output_foundry_simple_ts + .circulating_supply() + .cmp(&input_foundry_simple_ts.circulating_supply()) + { + Ordering::Greater => { + let minted_native_token_amount = output_foundry_simple_ts.circulating_supply() + - input_foundry_simple_ts.circulating_supply(); + + minted_native_tokens + .add_native_token(NativeToken::new(token_id, minted_native_token_amount)?)?; + } + Ordering::Less => { + let melted_native_token_amount = input_foundry_simple_ts.circulating_supply() + - output_foundry_simple_ts.circulating_supply(); + + melted_native_tokens + .add_native_token(NativeToken::new(token_id, melted_native_token_amount)?)?; + } + Ordering::Equal => {} + } + } + } + } + + // If we created the foundry with this transaction, then we need to add the circulating supply as minted + // tokens + if initial_creation { + let circulating_supply = output_foundry_simple_ts.circulating_supply(); + + if !circulating_supply.is_zero() { + minted_native_tokens + .add_native_token(NativeToken::new(output_foundry.token_id(), circulating_supply)?)?; + } + } + } + } + + Ok((minted_native_tokens, melted_native_tokens)) + } } 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 74561ed554..e481cf3023 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 @@ -50,7 +50,10 @@ impl InputSelection { for input in self.selected_inputs.iter() { let required_address = input .output - .required_address(self.slot_index, self.protocol_parameters.committable_age_range())? + .required_address( + self.latest_slot_commitment_id.slot_index(), + self.protocol_parameters.committable_age_range(), + )? .expect("expiration unlockable outputs already filtered out"); if &required_address == weight_address.address() { 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 4889f7f04e..39089dcb48 100644 --- a/sdk/src/client/api/block_builder/input_selection/transition.rs +++ b/sdk/src/client/api/block_builder/input_selection/transition.rs @@ -8,8 +8,8 @@ use super::{ use crate::{ client::secret::types::InputSigningData, types::block::output::{ - AccountOutput, AccountOutputBuilder, ChainId, FoundryOutput, FoundryOutputBuilder, NftOutput, NftOutputBuilder, - Output, OutputId, + AccountOutput, AccountOutputBuilder, FoundryOutput, FoundryOutputBuilder, NftOutput, NftOutputBuilder, Output, + OutputId, }, }; @@ -35,8 +35,7 @@ impl InputSelection { // Do not create an account output if it already exists. if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_account_with_id_non_null(output, &account_id)) { log::debug!("No transition of {output_id:?}/{account_id:?} as output already exists"); @@ -44,7 +43,7 @@ impl InputSelection { } let mut highest_foundry_serial_number = 0; - for output in self.outputs.iter() { + for output in self.non_remainder_outputs() { if let Output::Foundry(foundry) = output { if *foundry.account_address().account_id() == account_id { highest_foundry_serial_number = u32::max(highest_foundry_serial_number, foundry.serial_number()); @@ -61,18 +60,15 @@ impl InputSelection { .with_features(features); if input.is_block_issuer() { - // TODO https://github.com/iotaledger/iota-sdk/issues/1918 builder = builder.with_mana(Output::from(input.clone()).available_mana( &self.protocol_parameters, output_id.transaction_id().slot_index(), - self.slot_index, + self.creation_slot, )?) } let output = builder.finish_output()?; - self.automatically_transitioned.insert(ChainId::from(account_id)); - log::debug!("Automatic transition of {output_id:?}/{account_id:?}"); Ok(Some(output)) @@ -95,8 +91,7 @@ impl InputSelection { // Do not create an nft output if it already exists. if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_nft_with_id_non_null(output, &nft_id)) { log::debug!("No transition of {output_id:?}/{nft_id:?} as output already exists"); @@ -111,8 +106,6 @@ impl InputSelection { .with_features(features) .finish_output()?; - self.automatically_transitioned.insert(ChainId::from(nft_id)); - log::debug!("Automatic transition of {output_id:?}/{nft_id:?}"); Ok(Some(output)) @@ -139,8 +132,7 @@ impl InputSelection { // Do not create a foundry output if it already exists. if self - .outputs - .iter() + .non_remainder_outputs() .any(|output| is_foundry_with_id(output, &foundry_id)) { log::debug!("No transition of {output_id:?}/{foundry_id:?} as output already exists"); @@ -149,8 +141,6 @@ impl InputSelection { let output = FoundryOutputBuilder::from(input).finish_output()?; - self.automatically_transitioned.insert(ChainId::from(foundry_id)); - log::debug!("Automatic transition of {output_id:?}/{foundry_id:?}"); Ok(Some(output)) diff --git a/sdk/src/client/secret/ledger_nano.rs b/sdk/src/client/secret/ledger_nano.rs index 07f9f6a754..40b75a37e3 100644 --- a/sdk/src/client/secret/ledger_nano.rs +++ b/sdk/src/client/secret/ledger_nano.rs @@ -524,7 +524,7 @@ fn merge_unlocks( mut unlocks: impl Iterator, protocol_parameters: &ProtocolParameters, ) -> Result, Error> { - let slot_index = prepared_transaction_data + let commitment_slot_index = prepared_transaction_data .transaction .context_inputs() .commitment() @@ -539,7 +539,7 @@ fn merge_unlocks( // Get the address that is required to unlock the input let required_address = input .output - .required_address(slot_index, protocol_parameters.committable_age_range())? + .required_address(commitment_slot_index, protocol_parameters.committable_age_range())? // Time in which no address can unlock the output because of an expiration unlock condition .ok_or(Error::ExpirationDeadzone)?; diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index 9194402f1f..c9213bff69 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -574,7 +574,7 @@ where let transaction_signing_hash = prepared_transaction_data.transaction.signing_hash(); let mut blocks = Vec::new(); let mut block_indexes = HashMap::::new(); - let slot_index = prepared_transaction_data + let commitment_slot_index = prepared_transaction_data .transaction .context_inputs() .commitment() @@ -585,7 +585,7 @@ where // Get the address that is required to unlock the input let required_address = input .output - .required_address(slot_index, protocol_parameters.committable_age_range())? + .required_address(commitment_slot_index, protocol_parameters.committable_age_range())? .ok_or(crate::client::Error::ExpirationDeadzone)?; // Convert restricted and implicit addresses to Ed25519 address, so they're the same entry in `block_indexes`. diff --git a/sdk/src/types/block/macro.rs b/sdk/src/types/block/macro.rs index 905ed7d022..dd2a88ff6e 100644 --- a/sdk/src/types/block/macro.rs +++ b/sdk/src/types/block/macro.rs @@ -119,6 +119,13 @@ macro_rules! impl_id { slot_index: slot_index.into().to_le_bytes(), } } + + pub const fn [](self, slot_index: $crate::types::block::slot::SlotIndex) -> $id_name { + $id_name { + hash: self, + slot_index: slot_index.0.to_le_bytes(), + } + } } } diff --git a/sdk/src/types/block/mana/parameters.rs b/sdk/src/types/block/mana/parameters.rs index 8ea61bbcae..f08af0fd9c 100644 --- a/sdk/src/types/block/mana/parameters.rs +++ b/sdk/src/types/block/mana/parameters.rs @@ -102,7 +102,7 @@ impl Default for ManaParameters { fn default() -> Self { // TODO: use actual values Self { - bits_count: 10, + bits_count: 63, generation_rate: Default::default(), generation_rate_exponent: Default::default(), decay_factors: Default::default(), diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index d6e33374d1..6737e268fe 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -350,20 +350,20 @@ impl Output { /// Returns the address that is required to unlock this [`Output`]. pub fn required_address( &self, - slot_index: impl Into>, + commitment_slot_index: impl Into>, committable_age_range: CommittableAgeRange, ) -> Result, Error> { Ok(match self { Self::Basic(output) => output .unlock_conditions() - .locked_address(output.address(), slot_index, committable_age_range)? + .locked_address(output.address(), commitment_slot_index, committable_age_range)? .cloned(), Self::Account(output) => Some(output.address().clone()), Self::Anchor(_) => return Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), Self::Foundry(output) => Some(Address::Account(*output.account_address())), Self::Nft(output) => output .unlock_conditions() - .locked_address(output.address(), slot_index, committable_age_range)? + .locked_address(output.address(), commitment_slot_index, committable_age_range)? .cloned(), Self::Delegation(output) => Some(output.address().clone()), }) diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index 8c374a08bd..c1744e5b7e 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -67,8 +67,8 @@ impl TransactionBuilder { } /// Sets the inputs of a [`TransactionBuilder`]. - pub fn with_inputs(mut self, inputs: impl Into>) -> Self { - self.inputs = inputs.into(); + pub fn with_inputs(mut self, inputs: impl IntoIterator) -> Self { + self.inputs = inputs.into_iter().collect(); self } @@ -96,8 +96,8 @@ impl TransactionBuilder { self } - pub fn with_capabilities(mut self, capabilities: TransactionCapabilities) -> Self { - self.capabilities = capabilities; + pub fn with_capabilities(mut self, capabilities: impl Into) -> Self { + self.capabilities = capabilities.into(); self } @@ -113,8 +113,8 @@ impl TransactionBuilder { } /// Sets the outputs of a [`TransactionBuilder`]. - pub fn with_outputs(mut self, outputs: impl Into>) -> Self { - self.outputs = outputs.into(); + pub fn with_outputs(mut self, outputs: impl IntoIterator) -> Self { + self.outputs = outputs.into_iter().collect(); self } diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index 0b2f1865e4..5475267949 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -255,6 +255,14 @@ impl<'a> SemanticValidationContext<'a> { } } + // Add allotted mana + for mana_allotment in self.transaction.allotments().iter() { + self.output_mana = self + .output_mana + .checked_add(mana_allotment.mana()) + .ok_or(Error::CreatedManaOverflow)?; + } + // Validation of outputs. for (index, created_output) in self.transaction.outputs().iter().enumerate() { let (amount, mana, created_native_token, features) = match created_output { @@ -342,14 +350,6 @@ impl<'a> SemanticValidationContext<'a> { // Add stored mana self.output_mana = self.output_mana.checked_add(mana).ok_or(Error::CreatedManaOverflow)?; - // Add allotted mana - for mana_allotment in self.transaction.allotments().iter() { - self.output_mana = self - .output_mana - .checked_add(mana_allotment.mana()) - .ok_or(Error::CreatedManaOverflow)?; - } - if let Some(created_native_token) = created_native_token { let native_token_amount = self .output_native_tokens diff --git a/sdk/src/types/block/slot/mod.rs b/sdk/src/types/block/slot/mod.rs index ef8e74277b..3db1a56965 100644 --- a/sdk/src/types/block/slot/mod.rs +++ b/sdk/src/types/block/slot/mod.rs @@ -8,5 +8,9 @@ mod index; mod roots_id; pub use self::{ - commitment::SlotCommitment, commitment_id::SlotCommitmentId, epoch::EpochIndex, index::SlotIndex, roots_id::RootsId, + commitment::SlotCommitment, + commitment_id::{SlotCommitmentHash, SlotCommitmentId}, + epoch::EpochIndex, + index::SlotIndex, + roots_id::RootsId, }; diff --git a/sdk/src/wallet/operations/helpers/time.rs b/sdk/src/wallet/operations/helpers/time.rs index ac85ce9a8e..62a504e592 100644 --- a/sdk/src/wallet/operations/helpers/time.rs +++ b/sdk/src/wallet/operations/helpers/time.rs @@ -10,18 +10,18 @@ use crate::{ pub(crate) fn can_output_be_unlocked_now( wallet_address: &Address, output_data: &OutputData, - slot_index: impl Into + Copy, + commitment_slot_index: impl Into + Copy, committable_age_range: CommittableAgeRange, ) -> crate::wallet::Result { if let Some(unlock_conditions) = output_data.output.unlock_conditions() { - if unlock_conditions.is_timelocked(slot_index, committable_age_range.min) { + if unlock_conditions.is_timelocked(commitment_slot_index, committable_age_range.min) { return Ok(false); } } let required_address = output_data .output - .required_address(slot_index.into(), committable_age_range)?; + .required_address(commitment_slot_index.into(), committable_age_range)?; // In case of `None` the output can currently not be unlocked because of expiration unlock condition Ok(required_address.map_or_else(|| false, |required_address| wallet_address == &required_address)) diff --git a/sdk/src/wallet/operations/output_claiming.rs b/sdk/src/wallet/operations/output_claiming.rs index 14e1c6c999..eaec534a7c 100644 --- a/sdk/src/wallet/operations/output_claiming.rs +++ b/sdk/src/wallet/operations/output_claiming.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use std::collections::HashSet; @@ -215,9 +215,7 @@ where } })?; - let claim_tx = self - .sign_and_submit_transaction(prepared_transaction, None, None) - .await?; + let claim_tx = self.sign_and_submit_transaction(prepared_transaction, None).await?; log::debug!( "[OUTPUT_CLAIMING] Claiming transaction created: block_id: {:?} tx_id: {:?}", @@ -298,17 +296,16 @@ where self.prepare_transaction( // We only need to provide the NFT outputs, ISA automatically creates basic outputs as remainder outputs nft_outputs_to_send, - Some(TransactionOptions { - custom_inputs: Some( - outputs_to_claim - .iter() - .map(|o| o.output_id) - // add additional inputs - .chain(possible_additional_inputs.iter().map(|o| o.output_id)) - .collect::>(), - ), + TransactionOptions { + required_inputs: outputs_to_claim + .iter() + .map(|o| o.output_id) + // add additional inputs + .chain(possible_additional_inputs.iter().map(|o| o.output_id)) + .collect(), + allow_additional_input_selection: false, ..Default::default() - }), + }, ) .await } diff --git a/sdk/src/wallet/operations/output_consolidation.rs b/sdk/src/wallet/operations/output_consolidation.rs index f30904437c..e4fe56c5ed 100644 --- a/sdk/src/wallet/operations/output_consolidation.rs +++ b/sdk/src/wallet/operations/output_consolidation.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2024 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use std::collections::{HashMap, HashSet}; @@ -77,9 +77,7 @@ where /// consolidates the amount of outputs that fit into a single transaction. pub async fn consolidate_outputs(&self, params: ConsolidationParams) -> Result { let prepared_transaction = self.prepare_consolidate_outputs(params).await?; - let consolidation_tx = self - .sign_and_submit_transaction(prepared_transaction, None, None) - .await?; + let consolidation_tx = self.sign_and_submit_transaction(prepared_transaction, None).await?; log::debug!( "[OUTPUT_CONSOLIDATION] consolidation transaction created: block_id: {:?} tx_id: {:?}", @@ -98,13 +96,14 @@ where let outputs_to_consolidate = self.get_outputs_to_consolidate(¶ms).await?; let options = Some(TransactionOptions { - custom_inputs: Some(outputs_to_consolidate.into_iter().map(|o| o.output_id).collect()), + required_inputs: outputs_to_consolidate.into_iter().map(|o| o.output_id).collect(), remainder_value_strategy: RemainderValueStrategy::CustomAddress( params .target_address .map(|bech32| bech32.into_inner()) .unwrap_or_else(|| wallet_address.into_inner()), ), + allow_additional_input_selection: false, ..Default::default() }); diff --git a/sdk/src/wallet/operations/participation/voting.rs b/sdk/src/wallet/operations/participation/voting.rs index b8642af3c6..0979b21adf 100644 --- a/sdk/src/wallet/operations/participation/voting.rs +++ b/sdk/src/wallet/operations/participation/voting.rs @@ -111,7 +111,7 @@ where Some(TransactionOptions { // Only use previous voting output as input. custom_inputs: Some(vec![voting_output.output_id]), - mandatory_inputs: Some(vec![voting_output.output_id]), + required_inputs: Some(vec![voting_output.output_id]), tagged_data_payload: Some(TaggedDataPayload::new( PARTICIPATION_TAG.as_bytes().to_vec(), participation_bytes, @@ -182,7 +182,7 @@ where Some(TransactionOptions { // Only use previous voting output as input. custom_inputs: Some(vec![voting_output.output_id]), - mandatory_inputs: Some(vec![voting_output.output_id]), + required_inputs: Some(vec![voting_output.output_id]), tagged_data_payload: Some(TaggedDataPayload::new( PARTICIPATION_TAG.as_bytes().to_vec(), participation_bytes, diff --git a/sdk/src/wallet/operations/participation/voting_power.rs b/sdk/src/wallet/operations/participation/voting_power.rs index 550ffe64d9..5514929c87 100644 --- a/sdk/src/wallet/operations/participation/voting_power.rs +++ b/sdk/src/wallet/operations/participation/voting_power.rs @@ -62,7 +62,7 @@ where new_output, Some(TransactionOptions { // Use the previous voting output and additionally other for the additional amount. - mandatory_inputs: Some(vec![current_output_data.output_id]), + required_inputs: Some(vec![current_output_data.output_id]), tagged_data_payload: Some(tagged_data_payload), ..Default::default() }), @@ -119,7 +119,7 @@ where Some(TransactionOptions { // Use the previous voting output and additionally others for possible additional required amount for // the remainder to reach the minimum required storage deposit. - mandatory_inputs: Some(vec![current_output_data.output_id]), + required_inputs: Some(vec![current_output_data.output_id]), tagged_data_payload, ..Default::default() }), diff --git a/sdk/src/wallet/operations/transaction/account.rs b/sdk/src/wallet/operations/transaction/account.rs index c915f3631b..b7b6c5b048 100644 --- a/sdk/src/wallet/operations/transaction/account.rs +++ b/sdk/src/wallet/operations/transaction/account.rs @@ -1,11 +1,10 @@ -// Copyright 2023 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, types::block::{ address::Address, - context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput}, output::{ feature::{ BlockIssuerFeature, BlockIssuerKey, BlockIssuerKeySource, BlockIssuerKeys, @@ -36,8 +35,10 @@ where self.sign_and_submit_transaction( self.prepare_implicit_account_transition(output_id, key_source).await?, - issuer_id, - None, + TransactionOptions { + issuer_id: Some(issuer_id), + ..Default::default() + }, ) .await } @@ -87,11 +88,6 @@ where let account_id = AccountId::from(output_id); let account = AccountOutput::build_with_amount(implicit_account.amount(), account_id) - .with_mana(implicit_account_data.output.available_mana( - &self.client().get_protocol_parameters().await?, - implicit_account_data.output_id.transaction_id().slot_index(), - self.client().get_slot_index().await?, - )?) .with_unlock_conditions([AddressUnlockCondition::from(Address::from(ed25519_address))]) .with_features([BlockIssuerFeature::new( u32::MAX, @@ -101,16 +97,10 @@ where drop(wallet_data); - // TODO https://github.com/iotaledger/iota-sdk/issues/1740 - let issuance = self.client().get_issuance().await?; - let transaction_options = TransactionOptions { - context_inputs: Some(vec![ - // TODO Remove in https://github.com/iotaledger/iota-sdk/pull/1872 - CommitmentContextInput::new(issuance.latest_commitment.id()).into(), - BlockIssuanceCreditContextInput::new(account_id).into(), - ]), - custom_inputs: Some(vec![*output_id]), + required_inputs: [*output_id].into(), + issuer_id: Some(account_id), + allow_additional_input_selection: false, ..Default::default() }; diff --git a/sdk/src/wallet/operations/transaction/build_transaction.rs b/sdk/src/wallet/operations/transaction/build_transaction.rs deleted file mode 100644 index 63ec5b6177..0000000000 --- a/sdk/src/wallet/operations/transaction/build_transaction.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashSet; - -use instant::Instant; - -use crate::{ - client::{ - api::{input_selection::Selected, transaction::validate_transaction_length, PreparedTransactionData}, - secret::SecretManage, - }, - types::block::{ - context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput, ContextInput, RewardContextInput}, - input::{Input, UtxoInput}, - output::{DelegationOutputBuilder, Output}, - payload::signed_transaction::Transaction, - }, - wallet::{operations::transaction::TransactionOptions, Wallet}, -}; - -impl Wallet -where - crate::wallet::Error: From, -{ - /// Builds the transaction from the selected inputs and outputs. - pub(crate) async fn build_transaction( - &self, - mut selected_transaction_data: Selected, - options: impl Into> + Send, - ) -> crate::wallet::Result { - log::debug!("[TRANSACTION] build_transaction"); - - let build_transaction_start_time = Instant::now(); - let protocol_parameters = self.client().get_protocol_parameters().await?; - - let mut inputs: Vec = Vec::new(); - let mut context_inputs = HashSet::new(); - - let issuance = self.client().get_issuance().await?; - let latest_slot_commitment_id = issuance.latest_commitment.id(); - - let mut needs_commitment_context = false; - - for (idx, input) in selected_transaction_data.inputs.iter().enumerate() { - // Transitioning an issuer account requires a BlockIssuanceCreditContextInput. - if let Output::Account(account) = &input.output { - if account.features().block_issuer().is_some() { - context_inputs.insert(ContextInput::from(BlockIssuanceCreditContextInput::from( - account.account_id_non_null(input.output_id()), - ))); - } - } - - // Inputs with timelock or expiration unlock condition require a CommitmentContextInput - if input - .output - .unlock_conditions() - .map_or(false, |u| u.iter().any(|u| u.is_timelock() || u.is_expiration())) - { - needs_commitment_context = true; - } - - inputs.push(Input::Utxo(UtxoInput::from(*input.output_id()))); - - if selected_transaction_data.mana_rewards.get(input.output_id()).is_some() { - context_inputs.insert(ContextInput::from(RewardContextInput::new(idx as _)?)); - needs_commitment_context = true; - } - } - - // TODO https://github.com/iotaledger/iota-sdk/issues/1937 - for output in selected_transaction_data - .outputs - .iter_mut() - .filter(|o| o.is_delegation()) - { - // Created delegations have their start epoch set, and delayed delegations have their end set - if output.as_delegation().delegation_id().is_null() { - *output = DelegationOutputBuilder::from(output.as_delegation()) - .with_start_epoch(protocol_parameters.delegation_start_epoch(latest_slot_commitment_id)) - .finish_output()?; - } else { - *output = DelegationOutputBuilder::from(output.as_delegation()) - .with_end_epoch(protocol_parameters.delegation_end_epoch(latest_slot_commitment_id)) - .finish_output()?; - } - needs_commitment_context = true; - } - - // Build transaction - - // TODO: Add an appropriate mana allotment here for this account - let mut builder = Transaction::builder(protocol_parameters.network_id()) - .with_inputs(inputs) - .with_outputs(selected_transaction_data.outputs); - - if let Some(options) = options.into() { - // Optional add a tagged payload - builder = builder.with_payload(options.tagged_data_payload); - - if let Some(context_inputs_opt) = options.context_inputs { - context_inputs.extend(context_inputs_opt); - } - - if let Some(capabilities) = options.capabilities { - builder = builder.add_capabilities(capabilities.capabilities_iter()); - } - - if let Some(mana_allotments) = options.mana_allotments { - builder = builder.with_mana_allotments(mana_allotments); - } - } - - // BlockIssuanceCreditContextInput requires a CommitmentContextInput. - if context_inputs - .iter() - .any(|c| c.kind() == BlockIssuanceCreditContextInput::KIND) - { - // TODO https://github.com/iotaledger/iota-sdk/issues/1740 - needs_commitment_context = true; - } - - if needs_commitment_context && !context_inputs.iter().any(|c| c.kind() == CommitmentContextInput::KIND) { - // TODO https://github.com/iotaledger/iota-sdk/issues/1740 - context_inputs.insert(CommitmentContextInput::new(latest_slot_commitment_id).into()); - } - - let transaction = builder - .with_context_inputs(context_inputs) - .finish_with_params(&protocol_parameters)?; - - validate_transaction_length(&transaction)?; - - let prepared_transaction_data = PreparedTransactionData { - transaction, - inputs_data: selected_transaction_data.inputs, - remainders: selected_transaction_data.remainders, - mana_rewards: selected_transaction_data.mana_rewards.into_iter().collect(), - }; - - log::debug!( - "[TRANSACTION] finished build_transaction in {:.2?}", - build_transaction_start_time.elapsed() - ); - Ok(prepared_transaction_data) - } -} diff --git a/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs b/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs index f1ac22f624..d379a611e2 100644 --- a/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs +++ b/sdk/src/wallet/operations/transaction/high_level/allot_mana.rs @@ -23,8 +23,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_allot_mana(allotments, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } pub async fn prepare_allot_mana( @@ -37,20 +36,9 @@ where let mut options = options.into().unwrap_or_default(); for allotment in allotments { - let allotment = allotment.into(); - - match options.mana_allotments.as_mut() { - Some(mana_allotments) => { - match mana_allotments - .iter_mut() - .find(|a| a.account_id == allotment.account_id) - { - Some(mana_allotment) => mana_allotment.mana += allotment.mana, - None => mana_allotments.push(allotment), - } - } - None => options.mana_allotments = Some(vec![allotment]), - } + let ManaAllotment { account_id, mana } = allotment.into(); + + *options.mana_allotments.entry(account_id).or_default() += mana; } self.prepare_transaction([], options).await diff --git a/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs index 70cde54392..c09dbb8d57 100644 --- a/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use primitive_types::U256; @@ -37,8 +37,7 @@ where .prepare_melt_native_token(token_id, melt_amount, options.clone()) .await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::melt_native_token()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs b/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs index 12e0341087..8d615eb482 100644 --- a/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs +++ b/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use crate::{ @@ -22,7 +22,7 @@ impl Wallet { let options = options.into(); let prepared = self.prepare_burn(burn, options.clone()).await?; - self.sign_and_submit_transaction(prepared, None, options).await + self.sign_and_submit_transaction(prepared, options).await } /// A generic `prepare_burn()` function that can be used to prepare the burn of native tokens, nfts, delegations, @@ -36,11 +36,11 @@ impl Wallet { burn: impl Into + Send, options: impl Into> + Send, ) -> crate::wallet::Result { - let mut options: TransactionOptions = options.into().unwrap_or_default(); + let mut options = options.into().unwrap_or_default(); options.burn = Some(burn.into()); // The empty list of outputs is used. Outputs will be generated by // the input selection algorithm based on the content of the [`Burn`] object. - self.prepare_transaction([], Some(options)).await + self.prepare_transaction([], options).await } } diff --git a/sdk/src/wallet/operations/transaction/high_level/create_account.rs b/sdk/src/wallet/operations/transaction/high_level/create_account.rs index 063e310f6d..b57d5db157 100644 --- a/sdk/src/wallet/operations/transaction/high_level/create_account.rs +++ b/sdk/src/wallet/operations/transaction/high_level/create_account.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; @@ -59,8 +59,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_create_account_output(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::create_account_output()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs b/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs index 64a10cc25a..6b58ae9a8b 100644 --- a/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs +++ b/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs @@ -70,7 +70,7 @@ where let options = options.into(); let prepared = self.prepare_create_delegation_output(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared.transaction, None, options) + self.sign_and_submit_transaction(prepared.transaction, options) .await .map(|transaction| CreateDelegationTransaction { delegation_id: prepared.delegation_id, diff --git a/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs b/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs index 8e7c70d9aa..83ba6f38b7 100644 --- a/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs +++ b/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs @@ -25,7 +25,7 @@ where .prepare_delay_delegation_claiming(delegation_id, reclaim_excess) .await?; - self.sign_and_submit_transaction(prepared_transaction, None, None).await + self.sign_and_submit_transaction(prepared_transaction, None).await } /// Prepare to delay a delegation's claiming. The `reclaim_excess` flag indicates whether excess value diff --git a/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs index 16a9faa2c6..3839f68a62 100644 --- a/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs @@ -78,7 +78,7 @@ where let options = options.into(); let prepared = self.prepare_create_native_token(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared.transaction, None, options) + self.sign_and_submit_transaction(prepared.transaction, options) .await .map(|transaction| CreateNativeTokenTransaction { token_id: prepared.token_id, @@ -105,11 +105,6 @@ where // Create the new account output with the same features, just updated mana and foundry_counter. let new_account_output_builder = AccountOutputBuilder::from(account_output) .with_account_id(account_id) - .with_mana(account_output_data.output.available_mana( - &protocol_parameters, - account_output_data.output_id.transaction_id().slot_index(), - self.client().get_slot_index().await?, - )?) .with_foundry_counter(account_output.foundry_counter() + 1); // create foundry output with minted native tokens diff --git a/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs index 4342e48b97..6f94e1e941 100644 --- a/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use primitive_types::U256; @@ -41,7 +41,7 @@ where let prepared = self .prepare_mint_native_token(token_id, mint_amount, options.clone()) .await?; - let transaction = self.sign_and_submit_transaction(prepared, None, options).await?; + let transaction = self.sign_and_submit_transaction(prepared, options).await?; Ok(transaction) } diff --git a/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs index c4b7de591a..3c91e692df 100644 --- a/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use getset::Getters; @@ -145,8 +145,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_mint_nfts(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::mint_nfts()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/send.rs b/sdk/src/wallet/operations/transaction/high_level/send.rs index f51d01afaf..a177927306 100644 --- a/sdk/src/wallet/operations/transaction/high_level/send.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use getset::Getters; @@ -121,8 +121,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_send(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::send()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs b/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs index 255931a5f3..c1c153447c 100644 --- a/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use getset::Getters; @@ -111,8 +111,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_send_native_tokens(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::send_native_tokens()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/send_nft.rs b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs index ff47666b93..8675a8659b 100644 --- a/sdk/src/wallet/operations/transaction/high_level/send_nft.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use getset::Getters; @@ -76,8 +76,7 @@ where let options = options.into(); let prepared_transaction = self.prepare_send_nft(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction, None, options) - .await + self.sign_and_submit_transaction(prepared_transaction, options).await } /// Prepares the transaction for [Wallet::send_nft()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs b/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs index 9129046698..1b7b844dc0 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs @@ -39,7 +39,7 @@ where let options = options.into(); let prepared = self.prepare_begin_staking(params, options.clone()).await?; - self.sign_and_submit_transaction(prepared, None, options).await + self.sign_and_submit_transaction(prepared, options).await } /// Prepares the transaction for [Wallet::begin_staking()]. diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/end.rs b/sdk/src/wallet/operations/transaction/high_level/staking/end.rs index d6524b40bb..fd991ae8fa 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/end.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/end.rs @@ -3,10 +3,7 @@ use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, - types::block::{ - context_input::{ContextInput, RewardContextInput}, - output::{AccountId, AccountOutputBuilder}, - }, + types::block::output::{AccountId, AccountOutputBuilder}, wallet::{types::TransactionWithMetadata, TransactionOptions, Wallet}, }; @@ -18,7 +15,7 @@ where pub async fn end_staking(&self, account_id: AccountId) -> crate::wallet::Result { let prepared = self.prepare_end_staking(account_id).await?; - self.sign_and_submit_transaction(prepared, None, None).await + self.sign_and_submit_transaction(prepared, None).await } /// Prepares the transaction for [Wallet::end_staking()]. @@ -65,9 +62,10 @@ where .with_features(features) .finish_output()?; - let mut options = TransactionOptions::default(); - options.custom_inputs = Some(vec![account_output_data.output_id]); - options.context_inputs = Some(vec![ContextInput::from(RewardContextInput::new(0)?)]); + let options = TransactionOptions { + required_inputs: [account_output_data.output_id].into(), + ..Default::default() + }; let transaction = self.prepare_transaction([output], options).await?; diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs b/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs index 06d0b35b2d..535a3778c5 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs @@ -3,10 +3,7 @@ use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, - types::block::{ - context_input::{ContextInput, RewardContextInput}, - output::{feature::StakingFeature, AccountId, AccountOutputBuilder}, - }, + types::block::output::{feature::StakingFeature, AccountId, AccountOutputBuilder}, wallet::{types::TransactionWithMetadata, TransactionOptions, Wallet}, }; @@ -22,7 +19,7 @@ where ) -> crate::wallet::Result { let prepared = self.prepare_extend_staking(account_id, additional_epochs).await?; - self.sign_and_submit_transaction(prepared, None, None).await + self.sign_and_submit_transaction(prepared, None).await } /// Prepares the transaction for [Wallet::extend_staking()]. @@ -83,8 +80,7 @@ where past_bounded_epoch, end_epoch, )); - options.custom_inputs = Some(vec![account_output_data.output_id]); - options.context_inputs = Some(vec![ContextInput::from(RewardContextInput::new(0)?)]); + options.required_inputs = [account_output_data.output_id].into(); } let output = output_builder.finish_output()?; diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index 5be57db361..b7cdd895b2 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -1,25 +1,24 @@ -// Copyright 2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::collections::{hash_map::Values, HashMap, HashSet}; +use alloc::collections::BTreeSet; +use std::collections::HashMap; #[cfg(feature = "events")] use crate::wallet::events::types::{TransactionProgressEvent, WalletEvent}; use crate::{ client::{ - api::input_selection::{Burn, InputSelection, Selected}, + api::{input_selection::InputSelection, transaction::validate_transaction_length, PreparedTransactionData}, secret::{types::InputSigningData, SecretManage}, }, types::block::{ - address::Address, - mana::ManaAllotment, output::{Output, OutputId}, protocol::CommittableAgeRange, slot::SlotIndex, }, wallet::{ core::WalletData, operations::helpers::time::can_output_be_unlocked_forever_from_now_on, types::OutputData, - Wallet, + RemainderValueStrategy, TransactionOptions, Wallet, }, }; @@ -32,18 +31,32 @@ where pub(crate) async fn select_inputs( &self, outputs: Vec, - custom_inputs: Option>, - mandatory_inputs: Option>, - remainder_address: Option
, - burn: Option<&Burn>, - mana_allotments: Option>, - ) -> crate::wallet::Result { + mut options: TransactionOptions, + ) -> crate::wallet::Result { log::debug!("[TRANSACTION] select_inputs"); // Voting output needs to be requested before to prevent a deadlock #[cfg(feature = "participation")] let voting_output = self.get_voting_output().await?; let protocol_parameters = self.client().get_protocol_parameters().await?; - let slot_index = self.client().get_slot_index().await?; + let creation_slot = self.client().get_slot_index().await?; + let slot_commitment_id = self.client().get_issuance().await?.latest_commitment.id(); + if options.issuer_id.is_none() { + options.issuer_id = self.data().await.first_account_id(); + } + let reference_mana_cost = if let Some(issuer_id) = options.issuer_id { + Some( + self.client() + .get_account_congestion(&issuer_id, None) + .await? + .reference_mana_cost, + ) + } else { + None + }; + let remainder_address = match options.remainder_value_strategy { + RemainderValueStrategy::ReuseAddress => None, + RemainderValueStrategy::CustomAddress(address) => Some(address), + }; // lock so the same inputs can't be selected in multiple transactions let mut wallet_data = self.data_mut().await; @@ -59,10 +72,7 @@ where // Prevent consuming the voting output if not actually wanted #[cfg(feature = "participation")] if let Some(voting_output) = &voting_output { - let required = mandatory_inputs.as_ref().map_or(false, |mandatory_inputs| { - mandatory_inputs.contains(&voting_output.output_id) - }); - if !required { + if !options.required_inputs.contains(&voting_output.output_id) { forbidden_inputs.insert(voting_output.output_id); } } @@ -72,21 +82,20 @@ where let available_outputs_signing_data = filter_inputs( &wallet_data, wallet_data.unspent_outputs.values(), - slot_index, + creation_slot, protocol_parameters.committable_age_range(), - custom_inputs.as_ref(), - mandatory_inputs.as_ref(), + &options.required_inputs, )?; let mut mana_rewards = HashMap::new(); - if let Some(burn) = burn { + if let Some(burn) = &options.burn { for delegation_id in burn.delegations() { if let Some(output) = wallet_data.unspent_delegation_output(delegation_id) { mana_rewards.insert( output.output_id, self.client() - .get_output_mana_rewards(&output.output_id, slot_index) + .get_output_mana_rewards(&output.output_id, slot_commitment_id.slot_index()) .await? .rewards, ); @@ -94,162 +103,72 @@ where } } - // if custom inputs are provided we should only use them (validate if we have the outputs in this account and - // that the amount is enough) - if let Some(custom_inputs) = custom_inputs { - // Check that no input got already locked - for output_id in &custom_inputs { - if wallet_data.locked_outputs.contains(output_id) { - return Err(crate::wallet::Error::CustomInput(format!( - "provided custom input {output_id} is already used in another transaction", - ))); - } - if let Some(input) = wallet_data.outputs.get(output_id) { - if input.output.can_claim_rewards(outputs.iter().find(|o| { - input - .output - .chain_id() - .map(|chain_id| chain_id.or_from_output_id(output_id)) - == o.chain_id() - })) { - mana_rewards.insert( - *output_id, - self.client() - .get_output_mana_rewards(output_id, slot_index) - .await? - .rewards, - ); - } - } - } - - let mut input_selection = InputSelection::new( - available_outputs_signing_data, - outputs, - Some(wallet_data.address.clone().into_inner()), - slot_index, - protocol_parameters.clone(), - ) - .with_required_inputs(custom_inputs) - .with_forbidden_inputs(forbidden_inputs) - .with_mana_rewards(mana_rewards); - - if let Some(address) = remainder_address { - input_selection = input_selection.with_remainder_address(address); - } - - if let Some(burn) = burn { - input_selection = input_selection.with_burn(burn.clone()); - } - - if let Some(mana_allotments) = mana_allotments { - input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); - } - - let selected_transaction_data = input_selection.select()?; - - // lock outputs so they don't get used by another transaction - for output in &selected_transaction_data.inputs { - wallet_data.locked_outputs.insert(*output.output_id()); + // Check that no input got already locked + for output_id in &options.required_inputs { + if wallet_data.locked_outputs.contains(output_id) { + return Err(crate::wallet::Error::CustomInput(format!( + "provided custom input {output_id} is already used in another transaction", + ))); } - - return Ok(selected_transaction_data); - } else if let Some(mandatory_inputs) = mandatory_inputs { - // Check that no input got already locked - for output_id in &mandatory_inputs { - if wallet_data.locked_outputs.contains(output_id) { - return Err(crate::wallet::Error::CustomInput(format!( - "provided custom input {output_id} is already used in another transaction", - ))); - } - if let Some(input) = wallet_data.outputs.get(output_id) { - if input.output.can_claim_rewards(outputs.iter().find(|o| { - input - .output - .chain_id() - .map(|chain_id| chain_id.or_from_output_id(output_id)) - == o.chain_id() - })) { - mana_rewards.insert( - *output_id, - self.client() - .get_output_mana_rewards(output_id, slot_index) - .await? - .rewards, - ); - } + if let Some(input) = wallet_data.outputs.get(output_id) { + if input.output.can_claim_rewards(outputs.iter().find(|o| { + input + .output + .chain_id() + .map(|chain_id| chain_id.or_from_output_id(output_id)) + == o.chain_id() + })) { + mana_rewards.insert( + *output_id, + self.client() + .get_output_mana_rewards(output_id, creation_slot) + .await? + .rewards, + ); } } - - let mut input_selection = InputSelection::new( - available_outputs_signing_data, - outputs, - Some(wallet_data.address.clone().into_inner()), - slot_index, - protocol_parameters.clone(), - ) - .with_required_inputs(mandatory_inputs) - .with_forbidden_inputs(forbidden_inputs) - .with_mana_rewards(mana_rewards); - - if let Some(address) = remainder_address { - input_selection = input_selection.with_remainder_address(address); - } - - if let Some(burn) = burn { - input_selection = input_selection.with_burn(burn.clone()); - } - - if let Some(mana_allotments) = mana_allotments { - input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); - } - - let selected_transaction_data = input_selection.select()?; - - // lock outputs so they don't get used by another transaction - for output in &selected_transaction_data.inputs { - wallet_data.locked_outputs.insert(*output.output_id()); - } - - // lock outputs so they don't get used by another transaction - for output in &selected_transaction_data.inputs { - wallet_data.locked_outputs.insert(*output.output_id()); - } - - return Ok(selected_transaction_data); } let mut input_selection = InputSelection::new( available_outputs_signing_data, outputs, Some(wallet_data.address.clone().into_inner()), - slot_index, + creation_slot, + slot_commitment_id, protocol_parameters.clone(), ) + .with_required_inputs(options.required_inputs) .with_forbidden_inputs(forbidden_inputs) - .with_mana_rewards(mana_rewards); - - if let Some(address) = remainder_address { - input_selection = input_selection.with_remainder_address(address); + .with_context_inputs(options.context_inputs) + .with_mana_rewards(mana_rewards) + .with_payload(options.tagged_data_payload) + .with_mana_allotments(options.mana_allotments) + .with_remainder_address(remainder_address) + .with_burn(options.burn); + + if let (Some(account_id), Some(reference_mana_cost)) = (options.issuer_id, reference_mana_cost) { + input_selection = input_selection.with_min_mana_allotment(account_id, reference_mana_cost); } - if let Some(burn) = burn { - input_selection = input_selection.with_burn(burn.clone()); + if !options.allow_additional_input_selection { + input_selection = input_selection.disable_additional_input_selection(); } - if let Some(mana_allotments) = mana_allotments { - input_selection = input_selection.with_mana_allotments(mana_allotments.iter()); + if let Some(capabilities) = options.capabilities { + input_selection = input_selection.with_transaction_capabilities(capabilities) } - let selected_transaction_data = input_selection.select()?; + let prepared_transaction_data = input_selection.select()?; + + validate_transaction_length(&prepared_transaction_data.transaction)?; // lock outputs so they don't get used by another transaction - for output in &selected_transaction_data.inputs { + for output in &prepared_transaction_data.inputs_data { log::debug!("[TRANSACTION] locking: {}", output.output_id()); wallet_data.locked_outputs.insert(*output.output_id()); } - Ok(selected_transaction_data) + Ok(prepared_transaction_data) } } @@ -257,24 +176,17 @@ where /// Note: this is only for the default input selection, it's still possible to send these outputs by using /// `claim_outputs` or providing their OutputId's in the custom_inputs #[allow(clippy::too_many_arguments)] -fn filter_inputs( +fn filter_inputs<'a>( wallet_data: &WalletData, - available_outputs: Values<'_, OutputId, OutputData>, + available_outputs: impl IntoIterator, slot_index: impl Into + Copy, committable_age_range: CommittableAgeRange, - custom_inputs: Option<&HashSet>, - mandatory_inputs: Option<&HashSet>, + required_inputs: &BTreeSet, ) -> crate::wallet::Result> { let mut available_outputs_signing_data = Vec::new(); for output_data in available_outputs { - if !custom_inputs - .map(|inputs| inputs.contains(&output_data.output_id)) - .unwrap_or(false) - && !mandatory_inputs - .map(|inputs| inputs.contains(&output_data.output_id)) - .unwrap_or(false) - { + if !required_inputs.contains(&output_data.output_id) { let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output diff --git a/sdk/src/wallet/operations/transaction/mod.rs b/sdk/src/wallet/operations/transaction/mod.rs index 0db7cf7de3..f75de0f89b 100644 --- a/sdk/src/wallet/operations/transaction/mod.rs +++ b/sdk/src/wallet/operations/transaction/mod.rs @@ -1,8 +1,7 @@ -// Copyright 2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 pub(crate) mod account; -mod build_transaction; pub(crate) mod high_level; mod input_selection; mod options; @@ -20,10 +19,7 @@ use crate::{ }, types::{ api::core::OutputWithMetadataResponse, - block::{ - output::{AccountId, Output}, - payload::signed_transaction::SignedTransactionPayload, - }, + block::{output::Output, payload::signed_transaction::SignedTransactionPayload}, }, wallet::{ types::{InclusionState, TransactionWithMetadata}, @@ -80,7 +76,7 @@ where let prepared_transaction_data = self.prepare_transaction(outputs, options.clone()).await?; - self.sign_and_submit_transaction(prepared_transaction_data, None, options) + self.sign_and_submit_transaction(prepared_transaction_data, options) .await } @@ -88,7 +84,6 @@ where pub async fn sign_and_submit_transaction( &self, prepared_transaction_data: PreparedTransactionData, - issuer_id: impl Into> + Send, options: impl Into> + Send, ) -> crate::wallet::Result { log::debug!("[TRANSACTION] sign_and_submit_transaction"); @@ -102,7 +97,7 @@ where } }; - self.submit_and_store_transaction(signed_transaction_data, issuer_id, options) + self.submit_and_store_transaction(signed_transaction_data, options) .await } @@ -110,7 +105,6 @@ where pub async fn submit_and_store_transaction( &self, signed_transaction_data: SignedTransactionData, - issuer_id: impl Into> + Send, options: impl Into> + Send, ) -> crate::wallet::Result { log::debug!( @@ -139,7 +133,10 @@ where // Ignore errors from sending, we will try to send it again during [`sync_pending_transactions`] let block_id = match self - .submit_signed_transaction(signed_transaction_data.payload.clone(), issuer_id) + .submit_signed_transaction( + signed_transaction_data.payload.clone(), + options.as_ref().and_then(|options| options.issuer_id), + ) .await { Ok(block_id) => Some(block_id), diff --git a/sdk/src/wallet/operations/transaction/options.rs b/sdk/src/wallet/operations/transaction/options.rs index 3fe8655d8d..f3bc9bcde4 100644 --- a/sdk/src/wallet/operations/transaction/options.rs +++ b/sdk/src/wallet/operations/transaction/options.rs @@ -1,6 +1,8 @@ -// Copyright 2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use alloc::collections::{BTreeMap, BTreeSet}; + use serde::{Deserialize, Serialize}; use crate::{ @@ -8,36 +10,56 @@ use crate::{ types::block::{ address::Address, context_input::ContextInput, - mana::ManaAllotment, - output::OutputId, + output::{AccountId, OutputId}, payload::{signed_transaction::TransactionCapabilities, tagged_data::TaggedDataPayload}, }, }; /// Options for transactions -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(default)] pub struct TransactionOptions { - #[serde(default)] + /// The strategy applied for base coin remainders. pub remainder_value_strategy: RemainderValueStrategy, - #[serde(default)] + /// An optional tagged data payload. pub tagged_data_payload: Option, - #[serde(default)] - pub context_inputs: Option>, - // If custom inputs are provided only they are used. If also other additional inputs should be used, - // `mandatory_inputs` should be used instead. - #[serde(default)] - pub custom_inputs: Option>, - #[serde(default)] - pub mandatory_inputs: Option>, + /// Transaction context inputs to include. + pub context_inputs: Vec, + /// Inputs that must be used for the transaction. + pub required_inputs: BTreeSet, + /// Specifies what needs to be burned during input selection. pub burn: Option, + /// A string attached to the transaction. pub note: Option, - #[serde(default)] + /// Whether to allow sending a micro amount. pub allow_micro_amount: bool, - #[serde(default)] + /// Whether to allow the selection of additional inputs for this transaction. + pub allow_additional_input_selection: bool, + /// Transaction capabilities. pub capabilities: Option, - #[serde(default)] - pub mana_allotments: Option>, + /// Mana allotments for the transaction. + pub mana_allotments: BTreeMap, + /// Optional block issuer to which the transaction will have required mana allotted. + pub issuer_id: Option, +} + +impl Default for TransactionOptions { + fn default() -> Self { + Self { + remainder_value_strategy: Default::default(), + tagged_data_payload: Default::default(), + context_inputs: Default::default(), + required_inputs: Default::default(), + burn: Default::default(), + note: Default::default(), + allow_micro_amount: false, + allow_additional_input_selection: true, + capabilities: Default::default(), + mana_allotments: Default::default(), + issuer_id: Default::default(), + } + } } #[allow(clippy::enum_variant_names)] diff --git a/sdk/src/wallet/operations/transaction/prepare_transaction.rs b/sdk/src/wallet/operations/transaction/prepare_transaction.rs index 04cc7d1371..54113ac0e7 100644 --- a/sdk/src/wallet/operations/transaction/prepare_transaction.rs +++ b/sdk/src/wallet/operations/transaction/prepare_transaction.rs @@ -1,18 +1,13 @@ -// Copyright 2021 IOTA Stiftung +// Copyright 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashSet; - use instant::Instant; use packable::bounded::TryIntoBoundedU16Error; use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, - types::block::{input::INPUT_COUNT_RANGE, output::Output}, - wallet::{ - operations::transaction::{RemainderValueStrategy, TransactionOptions}, - Wallet, - }, + types::block::{input::INPUT_COUNT_MAX, output::Output}, + wallet::{operations::transaction::TransactionOptions, Wallet}, }; impl Wallet @@ -27,7 +22,7 @@ where options: impl Into> + Send, ) -> crate::wallet::Result { log::debug!("[TRANSACTION] prepare_transaction"); - let options = options.into(); + let options = options.into().unwrap_or_default(); let outputs = outputs.into(); let prepare_transaction_start_time = Instant::now(); let storage_score_params = self.client().get_storage_score_parameters().await?; @@ -37,56 +32,13 @@ where output.verify_storage_deposit(storage_score_params)?; } - if let Some(custom_inputs) = options.as_ref().and_then(|options| options.custom_inputs.as_ref()) { - // validate inputs amount - if !INPUT_COUNT_RANGE.contains(&(custom_inputs.len() as u16)) { - return Err(crate::types::block::Error::InvalidInputCount( - TryIntoBoundedU16Error::Truncated(custom_inputs.len()), - ))?; - } - } - - if let Some(mandatory_inputs) = options.as_ref().and_then(|options| options.mandatory_inputs.as_ref()) { - // validate inputs amount - if !INPUT_COUNT_RANGE.contains(&(mandatory_inputs.len() as u16)) { - return Err(crate::types::block::Error::InvalidInputCount( - TryIntoBoundedU16Error::Truncated(mandatory_inputs.len()), - ))?; - } + if options.required_inputs.len() as u16 > INPUT_COUNT_MAX { + return Err(crate::types::block::Error::InvalidInputCount( + TryIntoBoundedU16Error::Truncated(options.required_inputs.len()), + ))?; } - let remainder_address = options - .as_ref() - .and_then(|options| match &options.remainder_value_strategy { - RemainderValueStrategy::ReuseAddress => None, - RemainderValueStrategy::CustomAddress(address) => Some(address.clone()), - }); - - let selected_transaction_data = self - .select_inputs( - outputs, - options - .as_ref() - .and_then(|options| options.custom_inputs.as_ref()) - .map(|inputs| HashSet::from_iter(inputs.clone())), - options - .as_ref() - .and_then(|options| options.mandatory_inputs.as_ref()) - .map(|inputs| HashSet::from_iter(inputs.clone())), - remainder_address, - options.as_ref().and_then(|options| options.burn.as_ref()), - options.as_ref().and_then(|options| options.mana_allotments.clone()), - ) - .await?; - - let prepared_transaction_data = match self.build_transaction(selected_transaction_data.clone(), options).await { - Ok(res) => res, - Err(err) => { - // unlock outputs so they are available for a new transaction - self.unlock_inputs(&selected_transaction_data.inputs).await?; - return Err(err); - } - }; + let prepared_transaction_data = self.select_inputs(outputs, options).await?; log::debug!( "[TRANSACTION] finished prepare_transaction in {:.2?}", diff --git a/sdk/src/wallet/types/mod.rs b/sdk/src/wallet/types/mod.rs index e6e3b207f1..55a1343151 100644 --- a/sdk/src/wallet/types/mod.rs +++ b/sdk/src/wallet/types/mod.rs @@ -56,12 +56,12 @@ impl OutputData { pub fn input_signing_data( &self, wallet_data: &WalletData, - slot_index: impl Into, + commitment_slot_index: impl Into, committable_age_range: CommittableAgeRange, ) -> crate::wallet::Result> { let required_address = self .output - .required_address(slot_index.into(), committable_age_range)? + .required_address(commitment_slot_index.into(), committable_age_range)? .ok_or(crate::client::Error::ExpirationDeadzone)?; let chain = if let Some(required_ed25519) = required_address.backing_ed25519() { diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index bc5559726d..64a55c4fcb 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -4,11 +4,19 @@ use std::str::FromStr; use iota_sdk::{ - client::api::input_selection::{Burn, Error, InputSelection, Requirement}, + client::{ + api::input_selection::{Burn, Error, InputSelection, Requirement}, + secret::types::InputSigningData, + }, types::block::{ address::Address, - output::{AccountId, AccountOutputBuilder, Output}, + mana::ManaAllotment, + output::{ + feature::SenderFeature, unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, + BasicOutputBuilder, Output, + }, protocol::protocol_parameters, + rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, }; use pretty_assertions::{assert_eq, assert_ne}; @@ -17,7 +25,7 @@ use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic}, ACCOUNT_ID_0, ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ACCOUNT_1, BECH32_ADDRESS_ED25519_0, - BECH32_ADDRESS_ED25519_1, BECH32_ADDRESS_NFT_1, SLOT_INDEX, + BECH32_ADDRESS_ED25519_1, BECH32_ADDRESS_NFT_1, SLOT_COMMITMENT_ID, SLOT_INDEX, }; #[test] @@ -51,13 +59,14 @@ fn input_account_eq_output_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -92,13 +101,14 @@ fn transition_account_id_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } // #[test] @@ -230,9 +240,9 @@ fn transition_account_id_zero() { // .select() // .unwrap(); -// assert!(unsorted_eq(&selected.inputs, &inputs)); +// assert!(unsorted_eq(&selected.inputs_data, &inputs)); // // basic output + account remainder -// assert_eq!(selected.outputs.len(), 2); +// assert_eq!(selected.transaction.outputs().len(), 2); // } #[test] @@ -268,16 +278,17 @@ fn create_account() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder - assert_eq!(selected.outputs.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); // Output contains the new minted account id - assert!(selected.outputs.iter().any(|output| { + assert!(selected.transaction.outputs().iter().any(|output| { if let Output::Account(account_output) = output { *account_output.account_id() == account_id_0 } else { @@ -319,14 +330,15 @@ fn burn_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_2)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } // #[test] @@ -407,6 +419,7 @@ fn missing_input_for_account_output() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -463,6 +476,7 @@ fn missing_input_for_account_output_2() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -506,6 +520,7 @@ fn missing_input_for_account_output_but_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -564,13 +579,14 @@ fn account_in_output_and_sender() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -604,6 +620,7 @@ fn missing_ed25519_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -647,6 +664,7 @@ fn missing_ed25519_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -688,6 +706,7 @@ fn missing_ed25519_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -726,6 +745,7 @@ fn missing_account_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -769,6 +789,7 @@ fn missing_account_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -810,6 +831,7 @@ fn missing_account_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -848,6 +870,7 @@ fn missing_nft_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -891,6 +914,7 @@ fn missing_nft_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -932,6 +956,7 @@ fn missing_nft_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -984,13 +1009,14 @@ fn increase_account_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1038,16 +1064,17 @@ fn decrease_account_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1106,14 +1133,15 @@ fn prefer_basic_to_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -1163,15 +1191,16 @@ fn take_amount_from_account_to_fund_basic() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_account()); assert_eq!(output.amount(), 1_800_000); @@ -1234,17 +1263,18 @@ fn account_burn_should_validate_account_sender() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1303,17 +1333,18 @@ fn account_burn_should_validate_account_address() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1358,15 +1389,16 @@ fn transitioned_zero_account_id_no_longer_is_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_account()); assert_eq!(output.amount(), 1_000_000); @@ -1428,17 +1460,19 @@ fn two_accounts_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!( selected - .outputs + .transaction + .outputs() .iter() .any(|output| if let Output::Account(output) = output { output.account_id() == &account_id_1 @@ -1448,7 +1482,8 @@ fn two_accounts_required() { ); assert!( selected - .outputs + .transaction + .outputs() .iter() .any(|output| if let Output::Account(output) = output { output.account_id() == &account_id_2 @@ -1491,14 +1526,15 @@ fn state_controller_sender_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -1543,14 +1579,15 @@ fn state_controller_sender_required_already_selected() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[0].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1584,14 +1621,15 @@ fn state_transition_and_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[0].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1625,15 +1663,16 @@ fn remainder_address_in_state_controller() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1644,3 +1683,391 @@ fn remainder_address_in_state_controller() { } }); } + +#[test] +fn min_allot_account_mana() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let mut inputs = Vec::new(); + let mana_input_amount = 1_000_000; + + let account_output = AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(mana_input_amount) + .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 outputs = build_outputs([Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: Some(Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()), + sdruc: None, + timelock: None, + expiration: None, + }]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 500) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert_eq!(selected.transaction.allotments().len(), 1); + let mana_cost = 230_000; + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, mana_cost).unwrap() + ); + assert_eq!( + selected.transaction.outputs()[1].as_account().mana(), + mana_input_amount - mana_cost + ); +} + +#[test] +fn min_allot_account_mana_additional() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let additional_allotment = 1000; + let txn_required_mana_allotment = 240_000; + // The account does not have enough to cover the requirement + let account_mana = txn_required_mana_allotment - 100; + // But there is additional available mana elsewhere + let additional_available_mana = 111; + + let inputs = [ + AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(account_mana) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(additional_available_mana) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = [BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .add_feature(SenderFeature::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap()]; + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 500) + .with_mana_allotments(Some((account_id_1, additional_allotment))) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + + assert_eq!(selected.transaction.allotments().len(), 1); + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, txn_required_mana_allotment).unwrap() + ); + assert_eq!( + selected.transaction.outputs().iter().map(|o| o.mana()).sum::(), + account_mana + additional_available_mana - txn_required_mana_allotment + ); +} + +#[test] +fn min_allot_account_mana_cannot_select_additional() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + let account_id_2 = AccountId::from_str(ACCOUNT_ID_2).unwrap(); + + let additional_allotment = 1000; + let txn_required_mana_allotment = 271_000; + // The account does not have enough to cover the requirement + let account_mana = txn_required_mana_allotment - 100; + // But there is additional available mana elsewhere + let additional_available_mana = additional_allotment + 111; + + let inputs = [ + AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(account_mana) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(additional_available_mana) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 500) + .with_mana_allotments(Some((account_id_2, additional_allotment))) + .with_required_inputs([*inputs[0].output_id()]) + .disable_additional_input_selection() + .select() + .unwrap_err(); + + assert!( + matches!(selected, Error::AdditionalInputsRequired(_)), + "expected AdditionalInputsRequired, found {selected:?}" + ); +} + +#[test] +fn min_allot_account_mana_requirement_twice() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [ + AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(1000) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(100) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = build_outputs([Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: Some(Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()), + sdruc: None, + timelock: None, + expiration: None, + }]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 2) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert_eq!(selected.transaction.allotments().len(), 1); + let mana_cost = 960; + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, mana_cost).unwrap() + ); + assert_eq!(selected.transaction.outputs()[1].as_account().mana(), 140); +} + +#[test] +fn min_allot_account_mana_requirement_covered() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let additional_allotment = 1100; + + let inputs = [ + AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(1000) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(100) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = build_outputs([Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: Some(Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()), + sdruc: None, + timelock: None, + expiration: None, + }]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 2) + .with_mana_allotments(Some((account_id_1, additional_allotment))) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert_eq!(selected.transaction.allotments().len(), 1); + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, additional_allotment).unwrap() + ); + assert_eq!(selected.transaction.outputs()[1].as_account().mana(), 0); +} + +#[test] +fn min_allot_account_mana_requirement_covered_2() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let additional_allotment = 1100; + + let inputs = [ + AccountOutputBuilder::new_with_amount(2_000_000, account_id_1) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(100) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(1000) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = build_outputs([Basic { + amount: 1_000_000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: Some(Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()), + sdruc: None, + timelock: None, + expiration: None, + }]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 2) + .with_mana_allotments(Some((account_id_1, additional_allotment))) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert_eq!(selected.transaction.allotments().len(), 1); + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, additional_allotment).unwrap() + ); + assert_eq!(selected.transaction.outputs()[1].as_account().mana(), 0); +} diff --git a/sdk/tests/client/input_selection/basic_outputs.rs b/sdk/tests/client/input_selection/basic_outputs.rs index 0e86c5f178..6cc3658429 100644 --- a/sdk/tests/client/input_selection/basic_outputs.rs +++ b/sdk/tests/client/input_selection/basic_outputs.rs @@ -17,7 +17,8 @@ use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic, Nft}, ACCOUNT_ID_0, ACCOUNT_ID_1, BECH32_ADDRESS_ACCOUNT_1, BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1, - BECH32_ADDRESS_ED25519_2, BECH32_ADDRESS_NFT_1, BECH32_ADDRESS_REMAINDER, NFT_ID_0, NFT_ID_1, SLOT_INDEX, + BECH32_ADDRESS_ED25519_2, BECH32_ADDRESS_NFT_1, BECH32_ADDRESS_REMAINDER, NFT_ID_0, NFT_ID_1, SLOT_COMMITMENT_ID, + SLOT_INDEX, }; #[test] @@ -54,13 +55,14 @@ fn input_amount_equal_output_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -97,6 +99,7 @@ fn input_amount_lower_than_output_amount() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -158,6 +161,7 @@ fn input_amount_lower_than_output_amount_2() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -205,16 +209,17 @@ fn input_amount_greater_than_output_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -261,17 +266,18 @@ fn input_amount_greater_than_output_amount_with_remainder_address() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_remainder_address(remainder_address) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -331,17 +337,18 @@ fn two_same_inputs_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); // One input has enough amount. - assert_eq!(selected.inputs.len(), 1); + assert_eq!(selected.inputs_data.len(), 1); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -401,13 +408,14 @@ fn two_inputs_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs, [inputs[0].clone()]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data, [inputs[0].clone()]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -458,13 +466,14 @@ fn two_inputs_one_needed_reversed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs, [inputs[1].clone()]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data, [inputs[1].clone()]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -515,13 +524,14 @@ fn two_inputs_both_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -572,16 +582,17 @@ fn two_inputs_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -723,21 +734,22 @@ fn ed25519_sender() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); // Sender + another for amount - assert_eq!(selected.inputs.len(), 2); + assert_eq!(selected.inputs_data.len(), 2); assert!( selected - .inputs + .inputs_data .iter() .any(|input| *input.output.as_basic().address() == sender) ); // Provided output + remainder - assert_eq!(selected.outputs.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); } #[test] @@ -774,6 +786,7 @@ fn missing_ed25519_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -867,22 +880,23 @@ fn account_sender() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); // Sender + another for amount - assert_eq!(selected.inputs.len(), 2); + assert_eq!(selected.inputs_data.len(), 2); assert!( selected - .inputs + .inputs_data .iter() .any(|input| input.output.is_account() && *input.output.as_account().account_id() == account_id_1) ); // Provided output + account - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -933,16 +947,18 @@ fn account_sender_zero_id() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); assert!( selected - .outputs + .transaction + .outputs() .iter() .any(|output| output.is_account() && *output.as_account().account_id() == account_id) ); @@ -982,6 +998,7 @@ fn missing_account_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1077,23 +1094,24 @@ fn nft_sender() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); // Sender + another for amount - assert_eq!(selected.inputs.len(), 2); + assert_eq!(selected.inputs_data.len(), 2); assert!( selected - .inputs + .inputs_data .iter() .any(|input| input.output.is_nft() && *input.output.as_nft().nft_id() == nft_id_1) ); // Provided output + nft - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&inputs[2].output)); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&inputs[2].output)); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -1146,16 +1164,18 @@ fn nft_sender_zero_id() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); assert!( selected - .outputs + .transaction + .outputs() .iter() .any(|output| output.is_nft() && *output.as_nft().nft_id() == nft_id) ); @@ -1195,6 +1215,7 @@ fn missing_nft_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1239,15 +1260,16 @@ fn simple_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1373,13 +1395,14 @@ fn one_provided_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1416,6 +1439,7 @@ fn insufficient_amount() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1477,16 +1501,17 @@ fn two_inputs_remainder_2() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1546,15 +1571,16 @@ fn two_inputs_remainder_3() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1596,10 +1622,10 @@ fn two_inputs_remainder_3() { // .select() // .unwrap(); -// assert!(unsorted_eq(&selected.inputs, &inputs)); -// assert_eq!(selected.outputs.len(), 2); -// assert!(selected.outputs.contains(&outputs[0])); -// selected.outputs.iter().for_each(|output| { +// assert!(unsorted_eq(&selected.inputs_data, &inputs)); +// assert_eq!(selected.transaction.outputs().len(), 2); +// assert!(selected.transaction.outputs().contains(&outputs[0])); +// selected.transaction.outputs().iter().for_each(|output| { // if !outputs.contains(output) { // assert!(is_remainder_or_return(output, 800_000, // Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None)); } @@ -1643,14 +1669,15 @@ fn sender_already_selected() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[0].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1690,14 +1717,15 @@ fn single_mandatory_input() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[0].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1738,6 +1766,7 @@ fn too_many_inputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1804,13 +1833,14 @@ fn more_than_max_inputs_only_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &needed_input)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &needed_input)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1850,6 +1880,7 @@ fn too_many_outputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1898,6 +1929,7 @@ fn too_many_outputs_with_remainder() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1995,14 +2027,15 @@ fn restricted_ed25519() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs, [inputs[2].clone()]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data, [inputs[2].clone()]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -2056,14 +2089,15 @@ fn restricted_nft() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -2115,14 +2149,15 @@ fn restricted_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -2214,21 +2249,22 @@ fn restricted_ed25519_sender() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); // Sender + another for amount - assert_eq!(selected.inputs.len(), 2); + assert_eq!(selected.inputs_data.len(), 2); assert!( selected - .inputs + .inputs_data .iter() .any(|input| *input.output.as_basic().address() == sender) ); // Provided output + remainder - assert_eq!(selected.outputs.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); } #[test] @@ -2309,14 +2345,15 @@ fn multi_address_sender_already_fulfilled() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_2).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[0].output_id(), *inputs[1].output_id(), *inputs[2].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -2385,12 +2422,13 @@ fn ed25519_backed_available_address() { // Restricted address is provided, but it can also unlock the ed25519 one [restricted_address], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Provided outputs - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.transaction.outputs(), outputs); } diff --git a/sdk/tests/client/input_selection/burn.rs b/sdk/tests/client/input_selection/burn.rs index 98a1ebad26..f79687905f 100644 --- a/sdk/tests/client/input_selection/burn.rs +++ b/sdk/tests/client/input_selection/burn.rs @@ -19,8 +19,8 @@ use pretty_assertions::assert_eq; use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic, Foundry, Nft}, - ACCOUNT_ID_0, ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, NFT_ID_0, NFT_ID_1, NFT_ID_2, SLOT_INDEX, - TOKEN_ID_1, TOKEN_ID_2, + ACCOUNT_ID_0, ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, NFT_ID_0, NFT_ID_1, NFT_ID_2, + SLOT_COMMITMENT_ID, SLOT_INDEX, TOKEN_ID_1, TOKEN_ID_2, }; #[test] @@ -70,15 +70,16 @@ fn burn_account_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -128,6 +129,7 @@ fn burn_account_present_and_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) @@ -135,9 +137,9 @@ fn burn_account_present_and_required() { .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -190,15 +192,16 @@ fn burn_account_id_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id)) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -236,6 +239,7 @@ fn burn_account_absent() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) @@ -305,14 +309,15 @@ fn burn_accounts_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().set_accounts(HashSet::from([account_id_1, account_id_2]))) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -371,6 +376,7 @@ fn burn_account_in_outputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) @@ -431,15 +437,16 @@ fn burn_nft_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -491,6 +498,7 @@ fn burn_nft_present_and_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) @@ -498,9 +506,9 @@ fn burn_nft_present_and_required() { .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -551,15 +559,16 @@ fn burn_nft_id_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id)) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -597,6 +606,7 @@ fn burn_nft_absent() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) @@ -670,14 +680,15 @@ fn burn_nfts_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().set_nfts(HashSet::from([nft_id_1, nft_id_2]))) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -740,6 +751,7 @@ fn burn_nft_in_outputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) @@ -808,18 +820,19 @@ fn burn_foundry_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_foundry(inputs[0].output.as_foundry().id())) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert!(selected.inputs.contains(&inputs[0])); - assert!(selected.inputs.contains(&inputs[1])); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 2); + assert!(selected.inputs_data.contains(&inputs[0])); + assert!(selected.inputs_data.contains(&inputs[1])); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { if output.is_basic() { assert!(is_remainder_or_return( @@ -908,6 +921,7 @@ fn burn_foundry_absent() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_foundry(foundry_id_1)) @@ -974,6 +988,7 @@ fn burn_foundries_present() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().set_foundries(HashSet::from([ @@ -983,10 +998,10 @@ fn burn_foundries_present() { .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_account()); assert_eq!(output.amount(), 1_000_000); @@ -1059,6 +1074,7 @@ fn burn_foundry_in_outputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_foundry(foundry_id_1)) @@ -1106,9 +1122,10 @@ fn burn_native_tokens() { let selected = InputSelection::new( inputs.clone(), - Vec::new(), + None, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().set_native_tokens(HashMap::from([ @@ -1118,17 +1135,17 @@ fn burn_native_tokens() { .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); let nt_remainder_output_amount = 106000; assert!( is_remainder_or_return( - &selected.outputs[0], + &selected.transaction.outputs()[0], nt_remainder_output_amount, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 80)) ) && is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 2_000_000 - nt_remainder_output_amount, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_2, 70)) @@ -1193,6 +1210,7 @@ fn burn_foundry_and_its_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn( @@ -1203,13 +1221,13 @@ fn burn_foundry_and_its_account() { .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert!(selected.inputs.contains(&inputs[0])); - assert!(selected.inputs.contains(&inputs[1])); + assert_eq!(selected.inputs_data.len(), 2); + assert!(selected.inputs_data.contains(&inputs[0])); + assert!(selected.inputs_data.contains(&inputs[1])); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, diff --git a/sdk/tests/client/input_selection/delegation_outputs.rs b/sdk/tests/client/input_selection/delegation_outputs.rs index cbece610ef..f9d8e2420f 100644 --- a/sdk/tests/client/input_selection/delegation_outputs.rs +++ b/sdk/tests/client/input_selection/delegation_outputs.rs @@ -22,7 +22,7 @@ use iota_sdk::{ }; use pretty_assertions::assert_eq; -use crate::client::{BECH32_ADDRESS_ED25519_0, SLOT_INDEX}; +use crate::client::{BECH32_ADDRESS_ED25519_0, SLOT_COMMITMENT_ID, SLOT_INDEX}; #[test] fn remainder_needed_for_mana() { @@ -79,6 +79,7 @@ fn remainder_needed_for_mana() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters.clone(), ) .with_burn(Burn::from(delegation_id)) @@ -86,19 +87,19 @@ fn remainder_needed_for_mana() { .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert_eq!( mana_rewards + selected - .inputs + .inputs_data .iter() .map(|i| i .output .available_mana(&protocol_parameters, SlotIndex(0), SLOT_INDEX) .unwrap()) .sum::(), - selected.outputs.iter().map(|o| o.mana()).sum::() + selected.transaction.outputs().iter().map(|o| o.mana()).sum::() ); } diff --git a/sdk/tests/client/input_selection/expiration.rs b/sdk/tests/client/input_selection/expiration.rs index 42fbbffcd7..a074f221d8 100644 --- a/sdk/tests/client/input_selection/expiration.rs +++ b/sdk/tests/client/input_selection/expiration.rs @@ -9,7 +9,7 @@ use iota_sdk::{ address::Address, output::{AccountId, NftId}, protocol::protocol_parameters, - slot::SlotIndex, + slot::{SlotCommitmentHash, SlotIndex}, }, }; use pretty_assertions::assert_eq; @@ -55,6 +55,7 @@ fn one_output_expiration_not_expired() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select(); @@ -96,13 +97,14 @@ fn expiration_equal_timestamp() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 200, + SlotCommitmentHash::null().into_slot_commitment_id(199), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -139,13 +141,14 @@ fn one_output_expiration_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -196,14 +199,15 @@ fn two_outputs_one_expiration_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -254,14 +258,15 @@ fn two_outputs_one_unexpired_one_missing() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -324,14 +329,15 @@ fn two_outputs_two_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_2).unwrap()], 200, + SlotCommitmentHash::null().into_slot_commitment_id(199), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -385,13 +391,14 @@ fn two_outputs_two_expired_2() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_2).unwrap(), ], 200, + SlotCommitmentHash::null().into_slot_commitment_id(199), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -428,13 +435,14 @@ fn expiration_expired_with_sdr() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -471,13 +479,14 @@ fn expiration_expired_with_sdr_2() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -514,13 +523,14 @@ fn expiration_expired_with_sdr_and_timelock() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -557,13 +567,14 @@ fn expiration_expired_with_sdr_and_timelock_2() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -653,14 +664,15 @@ fn sender_in_expiration() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[2])); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[2])); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -700,14 +712,15 @@ fn sender_in_expiration_already_selected() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .with_required_inputs([*inputs[0].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -747,15 +760,16 @@ fn remainder_in_expiration() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -801,13 +815,14 @@ fn expiration_expired_non_ed25519_in_address_unlock_condition() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -858,13 +873,14 @@ fn expiration_expired_only_account_addresses() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); } #[test] @@ -902,13 +918,14 @@ fn one_nft_output_expiration_unexpired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -946,11 +963,12 @@ fn one_nft_output_expiration_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } diff --git a/sdk/tests/client/input_selection/foundry_outputs.rs b/sdk/tests/client/input_selection/foundry_outputs.rs index 77a6536f1d..5c98a5568a 100644 --- a/sdk/tests/client/input_selection/foundry_outputs.rs +++ b/sdk/tests/client/input_selection/foundry_outputs.rs @@ -23,7 +23,7 @@ use pretty_assertions::assert_eq; use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic, Foundry}, - ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, SLOT_INDEX, + ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, SLOT_COMMITMENT_ID, SLOT_INDEX, }; #[test] @@ -59,6 +59,7 @@ fn missing_input_account_for_foundry() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -102,9 +103,9 @@ fn missing_input_account_for_foundry() { // .select() // .unwrap(); -// assert!(unsorted_eq(&selected.inputs, &inputs)); +// assert!(unsorted_eq(&selected.inputs_data, &inputs)); // // Account next state + foundry -// assert_eq!(selected.outputs.len(), 2); +// assert_eq!(selected.transaction.outputs().len(), 2); // } #[test] @@ -152,15 +153,16 @@ fn minted_native_tokens_in_new_remainder() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state + foundry + basic output with native tokens - assert_eq!(selected.outputs.len(), 3); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 3); + selected.transaction.outputs().iter().for_each(|output| { if let Output::Basic(_basic_output) = &output { // Basic output remainder has the minted native tokens // TODO reenable when ISA supports NTs again @@ -227,16 +229,17 @@ fn minted_native_tokens_in_provided_output() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - assert!(selected.outputs.contains(&outputs[1])); - assert!(selected.outputs.iter().any(|output| output.is_account())); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert!(selected.transaction.outputs().contains(&outputs[1])); + assert!(selected.transaction.outputs().iter().any(|output| output.is_account())); } #[test] @@ -299,15 +302,16 @@ fn melt_native_tokens() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state + foundry + basic output with native tokens - assert_eq!(selected.outputs.len(), 3); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 3); + selected.transaction.outputs().iter().for_each(|output| { if let Output::Basic(_basic_output) = &output { // Basic output remainder has the remaining native tokens // TODO reenable when ISA supports NTs again @@ -358,15 +362,16 @@ fn destroy_foundry_with_account_state_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_foundry(inputs[1].output.as_foundry().id())) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state - assert_eq!(selected.outputs.len(), 1); + assert_eq!(selected.transaction.outputs().len(), 1); } #[test] @@ -414,6 +419,7 @@ fn destroy_foundry_with_account_burn() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn( @@ -424,10 +430,10 @@ fn destroy_foundry_with_account_burn() { .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -496,14 +502,15 @@ fn prefer_basic_to_foundry() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[2]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[2]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -564,17 +571,18 @@ fn simple_foundry_transition_basic_not_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert!(selected.inputs.contains(&inputs[1])); - assert!(selected.inputs.contains(&inputs[2])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 2); + assert!(selected.inputs_data.contains(&inputs[1])); + assert!(selected.inputs_data.contains(&inputs[2])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_account()); assert_eq!(output.amount(), 2_000_000); @@ -647,17 +655,18 @@ fn simple_foundry_transition_basic_not_needed_with_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert!(selected.inputs.contains(&inputs[1])); - assert!(selected.inputs.contains(&inputs[2])); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 2); + assert!(selected.inputs_data.contains(&inputs[1])); + assert!(selected.inputs_data.contains(&inputs[2])); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { if output.is_account() { assert_eq!(output.amount(), 2_000_000); @@ -713,11 +722,11 @@ fn simple_foundry_transition_basic_not_needed_with_remainder() { // .select() // .unwrap(); -// assert_eq!(selected.inputs.len(), 1); -// assert!(selected.inputs.contains(&inputs[2])); -// // assert_eq!(selected.outputs.len(), 3); -// // assert!(selected.outputs.contains(&outputs[0])); -// // selected.outputs.iter().for_each(|output| { +// assert_eq!(selected.inputs_data.len(), 1); +// assert!(selected.inputs_data.contains(&inputs[2])); +// // assert_eq!(selected.transaction.outputs().len(), 3); +// // assert!(selected.transaction.outputs().contains(&outputs[0])); +// // selected.transaction.outputs().iter().for_each(|output| { // // if !outputs.contains(output) { // // if output.is_account() { // // assert_eq!(output.amount(), 2_000_000); @@ -796,6 +805,7 @@ fn mint_and_burn_at_the_same_time() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_native_token(token_id, 10)) @@ -868,18 +878,24 @@ fn take_amount_from_account_and_foundry_to_fund_basic() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - assert!(selected.outputs.iter().any(|output| output.is_account())); - assert!(selected.outputs.iter().any(|output| output.is_foundry())); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert!(selected.transaction.outputs().iter().any(|output| output.is_account())); + assert!(selected.transaction.outputs().iter().any(|output| output.is_foundry())); assert_eq!( - selected.outputs.iter().map(|output| output.amount()).sum::(), + selected + .transaction + .outputs() + .iter() + .map(|output| output.amount()) + .sum::(), 4_000_000 ); } @@ -929,17 +945,18 @@ fn create_native_token_but_burn_account() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -996,6 +1013,7 @@ fn melted_tokens_not_provided() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1054,6 +1072,7 @@ fn burned_tokens_not_provided() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_native_token(token_id_1, 100)) @@ -1111,16 +1130,17 @@ fn foundry_in_outputs_and_required() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs([*inputs[1].output_id()]) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert_eq!(*output.as_account().account_id(), account_id_2); } @@ -1186,6 +1206,7 @@ fn melt_and_burn_native_tokens() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) // Burn 456 native tokens @@ -1193,11 +1214,11 @@ fn melt_and_burn_native_tokens() { .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state + foundry + basic output with native tokens - assert_eq!(selected.outputs.len(), 3); + assert_eq!(selected.transaction.outputs().len(), 3); // Account state index is increased - selected.outputs.iter().for_each(|output| { + selected.transaction.outputs().iter().for_each(|output| { if let Output::Basic(_basic_output) = &output { // Basic output remainder has the remaining native tokens // TODO reenable when ISA supports NTs again diff --git a/sdk/tests/client/input_selection/native_tokens.rs b/sdk/tests/client/input_selection/native_tokens.rs index ce62dbd0e1..2bd0267942 100644 --- a/sdk/tests/client/input_selection/native_tokens.rs +++ b/sdk/tests/client/input_selection/native_tokens.rs @@ -12,7 +12,7 @@ use primitive_types::U256; use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::Basic, BECH32_ADDRESS_ED25519_0, - SLOT_INDEX, TOKEN_ID_1, TOKEN_ID_2, + SLOT_COMMITMENT_ID, SLOT_INDEX, TOKEN_ID_1, TOKEN_ID_2, }; #[test] @@ -63,14 +63,15 @@ fn two_native_tokens_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs.len(), 1); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs().len(), 1); + assert!(selected.transaction.outputs().contains(&outputs[0])); } #[test] @@ -132,15 +133,16 @@ fn two_native_tokens_both_needed_plus_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -212,15 +214,16 @@ fn three_inputs_two_needed_plus_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -292,13 +295,14 @@ fn three_inputs_two_needed_no_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 2); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -335,6 +339,7 @@ fn insufficient_native_tokens_one_input() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -408,6 +413,7 @@ fn insufficient_native_tokens_three_inputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -469,6 +475,7 @@ fn burn_and_send_at_the_same_time() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn( @@ -479,10 +486,10 @@ fn burn_and_send_at_the_same_time() { .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -516,19 +523,20 @@ fn burn_one_input_no_output() { let selected = InputSelection::new( inputs.clone(), - Vec::new(), + None, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_native_token(TokenId::from_str(TOKEN_ID_1).unwrap(), 50)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 1); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 1); assert!(is_remainder_or_return( - &selected.outputs[0], + &selected.transaction.outputs()[0], 1_000_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 50)) @@ -583,14 +591,15 @@ fn multiple_native_tokens() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -627,6 +636,7 @@ fn insufficient_native_tokens() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -674,6 +684,7 @@ fn insufficient_native_tokens_2() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -721,6 +732,7 @@ fn insufficient_amount_for_remainder() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -768,13 +780,14 @@ fn single_output_native_token_no_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -811,16 +824,17 @@ fn single_output_native_token_remainder_1() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[0], + &selected.transaction.outputs()[0], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 50)) @@ -861,16 +875,17 @@ fn single_output_native_token_remainder_2() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None @@ -925,17 +940,18 @@ fn two_basic_outputs_1() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 100)), @@ -990,17 +1006,18 @@ fn two_basic_outputs_2() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 50)), @@ -1055,17 +1072,18 @@ fn two_basic_outputs_3() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 25)), @@ -1120,17 +1138,18 @@ fn two_basic_outputs_4() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, @@ -1185,17 +1204,18 @@ fn two_basic_outputs_5() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, @@ -1250,16 +1270,17 @@ fn two_basic_outputs_6() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 1_500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), Some((TOKEN_ID_1, 50)), @@ -1314,16 +1335,17 @@ fn two_basic_outputs_7() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 1_500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, @@ -1378,6 +1400,7 @@ fn two_basic_outputs_8() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -1439,17 +1462,18 @@ fn two_basic_outputs_native_tokens_not_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[1])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[1])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); assert!(is_remainder_or_return( - &selected.outputs[1], + &selected.transaction.outputs()[1], 500_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, @@ -1528,16 +1552,17 @@ fn multiple_remainders() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 4); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); + assert_eq!(selected.inputs_data.len(), 4); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); let nt_remainder_min_storage_deposit = 106000; - selected.outputs.iter().for_each(|output| { + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!( is_remainder_or_return( @@ -1611,12 +1636,12 @@ fn multiple_remainders() { // .select() // .unwrap(); -// assert_eq!(selected.inputs.len(), 1); -// assert!(selected.inputs.contains(&inputs[1])); -// assert_eq!(selected.outputs.len(), 2); -// assert!(selected.outputs.contains(&outputs[0])); +// assert_eq!(selected.inputs_data.len(), 1); +// assert!(selected.inputs_data.contains(&inputs[1])); +// assert_eq!(selected.transaction.outputs().len(), 2); +// assert!(selected.transaction.outputs().contains(&outputs[0])); // assert!(is_remainder_or_return( -// &selected.outputs[1], +// &selected.transaction.outputs()[1], // 5_000_000, // BECH32_ADDRESS_ED25519_0, // Some(input_native_tokens_1.iter().map(|(t, a)| (t.as_str(), *a)).collect()), diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index 115604d631..8ba0ab353c 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -21,7 +21,7 @@ use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Basic, Nft}, BECH32_ADDRESS_ACCOUNT_1, BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1, BECH32_ADDRESS_NFT_1, NFT_ID_0, - NFT_ID_1, NFT_ID_2, SLOT_INDEX, + NFT_ID_1, NFT_ID_2, SLOT_COMMITMENT_ID, SLOT_INDEX, }; #[test] @@ -59,13 +59,14 @@ fn input_nft_eq_output_nft() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -104,13 +105,14 @@ fn transition_nft_id_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } // #[test] @@ -196,9 +198,9 @@ fn transition_nft_id_zero() { // .select() // .unwrap(); -// assert!(unsorted_eq(&selected.inputs, &inputs)); +// assert!(unsorted_eq(&selected.inputs_data, &inputs)); // // basic output + nft remainder -// assert_eq!(selected.outputs.len(), 2); +// assert_eq!(selected.transaction.outputs().len(), 2); // } #[test] @@ -236,16 +238,17 @@ fn mint_nft() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder - assert_eq!(selected.outputs.len(), 2); + assert_eq!(selected.transaction.outputs().len(), 2); // Output contains the new minted nft id - assert!(selected.outputs.iter().any(|output| { + assert!(selected.transaction.outputs().iter().any(|output| { if let Output::Nft(nft_output) = output { *nft_output.nft_id() == nft_id_0 } else { @@ -289,14 +292,15 @@ fn burn_nft() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_2)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } // #[test] @@ -379,6 +383,7 @@ fn missing_input_for_nft_output() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -424,6 +429,7 @@ fn missing_input_for_nft_output_but_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -491,21 +497,22 @@ fn nft_in_output_and_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.iter().any(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().iter().any(|output| { if let Output::Nft(nft_output) = output { *nft_output.nft_id() == nft_id_1 } else { false } })); - assert!(selected.outputs.iter().any(|output| output.is_basic())); + assert!(selected.transaction.outputs().iter().any(|output| output.is_basic())); } #[test] @@ -543,6 +550,7 @@ fn missing_ed25519_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -588,6 +596,7 @@ fn missing_ed25519_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -633,6 +642,7 @@ fn missing_ed25519_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -675,6 +685,7 @@ fn missing_account_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -720,6 +731,7 @@ fn missing_account_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -765,6 +777,7 @@ fn missing_account_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -807,6 +820,7 @@ fn missing_nft_sender() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -852,6 +866,7 @@ fn missing_nft_issuer_created() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -897,6 +912,7 @@ fn missing_nft_issuer_transition() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -953,13 +969,14 @@ fn increase_nft_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1011,16 +1028,17 @@ fn decrease_nft_amount() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[0]); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[0]); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -1081,14 +1099,15 @@ fn prefer_basic_to_nft() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert_eq!(selected.outputs, outputs); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert_eq!(selected.transaction.outputs(), outputs); } #[test] @@ -1140,15 +1159,16 @@ fn take_amount_from_nft_to_fund_basic() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_nft()); assert_eq!(output.amount(), 1_800_000); @@ -1211,14 +1231,15 @@ fn nft_burn_should_validate_nft_sender() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1270,14 +1291,15 @@ fn nft_burn_should_validate_nft_address() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -1315,15 +1337,16 @@ fn transitioned_zero_nft_id_no_longer_is_zero() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_nft()); assert_eq!(output.amount(), 1_000_000); @@ -1392,6 +1415,7 @@ fn changed_immutable_metadata() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); diff --git a/sdk/tests/client/input_selection/outputs.rs b/sdk/tests/client/input_selection/outputs.rs index d04deec221..1f15170983 100644 --- a/sdk/tests/client/input_selection/outputs.rs +++ b/sdk/tests/client/input_selection/outputs.rs @@ -4,15 +4,23 @@ use std::{collections::HashSet, str::FromStr}; use iota_sdk::{ - client::api::input_selection::{Burn, Error, InputSelection}, - types::block::{address::Address, output::AccountId, protocol::protocol_parameters}, + client::{ + api::input_selection::{Burn, Error, InputSelection}, + secret::types::InputSigningData, + }, + types::block::{ + address::Address, + output::{unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder}, + protocol::protocol_parameters, + rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, + }, }; use pretty_assertions::assert_eq; use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic}, - ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1, SLOT_INDEX, + ACCOUNT_ID_1, ACCOUNT_ID_2, BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1, SLOT_COMMITMENT_ID, SLOT_INDEX, }; #[test] @@ -35,6 +43,7 @@ fn no_inputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -68,6 +77,7 @@ fn no_outputs() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -101,17 +111,18 @@ fn no_outputs_but_required_input() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_required_inputs(HashSet::from([*inputs[0].output_id()])) .select() .unwrap(); - assert_eq!(selected.inputs, inputs); + assert_eq!(selected.inputs_data, inputs); // Just a remainder - assert_eq!(selected.outputs.len(), 1); + assert_eq!(selected.transaction.outputs().len(), 1); assert!(is_remainder_or_return( - &selected.outputs[0], + &selected.transaction.outputs()[0], 1_000_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None @@ -143,16 +154,17 @@ fn no_outputs_but_burn() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_2)) .select() .unwrap(); - assert_eq!(selected.inputs, inputs); - assert_eq!(selected.outputs.len(), 1); + assert_eq!(selected.inputs_data, inputs); + assert_eq!(selected.transaction.outputs().len(), 1); assert!(is_remainder_or_return( - &selected.outputs[0], + &selected.transaction.outputs()[0], 2_000_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None @@ -188,7 +200,15 @@ fn no_address_provided() { expiration: None, }]); - let selected = InputSelection::new(inputs, outputs, [], SLOT_INDEX, protocol_parameters).select(); + let selected = InputSelection::new( + inputs, + outputs, + None, + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .select(); assert!(matches!(selected, Err(Error::NoAvailableInputsProvided))); } @@ -227,6 +247,7 @@ fn no_matching_address_provided() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -282,6 +303,7 @@ fn two_addresses_one_missing() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -346,11 +368,76 @@ fn two_addresses() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); +} + +#[test] +fn consolidate_with_min_allotment() { + let protocol_parameters = protocol_parameters(); + + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [ + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(2000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_minimum_amount(protocol_parameters.storage_score_parameters()) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let selected = InputSelection::new( + inputs.clone(), + None, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) + .with_min_mana_allotment(account_id_1, 10) + .with_required_inputs(inputs.iter().map(|i| *i.output_id())) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.transaction.outputs().len(), 1); + assert_eq!(selected.transaction.allotments().len(), 1); + assert_eq!(selected.transaction.allotments()[0].mana(), 5000); + assert_eq!(selected.transaction.outputs().iter().map(|o| o.mana()).sum::(), 0); } diff --git a/sdk/tests/client/input_selection/storage_deposit_return.rs b/sdk/tests/client/input_selection/storage_deposit_return.rs index 9d44ae17b5..52aeacc99b 100644 --- a/sdk/tests/client/input_selection/storage_deposit_return.rs +++ b/sdk/tests/client/input_selection/storage_deposit_return.rs @@ -13,7 +13,7 @@ use crate::client::{ build_inputs, build_outputs, is_remainder_or_return, unsorted_eq, Build::{Account, Basic}, ACCOUNT_ID_1, BECH32_ADDRESS_ACCOUNT_1, BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1, - BECH32_ADDRESS_ED25519_2, SLOT_INDEX, + BECH32_ADDRESS_ED25519_2, SLOT_COMMITMENT_ID, SLOT_INDEX, }; #[test] @@ -50,15 +50,16 @@ fn sdruc_output_not_provided_no_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -115,13 +116,14 @@ fn sdruc_output_provided_no_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -158,15 +160,16 @@ fn sdruc_output_provided_remainder() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -226,15 +229,16 @@ fn two_sdrucs_to_the_same_address_both_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -294,16 +298,17 @@ fn two_sdrucs_to_the_same_address_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -363,15 +368,16 @@ fn two_sdrucs_to_different_addresses_both_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - assert!(selected.outputs.iter().any(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert!(selected.transaction.outputs().iter().any(|output| { is_remainder_or_return( output, 1_000_000, @@ -379,7 +385,7 @@ fn two_sdrucs_to_different_addresses_both_needed() { None, ) })); - assert!(selected.outputs.iter().any(|output| { + assert!(selected.transaction.outputs().iter().any(|output| { is_remainder_or_return( output, 1_000_000, @@ -437,16 +443,17 @@ fn two_sdrucs_to_different_addresses_one_needed() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert!(selected.inputs.contains(&inputs[0])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 1); + assert!(selected.inputs_data.contains(&inputs[0])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -492,6 +499,7 @@ fn insufficient_amount_because_of_sdruc() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select(); @@ -556,15 +564,16 @@ fn useless_sdruc_required_for_sender_feature() { Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(), ], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(is_remainder_or_return( output, @@ -623,15 +632,16 @@ fn sdruc_required_non_ed25519_in_address_unlock() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert_eq!(selected.outputs.len(), 3); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 3); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) && !output.is_account() { assert!(is_remainder_or_return( output, @@ -702,17 +712,18 @@ fn useless_sdruc_non_ed25519_in_address_unlock() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], SLOT_INDEX, + SLOT_COMMITMENT_ID, protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 2); - assert!(selected.inputs.contains(&inputs[1])); - assert!(selected.inputs.contains(&inputs[2])); - assert_eq!(selected.outputs.len(), 2); - assert!(selected.outputs.contains(&outputs[0])); - selected.outputs.iter().for_each(|output| { + assert_eq!(selected.inputs_data.len(), 2); + assert!(selected.inputs_data.contains(&inputs[1])); + assert!(selected.inputs_data.contains(&inputs[2])); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs().contains(&outputs[0])); + selected.transaction.outputs().iter().for_each(|output| { if !outputs.contains(output) { assert!(output.is_account()); } diff --git a/sdk/tests/client/input_selection/timelock.rs b/sdk/tests/client/input_selection/timelock.rs index d1decc4387..974e40accb 100644 --- a/sdk/tests/client/input_selection/timelock.rs +++ b/sdk/tests/client/input_selection/timelock.rs @@ -3,7 +3,11 @@ use iota_sdk::{ client::api::input_selection::{Error, InputSelection}, - types::block::{address::Address, protocol::protocol_parameters, slot::SlotIndex}, + types::block::{ + address::Address, + protocol::protocol_parameters, + slot::{SlotCommitmentHash, SlotIndex}, + }, }; use pretty_assertions::assert_eq; @@ -45,6 +49,7 @@ fn one_output_timelock_not_expired() { outputs, [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select(); @@ -86,13 +91,14 @@ fn timelock_equal_timestamp() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 200, + SlotCommitmentHash::null().into_slot_commitment_id(199), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -143,14 +149,15 @@ fn two_outputs_one_timelock_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -201,14 +208,15 @@ fn two_outputs_one_timelocked_one_missing() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert_eq!(selected.inputs.len(), 1); - assert_eq!(selected.inputs[0], inputs[1]); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert_eq!(selected.inputs_data.len(), 1); + assert_eq!(selected.inputs_data[0], inputs[1]); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } #[test] @@ -245,11 +253,12 @@ fn one_output_timelock_expired() { outputs.clone(), [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], 100, + SlotCommitmentHash::null().into_slot_commitment_id(99), protocol_parameters, ) .select() .unwrap(); - assert!(unsorted_eq(&selected.inputs, &inputs)); - assert!(unsorted_eq(&selected.outputs, &outputs)); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } diff --git a/sdk/tests/client/mod.rs b/sdk/tests/client/mod.rs index c09a695be5..00643214d6 100644 --- a/sdk/tests/client/mod.rs +++ b/sdk/tests/client/mod.rs @@ -36,7 +36,7 @@ use iota_sdk::{ output::rand_output_metadata_with_id, transaction::{rand_transaction_id, rand_transaction_id_with_slot_index}, }, - slot::SlotIndex, + slot::{SlotCommitmentHash, SlotCommitmentId, SlotIndex}, }, }; @@ -59,6 +59,7 @@ const BECH32_ADDRESS_ACCOUNT_2: &str = "rms1pq3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3z const BECH32_ADDRESS_NFT_1: &str = "rms1zqg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zxddmy7"; // Corresponds to NFT_ID_1 const _BECH32_ADDRESS_NFT_2: &str = "rms1zq3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zynm6ctf"; // Corresponds to NFT_ID_2 const SLOT_INDEX: SlotIndex = SlotIndex(10); +const SLOT_COMMITMENT_ID: SlotCommitmentId = SlotCommitmentHash::null().const_into_slot_commitment_id(SlotIndex(9)); #[derive(Debug, Clone)] enum Build<'a> { diff --git a/sdk/tests/client/signing/mod.rs b/sdk/tests/client/signing/mod.rs index 6f69f01dbd..f08d6bfbe0 100644 --- a/sdk/tests/client/signing/mod.rs +++ b/sdk/tests/client/signing/mod.rs @@ -26,7 +26,7 @@ use iota_sdk::{ output::{AccountId, NftId}, payload::{signed_transaction::Transaction, SignedTransactionPayload}, protocol::protocol_parameters, - slot::{SlotCommitmentId, SlotIndex}, + slot::{SlotCommitmentHash, SlotCommitmentId, SlotIndex}, unlock::{SignatureUnlock, Unlock}, }, }; @@ -73,6 +73,7 @@ async fn all_combined() -> Result<()> { let nft_4 = Address::Nft(NftAddress::new(nft_id_4)); let slot_index = SlotIndex::from(90); + let slot_commitment_id = SlotCommitmentHash::null().into_slot_commitment_id(89); let inputs = build_inputs( [ @@ -372,6 +373,7 @@ async fn all_combined() -> Result<()> { outputs.clone(), [ed25519_0, ed25519_1, ed25519_2], slot_index, + slot_commitment_id, protocol_parameters.clone(), ) .select() @@ -383,7 +385,7 @@ async fn all_combined() -> Result<()> { ))]) .with_inputs( selected - .inputs + .inputs_data .iter() .map(|i| Input::Utxo(UtxoInput::from(*i.output_metadata.output_id()))) .collect::>(), @@ -394,7 +396,7 @@ async fn all_combined() -> Result<()> { let prepared_transaction_data = PreparedTransactionData { transaction, - inputs_data: selected.inputs, + inputs_data: selected.inputs_data, remainders: Vec::new(), mana_rewards: Default::default(), };