diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1d9c458c50..03fc6e9be9 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -49,6 +49,7 @@ - [Pre-uploading code](./preuploading-code.md) - [Custom transactions](./custom-transactions/index.md) - [Transaction builders](./custom-transactions/transaction-builders.md) + - [Assemble transactions](./custom-transactions/assemble_tx.md) - [Custom contract and script calls](./custom-transactions/custom-calls.md) - [Types](./types/index.md) - [`Bytes32`](./types/bytes32.md) diff --git a/docs/src/calling-contracts/other-contracts.md b/docs/src/calling-contracts/other-contracts.md index 28cfb902db..f6848648a2 100644 --- a/docs/src/calling-contracts/other-contracts.md +++ b/docs/src/calling-contracts/other-contracts.md @@ -1,15 +1,13 @@ # Calling other contracts -If your contract method is calling other contracts you will have to add the appropriate `Inputs` and `Outputs` to your transaction. For your convenience, the `CallHandler` provides methods that prepare those inputs and outputs for you. You have two methods that you can use: `with_contracts(&[&contract_instance, ...])` and `with_contract_ids(&[&contract_id, ...])`. - -`with_contracts(&[&contract_instance, ...])` requires contract instances that were created using the `abigen` macro. When setting the external contracts with this method, logs and require revert errors originating from the external contract can be propagated and decoded by the calling contract. +If your contract method is calling other contracts you will have to add the appropriate `Inputs` and `Outputs` to your transaction. For your convenience, the `CallHandler` will fill in all missing `Inputs`/`Outputs` before sending the transaction. ```rust,ignore {{#include ../../../e2e/tests/contracts.rs:external_contract}} ``` - If however, you do not need to decode logs or you do not have a contract instance that was generated using the `abigen` macro you can use `with_contract_ids(&[&contract_id, ...])` and provide the required contract ids. +If you need to decode logs and require revert errors originating from the external contract you will need to pass the `LogDecoder` from the external contract to the contract instance making the call. ```rust,ignore -{{#include ../../../e2e/tests/contracts.rs:external_contract_ids}} +{{#include ../../../e2e/tests/contracts.rs:external_contract_logs}} ``` diff --git a/docs/src/calling-contracts/tx-dependency-estimation.md b/docs/src/calling-contracts/tx-dependency-estimation.md index b583559dac..60ce5358fd 100644 --- a/docs/src/calling-contracts/tx-dependency-estimation.md +++ b/docs/src/calling-contracts/tx-dependency-estimation.md @@ -1,28 +1,9 @@ # Transaction dependency estimation -Previously, we mentioned that a contract call might require you to manually specify external contracts, variable outputs, or output messages. The SDK can also attempt to estimate and set these dependencies for you at the cost of running multiple simulated calls in the background. +Previously, we mentioned that a contract call might require you to manually specify external contracts, variable outputs, or output messages. The SDK will estimate and set these dependencies for you. -The following example uses a contract call that calls an external contract and later mints assets to a specified address. Calling it without including the dependencies will result in a revert: - -```rust,ignore -{{#include ../../../examples/contracts/src/lib.rs:dependency_estimation_fail}} -``` - -As mentioned in previous chapters, you can specify the external contract and add an output variable to resolve this: - -```rust,ignore -{{#include ../../../examples/contracts/src/lib.rs:dependency_estimation_manual}} -``` - -But this requires you to know the contract ID of the external contract and the needed number of output variables. Alternatively, by chaining - -- `.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)` and -- `.determine_missing_contracts()` - -the dependencies will be estimated by the SDK and set automatically. +The following example uses a contract call that calls an external contract and later mints assets to a specified address. ```rust,ignore {{#include ../../../examples/contracts/src/lib.rs:dependency_estimation}} ``` - -> **Note:** Both `with_variable_output_policy` and `determine_missing_contracts` can also be used when working with script calls or multi calls. `determine_missing_contracts()` will not enable logging from an external contract. For more information, see [here](./other-contracts.md). diff --git a/docs/src/calling-contracts/tx-policies.md b/docs/src/calling-contracts/tx-policies.md index 12eadfd2db..5c0b370b8f 100644 --- a/docs/src/calling-contracts/tx-policies.md +++ b/docs/src/calling-contracts/tx-policies.md @@ -15,9 +15,6 @@ Where: 3. **Maturity** - Block until which the transaction cannot be included. 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. If the **Witness Limit** is not set, the SDK will set it to the size of all witnesses and signatures defined in the transaction builder. @@ -40,3 +37,8 @@ This way: ``` As you might have noticed, `TxPolicies` can also be specified when deploying contracts or transferring assets by passing it to the respective methods. + +> **Note:** The `Script Gas Limit` is set directly on the call handler: + +```rust,ignore +{{#include ../../../examples/contracts/src/lib.rs:tx_script_gas_limit}} diff --git a/docs/src/calling-contracts/variable-outputs.md b/docs/src/calling-contracts/variable-outputs.md index 7dc42cdcb9..8b6e9bc1b9 100644 --- a/docs/src/calling-contracts/variable-outputs.md +++ b/docs/src/calling-contracts/variable-outputs.md @@ -11,15 +11,14 @@ Let's say you deployed a contract with the following method: {{#include ../../../e2e/sway/contracts/token_ops/src/main.sw:variable_outputs}} ``` -When calling `transfer_coins_to_output` with the SDK, you can specify the number of variable outputs: +When calling `transfer_coins_to_output` with the SDK, the correct number of variable outputs will be estimated and you will not have to change your code. ```rust,ignore {{#include ../../../examples/contracts/src/lib.rs:variable_outputs}} ``` - -`with_variable_output_policy` sets the policy regarding variable outputs. You can either set the number of variable outputs yourself by providing `VariableOutputPolicy::Exactly(n)` or let the SDK estimate it for you with `VariableOutputPolicy::EstimateMinimum`. A variable output indicates that the amount and the owner may vary based on transaction execution. +However, if you convert the contract call into a `TransactionBuilder` you have to make sure that you have the appropriate number of variable outputs present. You can use the `with_variable_output_policy` method to either set the number of variable outputs yourself by providing `VariableOutputPolicy::Exactly(n)` or let the SDK estimate it for you with `VariableOutputPolicy::EstimateMinimum`. A variable output indicates that the amount and the owner may vary based on transaction execution. > **Note:** that the Sway `lib-std` function `mint_to_address` calls `transfer_to_address` under the hood, so you need to call `with_variable_output_policy` in the Rust SDK tests like you would for `transfer_to_address`. diff --git a/docs/src/custom-transactions/assemble_tx.md b/docs/src/custom-transactions/assemble_tx.md new file mode 100644 index 0000000000..bf27675642 --- /dev/null +++ b/docs/src/custom-transactions/assemble_tx.md @@ -0,0 +1,35 @@ +# Assemble Transactions + +Assemble transactions makes it possible to create a minimal `TransactionBuilder` and let the fuel node fill in the missing details. The node will add missing inputs, outputs, set transactions limits etc. Below is an example how the assemble strategy can be used to create a transfer. + +Let's first launch a local node with a funded wallet and create a random wallet that will receive some base asset. + +```rust,ignore +{{#include ../../../e2e/tests/providers.rs:assemble_wallets}} +``` + +Next, we create an base asset output to the receiver wallet. + +```rust,ignore +{{#include ../../../e2e/tests/providers.rs:assemble_output}} +``` + +Now we tell the node what kind of inputs do we require. Note that we do not specify any inputs just the amount, asset id and which require balance should be used to pay for the fees. + +```rust,ignore +{{#include ../../../e2e/tests/providers.rs:assemble_req_balance}} +``` + +We can now build the transaction using the assemble strategy. + +```rust,ignore +{{#include ../../../e2e/tests/providers.rs:assemble_tb}} +``` + +> **Note** The assemble strategy will make sure that we have enough base asset coins in the inputs to cover the transfer and the fee. Also a change output is added to the transaction. + +At the end, we send the transaction and make sure that the receiver balance matches the sent amount. + +```rust,ignore +{{#include ../../../e2e/tests/providers.rs:assemble_response}} +``` diff --git a/docs/src/predicates/index.md b/docs/src/predicates/index.md index bbe033fe52..f139dba934 100644 --- a/docs/src/predicates/index.md +++ b/docs/src/predicates/index.md @@ -54,3 +54,5 @@ Each configurable constant will get a dedicated `with` method in the SDK. For ex ```rust,ignore {{#include ../../../e2e/tests/predicates.rs:predicate_configurables}} ``` + +> **Note:** if a custom transaction is using predicates where the execution is dependant on some malleable fields and the fields are changed, then you will have to re-estimate the predicates to set the right gas limit. diff --git a/docs/src/running-scripts.md b/docs/src/running-scripts.md index e94449541d..a983b654bd 100644 --- a/docs/src/running-scripts.md +++ b/docs/src/running-scripts.md @@ -40,20 +40,12 @@ Script calls provide the same logging functions, `decode_logs()` and `decode_log ## Calling contracts from scripts -Scripts use the same interfaces for setting external contracts as [contract methods](./calling-contracts/other-contracts.md). - -Below is an example that uses `with_contracts(&[&contract_instance, ...])`. +Similarly to [contract methods](./calling-contracts/other-contracts.md), script calls will automatically estimate all missing contracts inputs. If you need to decode logs or revert errors from the external contract you should add the `LogDecoder` with `add_log_decoder`. ```rust,ignore {{#include ../../e2e/tests/logs.rs:external_contract}} ``` -And this is an example that uses `with_contract_ids(&[&contract_id, ...])`. - -```rust,ignore -{{#include ../../e2e/tests/logs.rs:external_contract_ids}} -``` - ## Configurable constants Same as contracts, you can define `configurable` constants in `scripts` which can be changed during the script execution. Here is an example how the constants are defined. diff --git a/e2e/tests/contracts.rs b/e2e/tests/contracts.rs index 0a38d02516..cc90206a68 100644 --- a/e2e/tests/contracts.rs +++ b/e2e/tests/contracts.rs @@ -91,8 +91,6 @@ async fn test_contract_calling_contract() -> Result<()> { let response = contract_caller_instance .methods() .increment_from_contracts(lib_contract_id, lib_contract_id2, 42) - // Note that the two lib_contract_instances have different types - .with_contracts(&[&lib_contract_instance, &lib_contract_instance2]) .call() .await?; @@ -102,21 +100,20 @@ async fn test_contract_calling_contract() -> Result<()> { let response = contract_caller_instance .methods() .increment_from_contract(lib_contract_id, 42) - .with_contracts(&[&lib_contract_instance]) .call() .await?; // ANCHOR_END: external_contract assert_eq!(43, response.value); - // ANCHOR: external_contract_ids + // ANCHOR: external_contract_logs let response = contract_caller_instance .methods() - .increment_from_contract(lib_contract_id, 42) - .with_contract_ids(&[lib_contract_id.clone()]) + .increment_from_contract(lib_contract_instance.contract_id(), 42) + .add_log_decoder(lib_contract_instance.log_decoder()) .call() .await?; - // ANCHOR_END: external_contract_ids + // ANCHOR_END: external_contract_logs assert_eq!(43, response.value); Ok(()) @@ -276,7 +273,7 @@ async fn test_multi_call_pro() -> Result<()> { } #[tokio::test] -async fn test_contract_call_fee_estimation() -> Result<()> { +async fn contract_call_fee_estimation() -> Result<()> { setup_program_test!( Wallets("wallet"), Abigen(Contract( @@ -291,21 +288,20 @@ async fn test_contract_call_fee_estimation() -> Result<()> { ), ); - let gas_limit = 800; + let gas_limit = 3800; let tolerance = Some(0.2); let block_horizon = Some(1); - let expected_script_gas = 800; - let expected_total_gas = 8463; + let expected_total_gas = 10500; let expected_metered_bytes_size = 824; let estimated_transaction_cost = contract_instance .methods() .initialize_counter(42) - .with_tx_policies(TxPolicies::default().with_script_gas_limit(gas_limit)) + .with_script_gas_limit(gas_limit) .estimate_transaction_cost(tolerance, block_horizon) .await?; - assert_eq!(estimated_transaction_cost.script_gas, expected_script_gas); + assert!(estimated_transaction_cost.script_gas < gas_limit); assert_eq!(estimated_transaction_cost.total_gas, expected_total_gas); assert_eq!( estimated_transaction_cost.metered_bytes_size, @@ -601,7 +597,6 @@ async fn test_contract_setup_macro_deploy_with_salt() -> Result<()> { let response = contract_caller_instance .methods() .increment_from_contract(lib_contract_id, 42) - .with_contracts(&[&lib_contract_instance]) .call() .await?; @@ -610,7 +605,6 @@ async fn test_contract_setup_macro_deploy_with_salt() -> Result<()> { let response = contract_caller_instance2 .methods() .increment_from_contract(lib_contract_id, 42) - .with_contracts(&[&lib_contract_instance]) .call() .await?; @@ -666,9 +660,7 @@ async fn test_connect_wallet() -> Result<()> { // ANCHOR_END: contract_setup_macro_manual_wallet // pay for call with wallet - let tx_policies = TxPolicies::default() - .with_tip(100) - .with_script_gas_limit(1_000_000); + let tx_policies = TxPolicies::default().with_tip(100); contract_instance .methods() @@ -724,7 +716,7 @@ async fn setup_output_variable_estimation_test() } #[tokio::test] -async fn test_output_variable_estimation() -> Result<()> { +async fn output_variable_estimation() -> Result<()> { abigen!(Contract( name = "MyContract", abi = "e2e/sway/contracts/token_ops/out/release/token_ops-abi.json" @@ -737,38 +729,22 @@ async fn test_output_variable_estimation() -> Result<()> { let contract_methods = contract_instance.methods(); let amount = 1000; - { - // Should fail due to lack of output variables - let response = contract_methods - .mint_to_addresses(amount, addresses) - .call() - .await; - - assert!(matches!( - response, - Err(Error::Transaction(Reason::Failure { .. })) - )); - } - - { - // Should add 3 output variables automatically - let _ = contract_methods - .mint_to_addresses(amount, addresses) - .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) - .call() - .await?; + // Should add 3 output variables automatically + let _ = contract_methods + .mint_to_addresses(amount, addresses) + .call() + .await?; - for wallet in wallets.iter() { - let balance = wallet.get_asset_balance(&mint_asset_id).await?; - assert_eq!(balance, amount); - } + for wallet in wallets.iter() { + let balance = wallet.get_asset_balance(&mint_asset_id).await?; + assert_eq!(balance, amount); } Ok(()) } #[tokio::test] -async fn test_output_variable_estimation_multicall() -> Result<()> { +async fn output_variable_estimation_multicall() -> Result<()> { abigen!(Contract( name = "MyContract", abi = "e2e/sway/contracts/token_ops/out/release/token_ops-abi.json" @@ -803,10 +779,7 @@ async fn test_output_variable_estimation_multicall() -> Result<()> { let call_handler = contract_methods.send_message(base_layer_address, amount); multi_call_handler = multi_call_handler.add_call(call_handler); - let _ = multi_call_handler - .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) - .call::<((), (), ())>() - .await?; + let _ = multi_call_handler.call::<((), (), ())>().await?; for wallet in wallets.iter() { let balance = wallet.get_asset_balance(&mint_asset_id).await?; @@ -896,7 +869,7 @@ async fn contract_call_futures_implement_send() -> Result<()> { } #[tokio::test] -async fn test_contract_set_estimation() -> Result<()> { +async fn external_contract_estimation() -> Result<()> { setup_program_test!( Wallets("wallet"), Abigen( @@ -927,25 +900,9 @@ async fn test_contract_set_estimation() -> Result<()> { let res = lib_contract_instance.methods().increment(42).call().await?; assert_eq!(43, res.value); - { - // Should fail due to missing external contracts - let res = contract_caller_instance - .methods() - .increment_from_contract(lib_contract_id, 42) - .call() - .await; - - assert!(matches!( - res, - Err(Error::Transaction(Reason::Failure { .. })) - )); - } - let res = contract_caller_instance .methods() .increment_from_contract(lib_contract_id, 42) - .determine_missing_contracts() - .await? .call() .await?; @@ -954,7 +911,7 @@ async fn test_contract_set_estimation() -> Result<()> { } #[tokio::test] -async fn test_output_variable_contract_id_estimation_multicall() -> Result<()> { +async fn output_variable_contract_id_estimation_multicall() -> Result<()> { setup_program_test!( Wallets("wallet"), Abigen( @@ -1009,11 +966,7 @@ async fn test_output_variable_contract_id_estimation_multicall() -> Result<()> { multi_call_handler = multi_call_handler.add_call(call_handler); - let call_response = multi_call_handler - .determine_missing_contracts() - .await? - .call::<(u64, u64, u64, u64)>() - .await?; + let call_response = multi_call_handler.call::<(u64, u64, u64, u64)>().await?; assert_eq!(call_response.value, (43, 43, 43, 11)); @@ -1274,12 +1227,10 @@ async fn low_level_call() -> Result<()> { caller_contract_instance .methods() .call_low_level_call( - target_contract_instance.id(), + target_contract_instance.contract_id(), Bytes(function_selector), Bytes(call_data), ) - .determine_missing_contracts() - .await? .call() .await?; @@ -1303,12 +1254,10 @@ async fn low_level_call() -> Result<()> { caller_contract_instance .methods() .call_low_level_call( - target_contract_instance.id(), + target_contract_instance.contract_id(), Bytes(function_selector), Bytes(call_data), ) - .determine_missing_contracts() - .await? .call() .await?; @@ -1743,11 +1692,7 @@ async fn contract_custom_call_no_signatures_strategy() -> Result<()> { // ANCHOR_END: tx_sign_with // ANCHOR_END: tb_no_signatures_strategy - let tx_id = provider.send_transaction(tx).await?; - tokio::time::sleep(Duration::from_millis(500)).await; - - let tx_status = provider.tx_status(&tx_id).await?; - + let tx_status = provider.send_transaction_and_await_commit(tx).await?; let response = call_handler.get_response(tx_status)?; assert_eq!(counter, response.value); @@ -1919,11 +1864,7 @@ async fn variable_output_estimation_is_optimized() -> Result<()> { let coins = 252; let recipient = Identity::Address(wallet.address().into()); let start = Instant::now(); - let _ = contract_methods - .mint(coins, recipient) - .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) - .call() - .await?; + let _ = contract_methods.mint(coins, recipient).call().await?; // debug builds are slower (20x for `fuel-core-lib`, 4x for a release-fuel-core-binary) // we won't validate in that case so we don't have to maintain two expectations @@ -2516,13 +2457,7 @@ async fn loader_works_via_proxy() -> Result<()> { .call() .await?; - let response = proxy - .methods() - .something() - .with_contract_ids(&[contract_id]) - .call() - .await? - .value; + let response = proxy.methods().something().call().await?.value; assert_eq!(response, 1001); @@ -2573,62 +2508,19 @@ async fn loader_storage_works_via_proxy() -> Result<()> { .call() .await?; - let response = proxy - .methods() - .read_some_u64() - .with_contract_ids(&[contract_id.clone()]) - .call() - .await? - .value; + let response = proxy.methods().read_some_u64().call().await?.value; assert_eq!(response, 42); - let _res = proxy - .methods() - .write_some_u64(36) - .with_contract_ids(&[contract_id.clone()]) - .call() - .await?; + let _res = proxy.methods().write_some_u64(36).call().await?; - let response = proxy - .methods() - .read_some_u64() - .with_contract_ids(&[contract_id]) - .call() - .await? - .value; + let response = proxy.methods().read_some_u64().call().await?.value; assert_eq!(response, 36); Ok(()) } -#[tokio::test] -async fn adjust_for_fee_errors() -> Result<()> { - setup_program_test!( - Wallets("wallet"), - Abigen(Contract( - name = "MyContract", - project = "e2e/sway/contracts/contract_test" - )), - ); - - let contract_binary = "sway/contracts/contract_test/out/release/contract_test.bin"; - - let err = Contract::load_from(contract_binary, LoadConfiguration::default())? - .deploy(&wallet, TxPolicies::default().with_tip(10_000_000_000_000)) - .await - .expect_err("should return error"); - - assert!( - matches!(err, Error::Provider(s) if s.contains("failed to adjust inputs to cover for missing \ - base asset: failed to get base asset \ - (0000000000000000000000000000000000000000000000000000000000000000) inputs with amount:")) - ); - - Ok(()) -} - #[tokio::test] async fn tx_input_output() -> Result<()> { let [wallet_1, wallet_2] = launch_custom_provider_and_get_wallets( diff --git a/e2e/tests/logs.rs b/e2e/tests/logs.rs index aceba07ed9..ab1578ad6b 100644 --- a/e2e/tests/logs.rs +++ b/e2e/tests/logs.rs @@ -390,13 +390,13 @@ async fn multi_call_contract_with_contract_logs() -> Result<()> { let call_handler_1 = contract_caller_instance .methods() - .logs_from_external_contract(contract_instance.id()) - .with_contracts(&[&contract_instance]); + .logs_from_external_contract(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()); let call_handler_2 = contract_caller_instance2 .methods() - .logs_from_external_contract(contract_instance.id()) - .with_contracts(&[&contract_instance]); + .logs_from_external_contract(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()); let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) @@ -750,8 +750,8 @@ async fn contract_with_contract_logs() -> Result<()> { let logs = contract_caller_instance .methods() - .logs_from_external_contract(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .logs_from_external_contract(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .call() .await? .decode_logs(); @@ -802,21 +802,13 @@ async fn script_logs_with_contract_logs() -> Result<()> { ]; // ANCHOR: instance_to_contract_id - let contract_id: ContractId = contract_instance.id().into(); + let contract_id: ContractId = contract_instance.contract_id().into(); // ANCHOR_END: instance_to_contract_id - // ANCHOR: external_contract_ids - let response = script_instance - .main(contract_id, MatchEnum::Logs) - .with_contract_ids(&[contract_id.into()]) - .call() - .await?; - // ANCHOR_END: external_contract_ids - // ANCHOR: external_contract let response = script_instance .main(contract_id, MatchEnum::Logs) - .with_contracts(&[&contract_instance]) + .add_log_decoder(contract_instance.log_decoder()) .call() .await?; // ANCHOR_END: external_contract @@ -1024,8 +1016,8 @@ async fn contract_require_from_contract() -> Result<()> { let error = contract_caller_instance .methods() - .require_from_contract(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .require_from_contract(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .call() .await .expect_err("should return a revert error"); @@ -1077,8 +1069,8 @@ async fn multi_call_contract_require_from_contract() -> Result<()> { let call_handler_2 = contract_caller_instance .methods() - .require_from_contract(lib_contract_instance.id()) - .with_contracts(&[&lib_contract_instance]); + .require_from_contract(lib_contract_instance.contract_id()) + .add_log_decoder(lib_contract_instance.log_decoder()); let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) @@ -1122,8 +1114,8 @@ async fn script_require_from_contract() -> Result<()> { ); let error = script_instance - .main(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .main(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .call() .await .expect_err("should return a revert error"); @@ -1167,8 +1159,8 @@ async fn loader_script_require_from_loader_contract() -> Result<()> { script_instance.convert_into_loader().await?; let error = script_instance - .main(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .main(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .call() .await .expect_err("should return a revert error"); @@ -1573,8 +1565,8 @@ async fn log_results() -> Result<()> { let expected_err = format!( "codec: missing log formatter for log_id: `LogId({:?}, \"128\")`, data: `{:?}`. \ - Consider adding external contracts using `with_contracts()`", - contract_instance.id().hash, + Consider adding external log decoder using `add_log_decoder()`", + contract_instance.contract_id().hash, [0u8; 8] ); @@ -1933,8 +1925,8 @@ async fn contract_with_contract_panic() -> Result<()> { ($method:ident, call, $msg:expr) => { let error = contract_caller_instance .methods() - .$method(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .$method(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .call() .await .expect_err("should return a revert error"); @@ -1944,8 +1936,8 @@ async fn contract_with_contract_panic() -> Result<()> { ($method:ident, simulate, $msg:expr) => { let error = contract_caller_instance .methods() - .$method(contract_instance.id()) - .with_contracts(&[&contract_instance]) + .$method(contract_instance.contract_id()) + .add_log_decoder(contract_instance.log_decoder()) .simulate(Execution::realistic()) .await .expect_err("should return a revert error"); @@ -2057,7 +2049,7 @@ async fn script_with_contract_panic() -> Result<()> { ($arg1:expr, $arg2:expr, call, $msg:expr) => { let error = script_instance .main($arg1, $arg2) - .with_contracts(&[&contract_instance]) + .add_log_decoder(contract_instance.log_decoder()) .call() .await .expect_err("should return a revert error"); @@ -2066,19 +2058,19 @@ async fn script_with_contract_panic() -> Result<()> { ($arg1:expr, $arg2:expr, simulate, $msg:expr) => { let error = script_instance .main($arg1, $arg2) - .with_contracts(&[&contract_instance]) + .add_log_decoder(contract_instance.log_decoder()) .simulate(Execution::realistic()) .await .expect_err("should return a revert error"); assert_revert_containing_msg($msg, error); }; } - let contract_id = contract_instance.id(); + let contract_id = contract_instance.contract_id(); { - reverts_with_msg!(&contract_id, MatchEnum::Panic, call, "some panic message"); + reverts_with_msg!(contract_id, MatchEnum::Panic, call, "some panic message"); reverts_with_msg!( - &contract_id, + contract_id, MatchEnum::Panic, simulate, "some panic message" @@ -2086,13 +2078,13 @@ async fn script_with_contract_panic() -> Result<()> { } { reverts_with_msg!( - &contract_id, + contract_id, MatchEnum::PanicError, call, "some complex error B: B { id: 42, val: 36 }" ); reverts_with_msg!( - &contract_id, + contract_id, MatchEnum::PanicError, simulate, "some complex error B: B { id: 42, val: 36 }" diff --git a/e2e/tests/predicates.rs b/e2e/tests/predicates.rs index 33209c6b30..43efa5be4b 100644 --- a/e2e/tests/predicates.rs +++ b/e2e/tests/predicates.rs @@ -236,7 +236,6 @@ async fn pay_with_predicate() -> Result<()> { )? .deploy_if_not_exists(&predicate, TxPolicies::default()) .await?; - let contract_methods = MyContract::new(deploy_response.contract_id.clone(), predicate.clone()).methods(); @@ -921,6 +920,9 @@ async fn predicate_can_access_manually_added_witnesses() -> Result<()> { tx.append_witness(witness.into())?; tx.append_witness(witness2.into())?; + // As we have changed the witnesses and the predicate code depends on them we need + // to estimate the predicates again before sending the tx. + tx.estimate_predicates(&provider).await?; let tx_status = provider.send_transaction_and_await_commit(tx).await?; let fee = tx_status.total_fee(); @@ -993,7 +995,10 @@ async fn tx_id_not_changed_after_adding_witnesses() -> Result<()> { tx.append_witness(witness2.into())?; let tx_id_after_witnesses = tx.id(chain_id); - let tx_id_from_provider = provider.send_transaction(tx).await?; + // As we have changed the witnesses and the predicate code depends on them we need + // to estimate the predicates again before sending the tx. + tx.estimate_predicates(&provider).await?; + let tx_id_from_provider = provider.submit(tx).await?; assert_eq!(tx_id, tx_id_after_witnesses); assert_eq!(tx_id, tx_id_from_provider); @@ -1355,7 +1360,7 @@ async fn predicate_tx_input_output() -> Result<()> { Deploy( name = "contract_instance", contract = "TestContract", - wallet = "wallet_1", + wallet = "wallet_2", random_salt = false, ), ); @@ -1402,7 +1407,6 @@ async fn predicate_tx_input_output() -> Result<()> { .methods() .initialize_counter(36) .with_inputs(custom_inputs) - .add_signer(wallet_2.signer().clone()) .with_outputs(custom_output) .call() .await? @@ -1432,6 +1436,7 @@ async fn predicate_tx_input_output() -> Result<()> { .methods() .initialize_counter(36) .with_inputs(custom_inputs) + .add_signer(wallet_1.signer().clone()) .call() .await .unwrap_err(); diff --git a/e2e/tests/providers.rs b/e2e/tests/providers.rs index 0a79e6bbf4..5cf7db8470 100644 --- a/e2e/tests/providers.rs +++ b/e2e/tests/providers.rs @@ -15,6 +15,7 @@ use fuels::{ Bits256, coin_type::CoinType, message::Message, + output::Output, transaction_builders::{BuildableTransaction, ScriptTransactionBuilder}, tx_status::{Success, TxStatus}, }, @@ -361,7 +362,7 @@ async fn test_gas_forwarded_defaults_to_tx_limit() -> Result<()> { let response = contract_instance .methods() .initialize_counter(42) - .with_tx_policies(TxPolicies::default().with_script_gas_limit(gas_limit)) + .with_script_gas_limit(gas_limit) .call() .await?; @@ -412,14 +413,12 @@ async fn test_amount_and_asset_forwarding() -> Result<()> { .await?; assert_eq!(balance_response.value, 5_000_000); - let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); // Forward 1_000_000 coin amount of base asset_id - // this is a big number for checking that amount can be a u64 let call_params = CallParameters::default().with_amount(1_000_000); let response = contract_methods .get_msg_amount() - .with_tx_policies(tx_policies) + .with_script_gas_limit(1_000_000) .call_params(call_params)? .call() .await?; @@ -445,7 +444,6 @@ async fn test_amount_and_asset_forwarding() -> Result<()> { // withdraw some tokens to wallet contract_methods .transfer(1_000_000, asset_id, address.into()) - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() .await?; @@ -453,11 +451,10 @@ async fn test_amount_and_asset_forwarding() -> Result<()> { let call_params = CallParameters::default() .with_amount(0) .with_asset_id(asset_id); - let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); let response = contract_methods .get_msg_amount() - .with_tx_policies(tx_policies) + .with_script_gas_limit(1_000_000) .call_params(call_params)? .call() .await?; @@ -509,28 +506,6 @@ async fn test_gas_errors() -> Result<()> { ), ); - // Test running out of gas. Gas price as `None` will be 0. - let gas_limit = 42; - let contract_instance_call = contract_instance - .methods() - .initialize_counter(42) // Build the ABI call - .with_tx_policies(TxPolicies::default().with_script_gas_limit(gas_limit)); - - // Test that the call will use more gas than the gas limit - let total_gas = contract_instance_call - .estimate_transaction_cost(None, None) - .await? - .total_gas; - assert!(total_gas > gas_limit); - - let response = contract_instance_call - .call() - .await - .expect_err("should error"); - - let expected = "transaction reverted: OutOfGas"; - assert!(response.to_string().starts_with(expected)); - // Test for insufficient base asset amount to pay for the transaction fee let response = contract_instance .methods() @@ -540,7 +515,7 @@ async fn test_gas_errors() -> Result<()> { .await .expect_err("should error"); - let expected = "Response errors; Validity(InsufficientFeeAmount"; + let expected = "the target cannot be met"; assert!(response.to_string().contains(expected)); Ok(()) @@ -566,7 +541,7 @@ async fn test_call_param_gas_errors() -> Result<()> { let contract_methods = contract_instance.methods(); let response = contract_methods .initialize_counter(42) - .with_tx_policies(TxPolicies::default().with_script_gas_limit(446000)) + .with_script_gas_limit(446000) .call_params(CallParameters::default().with_gas_forwarded(1))? .call() .await @@ -578,7 +553,7 @@ async fn test_call_param_gas_errors() -> Result<()> { // Call params gas_forwarded exceeds transaction limit let response = contract_methods .initialize_counter(42) - .with_tx_policies(TxPolicies::default().with_script_gas_limit(1)) + .with_script_gas_limit(1) .call_params(CallParameters::default().with_gas_forwarded(1_000))? .call() .await @@ -624,7 +599,7 @@ async fn test_parse_block_time() -> Result<()> { let coins = setup_single_asset_coins(signer.address(), asset_id, 1, DEFAULT_COIN_AMOUNT); let provider = setup_test_provider(coins.clone(), vec![], None, None).await?; let wallet = Wallet::new(signer, provider.clone()); - let tx_policies = TxPolicies::default().with_script_gas_limit(2000); + let tx_policies = TxPolicies::default(); let wallet_2 = wallet.lock(); let tx_response = wallet @@ -818,8 +793,8 @@ async fn transactions_with_the_same_utxo() -> Result<()> { let tx_1 = create_transfer(&wallet_1, 100, wallet_2.address()).await?; let tx_2 = create_transfer(&wallet_1, 101, wallet_2.address()).await?; - let _tx_id = provider.send_transaction(tx_1).await?; - let res = provider.send_transaction(tx_2).await; + let _tx_id = provider.submit(tx_1).await?; + let res = provider.submit(tx_2).await; let err = res.expect_err("is error"); @@ -835,6 +810,43 @@ async fn transactions_with_the_same_utxo() -> Result<()> { Ok(()) } +#[cfg(feature = "coin-cache")] +#[tokio::test] +async fn transfers_at_same_time_with_cache() -> Result<()> { + let amount = 1000; + let num_coins = 10; + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(1), Some(num_coins), Some(amount)), + Some(NodeConfig::default()), + None, + ) + .await?; + let wallet_1 = wallets.pop().unwrap(); + let provider = wallet_1.provider(); + let wallet_2 = Wallet::random(&mut thread_rng(), provider.clone()); + let asset_id = AssetId::zeroed(); + + let tx_1 = create_transfer(&wallet_1, 100, wallet_2.address()).await?; + let _tx_id = provider.submit(tx_1).await?; + + // will use assemble tx and exclude coins from the above submit + wallet_1 + .transfer( + // will use assemble tx and exclude coins from the above submit + wallet_2.address(), + 101, + AssetId::zeroed(), + TxPolicies::default(), + ) + .await?; + + let balance = wallet_2.get_asset_balance(&asset_id).await?; + + assert_eq!(201, balance); + + Ok(()) +} + #[cfg(feature = "coin-cache")] #[tokio::test] async fn coin_caching() -> Result<()> { @@ -858,7 +870,7 @@ async fn coin_caching() -> Result<()> { let mut tx_ids = vec![]; for _ in 0..num_iterations { let tx = create_transfer(&wallet_1, amount_to_send, wallet_2.address()).await?; - let tx_id = provider.send_transaction(tx).await?; + let tx_id = provider.submit(tx).await?; tx_ids.push(tx_id); } @@ -1080,7 +1092,7 @@ async fn send_transaction_and_subscribe_status() -> Result<()> { // When let mut statuses = provider.subscribe_transaction_status(&tx_id, true).await?; - let _ = provider.send_transaction(tx).await?; + let _ = provider.submit(tx).await?; // Then assert!(matches!( @@ -1127,7 +1139,7 @@ async fn can_produce_blocks_with_trig_never() -> Result<()> { let tx = tb.build(provider).await?; let tx_id = tx.id(consensus_parameters.chain_id()); - provider.send_transaction(tx).await?; + provider.submit(tx).await?; provider.produce_blocks(1, None).await?; tokio::time::sleep(std::time::Duration::from_millis(500)).await; @@ -1181,7 +1193,7 @@ async fn can_upload_executor_and_trigger_upgrade() -> Result<()> { wallet.adjust_for_fee(&mut builder, 0).await?; let tx = builder.build(provider.clone()).await?; - provider.send_transaction(tx).await?; + provider.submit(tx).await?; Ok(()) } @@ -1214,7 +1226,6 @@ async fn tx_respects_policies() -> Result<()> { Some(maturity), Some(expiration), Some(max_fee), - Some(script_gas_limit), ); // advance the block height to ensure the maturity is respected @@ -1227,6 +1238,7 @@ async fn tx_respects_policies() -> Result<()> { .methods() .initialize_counter(42) .with_tx_policies(tx_policies) + .with_script_gas_limit(script_gas_limit) .call() .await?; @@ -1244,7 +1256,7 @@ async fn tx_respects_policies() -> Result<()> { assert_eq!(script.tip().unwrap(), tip); assert_eq!(script.witness_limit().unwrap(), witness_limit); assert_eq!(script.max_fee().unwrap(), max_fee); - assert_eq!(script.gas_limit(), script_gas_limit); + assert_eq!(script.gas_limit(), 3000); Ok(()) } @@ -1453,3 +1465,51 @@ async fn is_account_query_test() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn assemble_tx_transfer() -> Result<()> { + // ANCHOR: assemble_wallets + let wallet = launch_provider_and_get_wallet().await?; + let provider = wallet.provider().clone(); + let receiver = Wallet::random(&mut thread_rng(), provider.clone()); + // ANCHOR_END: assemble_wallets + + // ANCHOR: assemble_output + let consensus_parameters = provider.consensus_parameters().await?; + let base_asset_id = *consensus_parameters.base_asset_id(); + + let amount_to_send = 78; + let outputs = vec![Output::Coin { + to: receiver.address().into(), + asset_id: base_asset_id, + amount: amount_to_send, + }]; + // ANCHOR_END: assemble_output + + // ANCHOR: assemble_req_balance + let fee_index = 0u16; + let required_balances = vec![wallet.required_balance(amount_to_send, base_asset_id, None)]; + // ANCHOR_END: assemble_req_balance + + // ANCHOR: assemble_tb + let mut tb = ScriptTransactionBuilder::prepare_transfer(vec![], outputs, TxPolicies::default()) + .with_build_strategy(ScriptBuildStrategy::AssembleTx { + required_balances, + fee_index, + }); + tb.add_signer(wallet.signer().clone())?; + + let tx = tb.build(&provider).await?; + // ANCHOR_END: assemble_tb + + // ANCHOR: assemble_response + let _tx_status = provider.send_transaction_and_await_commit(tx).await?; + + assert_eq!( + amount_to_send, + receiver.get_asset_balance(&base_asset_id).await? + ); + // ANCHOR_END: assemble_response + + Ok(()) +} diff --git a/e2e/tests/scripts.rs b/e2e/tests/scripts.rs index c426aa62df..e6ce9444d4 100644 --- a/e2e/tests/scripts.rs +++ b/e2e/tests/scripts.rs @@ -95,7 +95,7 @@ async fn test_basic_script_with_tx_policies() -> Result<()> { assert_eq!(result.value, "hello"); // ANCHOR: script_with_tx_policies - let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); + let tx_policies = TxPolicies::default().with_expiration(1_000); let result = script_instance .main(a, b) .with_tx_policies(tx_policies) @@ -139,7 +139,6 @@ async fn test_output_variable_estimation() -> Result<()> { let _ = script_call .with_inputs(inputs) .with_outputs(vec![output]) - .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) .call() .await?; @@ -324,7 +323,7 @@ async fn test_script_transaction_builder() -> Result<()> { let tx = tb.build(provider).await?; - let tx_id = provider.send_transaction(tx).await?; + let tx_id = provider.submit(tx).await?; tokio::time::sleep(Duration::from_millis(500)).await; let tx_status = provider.tx_status(&tx_id).await?; @@ -549,9 +548,14 @@ async fn high_level_blob_upload_sets_max_fee_tolerance() -> Result<()> { }) .unwrap(); - assert_eq!( - max_fee_of_sent_blob_tx, - (zero_tolerance_fee as f32 * (1.0 + DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE)).ceil() as u64, + let expected_fee_with_tolerance = (zero_tolerance_fee as f64 + * (1.0 + DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE as f64)) + .ceil() as u64; + + let gas_error_margin = 2; + assert!( + (max_fee_of_sent_blob_tx as i64 - expected_fee_with_tolerance as i64).abs() + < gas_error_margin, "the blob upload tx should have had the max fee increased by the default estimation tolerance" ); @@ -623,7 +627,6 @@ async fn loader_script_calling_loader_proxy() -> Result<()> { .convert_into_loader() .await? .main(proxy_id.clone()) - .with_contract_ids(&[contract_id, proxy_id]) .call() .await?; diff --git a/e2e/tests/types_contracts.rs b/e2e/tests/types_contracts.rs index 0e3f382471..2ef91ec18a 100644 --- a/e2e/tests/types_contracts.rs +++ b/e2e/tests/types_contracts.rs @@ -1857,7 +1857,6 @@ async fn contract_std_lib_string() -> Result<()> { // confirm encoding/decoding a string wasn't faulty and led to too high gas consumption let _resp = contract_methods .echoes_dynamic_string(String::from("Hello Fuel")) - .with_tx_policies(TxPolicies::default().with_script_gas_limit(3600)) .call() .await?; } diff --git a/e2e/tests/wallets.rs b/e2e/tests/wallets.rs index 124a960014..2d39c4ac1c 100644 --- a/e2e/tests/wallets.rs +++ b/e2e/tests/wallets.rs @@ -123,6 +123,27 @@ async fn adjust_fee_empty_transaction() -> Result<()> { Ok(()) } +#[tokio::test] +async fn adjust_for_fee_error() -> Result<()> { + let wallet = launch_provider_and_get_wallet().await?; + let tx_policies = TxPolicies::default().with_tip(10_000_000_000_000); + + let mut tb = ScriptTransactionBuilder::prepare_transfer(vec![], vec![], tx_policies); + + wallet.add_witnesses(&mut tb)?; + let err = wallet + .adjust_for_fee(&mut tb, 0) + .await + .expect_err("should return error"); + + assert!( + matches!(err, Error::Provider(s) if s.contains("failed to get base asset \ + (0000000000000000000000000000000000000000000000000000000000000000) inputs with amount:")) + ); + + Ok(()) +} + #[tokio::test] async fn adjust_for_fee_with_message_data_input() -> Result<()> { let wallet_signer = PrivateKeySigner::random(&mut rand::thread_rng()); @@ -264,13 +285,9 @@ async fn send_transfer_transactions() -> Result<()> { // Configure transaction policies let tip = 2; - let script_gas_limit = 500_000; let maturity = 0; - let tx_policies = TxPolicies::default() - .with_tip(tip) - .with_maturity(maturity) - .with_script_gas_limit(script_gas_limit); + let tx_policies = TxPolicies::default().with_tip(tip).with_maturity(maturity); // Transfer 1 from wallet 1 to wallet 2. let amount_to_send = 1; @@ -296,8 +313,6 @@ async fn send_transfer_transactions() -> Result<()> { TransactionType::Script(tx) => tx, _ => panic!("Received unexpected tx type!"), }; - // Transfer scripts uses set `script_gas_limit` despite not having script code - assert_eq!(script.gas_limit(), script_gas_limit); assert_eq!(script.maturity().unwrap(), maturity); let wallet_1_spendable_resources = wallet_1 diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 47211c9e65..c0f3e3a833 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -8,10 +8,7 @@ mod tests { prelude::{LoadConfiguration, NodeConfig, StorageConfiguration}, programs::debug::ScriptType, test_helpers::{ChainConfig, StateConfig}, - types::{ - Bits256, - errors::{Result, transaction::Reason}, - }, + types::{Bits256, errors::Result}, }; use rand::{Rng, thread_rng}; @@ -162,7 +159,6 @@ mod tests { // Optional: Configure deployment parameters let tx_policies = TxPolicies::default() .with_tip(1) - .with_script_gas_limit(1_000_000) .with_maturity(0) .with_expiration(10_000); @@ -301,7 +297,6 @@ mod tests { let tx_policies = TxPolicies::default() .with_tip(1) - .with_script_gas_limit(1_000_000) .with_maturity(0) .with_expiration(10_000); @@ -320,13 +315,20 @@ mod tests { .await?; // ANCHOR_END: tx_policies_default + // ANCHOR: tx_script_gas_limit + let response = contract_methods + .initialize_counter(42) + .with_script_gas_limit(42_000) + .call() + .await?; + // ANCHOR_END: tx_script_gas_limit + // ANCHOR: call_parameters let contract_methods = MyContract::new(contract_id, wallet.clone()).methods(); let tx_policies = TxPolicies::default(); // Forward 1_000_000 coin amount of base asset_id - // this is a big number for checking that amount can be a u64 let call_params = CallParameters::default().with_amount(1_000_000); let response = contract_methods @@ -437,7 +439,6 @@ mod tests { // withdraw some tokens to wallet let response = contract_methods .transfer(1_000_000, asset_id, address.into()) - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() .await?; // ANCHOR_END: variable_outputs @@ -474,46 +475,19 @@ mod tests { let contract_methods = MyContract::new(caller_contract_id.clone(), wallet.clone()).methods(); - // ANCHOR: dependency_estimation_fail let address = wallet.address(); let amount = 100; - let response = contract_methods - .mint_then_increment_from_contract(called_contract_id, amount, address.into()) - .call() - .await; - - assert!(matches!( - response, - Err(Error::Transaction(Reason::Failure { .. })) - )); - // ANCHOR_END: dependency_estimation_fail - - // ANCHOR: dependency_estimation_manual - let response = contract_methods - .mint_then_increment_from_contract(called_contract_id, amount, address.into()) - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) - .with_contract_ids(&[called_contract_id.into()]) - .call() - .await?; - // ANCHOR_END: dependency_estimation_manual - - let asset_id = caller_contract_id.asset_id(&Bits256::zeroed()); - let balance = wallet.get_asset_balance(&asset_id).await?; - assert_eq!(balance, amount); - // ANCHOR: dependency_estimation let response = contract_methods .mint_then_increment_from_contract(called_contract_id, amount, address.into()) - .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) - .determine_missing_contracts() - .await? .call() .await?; // ANCHOR_END: dependency_estimation + let asset_id = caller_contract_id.asset_id(&Bits256::zeroed()); let balance = wallet.get_asset_balance(&asset_id).await?; - assert_eq!(balance, 2 * amount); + assert_eq!(balance, amount); Ok(()) } @@ -576,12 +550,11 @@ mod tests { // Set the transaction `gas_limit` to 1_000_000 and `gas_forwarded` to 4300 to specify that // the contract call transaction may consume up to 1_000_000 gas, while the actual call may // only use 4300 gas - let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000); let call_params = CallParameters::default().with_gas_forwarded(4300); let response = contract_methods .get_msg_amount() // Our contract method. - .with_tx_policies(tx_policies) // Chain the tx policies. + .with_script_gas_limit(1_000_000) .call_params(call_params)? // Chain the call parameters. .call() // Perform the contract call. .await?; @@ -819,12 +792,10 @@ mod tests { caller_contract_instance .methods() .call_low_level_call( - target_contract_instance.id(), + target_contract_instance.contract_id(), Bytes(function_selector), Bytes(call_data), ) - .determine_missing_contracts() - .await? .call() .await?; // ANCHOR_END: low_level_call @@ -953,7 +924,7 @@ mod tests { let tx = tb.build(provider).await?; - let tx_id = provider.send_transaction(tx).await?; + let tx_id = provider.submit(tx).await?; tokio::time::sleep(Duration::from_millis(500)).await; let tx_status = provider.tx_status(&tx_id).await?; diff --git a/examples/cookbook/src/lib.rs b/examples/cookbook/src/lib.rs index 91296b479c..4cd743a571 100644 --- a/examples/cookbook/src/lib.rs +++ b/examples/cookbook/src/lib.rs @@ -75,7 +75,6 @@ mod tests { contract_methods .deposit(wallet.address().into()) .call_params(call_params)? - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() .await?; // ANCHOR_END: liquidity_deposit @@ -91,7 +90,6 @@ mod tests { contract_methods .withdraw(wallet.address().into()) .call_params(call_params)? - .with_variable_output_policy(VariableOutputPolicy::Exactly(1)) .call() .await?; @@ -326,7 +324,7 @@ mod tests { // ANCHOR: custom_tx_build let tx = tb.build(&provider).await?; - let tx_id = provider.send_transaction(tx).await?; + let tx_id = provider.submit(tx).await?; // ANCHOR_END: custom_tx_build tokio::time::sleep(Duration::from_millis(500)).await; diff --git a/packages/fuels-accounts/src/account.rs b/packages/fuels-accounts/src/account.rs index 1ee6534e47..81793b3a6a 100644 --- a/packages/fuels-accounts/src/account.rs +++ b/packages/fuels-accounts/src/account.rs @@ -2,9 +2,10 @@ use std::collections::HashMap; use async_trait::async_trait; use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest}; -use fuel_tx::{Output, TxId, TxPointer, UtxoId}; +use fuel_tx::{Address, Output, TxId, TxPointer, UtxoId}; use fuel_types::{AssetId, Bytes32, ContractId, Nonce}; use fuels_core::types::{ + assemble_tx::{Account as ClientAccount, ChangePolicy, RequiredBalance}, bech32::{Bech32Address, Bech32ContractId}, coin::Coin, coin_type::CoinType, @@ -13,7 +14,9 @@ use fuels_core::types::{ input::Input, message::Message, transaction::{Transaction, TxPolicies}, - transaction_builders::{BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder}, + transaction_builders::{ + BuildableTransaction, ScriptBuildStrategy, ScriptTransactionBuilder, TransactionBuilder, + }, transaction_response::TransactionResponse, tx_response::TxResponse, tx_status::Success, @@ -170,6 +173,22 @@ pub trait Account: ViewOnlyAccount { Ok(()) } + /// Create a required balance for assemble tx using a specific account type + fn required_balance( + &self, + amount: u64, + asset_id: AssetId, + change_address: Option
, + ) -> RequiredBalance { + let account_address = self.address().into(); + RequiredBalance { + asset_id, + amount, + account: ClientAccount::Address(account_address), + change_policy: ChangePolicy::Change(change_address.unwrap_or(account_address)), + } + } + /// Transfer funds from this account to another `Address`. /// Fails if amount for asset ID is larger than address's spendable coins. /// Returns the transaction ID that was sent and the list of receipts. @@ -181,27 +200,32 @@ pub trait Account: ViewOnlyAccount { tx_policies: TxPolicies, ) -> Result { let provider = self.try_provider()?; + let consensus_parameters = provider.consensus_parameters().await?; + let base_asset_id = *consensus_parameters.base_asset_id(); - let inputs = self - .get_asset_inputs_for_amount(asset_id, amount.into(), None) - .await?; - let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount); + let outputs = vec![Output::Coin { + to: to.into(), + asset_id, + amount, + }]; + + let mut fee_index = 0u16; + let mut required_balances = vec![self.required_balance(amount, asset_id, None)]; + + if asset_id != base_asset_id { + fee_index = 1; + required_balances.push(self.required_balance(0, base_asset_id, None)); + } let mut tx_builder = - ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_policies); + ScriptTransactionBuilder::prepare_transfer(vec![], outputs, tx_policies) + .with_build_strategy(ScriptBuildStrategy::AssembleTx { + required_balances, + fee_index, + }); self.add_witnesses(&mut tx_builder)?; - let consensus_parameters = provider.consensus_parameters().await?; - let used_base_amount = if asset_id == *consensus_parameters.base_asset_id() { - amount.into() - } else { - 0 - }; - self.adjust_for_fee(&mut tx_builder, used_base_amount) - .await - .context("failed to adjust inputs to cover for missing base asset")?; - let tx = tx_builder.build(provider).await?; let tx_id = tx.id(consensus_parameters.chain_id()); @@ -230,11 +254,13 @@ pub trait Account: ViewOnlyAccount { tx_policies: TxPolicies, ) -> Result { let provider = self.try_provider()?; + let consensus_parameters = provider.consensus_parameters().await?; + let base_asset_id = *consensus_parameters.base_asset_id(); let zeroes = Bytes32::zeroed(); let plain_contract_id: ContractId = to.into(); - let mut inputs = vec![Input::contract( + let inputs = vec![Input::contract( UtxoId::new(zeroes, 0), zeroes, zeroes, @@ -242,17 +268,16 @@ pub trait Account: ViewOnlyAccount { plain_contract_id, )]; - inputs.extend( - self.get_asset_inputs_for_amount(asset_id, balance.into(), None) - .await?, - ); + let outputs = vec![Output::contract(0, zeroes, zeroes)]; - let outputs = vec![ - Output::contract(0, zeroes, zeroes), - Output::change(self.address().into(), 0, asset_id), - ]; + let mut fee_index = 0u16; + let mut required_balances = vec![self.required_balance(balance, asset_id, None)]; + + if asset_id != base_asset_id { + fee_index = 1; + required_balances.push(self.required_balance(0, base_asset_id, None)); + } - // Build transaction and sign it let mut tb = ScriptTransactionBuilder::prepare_contract_transfer( plain_contract_id, balance, @@ -260,12 +285,13 @@ pub trait Account: ViewOnlyAccount { inputs, outputs, tx_policies, - ); + ) + .with_build_strategy(ScriptBuildStrategy::AssembleTx { + required_balances, + fee_index, + }); self.add_witnesses(&mut tb)?; - self.adjust_for_fee(&mut tb, balance.into()) - .await - .context("failed to adjust inputs to cover for missing base asset")?; let tx = tb.build(provider).await?; @@ -291,25 +317,25 @@ pub trait Account: ViewOnlyAccount { ) -> Result { let provider = self.try_provider()?; let consensus_parameters = provider.consensus_parameters().await?; + let base_asset_id = *consensus_parameters.base_asset_id(); - let inputs = self - .get_asset_inputs_for_amount(*consensus_parameters.base_asset_id(), amount.into(), None) - .await?; + let fee_index = 0u16; + let required_balances = vec![self.required_balance(amount, base_asset_id, None)]; - let mut tb = ScriptTransactionBuilder::prepare_message_to_output( + let mut tx_builder = ScriptTransactionBuilder::prepare_message_to_output( to.into(), amount, - inputs, + vec![], tx_policies, - *consensus_parameters.base_asset_id(), - ); + ) + .with_build_strategy(ScriptBuildStrategy::AssembleTx { + required_balances, + fee_index, + }); - self.add_witnesses(&mut tb)?; - self.adjust_for_fee(&mut tb, amount.into()) - .await - .context("failed to adjust inputs to cover for missing base asset")?; + self.add_witnesses(&mut tx_builder)?; - let tx = tb.build(provider).await?; + let tx = tx_builder.build(provider).await?; let tx_id = tx.id(consensus_parameters.chain_id()); let tx_status = provider.send_transaction_and_await_commit(tx).await?; @@ -334,7 +360,9 @@ mod tests { use fuel_tx::{Address, ConsensusParameters, Output, Transaction as FuelTransaction}; use fuels_core::{ traits::Signer, - types::{DryRun, DryRunner, transaction::Transaction}, + types::{ + DryRun, DryRunner, assemble_tx::AssembleTransactionResult, transaction::Transaction, + }, }; use super::*; @@ -363,11 +391,21 @@ mod tests { Ok(0) } - async fn estimate_predicates( + async fn estimate_predicates(&self, _: &FuelTransaction) -> Result { + unimplemented!() + } + + #[allow(clippy::too_many_arguments)] + async fn assemble_tx( &self, - _: &FuelTransaction, - _: Option, - ) -> Result { + _: impl Transaction + Send, + _: u32, + _: Vec, + _: u16, + _: Option<(Vec, Vec)>, + _: bool, + _: Option, + ) -> Result { unimplemented!() } } diff --git a/packages/fuels-accounts/src/coin_cache.rs b/packages/fuels-accounts/src/coin_cache.rs index 5b2040bebb..dd841d4881 100644 --- a/packages/fuels-accounts/src/coin_cache.rs +++ b/packages/fuels-accounts/src/coin_cache.rs @@ -176,7 +176,7 @@ mod tests { } #[test] - fn test_remove_items() { + fn remove_items() { let mut cache = CoinsCache::new(Duration::from_secs(60)); let key: CoinCacheKey = Default::default(); diff --git a/packages/fuels-accounts/src/predicate.rs b/packages/fuels-accounts/src/predicate.rs index 64b164b782..a69c90d032 100644 --- a/packages/fuels-accounts/src/predicate.rs +++ b/packages/fuels-accounts/src/predicate.rs @@ -1,5 +1,10 @@ use std::{fmt::Debug, fs}; +use async_trait::async_trait; +use fuel_tx::Address; +use fuels_core::types::assemble_tx::{ + Account as ClientAccount, ChangePolicy, Predicate as ClientPredicate, RequiredBalance, +}; #[cfg(feature = "std")] use fuels_core::types::{AssetId, coin_type_id::CoinTypeId, input::Input}; use fuels_core::{ @@ -127,5 +132,26 @@ impl ViewOnlyAccount for Predicate { } } -#[cfg(feature = "std")] -impl Account for Predicate {} +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Account for Predicate { + fn required_balance( + &self, + amount: u64, + asset_id: AssetId, + change_address: Option
, + ) -> RequiredBalance { + let address = self.address().into(); + let client_predicate = ClientPredicate { + address, + predicate: self.code.clone(), + predicate_data: self.data.clone(), + }; + + RequiredBalance { + asset_id, + amount, + account: ClientAccount::Predicate(client_predicate), + change_policy: ChangePolicy::Change(change_address.unwrap_or(address)), + } + } +} diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index 0d40ba5960..1b012f65dc 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -30,8 +30,9 @@ use fuels_core::{ constants::{DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON, DEFAULT_GAS_ESTIMATION_TOLERANCE}, types::{ DryRun, DryRunner, + assemble_tx::{AssembleTransactionResult, RequiredBalance}, bech32::{Bech32Address, Bech32ContractId}, - block::{Block, Header}, + block::Block, chain_info::ChainInfo, coin::Coin, coin_type::CoinType, @@ -46,6 +47,8 @@ use fuels_core::{ }, }; use futures::StreamExt; +#[cfg(feature = "coin-cache")] +use itertools::{Either, Itertools}; pub use retry_util::{Backoff, RetryConfig}; pub use supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION; use tai64::Tai64; @@ -187,7 +190,6 @@ impl Provider { self.check_inputs_already_in_cache(&tx.used_coins(&base_asset_id)) .await?; - let tx = self.prepare_transaction_for_sending(tx).await?; let tx_status = self .uncached_client() .submit_and_await_commit(&tx.clone().into()) @@ -223,10 +225,10 @@ impl Provider { #[cfg(feature = "coin-cache")] self.check_inputs_already_in_cache(&used_base_coins).await?; - let tx = self.prepare_transaction_for_sending(tx).await?.into(); + let fuel_tx = tx.into(); let mut stream = self .uncached_client() - .submit_and_await_status(&tx, include_preconfirmation) + .submit_and_await_status(&fuel_tx, include_preconfirmation) .await?; let mut statuses = Vec::new(); @@ -257,33 +259,6 @@ impl Provider { Ok(statuses) } - - async fn prepare_transaction_for_sending(&self, mut tx: T) -> Result { - let consensus_parameters = self.consensus_parameters().await?; - tx.precompute(&consensus_parameters.chain_id())?; - - let chain_info = self.chain_info().await?; - let Header { - height: latest_block_height, - state_transition_bytecode_version: latest_chain_executor_version, - .. - } = chain_info.latest_block.header; - - if tx.is_using_predicates() { - tx.estimate_predicates(self, Some(latest_chain_executor_version)) - .await?; - tx.clone() - .validate_predicates(&consensus_parameters, latest_block_height)?; - } - - Ok(tx) - } - - pub async fn send_transaction(&self, tx: T) -> Result { - let tx = self.prepare_transaction_for_sending(tx).await?; - self.submit(tx).await - } - pub async fn await_transaction_commit(&self, id: TxId) -> Result { Ok(self .uncached_client() @@ -293,7 +268,7 @@ impl Provider { } #[cfg(not(feature = "coin-cache"))] - async fn submit(&self, tx: T) -> Result { + pub async fn submit(&self, tx: T) -> Result { Ok(self.uncached_client().submit(&tx.into()).await?) } @@ -344,7 +319,23 @@ impl Provider { } #[cfg(feature = "coin-cache")] - async fn submit(&self, tx: T) -> Result { + async fn coins_and_messages_to_exclude<'a>( + &self, + coin_cache_keys: impl IntoIterator, + ) -> (Vec, Vec) { + let mut locked_cache = self.coins_cache.lock().await; + + coin_cache_keys + .into_iter() + .flat_map(|key| locked_cache.get_active(key)) + .partition_map(|coin_type_id| match coin_type_id { + CoinTypeId::UtxoId(utxo_id) => Either::Left(utxo_id), + CoinTypeId::Nonce(nonce) => Either::Right(nonce), + }) + } + + #[cfg(feature = "coin-cache")] + pub async fn submit(&self, tx: T) -> Result { let consensus_parameters = self.consensus_parameters().await?; let base_asset_id = consensus_parameters.base_asset_id(); @@ -415,6 +406,62 @@ impl Provider { Ok(tx_status) } + #[allow(clippy::too_many_arguments)] + pub async fn assemble_tx( + &self, + transaction: impl Transaction, + block_horizon: u32, + required_balances: Vec, + fee_address_index: u16, + exclude: Option<(Vec, Vec)>, + estimate_predicates: bool, + reserve_gas: Option, + ) -> Result { + #[cfg(feature = "coin-cache")] + let base_asset_id = *self.consensus_parameters().await?.base_asset_id(); + + #[cfg(feature = "coin-cache")] + let keys_from_required_balances: Vec<_> = required_balances + .iter() + .map(|rb| (Bech32Address::from(rb.account.owner()), rb.asset_id)) + .collect(); + + #[cfg(feature = "coin-cache")] + let (cache_utxos, cache_nonces) = self + .coins_and_messages_to_exclude( + transaction + .used_coins(&base_asset_id) + .keys() + .chain(keys_from_required_balances.iter()), + ) + .await; + + #[cfg(feature = "coin-cache")] + let exclude = match exclude { + Some((mut utxos, mut nonces)) => { + utxos.extend(cache_utxos); + nonces.extend(cache_nonces); + + Some((utxos, nonces)) + } + None => Some((cache_utxos, cache_nonces)), + }; + + Ok(self + .uncached_client() + .assemble_tx( + &transaction.into(), + block_horizon, + required_balances, + fee_address_index, + exclude, + estimate_predicates, + reserve_gas, + ) + .await? + .into()) + } + pub async fn dry_run_multiple( &self, transactions: Transactions, @@ -955,11 +1002,29 @@ impl DryRunner for Provider { Provider::consensus_parameters(self).await } - async fn estimate_predicates( - &self, - tx: &FuelTransaction, - _latest_chain_executor_version: Option, - ) -> Result { + async fn estimate_predicates(&self, tx: &FuelTransaction) -> Result { Ok(self.uncached_client().estimate_predicates(tx).await?) } + + async fn assemble_tx( + &self, + transaction: impl Transaction + Send, + block_horizon: u32, + required_balances: Vec, + fee_address_index: u16, + exclude: Option<(Vec, Vec)>, + estimate_predicates: bool, + reserve_gas: Option, + ) -> Result { + self.assemble_tx( + transaction, + block_horizon, + required_balances, + fee_address_index, + exclude, + estimate_predicates, + reserve_gas, + ) + .await + } } diff --git a/packages/fuels-accounts/src/provider/retryable_client.rs b/packages/fuels-accounts/src/provider/retryable_client.rs index 056108bec0..9efab5d371 100644 --- a/packages/fuels-accounts/src/provider/retryable_client.rs +++ b/packages/fuels-accounts/src/provider/retryable_client.rs @@ -10,6 +10,7 @@ use fuel_core_client::client::{ types::{ Balance, Blob, Block, ChainInfo, Coin, CoinType, ContractBalance, Message, MessageProof, NodeInfo, TransactionResponse, TransactionStatus, + assemble_tx::{AssembleTransactionResult, RequiredBalance}, gas_price::{EstimateGasPrice, LatestGasPrice}, primitives::{BlockId, TransactionId}, }, @@ -210,6 +211,31 @@ impl RetryableClient { .await } + #[allow(clippy::too_many_arguments)] + pub async fn assemble_tx( + &self, + tx: &Transaction, + block_horizon: u32, + required_balances: Vec, + fee_address_index: u16, + exclude: Option<(Vec, Vec)>, + estimate_predicates: bool, + reserve_gas: Option, + ) -> RequestResult { + self.wrap(|| { + self.client.assemble_tx( + tx, + block_horizon, + required_balances.clone(), + fee_address_index, + exclude.clone(), + estimate_predicates, + reserve_gas, + ) + }) + .await + } + pub async fn dry_run( &self, tx: &[Transaction], diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs index 17ecabd997..96b3ed72e9 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs @@ -64,6 +64,10 @@ pub(crate) fn contract_bindings( &self.contract_id } + pub fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder { + self.log_decoder.clone() + } + pub fn account(&self) -> &A { &self.account } @@ -114,18 +118,6 @@ pub(crate) fn contract_bindings( #contract_functions } - impl - ::fuels::programs::calls::ContractDependency for #name - { - fn id(&self) -> ::fuels::types::bech32::Bech32ContractId { - self.contract_id.clone() - } - - fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder { - self.log_decoder.clone() - } - } - #constant_configuration_code }; diff --git a/packages/fuels-core/src/codec/logs.rs b/packages/fuels-core/src/codec/logs.rs index 274a45a184..3eedf4ee96 100644 --- a/packages/fuels-core/src/codec/logs.rs +++ b/packages/fuels-core/src/codec/logs.rs @@ -172,7 +172,7 @@ impl LogDecoder { error!( Codec, "missing log formatter for log_id: `{:?}`, data: `{:?}`. \ - Consider adding external contracts using `with_contracts()`", + Consider adding external log decoder using `add_log_decoder()`", log_id, data ) diff --git a/packages/fuels-core/src/types/dry_runner.rs b/packages/fuels-core/src/types/dry_runner.rs index 7fd59ad269..a6fc0901e0 100644 --- a/packages/fuels-core/src/types/dry_runner.rs +++ b/packages/fuels-core/src/types/dry_runner.rs @@ -1,9 +1,15 @@ use std::fmt::Debug; use async_trait::async_trait; -use fuel_tx::{ConsensusParameters, Transaction as FuelTransaction}; +use fuel_tx::{ConsensusParameters, Transaction as FuelTransaction, UtxoId}; +use fuel_types::Nonce; -use crate::types::errors::Result; +use crate::types::{ + assemble_tx::{AssembleTransactionResult, RequiredBalance}, + errors::Result, +}; + +use super::transaction::Transaction; #[derive(Debug, Clone, Copy)] pub struct DryRun { @@ -26,11 +32,18 @@ pub trait DryRunner: Send + Sync { async fn dry_run(&self, tx: FuelTransaction) -> Result; async fn estimate_gas_price(&self, block_horizon: u32) -> Result; async fn consensus_parameters(&self) -> Result; - async fn estimate_predicates( + async fn estimate_predicates(&self, tx: &FuelTransaction) -> Result; + #[allow(clippy::too_many_arguments)] + async fn assemble_tx( &self, - tx: &FuelTransaction, - latest_chain_executor_version: Option, - ) -> Result; + transaction: impl Transaction + Send, + block_horizon: u32, + required_balances: Vec, + fee_address_index: u16, + exclude: Option<(Vec, Vec)>, + estimate_predicates: bool, + reserve_gas: Option, + ) -> Result; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -48,13 +61,31 @@ impl DryRunner for &T { (*self).consensus_parameters().await } - async fn estimate_predicates( + async fn estimate_predicates(&self, tx: &FuelTransaction) -> Result { + (*self).estimate_predicates(tx).await + } + + #[allow(clippy::too_many_arguments)] + async fn assemble_tx( &self, - tx: &FuelTransaction, - latest_chain_executor_version: Option, - ) -> Result { + transaction: impl Transaction + Send, + block_horizon: u32, + required_balances: Vec, + fee_address_index: u16, + exclude: Option<(Vec, Vec)>, + estimate_predicates: bool, + reserve_gas: Option, + ) -> Result { (*self) - .estimate_predicates(tx, latest_chain_executor_version) + .assemble_tx( + transaction, + block_horizon, + required_balances, + fee_address_index, + exclude, + estimate_predicates, + reserve_gas, + ) .await } } diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index 1465d4f012..e3b6426f21 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -11,14 +11,21 @@ use async_trait::async_trait; use fuel_asm::{GTFArgs, RegId, op}; use fuel_crypto::{Hasher, Message as CryptoMessage, Signature}; use fuel_tx::{ - Chargeable, ConsensusParameters, Create, Input as FuelInput, Output, Script, StorageSlot, - Transaction as FuelTransaction, TransactionFee, TxPointer, UniqueIdentifier, Upgrade, Upload, - UploadBody, Witness, - field::{Outputs, Policies as PoliciesField, ScriptGasLimit, Witnesses}, + Cacheable, Chargeable, ConsensusParameters, Create, Input as FuelInput, Output, Script, + StorageSlot, Transaction as FuelTransaction, TransactionFee, TxPointer, UniqueIdentifier, + Upgrade, Upload, UploadBody, Witness, + field::{ + Inputs, MaxFeeLimit, Outputs, Policies as PoliciesField, ScriptGasLimit, WitnessLimit, + Witnesses, + }, + input::{ + coin::CoinSigned, + message::{MessageCoinSigned, MessageDataSigned}, + }, policies::{Policies, PolicyType}, }; pub use fuel_tx::{UpgradePurpose, UploadSubsection}; -use fuel_types::{Bytes32, Salt, bytes::padded_len_usize}; +use fuel_types::{Bytes32, ChainId, Salt, bytes::padded_len_usize}; use itertools::Itertools; use script_tx_estimator::ScriptTxEstimator; @@ -27,6 +34,7 @@ use crate::{ traits::Signer, types::{ Address, AssetId, ContractId, DryRunner, + assemble_tx::RequiredBalance, bech32::Bech32Address, coin::Coin, coin_type::CoinType, @@ -78,6 +86,11 @@ pub enum ScriptBuildStrategy { /// are present. Meant only for transactions that are to be dry-run with validations off. /// Useful for reading state with unfunded accounts. StateReadOnly, + /// Transaction is estimated using `assemble_tx` and signatures are automatically added. + AssembleTx { + required_balances: Vec, + fee_index: u16, + }, } #[derive(Debug, Clone, Default)] @@ -90,6 +103,10 @@ pub enum Strategy { /// order as they appear in the inputs. Multiple coins with the same owner will have /// the same witness index. Make sure you sign the built transaction in the expected order. NoSignatures, + AssembleTx { + required_balances: Vec, + fee_index: u16, + }, } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -188,11 +205,12 @@ macro_rules! impl_tx_builder_trait { impl $crate::types::transaction_builders::TransactionBuilder for $ty { type TxType = $tx_ty; - - fn add_signer(&mut self, signer: impl Signer + Send + Sync + 'static) -> Result<&mut Self> { + fn add_signer( + &mut self, + signer: impl Signer + Send + Sync + 'static, + ) -> Result<&mut Self> { self.validate_no_signer_available(signer.address())?; - let index_offset = self.unresolved_signers.len() as u64; self.unresolved_witness_indexes .owner_to_idx_offset @@ -202,7 +220,10 @@ macro_rules! impl_tx_builder_trait { Ok(self) } - fn add_signers<'a>(&mut self, signers: impl IntoIterator>) -> Result<&mut Self> { + fn add_signers<'a>( + &mut self, + signers: impl IntoIterator>, + ) -> Result<&mut Self> { for signer in signers { self.validate_no_signer_available(signer.address())?; @@ -238,7 +259,7 @@ macro_rules! impl_tx_builder_trait { .await?; if tx.is_using_predicates() { - tx.estimate_predicates(&provider, None).await?; + tx.estimate_predicates(&provider).await?; } let consensus_parameters = provider.consensus_parameters().await?; @@ -248,7 +269,7 @@ macro_rules! impl_tx_builder_trait { .await?; $crate::types::transaction_builders::estimate_max_fee_w_tolerance( - tx.tx, + &tx.tx, self.max_fee_estimation_tolerance, gas_price, &consensus_parameters, @@ -313,7 +334,10 @@ macro_rules! impl_tx_builder_trait { } impl $ty { - fn validate_no_signer_available(&self, address: &$crate::types::bech32::Bech32Address) -> Result<()> { + fn validate_no_signer_available( + &self, + address: &$crate::types::bech32::Bech32Address, + ) -> Result<()> { if self .unresolved_witness_indexes .owner_to_idx_offset @@ -360,6 +384,16 @@ macro_rules! impl_tx_builder_trait { Ok(policies) } + fn generate_fuel_policies_assemble(&self) -> Policies { + let mut policies = Policies::default(); + + policies.set(PolicyType::Maturity, self.tx_policies.maturity()); + policies.set(PolicyType::Tip, self.tx_policies.tip()); + policies.set(PolicyType::Expiration, self.tx_policies.expiration()); + + policies + } + fn is_using_predicates(&self) -> bool { use $crate::types::transaction_builders::TransactionBuilder; self.inputs() @@ -367,46 +401,6 @@ 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, .. } => 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(); @@ -431,24 +425,17 @@ macro_rules! impl_tx_builder_trait { Ok(padded_len as u64) } - async fn set_max_fee_policy>( + async fn set_max_fee_policy( tx: &mut T, provider: impl DryRunner, block_horizon: u32, - is_using_predicates: bool, max_fee_estimation_tolerance: f32, ) -> Result<()> { - let mut wrapper_tx: $tx_ty = tx.clone().into(); - - if is_using_predicates { - wrapper_tx.estimate_predicates(&provider, None).await?; - } - let gas_price = provider.estimate_gas_price(block_horizon).await?; let consensus_parameters = provider.consensus_parameters().await?; let max_fee = $crate::types::transaction_builders::estimate_max_fee_w_tolerance( - wrapper_tx.tx, + tx, max_fee_estimation_tolerance, gas_price, &consensus_parameters, @@ -462,19 +449,75 @@ macro_rules! impl_tx_builder_trait { }; } +fn add_tolerance_to_max_fee(tx: &mut T, tolerance: f32) { + let max_fee = tx.max_fee_limit(); + let max_fee_w_tolerance = max_fee as f64 * (1.0 + f64::from(tolerance)); + + tx.policies_mut() + .set(PolicyType::MaxFee, Some(max_fee_w_tolerance.ceil() as u64)); +} + +async fn update_witnesses( + tx: &mut T, + unresolved_signers: &[Arc], + chain_id: &ChainId, +) -> Result<()> { + let id = tx.id(chain_id); + + for signer in unresolved_signers { + let message = CryptoMessage::from_bytes(*id); + let signature = signer.sign(message).await?; + let address = signer.address().into(); + + let witness_indexes = tx + .inputs() + .iter() + .filter_map(|input| match input { + FuelInput::CoinSigned(CoinSigned { + owner, + witness_index, + .. + }) + | FuelInput::MessageCoinSigned(MessageCoinSigned { + recipient: owner, + witness_index, + .. + }) + | FuelInput::MessageDataSigned(MessageDataSigned { + recipient: owner, + witness_index, + .. + }) if owner == &address => Some(*witness_index as usize), + _ => None, + }) + .sorted() + .dedup() + .collect_vec(); + + for w in witness_indexes { + if let Some(w) = tx.witnesses_mut().get_mut(w) { + *w = signature.as_ref().into(); + } + } + } + + Ok(()) +} + pub(crate) use impl_tx_builder_trait; +use super::transaction::TransactionType; + pub(crate) fn estimate_max_fee_w_tolerance( - tx: T, + tx: &T, tolerance: f32, gas_price: u64, consensus_parameters: &ConsensusParameters, ) -> Result { let gas_costs = &consensus_parameters.gas_costs(); - let fee_params = consensus_parameters.fee_params(); - let tx_fee = TransactionFee::checked_from_tx(gas_costs, fee_params, &tx, gas_price).ok_or( + let tx_fee = TransactionFee::checked_from_tx(gas_costs, fee_params, tx, gas_price).ok_or( error_transaction!( Builder, "error calculating `TransactionFee` in `TransactionBuilder`" @@ -535,6 +578,7 @@ pub struct ScriptTransactionBuilder { pub gas_price_estimation_block_horizon: u32, pub variable_output_policy: VariableOutputPolicy, pub build_strategy: ScriptBuildStrategy, + pub script_gas_limit: Option, unresolved_witness_indexes: UnresolvedWitnessIndexes, unresolved_signers: Vec>, enable_burn: bool, @@ -554,6 +598,7 @@ impl Default for ScriptTransactionBuilder { gas_price_estimation_block_horizon: GAS_ESTIMATION_BLOCK_HORIZON, variable_output_policy: Default::default(), build_strategy: Default::default(), + script_gas_limit: Default::default(), unresolved_witness_indexes: Default::default(), unresolved_signers: Default::default(), enable_burn: false, @@ -689,10 +734,8 @@ 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 mut enable_burn = self.enable_burn; let tx = match self.build_strategy { ScriptBuildStrategy::Complete => self.resolve_fuel_tx(&provider).await?, @@ -703,14 +746,119 @@ impl ScriptTransactionBuilder { self.resolve_fuel_tx(&provider).await? } ScriptBuildStrategy::StateReadOnly => { - self.resolve_fuel_tx_for_state_reading(provider).await? + enable_burn = true; + self.resolve_fuel_tx_for_state_reading(&provider).await? + } + ScriptBuildStrategy::AssembleTx { + ref mut required_balances, + fee_index, + } => { + let required_balances = std::mem::take(required_balances); + self.assemble_tx(required_balances, fee_index, &provider) + .await? } }; - Ok(ScriptTransaction { + let script_transaction = ScriptTransaction { is_using_predicates, tx, - }) + }; + + if !enable_burn { + script_transaction + .intercept_burn(provider.consensus_parameters().await?.base_asset_id())?; + } + + Ok(script_transaction) + } + + async fn assemble_tx( + self, + required_balances: Vec, + fee_index: u16, + dry_runner: impl DryRunner, + ) -> Result