diff --git a/e2e/tests/contracts.rs b/e2e/tests/contracts.rs index ff7758c48..513a22795 100644 --- a/e2e/tests/contracts.rs +++ b/e2e/tests/contracts.rs @@ -2,7 +2,7 @@ use std::time::Duration; use fuel_tx::{ consensus_parameters::{ConsensusParametersV1, FeeParametersV1}, - ConsensusParameters, FeeParameters, + ConsensusParameters, FeeParameters, Output, }; use fuels::{ core::codec::{calldata, encode_fn_selector, DecoderConfig, EncoderConfig}, @@ -1707,12 +1707,16 @@ async fn contract_custom_call_no_signatures_strategy() -> Result<()> { let mut tb = call_handler.transaction_builder().await?; + let base_asset_id = *provider.consensus_parameters().await?.base_asset_id(); + let amount = 10; let consensus_parameters = provider.consensus_parameters().await?; let new_base_inputs = wallet - .get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), amount, None) + .get_asset_inputs_for_amount(base_asset_id, amount, None) .await?; tb.inputs_mut().extend(new_base_inputs); + tb.outputs_mut() + .push(Output::change(wallet.address().into(), 0, base_asset_id)); // ANCHOR: tb_no_signatures_strategy let mut tx = tb diff --git a/e2e/tests/predicates.rs b/e2e/tests/predicates.rs index 9ffa593ec..3acd777e7 100644 --- a/e2e/tests/predicates.rs +++ b/e2e/tests/predicates.rs @@ -809,6 +809,7 @@ async fn predicate_adjust_fee_persists_message_w_data() -> Result<()> { TxPolicies::default().with_tip(1), ); predicate.adjust_for_fee(&mut tb, 1000).await?; + let tx = tb.build(&provider).await?; assert_eq!(tx.inputs().len(), 2); diff --git a/e2e/tests/scripts.rs b/e2e/tests/scripts.rs index a416b02b0..b418dd10b 100644 --- a/e2e/tests/scripts.rs +++ b/e2e/tests/scripts.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use fuel_tx::Output; use fuels::{ core::{ codec::{DecoderConfig, EncoderConfig}, @@ -132,8 +133,10 @@ async fn test_output_variable_estimation() -> Result<()> { let inputs = wallet .get_asset_inputs_for_amount(asset_id, amount, None) .await?; + let output = Output::change(wallet.address().into(), 0, asset_id); let _ = script_call .with_inputs(inputs) + .with_outputs(vec![output]) .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) .call() .await?; diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 28b483bca..2090364e2 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -19,7 +19,7 @@ use fuels_core::types::{ use crate::{ accounts_utils::{ - adjust_inputs_outputs, available_base_assets_and_amount, calculate_missing_base_amount, + add_base_change_if_needed, available_base_assets_and_amount, calculate_missing_base_amount, extract_message_nonce, split_into_utxo_ids_and_nonces, }, provider::{Provider, ResourceFilter}, @@ -117,7 +117,8 @@ pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone { excluded_coins: Option>, ) -> Result>; - /// Add base asset inputs to the transaction to cover the estimated fee. + /// Add base asset inputs to the transaction to cover the estimated fee + /// and add a change output for the base asset if needed. /// Requires contract inputs to be at the start of the transactions inputs vec /// so that their indexes are retained async fn adjust_for_fee( @@ -141,14 +142,11 @@ pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone { ) .await?; - adjust_inputs_outputs( - tb, - new_base_inputs, - self.address(), - consensus_parameters.base_asset_id(), - ); + tb.inputs_mut().extend(new_base_inputs); }; + add_base_change_if_needed(tb, self.address(), consensus_parameters.base_asset_id()); + Ok(()) } } @@ -407,10 +405,11 @@ mod tests { 1, Default::default(), ); + let change = Output::change(wallet.address().into(), 0, Default::default()); ScriptTransactionBuilder::prepare_transfer( vec![input_coin], - vec![output_coin], + vec![output_coin, change], Default::default(), ) }; @@ -433,7 +432,7 @@ mod tests { assert_eq!(signature, tx_signature); // Check if the signature is what we expect it to be - assert_eq!(signature, Signature::from_str("8afd30de7039faa07aac1cf2676970a77dc8ef3f779b44c1510ad7bf58ea56f43727b23142bd7252b79ae2c832e073927f84f6b0857fedf2f6d86e9535e48fd0")?); + assert_eq!(signature, Signature::from_str("faa616776a1c336ef6257f7cb0cb5cd932180e2d15faba5f17481dae1cbcaf314d94617bd900216a6680bccb1ea62438e4ca93b0d5733d33788ef9d79cc24e9f")?); // Recover the address that signed the transaction let recovered_address = signature.recover(&message)?; diff --git a/packages/fuels-accounts/src/accounts_utils.rs b/packages/fuels-accounts/src/accounts_utils.rs index a89458ffe..fb1069fa8 100644 --- a/packages/fuels-accounts/src/accounts_utils.rs +++ b/packages/fuels-accounts/src/accounts_utils.rs @@ -90,14 +90,11 @@ fn is_consuming_utxos(tb: &impl TransactionBuilder) -> bool { .any(|input| !matches!(input, Input::Contract { .. })) } -pub fn adjust_inputs_outputs( +pub fn add_base_change_if_needed( tb: &mut impl TransactionBuilder, - new_base_inputs: impl IntoIterator, address: &Bech32Address, base_asset_id: &AssetId, ) { - tb.inputs_mut().extend(new_base_inputs); - let is_base_change_present = tb.outputs().iter().any(|output| { matches!(output , Output::Change { asset_id , .. } if asset_id == base_asset_id) diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index 86bdad022..0f8ab9658 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -163,6 +163,7 @@ pub trait TransactionBuilder: BuildableTransaction + Send + sealed::Sealed { fn add_signer(&mut self, signer: impl Signer + Send + Sync) -> Result<&mut Self>; async fn estimate_max_fee(&self, provider: impl DryRunner) -> Result; + fn enable_burn(self, enable: bool) -> Self; fn with_tx_policies(self, tx_policies: TxPolicies) -> Self; fn with_inputs(self, inputs: Vec) -> Self; fn with_outputs(self, outputs: Vec) -> Self; @@ -216,6 +217,9 @@ macro_rules! impl_tx_builder_trait { .witnesses_mut() .extend(repeat(witness).take(self.unresolved_signers.len())); + // Temporarily enable burning to avoid errors when calculating the fee. + let fee_estimation_tb = fee_estimation_tb.enable_burn(true); + let mut tx = $crate::types::transaction_builders::BuildableTransaction::build( fee_estimation_tb, &provider, @@ -240,6 +244,11 @@ macro_rules! impl_tx_builder_trait { ) } + fn enable_burn(mut self, enable: bool) -> Self { + self.enable_burn = enable; + self + } + fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { self.tx_policies = tx_policies; @@ -331,6 +340,46 @@ macro_rules! impl_tx_builder_trait { .any(|input| matches!(input, Input::ResourcePredicate { .. })) } + fn intercept_burn(&self, base_asset_id: &$crate::types::AssetId) -> Result<()> { + use std::collections::HashSet; + + if self.enable_burn { + return Ok(()); + } + + let assets_w_change = self + .outputs + .iter() + .filter_map(|output| match output { + Output::Change { asset_id, .. } => Some(*asset_id), + _ => None, + }) + .collect::>(); + + let input_assets = self + .inputs + .iter() + .filter_map(|input| match input { + Input::ResourceSigned { resource } | + Input::ResourcePredicate { resource, .. } => Some(resource.asset_id(*base_asset_id)), + _ => None, + }) + .collect::>(); + + let diff = input_assets.difference(&assets_w_change).collect_vec(); + if !diff.is_empty() { + return Err(error_transaction!( + Builder, + "the following assets have no change outputs and may be burned unintentionally: {:?}. \ + To resolve this, either add the necessary change outputs manually or explicitly allow asset burning \ + by calling `.enable_burn(true)` on the transaction builder.", + diff + )); + } + + Ok(()) + } + fn num_witnesses(&self) -> Result { use $crate::types::transaction_builders::TransactionBuilder; let num_witnesses = self.witnesses().len(); @@ -461,6 +510,7 @@ pub struct ScriptTransactionBuilder { pub build_strategy: ScriptBuildStrategy, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, + enable_burn: bool, } impl Default for ScriptTransactionBuilder { @@ -479,6 +529,7 @@ impl Default for ScriptTransactionBuilder { build_strategy: Default::default(), unresolved_witness_indexes: Default::default(), unresolved_signers: Default::default(), + enable_burn: false, } } } @@ -497,6 +548,7 @@ pub struct CreateTransactionBuilder { pub build_strategy: Strategy, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, + enable_burn: bool, } impl Default for CreateTransactionBuilder { @@ -515,6 +567,7 @@ impl Default for CreateTransactionBuilder { build_strategy: Default::default(), unresolved_witness_indexes: Default::default(), unresolved_signers: Default::default(), + enable_burn: false, } } } @@ -539,6 +592,7 @@ pub struct UploadTransactionBuilder { pub build_strategy: Strategy, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, + enable_burn: bool, } impl Default for UploadTransactionBuilder { @@ -558,6 +612,7 @@ impl Default for UploadTransactionBuilder { build_strategy: Default::default(), unresolved_witness_indexes: Default::default(), unresolved_signers: Default::default(), + enable_burn: false, } } } @@ -574,6 +629,7 @@ pub struct UpgradeTransactionBuilder { pub build_strategy: Strategy, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, + enable_burn: bool, } impl Default for UpgradeTransactionBuilder { @@ -591,6 +647,7 @@ impl Default for UpgradeTransactionBuilder { unresolved_signers: Default::default(), max_fee_estimation_tolerance: Default::default(), build_strategy: Default::default(), + enable_burn: false, } } } @@ -602,6 +659,9 @@ impl_tx_builder_trait!(UpgradeTransactionBuilder, UpgradeTransaction); impl ScriptTransactionBuilder { async fn build(mut self, provider: impl DryRunner) -> Result { + let consensus_parameters = provider.consensus_parameters().await?; + self.intercept_burn(consensus_parameters.base_asset_id())?; + let is_using_predicates = self.is_using_predicates(); let tx = match self.build_strategy { @@ -913,6 +973,7 @@ impl ScriptTransactionBuilder { variable_output_policy: self.variable_output_policy, max_fee_estimation_tolerance: self.max_fee_estimation_tolerance, build_strategy: self.build_strategy.clone(), + enable_burn: self.enable_burn, } } } @@ -930,6 +991,9 @@ fn add_variable_outputs(tx: &mut fuel_tx::Script, variable_outputs: usize) { impl CreateTransactionBuilder { pub async fn build(mut self, provider: impl DryRunner) -> Result { + let consensus_parameters = provider.consensus_parameters().await?; + self.intercept_burn(consensus_parameters.base_asset_id())?; + let is_using_predicates = self.is_using_predicates(); let tx = match self.build_strategy { @@ -1047,12 +1111,16 @@ impl CreateTransactionBuilder { gas_price_estimation_block_horizon: self.gas_price_estimation_block_horizon, max_fee_estimation_tolerance: self.max_fee_estimation_tolerance, build_strategy: self.build_strategy.clone(), + enable_burn: self.enable_burn, } } } impl UploadTransactionBuilder { pub async fn build(mut self, provider: impl DryRunner) -> Result { + let consensus_parameters = provider.consensus_parameters().await?; + self.intercept_burn(consensus_parameters.base_asset_id())?; + let is_using_predicates = self.is_using_predicates(); let tx = match self.build_strategy { @@ -1182,12 +1250,16 @@ impl UploadTransactionBuilder { proof_set: vec![], max_fee_estimation_tolerance: self.max_fee_estimation_tolerance, build_strategy: self.build_strategy.clone(), + enable_burn: self.enable_burn, } } } impl UpgradeTransactionBuilder { pub async fn build(mut self, provider: impl DryRunner) -> Result { + let consensus_parameters = provider.consensus_parameters().await?; + self.intercept_burn(consensus_parameters.base_asset_id())?; + let is_using_predicates = self.is_using_predicates(); let tx = match self.build_strategy { Strategy::Complete => self.resolve_fuel_tx(&provider).await?, @@ -1286,6 +1358,7 @@ impl UpgradeTransactionBuilder { gas_price_estimation_block_horizon: self.gas_price_estimation_block_horizon, max_fee_estimation_tolerance: self.max_fee_estimation_tolerance, build_strategy: self.build_strategy.clone(), + enable_burn: self.enable_burn, } } } @@ -1535,16 +1608,20 @@ mod tests { } } + fn given_a_coin(tx_id: [u8; 32], owner: [u8; 32], amount: u64) -> Coin { + Coin { + utxo_id: UtxoId::new(tx_id.into(), 0), + owner: Bech32Address::new("fuel", owner), + amount, + ..Default::default() + } + } + fn given_inputs(num_inputs: u8) -> Vec { (0..num_inputs) .map(|i| { - let bytes = [i; 32]; - let coin = CoinType::Coin(Coin { - utxo_id: UtxoId::new(bytes.into(), 0), - owner: Bech32Address::new("fuel", bytes), - ..Default::default() - }); - Input::resource_signed(coin) + let coin = given_a_coin([i; 32], [num_inputs + i; 32], 1000); + Input::resource_signed(CoinType::Coin(coin)) }) .collect() } @@ -1586,10 +1663,10 @@ mod tests { async fn estimate_predicates( &self, - _tx: &FuelTransaction, + tx: &FuelTransaction, _: Option, ) -> Result { - unimplemented!("") + Ok(tx.clone()) } } @@ -1601,7 +1678,8 @@ mod tests { let tb = CreateTransactionBuilder::default() .with_witnesses(given_witnesses(num_witnesses)) - .with_inputs(given_inputs(num_inputs)); + .with_inputs(given_inputs(num_inputs)) + .enable_burn(true); // when let tx = tb @@ -1637,7 +1715,8 @@ mod tests { let tb = ScriptTransactionBuilder::default() .with_witnesses(given_witnesses(num_witnesses)) - .with_inputs(given_inputs(num_inputs)); + .with_inputs(given_inputs(num_inputs)) + .enable_burn(true); // when let tx = tb @@ -1665,6 +1744,65 @@ mod tests { Ok(()) } + #[tokio::test] + async fn build_w_enable_burn() -> Result<()> { + let coin = CoinType::Coin(given_a_coin([1; 32], [2; 32], 1000)); + test_enable_burn(Input::resource_signed(coin)).await + } + + #[tokio::test] + async fn build_w_enable_burn_predicates() -> Result<()> { + let predicate_coin = CoinType::Coin(given_a_coin([1; 32], [2; 32], 1000)); + + test_enable_burn(Input::resource_predicate( + predicate_coin, + op::ret(1).to_bytes().to_vec(), + vec![], + )) + .await + } + + #[tokio::test] + async fn build_w_enable_burn_messages() -> Result<()> { + let message = CoinType::Message(given_a_message(vec![1, 2, 3])); + + test_enable_burn(Input::resource_signed(message)).await + } + + #[tokio::test] + async fn build_w_enable_burn_predicates_message() -> Result<()> { + let message_predicate = CoinType::Message(given_a_message(vec![1, 2, 3])); + + test_enable_burn(Input::resource_predicate( + message_predicate, + op::ret(1).to_bytes().to_vec(), + vec![], + )) + .await + } + + async fn test_enable_burn(input: Input) -> Result<()> { + // Test failure case without enable_burn + let tb = ScriptTransactionBuilder::default().with_inputs(vec![input.clone()]); + let err = tb + .with_build_strategy(ScriptBuildStrategy::NoSignatures) + .build(&MockDryRunner::default()) + .await + .expect_err("should fail because of missing change outputs"); + + assert!(err.to_string().contains("no change outputs")); + + // Test success case with enable_burn + let tb = ScriptTransactionBuilder::default().with_inputs(vec![input]); + let _tx = tb + .with_build_strategy(ScriptBuildStrategy::NoSignatures) + .enable_burn(true) + .build(&MockDryRunner::default()) + .await?; + + Ok(()) + } + #[derive(Clone, Debug, Default)] struct MockSigner { address: Bech32Address, diff --git a/packages/fuels-core/src/types/transaction_builders/blob.rs b/packages/fuels-core/src/types/transaction_builders/blob.rs index af70f5289..1d3a4082d 100644 --- a/packages/fuels-core/src/types/transaction_builders/blob.rs +++ b/packages/fuels-core/src/types/transaction_builders/blob.rs @@ -97,6 +97,7 @@ pub struct BlobTransactionBuilder { pub blob: Blob, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, + enable_burn: bool, } impl Default for BlobTransactionBuilder { @@ -112,6 +113,7 @@ impl Default for BlobTransactionBuilder { blob: Default::default(), unresolved_witness_indexes: Default::default(), unresolved_signers: Default::default(), + enable_burn: false, } } } @@ -143,6 +145,9 @@ impl BlobTransactionBuilder { } pub async fn build(mut self, provider: impl DryRunner) -> Result { + let consensus_parameters = provider.consensus_parameters().await?; + self.intercept_burn(consensus_parameters.base_asset_id())?; + let is_using_predicates = self.is_using_predicates(); let tx = match self.build_strategy { @@ -172,6 +177,7 @@ impl BlobTransactionBuilder { max_fee_estimation_tolerance: self.max_fee_estimation_tolerance, build_strategy: self.build_strategy.clone(), blob: self.blob.clone(), + enable_burn: self.enable_burn, } } diff --git a/packages/fuels-core/src/types/wrappers/coin_type.rs b/packages/fuels-core/src/types/wrappers/coin_type.rs index 486a64490..c74be2751 100644 --- a/packages/fuels-core/src/types/wrappers/coin_type.rs +++ b/packages/fuels-core/src/types/wrappers/coin_type.rs @@ -51,6 +51,13 @@ impl CoinType { } } + pub fn asset_id(&self, base_asset_id: AssetId) -> AssetId { + match self { + CoinType::Coin(coin) => coin.asset_id, + CoinType::Message(_) => base_asset_id, + } + } + pub fn owner(&self) -> &Bech32Address { match self { CoinType::Coin(coin) => &coin.owner,