From 53125e043b16160304d3fb9d1b1de79be700593b Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Mon, 26 Feb 2024 13:30:53 -0500 Subject: [PATCH 01/10] Automatically set transaction capability flags in ISA --- bindings/nodejs/lib/types/client/burn.ts | 2 + .../lib/types/wallet/transaction-options.ts | 2 - bindings/python/iota_sdk/types/burn.py | 11 +- .../iota_sdk/types/transaction_options.py | 2 - .../api/block_builder/input_selection/burn.rs | 14 + .../api/block_builder/input_selection/mod.rs | 14 +- .../input_selection/remainder.rs | 29 +- .../input_selection/requirement/mana.rs | 15 + .../input_selection/requirement/mod.rs | 12 + .../requirement/native_tokens.rs | 9 +- .../operations/transaction/input_selection.rs | 4 - .../wallet/operations/transaction/options.rs | 5 +- .../client/input_selection/account_outputs.rs | 63 ++-- sdk/tests/client/input_selection/burn.rs | 282 ++++++++++++++++-- .../client/input_selection/foundry_outputs.rs | 45 ++- .../client/input_selection/native_tokens.rs | 9 + .../client/input_selection/nft_outputs.rs | 13 + sdk/tests/client/input_selection/outputs.rs | 5 + 18 files changed, 426 insertions(+), 110 deletions(-) diff --git a/bindings/nodejs/lib/types/client/burn.ts b/bindings/nodejs/lib/types/client/burn.ts index 6cfdf9da23..fc3af4b374 100644 --- a/bindings/nodejs/lib/types/client/burn.ts +++ b/bindings/nodejs/lib/types/client/burn.ts @@ -6,6 +6,8 @@ import { AccountId, FoundryId, NftId, TokenId } from '../block/id'; /** A DTO for [`Burn`] */ export interface Burn { + /** Mana to burn */ + mana?: boolean; /** Accounts to burn */ accounts?: AccountId[]; /** NFTs to burn */ diff --git a/bindings/nodejs/lib/types/wallet/transaction-options.ts b/bindings/nodejs/lib/types/wallet/transaction-options.ts index 100cde30f9..ae481d45a4 100644 --- a/bindings/nodejs/lib/types/wallet/transaction-options.ts +++ b/bindings/nodejs/lib/types/wallet/transaction-options.ts @@ -36,8 +36,6 @@ export interface TransactionOptions { * If this flag is disabled, additional inputs will be selected to cover the amount. */ allowAllottingFromAccountMana?: 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. */ diff --git a/bindings/python/iota_sdk/types/burn.py b/bindings/python/iota_sdk/types/burn.py index c176bd0c93..28e789a1fe 100644 --- a/bindings/python/iota_sdk/types/burn.py +++ b/bindings/python/iota_sdk/types/burn.py @@ -3,7 +3,8 @@ from __future__ import annotations # Allow reference to Burn in Burn class from typing import List, Optional -from dataclasses import dataclass +from dataclasses import dataclass, field +from dataclasses_json import config from iota_sdk.types.native_token import NativeToken from iota_sdk.types.common import HexStr, json @@ -14,17 +15,25 @@ class Burn: """A DTO for `Burn`. Attributes: + mana: Whether excess mana should be burned. accounts: The accounts to burn. nfts: The NFTs to burn. foundries: The foundries to burn. native_tokens: The native tokens to burn. """ + mana: bool accounts: Optional[List[HexStr]] = None nfts: Optional[List[HexStr]] = None foundries: Optional[List[HexStr]] = None native_tokens: Optional[List[NativeToken]] = None + def set_mana(self, mana: bool) -> Burn: + """Burn excess mana. + """ + self.mana = mana + return self + def add_account(self, account: HexStr) -> Burn: """Add an account to the burn. """ diff --git a/bindings/python/iota_sdk/types/transaction_options.py b/bindings/python/iota_sdk/types/transaction_options.py index 4223298f6e..0d85061d80 100644 --- a/bindings/python/iota_sdk/types/transaction_options.py +++ b/bindings/python/iota_sdk/types/transaction_options.py @@ -57,7 +57,6 @@ class TransactionOptions: allow_additional_input_selection: Whether to allow the selection of additional inputs for this transaction. allow_allotting_from_account_mana: Whether to allow allotting automatically calculated mana from the issuer account. If this flag is disabled, additional inputs will be selected to cover the amount. - capabilities: Transaction capabilities. mana_allotments: Mana allotments for the transaction. issuer_id: Optional block issuer to which the transaction will have required mana allotted. """ @@ -71,6 +70,5 @@ class TransactionOptions: allow_micro_amount: Optional[bool] = None allow_additional_input_selection: Optional[bool] = None allow_allotting_from_account_mana: Optional[bool] = None - capabilities: Optional[HexStr] = None mana_allotments: Optional[dict[HexStr, int]] = None issuer_id: Optional[HexStr] = None diff --git a/sdk/src/client/api/block_builder/input_selection/burn.rs b/sdk/src/client/api/block_builder/input_selection/burn.rs index c621b1dfb0..9952c0e503 100644 --- a/sdk/src/client/api/block_builder/input_selection/burn.rs +++ b/sdk/src/client/api/block_builder/input_selection/burn.rs @@ -14,6 +14,9 @@ use crate::types::block::output::{AccountId, DelegationId, FoundryId, NativeToke #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Burn { + // Whether excess mana should be burned. + #[serde(default)] + pub(crate) mana: bool, /// Accounts to burn. #[serde(default, skip_serializing_if = "HashSet::is_empty")] pub(crate) accounts: HashSet, @@ -37,6 +40,17 @@ impl Burn { Self::default() } + /// Sets the flag to [`Burn`] excess mana. + pub fn set_mana(mut self, burn_mana: bool) -> Self { + self.mana = burn_mana; + self + } + + /// Returns whether to [`Burn`] mana. + pub fn mana(&self) -> bool { + self.mana + } + /// Adds an account to [`Burn`]. pub fn add_account(mut self, account_id: AccountId) -> Self { self.accounts.insert(account_id); 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 1e5248ec49..cf1d3d2645 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -161,10 +161,7 @@ impl InputSelection { self.available_inputs .retain(|input| !self.forbidden_inputs.contains(input.output_id())); - // This is to avoid a borrow of self since there is a mutable borrow in the loop already. - let required_inputs = std::mem::take(&mut self.required_inputs); - - for required_input in required_inputs { + for required_input in self.required_inputs.clone() { // Checks that required input is not forbidden. if self.forbidden_inputs.contains(&required_input) { return Err(Error::RequiredInputIsForbidden(required_input)); @@ -439,15 +436,6 @@ impl InputSelection { 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.remainder_outputs()) } 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 c2cfcc4d67..7cd80d9ee0 100644 --- a/sdk/src/client/api/block_builder/input_selection/remainder.rs +++ b/sdk/src/client/api/block_builder/input_selection/remainder.rs @@ -125,11 +125,6 @@ impl InputSelection { 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"); - return Ok((storage_deposit_returns, Vec::new())); - } - let amount_diff = input_amount .checked_sub(output_amount) .ok_or(BlockError::ConsumedAmountOverflow)?; @@ -137,19 +132,24 @@ impl InputSelection { .checked_sub(output_mana) .ok_or(BlockError::ConsumedManaOverflow)?; + // If we are burning mana, then we can subtract out the burned amount. + if self.burn.as_ref().map_or(false, |b| b.mana()) { + mana_diff = mana_diff.saturating_sub(self.initial_mana_excess()?); + } + 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 && self.output_for_added_mana_exists(&remainder_address) { + if mana_diff > 0 && 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())); - } + } + + if input_amount == output_amount && mana_diff == 0 && native_tokens_diff.is_none() { + log::debug!("No remainder required"); + return Ok((storage_deposit_returns, Vec::new())); } let remainder_outputs = create_remainder_outputs( @@ -237,10 +237,15 @@ impl InputSelection { 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 + let mut mana_remainder = selected_mana > required_mana && remainder_address.map_or(true, |remainder_address| { !self.output_for_added_mana_exists(&remainder_address) }); + // If we are burning mana, we may not need a mana remainder + if self.burn.as_ref().map_or(false, |b| b.mana()) { + let initial_excess = self.initial_mana_excess()?; + mana_remainder &= selected_mana > required_mana + initial_excess; + } Ok((remainder_amount, native_tokens_remainder, mana_remainder)) } 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 53637fb508..946aa6fe9a 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 @@ -262,6 +262,21 @@ impl InputSelection { Ok(added_inputs) } + pub(crate) fn initial_mana_excess(&self) -> Result { + let output_mana = self.provided_outputs.iter().map(|o| o.mana()).sum::(); + let mut input_mana = 0; + + for input in self + .selected_inputs + .iter() + .filter(|i| self.required_inputs.contains(i.output_id())) + { + input_mana += self.total_mana(input)?; + } + + Ok(input_mana.saturating_sub(output_mana)) + } + pub(crate) fn mana_sums(&self, include_remainders: bool) -> Result<(u64, u64), Error> { let mut required_mana = self.non_remainder_outputs().map(|o| o.mana()).sum::() + self.mana_allotments.values().sum::(); 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 ac2751ae56..34a0e306ca 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 @@ -23,6 +23,7 @@ use crate::{ types::block::{ address::Address, output::{AccountId, ChainId, DelegationId, Features, FoundryId, NftId, Output}, + payload::signed_transaction::TransactionCapabilityFlag, }, }; @@ -163,6 +164,11 @@ impl InputSelection { /// Gets requirements from burn. pub(crate) fn burn_requirements(&mut self) -> Result<(), Error> { if let Some(burn) = self.burn.as_ref() { + if burn.mana() { + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::BurnMana); + } + for account_id in &burn.accounts { if self .non_remainder_outputs() @@ -174,6 +180,8 @@ impl InputSelection { let requirement = Requirement::Account(*account_id); log::debug!("Adding {requirement:?} from burn"); self.requirements.push(requirement); + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::DestroyAccountOutputs); } for foundry_id in &burn.foundries { @@ -187,6 +195,8 @@ impl InputSelection { let requirement = Requirement::Foundry(*foundry_id); log::debug!("Adding {requirement:?} from burn"); self.requirements.push(requirement); + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::DestroyFoundryOutputs); } for nft_id in &burn.nfts { @@ -200,6 +210,8 @@ impl InputSelection { let requirement = Requirement::Nft(*nft_id); log::debug!("Adding {requirement:?} from burn"); self.requirements.push(requirement); + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::DestroyNftOutputs); } for delegation_id in &burn.delegations { 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 aa0fef119e..7782caa61e 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 @@ -8,7 +8,10 @@ use primitive_types::U256; use super::{Error, InputSelection}; use crate::{ client::secret::types::InputSigningData, - types::block::output::{NativeToken, NativeTokens, NativeTokensBuilder, Output, TokenScheme}, + types::block::{ + output::{NativeToken, NativeTokens, NativeTokensBuilder, Output, TokenScheme}, + payload::signed_transaction::TransactionCapabilityFlag, + }, }; pub(crate) fn get_native_tokens<'a>(outputs: impl Iterator) -> Result { @@ -60,6 +63,10 @@ impl InputSelection { output_native_tokens.merge(melted_native_tokens)?; if let Some(burn) = self.burn.as_ref() { + if !burn.native_tokens.is_empty() { + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::BurnNativeTokens); + } output_native_tokens.merge(NativeTokensBuilder::from(burn.native_tokens.clone()))?; } diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index 2c49c5418e..b7c56c88f6 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -162,10 +162,6 @@ where input_selection = input_selection.disable_additional_input_selection(); } - if let Some(capabilities) = options.capabilities { - input_selection = input_selection.with_transaction_capabilities(capabilities) - } - let prepared_transaction_data = input_selection.select()?; validate_transaction_length(&prepared_transaction_data.transaction)?; diff --git a/sdk/src/wallet/operations/transaction/options.rs b/sdk/src/wallet/operations/transaction/options.rs index 76f7ccbb3f..f7c7348927 100644 --- a/sdk/src/wallet/operations/transaction/options.rs +++ b/sdk/src/wallet/operations/transaction/options.rs @@ -11,7 +11,7 @@ use crate::{ address::Address, context_input::ContextInput, output::{AccountId, OutputId}, - payload::{signed_transaction::TransactionCapabilities, tagged_data::TaggedDataPayload}, + payload::tagged_data::TaggedDataPayload, }, }; @@ -39,8 +39,6 @@ pub struct TransactionOptions { /// Whether to allow allotting automatically calculated mana from the issuer account. /// If this flag is disabled, additional inputs will be selected to cover the amount. pub allow_allotting_from_account_mana: bool, - /// Transaction capabilities. - pub capabilities: Option, /// Mana allotments for the transaction. pub mana_allotments: BTreeMap, /// Optional block issuer to which the transaction will have required mana allotted. @@ -59,7 +57,6 @@ impl Default for TransactionOptions { allow_micro_amount: false, allow_additional_input_selection: true, allow_allotting_from_account_mana: false, - capabilities: Default::default(), mana_allotments: Default::default(), issuer_id: Default::default(), } diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index 71989a3b20..e3785fb61f 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -14,6 +14,7 @@ use iota_sdk::{ output::{ unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, BasicOutputBuilder, Output, }, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, @@ -336,6 +337,10 @@ fn burn_account() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } @@ -1269,6 +1274,10 @@ fn account_burn_should_validate_account_sender() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. assert_eq!(selected.transaction.outputs().len(), 2); @@ -1468,28 +1477,24 @@ fn two_accounts_required() { 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| if let Output::Account(output) = output { - output.account_id() == &account_id_1 - } else { - false - }) - ); - assert!( - selected - .transaction - .outputs() - .iter() - .any(|output| if let Output::Account(output) = output { - output.account_id() == &account_id_2 - } else { - false - }) - ) + assert!(selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_1 + } else { + false + })); + assert!(selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_2 + } else { + false + })) } #[test] @@ -2109,14 +2114,12 @@ fn implicit_account_transition() { let input_output_id = *inputs[0].output_id(); let account_id = AccountId::from(&input_output_id); - let outputs = vec![ - AccountOutputBuilder::new_with_amount(1_000_000, account_id) - .add_unlock_condition(AddressUnlockCondition::new( - Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), - )) - .finish_output() - .unwrap(), - ]; + let outputs = vec![AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap()]; let selected = InputSelection::new( inputs.clone(), diff --git a/sdk/tests/client/input_selection/burn.rs b/sdk/tests/client/input_selection/burn.rs index 5616feaac3..4cbeabedb9 100644 --- a/sdk/tests/client/input_selection/burn.rs +++ b/sdk/tests/client/input_selection/burn.rs @@ -7,11 +7,19 @@ use std::{ }; 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, ChainId, NftId, SimpleTokenScheme, TokenId}, + output::{ + unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, BasicOutputBuilder, ChainId, + NftId, SimpleTokenScheme, TokenId, + }, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, + rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, }; use pretty_assertions::assert_eq; @@ -79,6 +87,10 @@ fn burn_account_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -139,6 +151,10 @@ fn burn_account_present_and_required() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -201,6 +217,10 @@ fn burn_account_id_zero() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -245,12 +265,13 @@ fn burn_account_absent() { protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( + assert_eq!( selected, - Err(Error::UnfulfillableRequirement(Requirement::Account(account_id))) if account_id == account_id_1 - )); + Error::UnfulfillableRequirement(Requirement::Account(account_id_1)) + ); } #[test] @@ -318,6 +339,10 @@ fn burn_accounts_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } @@ -382,12 +407,10 @@ fn burn_account_in_outputs() { protocol_parameters, ) .with_burn(Burn::new().add_account(account_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( - selected, - Err(Error::BurnAndTransition(ChainId::Account(account_id))) if account_id == account_id_1 - )); + assert_eq!(selected, Error::BurnAndTransition(ChainId::Account(account_id_1))); } #[test] @@ -446,6 +469,10 @@ fn burn_nft_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -508,6 +535,10 @@ fn burn_nft_present_and_required() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -568,6 +599,10 @@ fn burn_nft_id_zero() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert_eq!(selected.inputs_data.len(), 1); assert_eq!(selected.inputs_data[0], inputs[0]); assert_eq!(selected.transaction.outputs(), outputs); @@ -612,12 +647,10 @@ fn burn_nft_absent() { protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( - selected, - Err(Error::UnfulfillableRequirement(Requirement::Nft(nft_id))) if nft_id == nft_id_1 - )); + assert_eq!(selected, Error::UnfulfillableRequirement(Requirement::Nft(nft_id_1))); } #[test] @@ -689,6 +722,10 @@ fn burn_nfts_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } @@ -757,12 +794,10 @@ fn burn_nft_in_outputs() { protocol_parameters, ) .with_burn(Burn::new().add_nft(nft_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( - selected, - Err(Error::BurnAndTransition(ChainId::Nft(nft_id))) if nft_id == nft_id_1 - )); + assert_eq!(selected, Error::BurnAndTransition(ChainId::Nft(nft_id_1))); } #[test] @@ -829,6 +864,10 @@ fn burn_foundry_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyFoundryOutputs]) + ); assert_eq!(selected.inputs_data.len(), 2); assert!(selected.inputs_data.contains(&inputs[0])); assert!(selected.inputs_data.contains(&inputs[1])); @@ -927,12 +966,13 @@ fn burn_foundry_absent() { protocol_parameters, ) .with_burn(Burn::new().add_foundry(foundry_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( + assert_eq!( selected, - Err(Error::UnfulfillableRequirement(Requirement::Foundry(foundry_id))) if foundry_id == foundry_id_1 - )); + Error::UnfulfillableRequirement(Requirement::Foundry(foundry_id_1)) + ); } #[test] @@ -1000,6 +1040,10 @@ fn burn_foundries_present() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyFoundryOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert_eq!(selected.transaction.outputs().len(), 2); assert!(selected.transaction.outputs().contains(&outputs[0])); @@ -1080,12 +1124,10 @@ fn burn_foundry_in_outputs() { protocol_parameters, ) .with_burn(Burn::new().add_foundry(foundry_id_1)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( - selected, - Err(Error::BurnAndTransition(ChainId::Foundry(foundry_id))) if foundry_id == foundry_id_1 - )); + assert_eq!(selected, Error::BurnAndTransition(ChainId::Foundry(foundry_id_1))); } #[test] @@ -1139,6 +1181,10 @@ fn burn_native_tokens() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnNativeTokens]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert_eq!(selected.transaction.outputs().len(), 2); @@ -1224,6 +1270,13 @@ fn burn_foundry_and_its_account() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([ + TransactionCapabilityFlag::DestroyAccountOutputs, + TransactionCapabilityFlag::DestroyFoundryOutputs + ]) + ); assert_eq!(selected.inputs_data.len(), 2); assert!(selected.inputs_data.contains(&inputs[0])); assert!(selected.inputs_data.contains(&inputs[1])); @@ -1241,3 +1294,172 @@ fn burn_foundry_and_its_account() { } }); } + +#[test] +fn burn_mana() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + + let inputs = [BasicOutputBuilder::new_with_amount(1_000_000) + .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 outputs = [BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(500) + .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_required_inputs([*inputs[0].output_id()]) + .with_burn(Burn::new().set_mana(true)) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs(), &outputs); +} + +#[test] +fn burn_mana_need_additional() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(100_000) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(200) + .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_100_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(500) + .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_required_inputs([*inputs[0].output_id()]) + .with_burn(Burn::new().set_mana(true)) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 1); + assert_eq!(selected.transaction.outputs()[0].mana(), 700); +} + +#[test] +fn burn_mana_need_additional_account() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(100_000) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + AccountOutputBuilder::new_with_amount(1_200_000, account_id_1) + .with_mana(200) + .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_100_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(500) + .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_required_inputs([*inputs[0].output_id()]) + .with_burn(Burn::new().set_mana(true)) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert_eq!(selected.transaction.outputs()[0].mana(), 500); + assert_eq!(selected.transaction.outputs()[1].mana(), 200); +} diff --git a/sdk/tests/client/input_selection/foundry_outputs.rs b/sdk/tests/client/input_selection/foundry_outputs.rs index 6969fac375..e095fbf0ac 100644 --- a/sdk/tests/client/input_selection/foundry_outputs.rs +++ b/sdk/tests/client/input_selection/foundry_outputs.rs @@ -14,6 +14,7 @@ use iota_sdk::{ unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, FoundryId, FoundryOutputBuilder, Output, SimpleTokenScheme, TokenId, }, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, @@ -369,6 +370,10 @@ fn destroy_foundry_with_account_state_transition() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyFoundryOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state assert_eq!(selected.transaction.outputs().len(), 1); @@ -430,6 +435,13 @@ fn destroy_foundry_with_account_burn() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([ + TransactionCapabilityFlag::DestroyAccountOutputs, + TransactionCapabilityFlag::DestroyFoundryOutputs + ]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert_eq!(selected.transaction.outputs().len(), 2); assert!(selected.transaction.outputs().contains(&outputs[0])); @@ -809,12 +821,13 @@ fn mint_and_burn_at_the_same_time() { protocol_parameters, ) .with_burn(Burn::new().add_native_token(token_id, 10)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( + assert_eq!( selected, - Err(Error::UnfulfillableRequirement(Requirement::Foundry(id))) if id == foundry_id - )); + Error::UnfulfillableRequirement(Requirement::Foundry(foundry_id)) + ); } #[test] @@ -952,6 +965,10 @@ fn create_native_token_but_burn_account() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. assert_eq!(selected.transaction.outputs().len(), 2); @@ -1076,15 +1093,17 @@ fn burned_tokens_not_provided() { protocol_parameters, ) .with_burn(Burn::new().add_native_token(token_id_1, 100)) - .select(); + .select() + .unwrap_err(); - assert!(matches!( + assert_eq!( selected, - Err(Error::InsufficientNativeTokenAmount { - token_id, - found, - required, - }) if token_id == token_id_1 && found.as_u32() == 0 && required.as_u32() == 100)); + Error::InsufficientNativeTokenAmount { + token_id: token_id_1, + found: 0.into(), + required: 100.into(), + } + ); } #[test] @@ -1214,6 +1233,10 @@ fn melt_and_burn_native_tokens() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnNativeTokens]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); // Account next state + foundry + basic output with native tokens assert_eq!(selected.transaction.outputs().len(), 3); diff --git a/sdk/tests/client/input_selection/native_tokens.rs b/sdk/tests/client/input_selection/native_tokens.rs index 769c2bc3ea..e5870611a3 100644 --- a/sdk/tests/client/input_selection/native_tokens.rs +++ b/sdk/tests/client/input_selection/native_tokens.rs @@ -8,6 +8,7 @@ use iota_sdk::{ types::block::{ address::Address, output::{unlock_condition::AddressUnlockCondition, BasicOutputBuilder, NativeToken, TokenId}, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::{iota_mainnet_protocol_parameters, ProtocolParameters}, }, }; @@ -490,6 +491,10 @@ fn burn_and_send_at_the_same_time() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnNativeTokens]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert_eq!(selected.transaction.outputs().len(), 2); assert!(selected.transaction.outputs().contains(&outputs[0])); @@ -537,6 +542,10 @@ fn burn_one_input_no_output() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnNativeTokens]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert_eq!(selected.transaction.outputs().len(), 1); assert_remainder_or_return( diff --git a/sdk/tests/client/input_selection/nft_outputs.rs b/sdk/tests/client/input_selection/nft_outputs.rs index 59e9ea7154..6fbdcee3b8 100644 --- a/sdk/tests/client/input_selection/nft_outputs.rs +++ b/sdk/tests/client/input_selection/nft_outputs.rs @@ -11,6 +11,7 @@ use iota_sdk::{ types::block::{ address::Address, output::{feature::MetadataFeature, unlock_condition::AddressUnlockCondition, NftId, NftOutputBuilder, Output}, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, @@ -299,6 +300,10 @@ fn burn_nft() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } @@ -1238,6 +1243,10 @@ fn nft_burn_should_validate_nft_sender() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } @@ -1298,6 +1307,10 @@ fn nft_burn_should_validate_nft_address() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyNftOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); } diff --git a/sdk/tests/client/input_selection/outputs.rs b/sdk/tests/client/input_selection/outputs.rs index d92c814e08..22bab21048 100644 --- a/sdk/tests/client/input_selection/outputs.rs +++ b/sdk/tests/client/input_selection/outputs.rs @@ -11,6 +11,7 @@ use iota_sdk::{ types::block::{ address::Address, output::{unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder}, + payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, @@ -161,6 +162,10 @@ fn no_outputs_but_burn() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert_eq!(selected.inputs_data, inputs); assert_eq!(selected.transaction.outputs().len(), 1); assert_remainder_or_return( From 163cc25a3e96acfe1c118525c6291229bcd53202 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Mon, 26 Feb 2024 13:37:21 -0500 Subject: [PATCH 02/10] use initial mana excess when setting flag --- .../client/api/block_builder/input_selection/requirement/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 34a0e306ca..d0b88a7859 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 @@ -164,7 +164,7 @@ impl InputSelection { /// Gets requirements from burn. pub(crate) fn burn_requirements(&mut self) -> Result<(), Error> { if let Some(burn) = self.burn.as_ref() { - if burn.mana() { + if burn.mana() && self.initial_mana_excess()? > 0 { self.transaction_capabilities .add_capability(TransactionCapabilityFlag::BurnMana); } From ed6221e132efda1c4e58f96e3ea3d887807ef64d Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Mon, 26 Feb 2024 16:57:31 -0500 Subject: [PATCH 03/10] fmt and unused --- bindings/python/iota_sdk/types/burn.py | 3 +- .../client/input_selection/account_outputs.rs | 54 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/bindings/python/iota_sdk/types/burn.py b/bindings/python/iota_sdk/types/burn.py index 28e789a1fe..fd57a5b995 100644 --- a/bindings/python/iota_sdk/types/burn.py +++ b/bindings/python/iota_sdk/types/burn.py @@ -3,8 +3,7 @@ from __future__ import annotations # Allow reference to Burn in Burn class from typing import List, Optional -from dataclasses import dataclass, field -from dataclasses_json import config +from dataclasses import dataclass from iota_sdk.types.native_token import NativeToken from iota_sdk.types.common import HexStr, json diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index e3785fb61f..309ca4ac57 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -1477,24 +1477,28 @@ fn two_accounts_required() { 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| if let Output::Account(output) = output { - output.account_id() == &account_id_1 - } else { - false - })); - assert!(selected - .transaction - .outputs() - .iter() - .any(|output| if let Output::Account(output) = output { - output.account_id() == &account_id_2 - } else { - false - })) + assert!( + selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_1 + } else { + false + }) + ); + assert!( + selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_2 + } else { + false + }) + ) } #[test] @@ -2114,12 +2118,14 @@ fn implicit_account_transition() { let input_output_id = *inputs[0].output_id(); let account_id = AccountId::from(&input_output_id); - let outputs = vec![AccountOutputBuilder::new_with_amount(1_000_000, account_id) - .add_unlock_condition(AddressUnlockCondition::new( - Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), - )) - .finish_output() - .unwrap()]; + let outputs = vec![ + AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; let selected = InputSelection::new( inputs.clone(), From b374261de39db31e08f0745a0b25cffeb734c28c Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 27 Feb 2024 09:48:43 +0100 Subject: [PATCH 04/10] python: Optional mana --- bindings/python/iota_sdk/types/burn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/iota_sdk/types/burn.py b/bindings/python/iota_sdk/types/burn.py index fd57a5b995..3072eb9734 100644 --- a/bindings/python/iota_sdk/types/burn.py +++ b/bindings/python/iota_sdk/types/burn.py @@ -21,7 +21,7 @@ class Burn: native_tokens: The native tokens to burn. """ - mana: bool + mana: Optional[bool] = False accounts: Optional[List[HexStr]] = None nfts: Optional[List[HexStr]] = None foundries: Optional[List[HexStr]] = None From d0ba7c1a9853b9778f285407b1c1e1293ae85f75 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Tue, 27 Feb 2024 10:54:09 -0500 Subject: [PATCH 05/10] burn generated mana --- bindings/nodejs/lib/types/client/burn.ts | 4 +- bindings/python/iota_sdk/types/burn.py | 15 +- .../api/block_builder/input_selection/burn.rs | 14 ++ .../api/block_builder/input_selection/mod.rs | 15 +- .../input_selection/requirement/mana.rs | 32 ++-- .../input_selection/transition.rs | 24 ++- sdk/tests/client/input_selection/burn.rs | 164 ++++++++++++++++++ 7 files changed, 244 insertions(+), 24 deletions(-) diff --git a/bindings/nodejs/lib/types/client/burn.ts b/bindings/nodejs/lib/types/client/burn.ts index fc3af4b374..b720e31766 100644 --- a/bindings/nodejs/lib/types/client/burn.ts +++ b/bindings/nodejs/lib/types/client/burn.ts @@ -6,8 +6,10 @@ import { AccountId, FoundryId, NftId, TokenId } from '../block/id'; /** A DTO for [`Burn`] */ export interface Burn { - /** Mana to burn */ + /** Burn initial excess mana */ mana?: boolean; + /** Burn generated mana */ + generatedMana?: boolean; /** Accounts to burn */ accounts?: AccountId[]; /** NFTs to burn */ diff --git a/bindings/python/iota_sdk/types/burn.py b/bindings/python/iota_sdk/types/burn.py index 3072eb9734..c6dd7321d7 100644 --- a/bindings/python/iota_sdk/types/burn.py +++ b/bindings/python/iota_sdk/types/burn.py @@ -14,7 +14,8 @@ class Burn: """A DTO for `Burn`. Attributes: - mana: Whether excess mana should be burned. + mana: Whether initial excess mana should be burned. + generated_mana: Whether generated mana should be burned. accounts: The accounts to burn. nfts: The NFTs to burn. foundries: The foundries to burn. @@ -27,10 +28,16 @@ class Burn: foundries: Optional[List[HexStr]] = None native_tokens: Optional[List[NativeToken]] = None - def set_mana(self, mana: bool) -> Burn: - """Burn excess mana. + def set_mana(self, burn_mana: bool) -> Burn: + """Burn excess initial mana. """ - self.mana = mana + self.mana = burn_mana + return self + + def set_generated_mana(self, burn_generated_mana: bool) -> Burn: + """Burn generated mana. + """ + self.generated_mana = burn_generated_mana return self def add_account(self, account: HexStr) -> Burn: diff --git a/sdk/src/client/api/block_builder/input_selection/burn.rs b/sdk/src/client/api/block_builder/input_selection/burn.rs index 9952c0e503..fd6852b1da 100644 --- a/sdk/src/client/api/block_builder/input_selection/burn.rs +++ b/sdk/src/client/api/block_builder/input_selection/burn.rs @@ -17,6 +17,9 @@ pub struct Burn { // Whether excess mana should be burned. #[serde(default)] pub(crate) mana: bool, + // Whether generated mana should be burned. + #[serde(default)] + pub(crate) generated_mana: bool, /// Accounts to burn. #[serde(default, skip_serializing_if = "HashSet::is_empty")] pub(crate) accounts: HashSet, @@ -51,6 +54,17 @@ impl Burn { self.mana } + /// Sets the flag to [`Burn`] generated mana. + pub fn set_generated_mana(mut self, burn_generated_mana: bool) -> Self { + self.generated_mana = burn_generated_mana; + self + } + + /// Returns whether to [`Burn`] generated mana. + pub fn generated_mana(&self) -> bool { + self.generated_mana + } + /// Adds an account to [`Burn`]. pub fn add_account(mut self, account_id: AccountId) -> Self { self.accounts.insert(account_id); 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 cf1d3d2645..3773e5e90a 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -32,7 +32,7 @@ use crate::{ NativeTokensBuilder, NftOutput, NftOutputBuilder, Output, OutputId, OUTPUT_COUNT_RANGE, }, payload::{ - signed_transaction::{Transaction, TransactionCapabilities}, + signed_transaction::{Transaction, TransactionCapabilities, TransactionCapabilityFlag}, TaggedDataPayload, }, protocol::{CommittableAgeRange, ProtocolParameters}, @@ -268,6 +268,19 @@ impl InputSelection { } } + // If we're burning generated mana, set the capability flag. + if self.burn.as_ref().map_or(false, |b| b.generated_mana()) { + // Get the mana sums with generated mana to see whether there's a difference. + if !self + .transaction_capabilities + .has_capability(TransactionCapabilityFlag::BurnMana) + && input_mana < self.total_selected_mana(true)? + { + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::BurnMana); + } + } + let outputs = self .provided_outputs .into_iter() 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 946aa6fe9a..eb31129000 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 @@ -248,7 +248,7 @@ 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 += self.total_mana(&input)?; + selected_mana += self.total_mana(&input, None)?; if let Some(output) = self.select_input(input)? { required_mana += output.mana(); } @@ -271,7 +271,7 @@ impl InputSelection { .iter() .filter(|i| self.required_inputs.contains(i.output_id())) { - input_mana += self.total_mana(input)?; + input_mana += self.total_mana(input, None)?; } Ok(input_mana.saturating_sub(output_mana)) @@ -286,20 +286,32 @@ impl InputSelection { required_mana += self.remainder_outputs().map(|o| o.mana()).sum::() + self.remainders.added_mana; } + Ok((self.total_selected_mana(None)?, required_mana)) + } + + pub(crate) fn total_selected_mana(&self, include_generated: impl Into> + Copy) -> Result { let mut selected_mana = 0; for input in &self.selected_inputs { - selected_mana += self.total_mana(input)?; + selected_mana += self.total_mana(input, include_generated)?; } - Ok((selected_mana, required_mana)) + + Ok(selected_mana) } - fn total_mana(&self, input: &InputSigningData) -> Result { + fn total_mana(&self, input: &InputSigningData, include_generated: impl Into>) -> Result { + let include_generated = include_generated + .into() + .unwrap_or(self.burn.as_ref().map_or(true, |b| !b.generated_mana())); 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, - )?) + + if include_generated { + input.output.available_mana( + &self.protocol_parameters, + input.output_id().transaction_id().slot_index(), + self.creation_slot, + )? + } else { + input.output.mana() + }) } } 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 bbfa9f644f..cc86d3419d 100644 --- a/sdk/src/client/api/block_builder/input_selection/transition.rs +++ b/sdk/src/client/api/block_builder/input_selection/transition.rs @@ -7,9 +7,12 @@ use super::{ }; use crate::{ client::secret::types::InputSigningData, - types::block::output::{ - AccountOutput, AccountOutputBuilder, FoundryOutput, FoundryOutputBuilder, NftOutput, NftOutputBuilder, Output, - OutputId, + types::block::{ + output::{ + AccountOutput, AccountOutputBuilder, FoundryOutput, FoundryOutputBuilder, NftOutput, NftOutputBuilder, + Output, OutputId, + }, + payload::signed_transaction::TransactionCapabilityFlag, }, }; @@ -61,11 +64,16 @@ impl InputSelection { .with_features(features); if input.is_block_issuer() { - builder = builder.with_mana(input.available_mana( - &self.protocol_parameters, - output_id.transaction_id().slot_index(), - self.creation_slot, - )?) + if !self.burn.as_ref().map_or(false, |b| b.generated_mana()) { + builder = builder.with_mana(input.available_mana( + &self.protocol_parameters, + output_id.transaction_id().slot_index(), + self.creation_slot, + )?) + } else { + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::BurnMana); + } } let output = builder.finish_output()?; diff --git a/sdk/tests/client/input_selection/burn.rs b/sdk/tests/client/input_selection/burn.rs index 4cbeabedb9..39734d6250 100644 --- a/sdk/tests/client/input_selection/burn.rs +++ b/sdk/tests/client/input_selection/burn.rs @@ -20,6 +20,7 @@ use iota_sdk::{ payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::iota_mainnet_protocol_parameters, rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, + slot::SlotIndex, }, }; use pretty_assertions::assert_eq; @@ -1463,3 +1464,166 @@ fn burn_mana_need_additional_account() { assert_eq!(selected.transaction.outputs()[0].mana(), 500); assert_eq!(selected.transaction.outputs()[1].mana(), 200); } + +#[test] +fn burn_generated_mana() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(200) + .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(SlotIndex(5))), + chain: None, + }) + .collect::>(); + + let outputs = [BasicOutputBuilder::new_with_amount(2_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(1200) + .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_burn(Burn::new().set_generated_mana(true)) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert!(unsorted_eq(&selected.transaction.outputs(), &outputs)); +} + +#[test] +fn burn_generated_mana_remainder() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(200) + .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(SlotIndex(5))), + 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_burn(Burn::new().set_generated_mana(true)) + .with_required_inputs(inputs.iter().map(|i| *i.output_id())) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 1); + assert_eq!(selected.transaction.outputs()[0].mana(), 1200); +} + +#[test] +fn burn_generated_mana_account() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(1_000_000) + .with_mana(1000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + AccountOutputBuilder::new_with_amount(1_000_000, account_id_1) + .with_mana(200) + .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(SlotIndex(5))), + 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_burn(Burn::new().set_generated_mana(true)) + .with_required_inputs(inputs.iter().map(|i| *i.output_id())) + .select() + .unwrap(); + + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::BurnMana]) + ); + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert_eq!( + selected.transaction.outputs().iter().map(|o| o.mana()).sum::(), + 1200 + ); +} From a8a0ec3c6278a410baff361499c5bd67cee224b3 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Tue, 27 Feb 2024 14:33:34 -0500 Subject: [PATCH 06/10] docs --- bindings/nodejs/lib/types/client/burn.ts | 2 +- bindings/python/iota_sdk/types/burn.py | 7 ++++--- sdk/src/client/api/block_builder/input_selection/burn.rs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bindings/nodejs/lib/types/client/burn.ts b/bindings/nodejs/lib/types/client/burn.ts index b720e31766..8a518e4948 100644 --- a/bindings/nodejs/lib/types/client/burn.ts +++ b/bindings/nodejs/lib/types/client/burn.ts @@ -6,7 +6,7 @@ import { AccountId, FoundryId, NftId, TokenId } from '../block/id'; /** A DTO for [`Burn`] */ export interface Burn { - /** Burn initial excess mana */ + /** Burn initial excess mana (only from inputs/outputs that have been specified manually) */ mana?: boolean; /** Burn generated mana */ generatedMana?: boolean; diff --git a/bindings/python/iota_sdk/types/burn.py b/bindings/python/iota_sdk/types/burn.py index c6dd7321d7..c58398db15 100644 --- a/bindings/python/iota_sdk/types/burn.py +++ b/bindings/python/iota_sdk/types/burn.py @@ -14,7 +14,7 @@ class Burn: """A DTO for `Burn`. Attributes: - mana: Whether initial excess mana should be burned. + mana: Whether initial excess mana should be burned (only from inputs/outputs that have been specified manually). generated_mana: Whether generated mana should be burned. accounts: The accounts to burn. nfts: The NFTs to burn. @@ -22,14 +22,15 @@ class Burn: native_tokens: The native tokens to burn. """ - mana: Optional[bool] = False + mana: Optional[bool] = None + generated_mana: Optional[bool] = None accounts: Optional[List[HexStr]] = None nfts: Optional[List[HexStr]] = None foundries: Optional[List[HexStr]] = None native_tokens: Optional[List[NativeToken]] = None def set_mana(self, burn_mana: bool) -> Burn: - """Burn excess initial mana. + """Burn excess initial mana (only from inputs/outputs that have been specified manually). """ self.mana = burn_mana return self diff --git a/sdk/src/client/api/block_builder/input_selection/burn.rs b/sdk/src/client/api/block_builder/input_selection/burn.rs index fd6852b1da..03f1540d6f 100644 --- a/sdk/src/client/api/block_builder/input_selection/burn.rs +++ b/sdk/src/client/api/block_builder/input_selection/burn.rs @@ -14,7 +14,7 @@ use crate::types::block::output::{AccountId, DelegationId, FoundryId, NativeToke #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Burn { - // Whether excess mana should be burned. + // Whether initial excess mana should be burned (only from inputs/outputs that have been specified manually). #[serde(default)] pub(crate) mana: bool, // Whether generated mana should be burned. @@ -43,7 +43,7 @@ impl Burn { Self::default() } - /// Sets the flag to [`Burn`] excess mana. + /// Sets the flag to [`Burn`] initial excess mana. pub fn set_mana(mut self, burn_mana: bool) -> Self { self.mana = burn_mana; self From feb98a5d8c2848e70ebff3751a35828ae9c37281 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Wed, 28 Feb 2024 11:39:42 -0500 Subject: [PATCH 07/10] add check --- .../client/input_selection/account_outputs.rs | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index b046fb85b7..8543a93a2a 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -1348,6 +1348,10 @@ fn account_burn_should_validate_account_address() { .select() .unwrap(); + assert_eq!( + selected.transaction.capabilities(), + &TransactionCapabilities::from([TransactionCapabilityFlag::DestroyAccountOutputs]) + ); assert!(unsorted_eq(&selected.inputs_data, &inputs)); // One output should be added for the remainder. assert_eq!(selected.transaction.outputs().len(), 2); @@ -1477,28 +1481,24 @@ fn two_accounts_required() { 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| if let Output::Account(output) = output { - output.account_id() == &account_id_1 - } else { - false - }) - ); - assert!( - selected - .transaction - .outputs() - .iter() - .any(|output| if let Output::Account(output) = output { - output.account_id() == &account_id_2 - } else { - false - }) - ) + assert!(selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_1 + } else { + false + })); + assert!(selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_2 + } else { + false + })) } #[test] @@ -2118,14 +2118,12 @@ fn implicit_account_transition() { let input_output_id = *inputs[0].output_id(); let account_id = AccountId::from(&input_output_id); - let outputs = vec![ - AccountOutputBuilder::new_with_amount(1_000_000, account_id) - .add_unlock_condition(AddressUnlockCondition::new( - Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), - )) - .finish_output() - .unwrap(), - ]; + let outputs = vec![AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap()]; let selected = InputSelection::new( inputs.clone(), From 1af8ea5f1649c4c1fccb5b52cfa70b0011889950 Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Wed, 28 Feb 2024 12:29:13 -0500 Subject: [PATCH 08/10] cleaning --- .../input_selection/requirement/native_tokens.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 7782caa61e..be894bbecc 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 @@ -62,11 +62,9 @@ impl InputSelection { input_native_tokens.merge(minted_native_tokens)?; output_native_tokens.merge(melted_native_tokens)?; - if let Some(burn) = self.burn.as_ref() { - if !burn.native_tokens.is_empty() { - self.transaction_capabilities - .add_capability(TransactionCapabilityFlag::BurnNativeTokens); - } + if let Some(burn) = self.burn.as_ref().filter(|burn| !burn.native_tokens.is_empty()) { + self.transaction_capabilities + .add_capability(TransactionCapabilityFlag::BurnNativeTokens); output_native_tokens.merge(NativeTokensBuilder::from(burn.native_tokens.clone()))?; } From 5702f000bfaf4a419989c325ef25702cdd64895a Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Wed, 28 Feb 2024 12:32:18 -0500 Subject: [PATCH 09/10] :triumph: --- .../client/input_selection/account_outputs.rs | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index 8543a93a2a..cb01629154 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -1481,24 +1481,28 @@ fn two_accounts_required() { 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| if let Output::Account(output) = output { - output.account_id() == &account_id_1 - } else { - false - })); - assert!(selected - .transaction - .outputs() - .iter() - .any(|output| if let Output::Account(output) = output { - output.account_id() == &account_id_2 - } else { - false - })) + assert!( + selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_1 + } else { + false + }) + ); + assert!( + selected + .transaction + .outputs() + .iter() + .any(|output| if let Output::Account(output) = output { + output.account_id() == &account_id_2 + } else { + false + }) + ) } #[test] @@ -2118,12 +2122,14 @@ fn implicit_account_transition() { let input_output_id = *inputs[0].output_id(); let account_id = AccountId::from(&input_output_id); - let outputs = vec![AccountOutputBuilder::new_with_amount(1_000_000, account_id) - .add_unlock_condition(AddressUnlockCondition::new( - Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), - )) - .finish_output() - .unwrap()]; + let outputs = vec![ + AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; let selected = InputSelection::new( inputs.clone(), From fb8f02520ffcd48667601434e3f60fa042085a5f Mon Sep 17 00:00:00 2001 From: Alex Coats Date: Thu, 29 Feb 2024 08:35:26 -0500 Subject: [PATCH 10/10] slightly improve calls --- .../input_selection/requirement/mana.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 b0e56dd371..21e8fc873d 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 @@ -243,9 +243,10 @@ impl InputSelection { if !self.allow_additional_input_selection { return Err(Error::AdditionalInputsRequired(Requirement::Mana)); } + let include_generated = self.burn.as_ref().map_or(true, |b| !b.generated_mana()); // 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 += self.total_mana(&input, None)?; + selected_mana += self.total_mana(&input, include_generated)?; if let Some(output) = self.select_input(input)? { required_mana += output.mana(); } @@ -262,13 +263,14 @@ impl InputSelection { pub(crate) fn initial_mana_excess(&self) -> Result { let output_mana = self.provided_outputs.iter().map(|o| o.mana()).sum::(); let mut input_mana = 0; + let include_generated = self.burn.as_ref().map_or(true, |b| !b.generated_mana()); for input in self .selected_inputs .iter() .filter(|i| self.required_inputs.contains(i.output_id())) { - input_mana += self.total_mana(input, None)?; + input_mana += self.total_mana(input, include_generated)?; } Ok(input_mana.saturating_sub(output_mana)) @@ -288,6 +290,9 @@ impl InputSelection { pub(crate) fn total_selected_mana(&self, include_generated: impl Into> + Copy) -> Result { let mut selected_mana = 0; + let include_generated = include_generated + .into() + .unwrap_or(self.burn.as_ref().map_or(true, |b| !b.generated_mana())); for input in &self.selected_inputs { selected_mana += self.total_mana(input, include_generated)?; @@ -296,10 +301,7 @@ impl InputSelection { Ok(selected_mana) } - fn total_mana(&self, input: &InputSigningData, include_generated: impl Into>) -> Result { - let include_generated = include_generated - .into() - .unwrap_or(self.burn.as_ref().map_or(true, |b| !b.generated_mana())); + fn total_mana(&self, input: &InputSigningData, include_generated: bool) -> Result { Ok(self.mana_rewards.get(input.output_id()).copied().unwrap_or_default() + if include_generated { input.output.available_mana(