diff --git a/docs/src/calling-contracts/tx-policies.md b/docs/src/calling-contracts/tx-policies.md index c02d83964..12eadfd2d 100644 --- a/docs/src/calling-contracts/tx-policies.md +++ b/docs/src/calling-contracts/tx-policies.md @@ -13,8 +13,9 @@ Where: 1. **Tip** - amount to pay the block producer to prioritize the transaction. 2. **Witness Limit** - The maximum amount of witness data allowed for the transaction. 3. **Maturity** - Block until which the transaction cannot be included. -4. **Max Fee** - The maximum fee payable by this transaction. -5. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code. +4. **Expiration** - Block after which the transaction cannot be included. +5. **Max Fee** - The maximum fee payable by this transaction. +6. **Script Gas Limit** - The maximum amount of gas the transaction may consume for executing its script code. When the **Script Gas Limit** is not set, the Rust SDK will estimate the consumed gas in the background and set it as the limit. diff --git a/docs/src/custom-transactions/transaction-builders.md b/docs/src/custom-transactions/transaction-builders.md index 03cfbbe82..084a56f00 100644 --- a/docs/src/custom-transactions/transaction-builders.md +++ b/docs/src/custom-transactions/transaction-builders.md @@ -68,7 +68,7 @@ We need to do one more thing before we stop thinking about transaction inputs. E > **Note** It is recommended to add signers before calling `adjust_for_fee()` as the estimation will include the size of the witnesses. -We can also define transaction policies. For example, we can limit the gas price by doing the following: +We can also define transaction policies. For example, we can set the maturity and expiration with: ```rust,ignore {{#include ../../../examples/cookbook/src/lib.rs:custom_tx_policies}} diff --git a/e2e/tests/contracts.rs b/e2e/tests/contracts.rs index b41ce21bb..b2618a27b 100644 --- a/e2e/tests/contracts.rs +++ b/e2e/tests/contracts.rs @@ -385,7 +385,7 @@ async fn mult_call_has_same_estimated_and_used_gas() -> Result<()> { } #[tokio::test] -async fn contract_method_call_respects_maturity() -> Result<()> { +async fn contract_method_call_respects_maturity_and_expiration() -> Result<()> { setup_program_test!( Wallets("wallet"), Abigen(Contract( @@ -399,23 +399,42 @@ async fn contract_method_call_respects_maturity() -> Result<()> { random_salt = false, ), ); + let provider = wallet.try_provider()?; - let call_w_maturity = |maturity| { - contract_instance - .methods() - .calling_this_will_produce_a_block() - .with_tx_policies(TxPolicies::default().with_maturity(maturity)) - }; + let maturity = 10; + let expiration = 20; + let call_handler = contract_instance + .methods() + .calling_this_will_produce_a_block() + .with_tx_policies( + TxPolicies::default() + .with_maturity(maturity) + .with_expiration(expiration), + ); - call_w_maturity(1).call().await.expect( - "should have passed since we're calling with a maturity \ - that is less or equal to the current block height", - ); + { + let err = call_handler + .clone() + .call() + .await + .expect_err("maturity not reached"); - call_w_maturity(3).call().await.expect_err( - "should have failed since we're calling with a maturity \ - that is greater than the current block height", - ); + assert!(err.to_string().contains("TransactionMaturity")); + } + { + provider.produce_blocks(15, None).await?; + call_handler + .clone() + .call() + .await + .expect("should succeed. Block height between `maturity` and `expiration`"); + } + { + provider.produce_blocks(15, None).await?; + let err = call_handler.call().await.expect_err("expiration reached"); + + assert!(err.to_string().contains("TransactionExpiration")); + } Ok(()) } diff --git a/e2e/tests/predicates.rs b/e2e/tests/predicates.rs index 6759a2003..e68b994e4 100644 --- a/e2e/tests/predicates.rs +++ b/e2e/tests/predicates.rs @@ -1245,3 +1245,80 @@ async fn predicate_configurables_in_blobs() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn predicate_transfer_respects_maturity_and_expiration() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json" + )); + + let predicate_data = MyPredicateEncoder::default().encode_data(4097, 4097)?; + + let mut predicate: Predicate = + Predicate::load_from("sway/predicates/basic_predicate/out/release/basic_predicate.bin")? + .with_data(predicate_data); + + let num_coins = 4; + let num_messages = 8; + let amount = 16; + let (provider, predicate_balance, receiver, receiver_balance, asset_id, _) = + setup_predicate_test(predicate.address(), num_coins, num_messages, amount).await?; + + predicate.set_provider(provider.clone()); + + let maturity = 10; + let expiration = 20; + let tx_policies = TxPolicies::default() + .with_maturity(maturity) + .with_expiration(expiration); + let amount_to_send = 10; + + // TODO: https://github.com/FuelLabs/fuels-rs/issues/1394 + let expected_fee = 1; + + { + let err = predicate + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect_err("maturity not reached"); + + assert!(err.to_string().contains("TransactionMaturity")); + } + { + provider.produce_blocks(15, None).await?; + predicate + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect("should succeed. Block height between `maturity` and `expiration`"); + } + { + provider.produce_blocks(15, None).await?; + let err = predicate + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect_err("expiration reached"); + + assert!(err.to_string().contains("TransactionExpiration")); + } + + // The predicate has spent the funds + assert_address_balance( + predicate.address(), + &provider, + asset_id, + predicate_balance - amount_to_send - expected_fee, + ) + .await; + + // Funds were transferred + assert_address_balance( + receiver.address(), + &provider, + asset_id, + receiver_balance + amount_to_send, + ) + .await; + + Ok(()) +} diff --git a/e2e/tests/providers.rs b/e2e/tests/providers.rs index 71889e8f3..9906c1d4e 100644 --- a/e2e/tests/providers.rs +++ b/e2e/tests/providers.rs @@ -283,38 +283,51 @@ async fn can_retrieve_latest_block_time() -> Result<()> { } #[tokio::test] -async fn contract_deployment_respects_maturity() -> Result<()> { +async fn contract_deployment_respects_maturity_and_expiration() -> Result<()> { abigen!(Contract(name="MyContract", abi="e2e/sway/contracts/transaction_block_height/out/release/transaction_block_height-abi.json")); - let wallets = - launch_custom_provider_and_get_wallets(WalletsConfig::default(), None, None).await?; - let wallet = &wallets[0]; - let provider = wallet.try_provider()?; + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.try_provider()?.clone(); - let deploy_w_maturity = |maturity| { + let maturity = 10; + let expiration = 20; + + let deploy_w_maturity_and_expiration = || { Contract::load_from( "sway/contracts/transaction_block_height/out/release/transaction_block_height.bin", LoadConfiguration::default(), ) .map(|loaded_contract| { - loaded_contract - .deploy_if_not_exists(wallet, TxPolicies::default().with_maturity(maturity)) + loaded_contract.deploy( + &wallet, + TxPolicies::default() + .with_maturity(maturity) + .with_expiration(expiration), + ) }) }; - let err = deploy_w_maturity(1)?.await.expect_err( - "should not deploy contract since block height `0` is less than the requested maturity `1`", - ); + { + let err = deploy_w_maturity_and_expiration()? + .await + .expect_err("maturity not reached"); - let Error::Provider(s) = err else { - panic!("expected `Validation`, got: `{err}`"); - }; - assert!(s.contains("TransactionMaturity")); + assert!(err.to_string().contains("TransactionMaturity")); + } + { + provider.produce_blocks(15, None).await?; + deploy_w_maturity_and_expiration()? + .await + .expect("should succeed. Block height between `maturity` and `expiration`"); + } + { + provider.produce_blocks(15, None).await?; + let err = deploy_w_maturity_and_expiration()? + .await + .expect_err("expiration reached"); - provider.produce_blocks(1, None).await?; - deploy_w_maturity(1)? - .await - .expect("Should deploy contract since maturity `1` is <= than the block height `1`"); + assert!(err.to_string().contains("TransactionExpiration")); + } Ok(()) } @@ -1087,12 +1100,14 @@ async fn tx_respects_policies() -> Result<()> { let tip = 22; let witness_limit = 1000; let maturity = 4; + let expiration = 128; let max_fee = 10_000; let script_gas_limit = 3000; let tx_policies = TxPolicies::new( Some(tip), Some(witness_limit), Some(maturity), + Some(expiration), Some(max_fee), Some(script_gas_limit), ); @@ -1119,7 +1134,8 @@ async fn tx_respects_policies() -> Result<()> { _ => panic!("expected script transaction"), }; - assert_eq!(script.maturity(), maturity as u32); + assert_eq!(script.maturity().unwrap(), maturity); + assert_eq!(script.expiration().unwrap(), expiration); assert_eq!(script.tip().unwrap(), tip); assert_eq!(script.witness_limit().unwrap(), witness_limit); assert_eq!(script.max_fee().unwrap(), max_fee); diff --git a/e2e/tests/scripts.rs b/e2e/tests/scripts.rs index a36b038de..ef1b865de 100644 --- a/e2e/tests/scripts.rs +++ b/e2e/tests/scripts.rs @@ -682,3 +682,50 @@ async fn loader_can_be_presented_as_a_normal_script_with_shifted_configurables() Ok(()) } + +#[tokio::test] +async fn script_call_respects_maturity_and_expiration() -> Result<()> { + abigen!(Script( + name = "MyScript", + abi = "e2e/sway/scripts/basic_script/out/release/basic_script-abi.json" + )); + let wallet = launch_provider_and_get_wallet().await.expect(""); + let provider = wallet.try_provider()?.clone(); + let bin_path = "sway/scripts/basic_script/out/release/basic_script.bin"; + + let script_instance = MyScript::new(wallet, bin_path); + + let maturity = 10; + let expiration = 20; + let call_handler = script_instance.main(1, 2).with_tx_policies( + TxPolicies::default() + .with_maturity(maturity) + .with_expiration(expiration), + ); + + { + let err = call_handler + .clone() + .call() + .await + .expect_err("maturity not reached"); + + assert!(err.to_string().contains("TransactionMaturity")); + } + { + provider.produce_blocks(15, None).await?; + call_handler + .clone() + .call() + .await + .expect("should succeed. Block height between `maturity` and `expiration`"); + } + { + provider.produce_blocks(15, None).await?; + let err = call_handler.call().await.expect_err("expiration reached"); + + assert!(err.to_string().contains("TransactionExpiration")); + } + + Ok(()) +} diff --git a/e2e/tests/wallets.rs b/e2e/tests/wallets.rs index 6c2ca2085..43ab1d319 100644 --- a/e2e/tests/wallets.rs +++ b/e2e/tests/wallets.rs @@ -3,6 +3,19 @@ use fuels::{ types::{coin_type::CoinType, input::Input, output::Output}, }; +async fn assert_address_balance( + address: &Bech32Address, + provider: &Provider, + asset_id: AssetId, + amount: u64, +) { + let balance = provider + .get_asset_balance(address, asset_id) + .await + .expect("Could not retrieve balance"); + assert_eq!(balance, amount); +} + #[tokio::test] async fn test_wallet_balance_api_multi_asset() -> Result<()> { let mut wallet = WalletUnlocked::new_random(None); @@ -276,7 +289,7 @@ async fn send_transfer_transactions() -> Result<()> { }; // Transfer scripts uses set `script_gas_limit` despite not having script code assert_eq!(script.gas_limit(), script_gas_limit); - assert_eq!(script.maturity(), maturity as u32); + assert_eq!(script.maturity().unwrap(), maturity); let wallet_1_spendable_resources = wallet_1 .get_spendable_resources(base_asset_id, 1, None) @@ -485,3 +498,62 @@ async fn test_transfer_with_multiple_signatures() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn wallet_transfer_respects_maturity_and_expiration() -> Result<()> { + let wallet = launch_provider_and_get_wallet().await?; + let asset_id = AssetId::zeroed(); + let wallet_balance = wallet.get_asset_balance(&asset_id).await?; + + let provider = wallet.try_provider()?; + let receiver = WalletUnlocked::new_random(None); + + let maturity = 10; + let expiration = 20; + let tx_policies = TxPolicies::default() + .with_maturity(maturity) + .with_expiration(expiration); + let amount_to_send = 10; + + // TODO: https://github.com/FuelLabs/fuels-rs/issues/1394 + let expected_fee = 1; + + { + let err = wallet + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect_err("maturity not reached"); + + assert!(err.to_string().contains("TransactionMaturity")); + } + { + provider.produce_blocks(15, None).await?; + wallet + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect("should succeed. Block height between `maturity` and `expiration`"); + } + { + provider.produce_blocks(15, None).await?; + let err = wallet + .transfer(receiver.address(), amount_to_send, asset_id, tx_policies) + .await + .expect_err("expiration reached"); + + assert!(err.to_string().contains("TransactionExpiration")); + } + + // Wallet has spent the funds + assert_address_balance( + wallet.address(), + provider, + asset_id, + wallet_balance - amount_to_send - expected_fee, + ) + .await; + + // Funds were transferred + assert_address_balance(receiver.address(), provider, asset_id, amount_to_send).await; + + Ok(()) +} diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index e1bf29b86..464512bed 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -160,7 +160,8 @@ mod tests { let tx_policies = TxPolicies::default() .with_tip(1) .with_script_gas_limit(1_000_000) - .with_maturity(0); + .with_maturity(0) + .with_expiration(10_000); let contract_id_2 = Contract::load_from( "../../e2e/sway/contracts/contract_test/out/release/contract_test.bin", @@ -297,7 +298,8 @@ mod tests { let tx_policies = TxPolicies::default() .with_tip(1) .with_script_gas_limit(1_000_000) - .with_maturity(0); + .with_maturity(0) + .with_expiration(10_000); let response = contract_methods .initialize_counter(42) // Our contract method @@ -568,7 +570,8 @@ mod tests { // ANCHOR: multi_call_build let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) - .add_call(call_handler_2); + .add_call(call_handler_2) + .with_tx_policies(TxPolicies::default()); // ANCHOR_END: multi_call_build let multi_call_handler_tmp = multi_call_handler.clone(); diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 47e0b2d7b..1083b845a 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -253,6 +253,8 @@ mod tests { ) .await?; + provider.produce_blocks(100, None).await?; + hot_wallet.set_provider(provider.clone()); cold_wallet.set_provider(provider.clone()); predicate.set_provider(provider.clone()); @@ -312,7 +314,7 @@ mod tests { // ANCHOR_END: custom_tx_adjust // ANCHOR: custom_tx_policies - let tx_policies = TxPolicies::default().with_tip(1); + let tx_policies = TxPolicies::default().with_maturity(64).with_expiration(128); let tb = tb.with_tx_policies(tx_policies); // ANCHOR_END: custom_tx_policies diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index a9992a5c9..124dad55f 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -329,6 +329,7 @@ macro_rules! impl_tx_builder_trait { policies.set(PolicyType::MaxFee, self.tx_policies.tip().or(Some(0))); policies.set(PolicyType::Maturity, self.tx_policies.maturity()); policies.set(PolicyType::Tip, self.tx_policies.tip()); + policies.set(PolicyType::Expiration, self.tx_policies.expiration()); Ok(policies) } diff --git a/packages/fuels-core/src/types/wrappers/transaction.rs b/packages/fuels-core/src/types/wrappers/transaction.rs index 992aaa134..6d673bf82 100644 --- a/packages/fuels-core/src/types/wrappers/transaction.rs +++ b/packages/fuels-core/src/types/wrappers/transaction.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use fuel_crypto::{Message, Signature}; use fuel_tx::{ field::{ - Inputs, Maturity, MintAmount, MintAssetId, Outputs, Policies as PoliciesField, - Script as ScriptField, ScriptData, ScriptGasLimit, WitnessLimit, Witnesses, + Inputs, MintAmount, MintAssetId, Outputs, Policies as PoliciesField, Script as ScriptField, + ScriptData, ScriptGasLimit, WitnessLimit, Witnesses, }, input::{ coin::{CoinPredicate, CoinSigned}, @@ -107,6 +107,7 @@ pub struct TxPolicies { tip: Option, witness_limit: Option, maturity: Option, + expiration: Option, max_fee: Option, script_gas_limit: Option, } @@ -117,6 +118,7 @@ impl TxPolicies { tip: Option, witness_limit: Option, maturity: Option, + expiration: Option, max_fee: Option, script_gas_limit: Option, ) -> Self { @@ -124,6 +126,7 @@ impl TxPolicies { tip, witness_limit, maturity, + expiration, max_fee, script_gas_limit, } @@ -156,6 +159,15 @@ impl TxPolicies { self.maturity } + pub fn with_expiration(mut self, expiration: u64) -> Self { + self.expiration = Some(expiration); + self + } + + pub fn expiration(&self) -> Option { + self.expiration + } + pub fn with_max_fee(mut self, max_fee: u64) -> Self { self.max_fee = Some(max_fee); self @@ -244,9 +256,9 @@ pub trait Transaction: fn id(&self, chain_id: ChainId) -> Bytes32; - fn maturity(&self) -> u32; + fn maturity(&self) -> Option; - fn with_maturity(self, maturity: u32) -> Self; + fn expiration(&self) -> Option; fn metered_bytes_size(&self) -> usize; @@ -425,13 +437,12 @@ macro_rules! impl_tx_wrapper { self.tx.id(&chain_id) } - fn maturity(&self) -> u32 { - (*self.tx.maturity()).into() + fn maturity(&self) -> Option { + self.tx.policies().get(PolicyType::Maturity) } - fn with_maturity(mut self, maturity: u32) -> Self { - self.tx.set_maturity(maturity.into()); - self + fn expiration(&self) -> Option { + self.tx.policies().get(PolicyType::Expiration) } fn metered_bytes_size(&self) -> usize {