diff --git a/docs/src/calling-contracts/call-response.md b/docs/src/calling-contracts/call-response.md index f0660c6393..08e5fd279f 100644 --- a/docs/src/calling-contracts/call-response.md +++ b/docs/src/calling-contracts/call-response.md @@ -6,19 +6,19 @@ You've probably noticed that you're often chaining `.call().await.unwrap()`. Tha 1. You have to choose between `.call()` and `.simulate()` (more on this in the next section). 2. Contract calls are asynchronous, so you can choose to either `.await` it or perform concurrent tasks, making full use of Rust's async. -3. `.unwrap()` the `Result` returned by the contract call. +3. `.unwrap()` the `Result` returned by the contract call. - + -Once you unwrap the `FuelCallResponse`, you have access to this struct: +Once you unwrap the `CallResponse`, you have access to this struct: ```rust,ignore -{{#include ../../../packages/fuels-programs/src/call_response.rs:fuel_call_response}} +{{#include ../../../packages/fuels-programs/src/responses/call.rs:call_response}} ``` - + Where `value` will hold the value returned by its respective contract method, represented by the exact type returned by the FuelVM, E.g., if your contract returns a FuelVM's `u64`, `value`'s `D` will be a `u64`. If it's a FuelVM's tuple `(u8,bool)`, then `D` will be a `(u8,bool)`. If it's a custom type, for instance, a Sway struct `MyStruct` containing two components, a `u64`, and a `b256`, `D` will be a struct generated at compile-time, called `MyStruct` with `u64` and a `[u8; 32]` (the equivalent of `b256` in Rust). diff --git a/docs/src/calling-contracts/cost-estimation.md b/docs/src/calling-contracts/cost-estimation.md index 92886be3e1..696cea4341 100644 --- a/docs/src/calling-contracts/cost-estimation.md +++ b/docs/src/calling-contracts/cost-estimation.md @@ -1,6 +1,6 @@ # Estimating contract call cost -With the function `estimate_transaction_cost(tolerance: Option, block_horizon: Option)` provided by `ContractCallHandler` and `MultiContractCallHandler`, you can get a cost estimation for a specific call. The return type, `TransactionCost`, is a struct that contains relevant information for the estimation: +With the function `estimate_transaction_cost(tolerance: Option, block_horizon: Option)` provided by `CallHandler`, you can get a cost estimation for a specific call. The return type, `TransactionCost`, is a struct that contains relevant information for the estimation: ```rust,ignore {{#include ../../../packages/fuels-accounts/src/provider.rs:transaction_cost}} diff --git a/docs/src/calling-contracts/logs.md b/docs/src/calling-contracts/logs.md index 8c74671914..d278e44032 100644 --- a/docs/src/calling-contracts/logs.md +++ b/docs/src/calling-contracts/logs.md @@ -8,7 +8,7 @@ Consider the following contract method: {{#include ../../../e2e/sway/logs/contract_logs/src/main.sw:produce_logs}} ``` -You can access the logged values in Rust by calling `decode_logs_with_type::` from a `FuelCallResponse`, where `T` is the type of the logged variables you want to retrieve. The result will be a `Vec`: +You can access the logged values in Rust by calling `decode_logs_with_type::` from a `CallResponse`, where `T` is the type of the logged variables you want to retrieve. The result will be a `Vec`: ```rust,ignore {{#include ../../../e2e/tests/logs.rs:produce_logs}} diff --git a/docs/src/calling-contracts/multicalls.md b/docs/src/calling-contracts/multicalls.md index 881e535190..7b0c3c3699 100644 --- a/docs/src/calling-contracts/multicalls.md +++ b/docs/src/calling-contracts/multicalls.md @@ -1,6 +1,6 @@ # Multiple contract calls -With `MultiContractCallHandler`, you can execute multiple contract calls within a single transaction. To achieve this, you first prepare all the contract calls that you want to bundle: +With `CallHandler`, you can execute multiple contract calls within a single transaction. To achieve this, you first prepare all the contract calls that you want to bundle: ```rust,ignore {{#include ../../../examples/contracts/src/lib.rs:multi_call_prepare}} @@ -8,13 +8,13 @@ With `MultiContractCallHandler`, you can execute multiple contract calls within You can also set call parameters, variable outputs, or external contracts for every contract call, as long as you don't execute it with `call()` or `simulate()`. -Next, you provide the prepared calls to your `MultiContractCallHandler` and optionally configure transaction policies: +Next, you provide the prepared calls to your `CallHandler` and optionally configure transaction policies: ```rust,ignore {{#include ../../../examples/contracts/src/lib.rs:multi_call_build}} ``` -> **Note:** any transaction policies configured on separate contract calls are disregarded in favor of the parameters provided to `MultiContractCallHandler`. +> **Note:** any transaction policies configured on separate contract calls are disregarded in favor of the parameters provided to the multi-call `CallHandler`. Furthermore, if you need to separate submission from value retrieval for any reason, you can do so as follows: @@ -30,10 +30,8 @@ To get the output values of the bundled calls, you need to provide explicit type {{#include ../../../examples/contracts/src/lib.rs:multi_call_values}} ``` -You can also interact with the `FuelCallResponse` by moving the type annotation to the invoked method: +You can also interact with the `CallResponse` by moving the type annotation to the invoked method: ```rust,ignore {{#include ../../../examples/contracts/src/lib.rs:multi_contract_call_response}} ``` - -> **Note:** The `MultiContractCallHandler` supports only one contract call that returns a heap type. Because of the way heap types are handled, this contract call needs to be at the last position, i.e., added last with `add_call`. This is a temporary limitation that we hope to lift soon. In the meantime, if you have multiple calls handling heap types, split them across multiple regular, single calls. diff --git a/docs/src/calling-contracts/other-contracts.md b/docs/src/calling-contracts/other-contracts.md index f7a02cad96..c2077a855c 100644 --- a/docs/src/calling-contracts/other-contracts.md +++ b/docs/src/calling-contracts/other-contracts.md @@ -1,6 +1,6 @@ # 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 `ContractCallHandler` 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, ...])`. +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. diff --git a/docs/src/custom-transactions/custom-calls.md b/docs/src/custom-transactions/custom-calls.md index b1727582c7..fa01a8b784 100644 --- a/docs/src/custom-transactions/custom-calls.md +++ b/docs/src/custom-transactions/custom-calls.md @@ -1,6 +1,6 @@ # Custom contract and script calls -When preparing a contract call via `ContractCallHandler` or a script call via `ScriptCallHandler`, the Rust SDK uses a transaction builder in the background. You can fetch this builder and customize it before submitting it to the network. After the transaction is executed successfully, you can use the corresponding `ContractCallHandler` or `ScriptCallHandler` to generate a [call response](../calling-contracts/call-response.md). The call response can be used to decode return values and logs. Below are examples for both contract and script calls. +When preparing a contract call via `CallHandler`, the Rust SDK uses a transaction builder in the background. You can fetch this builder and customize it before submitting it to the network. After the transaction is executed successfully, you can use the corresponding `CallHandler` to generate a [call response](../calling-contracts/call-response.md). The call response can be used to decode return values and logs. Below are examples for both contract and script calls. ## Custom contract call diff --git a/e2e/build.rs b/e2e/build.rs index e857bf8d23..a1add9b889 100644 --- a/e2e/build.rs +++ b/e2e/build.rs @@ -1,9 +1,10 @@ -use flate2::read::GzDecoder; -use fuels_accounts::provider::SUPPORTED_FUEL_CORE_VERSION; use std::{ io::Cursor, path::{Path, PathBuf}, }; + +use flate2::read::GzDecoder; +use fuels_accounts::provider::SUPPORTED_FUEL_CORE_VERSION; use tar::Archive; struct Downloader { diff --git a/e2e/tests/bindings.rs b/e2e/tests/bindings.rs index 4bb753b68b..9f24687e8e 100644 --- a/e2e/tests/bindings.rs +++ b/e2e/tests/bindings.rs @@ -44,7 +44,7 @@ async fn compile_bindings_from_contract_file() { .methods() .takes_int_returns_bool(42); - let encoded_args = call_handler.contract_call.encoded_args.unwrap(); + let encoded_args = call_handler.call.encoded_args.unwrap(); assert_eq!(encoded_args, [0, 0, 0, 42]); } @@ -95,7 +95,7 @@ async fn compile_bindings_from_inline_contract() -> Result<()> { let contract_instance = SimpleContract::new(null_contract_id(), wallet); let call_handler = contract_instance.methods().takes_ints_returns_bool(42_u32); - let encoded_args = call_handler.contract_call.encoded_args.unwrap(); + let encoded_args = call_handler.call.encoded_args.unwrap(); assert_eq!(encoded_args, [0, 0, 0, 42]); diff --git a/e2e/tests/contracts.rs b/e2e/tests/contracts.rs index 04d1738d0a..6bc360bcb6 100644 --- a/e2e/tests/contracts.rs +++ b/e2e/tests/contracts.rs @@ -188,9 +188,7 @@ async fn test_multi_call_beginner() -> Result<()> { let call_handler_1 = contract_methods.get_single(7); let call_handler_2 = contract_methods.get_single(42); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -228,9 +226,7 @@ async fn test_multi_call_pro() -> Result<()> { let call_handler_5 = contract_methods.get_array([7; 2]); let call_handler_6 = contract_methods.get_array([42; 2]); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2) .add_call(call_handler_3) @@ -348,9 +344,7 @@ async fn mult_call_has_same_estimated_and_used_gas() -> Result<()> { let call_handler_1 = contract_methods.initialize_counter(42); let call_handler_2 = contract_methods.get_array([42; 2]); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -473,7 +467,7 @@ async fn test_large_return_data() -> Result<()> { let res = contract_methods.get_contract_id().call().await?; - // First `value` is from `FuelCallResponse`. + // First `value` is from `CallResponse`. // Second `value` is from the `ContractId` type. assert_eq!( res.value, @@ -733,11 +727,11 @@ async fn test_output_variable_estimation_multicall() -> Result<()> { let amount = 1000; let total_amount = amount * NUM_OF_CALLS; - let mut multi_call_handler = MultiContractCallHandler::new(wallets[0].clone()); - (0..NUM_OF_CALLS).for_each(|_| { + let mut multi_call_handler = CallHandler::new_multi_call(wallets[0].clone()); + for _ in 0..NUM_OF_CALLS { let call_handler = contract_methods.mint_to_addresses(amount, addresses); - multi_call_handler.add_call(call_handler); - }); + multi_call_handler = multi_call_handler.add_call(call_handler); + } wallets[0] .force_transfer_to_contract( @@ -751,7 +745,7 @@ async fn test_output_variable_estimation_multicall() -> Result<()> { let base_layer_address = Bits256([1u8; 32]); let call_handler = contract_methods.send_message(base_layer_address, amount); - multi_call_handler.add_call(call_handler); + multi_call_handler = multi_call_handler.add_call(call_handler); let _ = multi_call_handler .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) @@ -938,18 +932,18 @@ async fn test_output_variable_contract_id_estimation_multicall() -> Result<()> { let contract_methods = contract_caller_instance.methods(); let mut multi_call_handler = - MultiContractCallHandler::new(wallet.clone()).with_tx_policies(Default::default()); + CallHandler::new_multi_call(wallet.clone()).with_tx_policies(Default::default()); - (0..3).for_each(|_| { + for _ in 0..3 { let call_handler = contract_methods.increment_from_contract(lib_contract_id, 42); - multi_call_handler.add_call(call_handler); - }); + multi_call_handler = multi_call_handler.add_call(call_handler); + } // add call that does not need ContractId let contract_methods = contract_test_instance.methods(); let call_handler = contract_methods.get(5, 6); - multi_call_handler.add_call(call_handler); + multi_call_handler = multi_call_handler.add_call(call_handler); let call_response = multi_call_handler .determine_missing_contracts(None) @@ -1172,9 +1166,7 @@ async fn multi_call_from_calls_with_different_account_types() -> Result<()> { let call_handler_1 = contract_methods_wallet.initialize_counter(42); let call_handler_2 = contract_methods_predicate.get_array([42; 2]); - let mut multi_call_handler = MultiContractCallHandler::new(wallet); - - multi_call_handler + let _multi_call_handler = CallHandler::new_multi_call(wallet) .add_call(call_handler_1) .add_call(call_handler_2); @@ -1434,7 +1426,7 @@ async fn can_configure_decoding_of_contract_return() -> Result<()> { } { // Multi call: Will not work if max_tokens not big enough - MultiContractCallHandler::new(wallet.clone()) + CallHandler::new_multi_call(wallet.clone()) .add_call(methods.i_return_a_1k_el_array()) .with_decoder_config(DecoderConfig { max_tokens: 100, ..Default::default() }) .call::<([u8; 1000],)>().await.expect_err( @@ -1443,7 +1435,7 @@ async fn can_configure_decoding_of_contract_return() -> Result<()> { } { // Multi call: Works when configured - MultiContractCallHandler::new(wallet.clone()) + CallHandler::new_multi_call(wallet.clone()) .add_call(methods.i_return_a_1k_el_array()) .with_decoder_config(DecoderConfig { max_tokens: 1001, @@ -1483,9 +1475,7 @@ async fn test_contract_submit_and_response() -> Result<()> { let call_handler_1 = contract_methods.get_single(7); let call_handler_2 = contract_methods.get_single(42); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -1514,34 +1504,30 @@ async fn test_heap_type_multicall() -> Result<()> { ), Deploy( name = "contract_instance", - contract = "TestContract", + contract = "VectorOutputContract", wallet = "wallet" ), Deploy( name = "contract_instance_2", - contract = "VectorOutputContract", + contract = "TestContract", wallet = "wallet" ), ); { - // One heap type at the last position is allowed - let call_handler_1 = contract_instance.methods().get_single(7); - let call_handler_2 = contract_instance.methods().get_single(42); - let call_handler_3 = contract_instance_2.methods().u8_in_vec(3); - - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); + let call_handler_1 = contract_instance.methods().u8_in_vec(5); + let call_handler_2 = contract_instance_2.methods().get_single(7); + let call_handler_3 = contract_instance.methods().u8_in_vec(3); - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2) .add_call(call_handler_3); - let handle = multi_call_handler.submit().await?; - let (val_1, val_2, val_3): (u64, u64, Vec) = handle.response().await?.value; + let (val_1, val_2, val_3): (Vec, u64, Vec) = multi_call_handler.call().await?.value; - assert_eq!(val_1, 7); - assert_eq!(val_2, 42); + assert_eq!(val_1, vec![0, 1, 2, 3, 4]); + assert_eq!(val_2, 7); assert_eq!(val_3, vec![0, 1, 2]); } @@ -1638,9 +1624,7 @@ async fn test_arguments_with_gas_forwarded() -> Result<()> { let call_handler_1 = contract_instance.methods().get_single(x); let call_handler_2 = contract_instance_2.methods().u32_vec(vec_input); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -1871,8 +1855,7 @@ async fn variable_output_estimation_is_optimized() -> Result<()> { #[tokio::test] async fn contract_call_with_non_zero_base_asset_id_and_tip() -> Result<()> { - use fuels::prelude::*; - use fuels::tx::ConsensusParameters; + use fuels::{prelude::*, tx::ConsensusParameters}; abigen!(Contract( name = "MyContract", diff --git a/e2e/tests/logs.rs b/e2e/tests/logs.rs index 9b907c6c52..726c455adb 100644 --- a/e2e/tests/logs.rs +++ b/e2e/tests/logs.rs @@ -269,9 +269,7 @@ async fn test_multi_call_log_single_contract() -> Result<()> { let call_handler_1 = contract_methods.produce_logs_values(); let call_handler_2 = contract_methods.produce_logs_variables(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -322,9 +320,7 @@ async fn test_multi_call_log_multiple_contracts() -> Result<()> { let call_handler_1 = contract_instance.methods().produce_logs_values(); let call_handler_2 = contract_instance2.methods().produce_logs_variables(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -394,9 +390,7 @@ async fn test_multi_call_contract_with_contract_logs() -> Result<()> { .logs_from_external_contract(contract_id) .with_contracts(&[&contract_instance]); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -498,9 +492,7 @@ async fn test_multi_call_require_log_single_contract() -> Result<()> { let call_handler_1 = contract_methods.require_string(); let call_handler_2 = contract_methods.require_custom_generic(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let mut multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -522,9 +514,7 @@ async fn test_multi_call_require_log_single_contract() -> Result<()> { let call_handler_1 = contract_methods.require_custom_generic(); let call_handler_2 = contract_methods.require_string(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let mut multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -575,9 +565,7 @@ async fn test_multi_call_require_log_multi_contract() -> Result<()> { let call_handler_1 = contract_methods.require_string(); let call_handler_2 = contract_methods2.require_custom_generic(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let mut multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -599,9 +587,7 @@ async fn test_multi_call_require_log_multi_contract() -> Result<()> { let call_handler_1 = contract_methods2.require_custom_generic(); let call_handler_2 = contract_methods.require_string(); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let mut multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -1030,9 +1016,7 @@ async fn test_multi_call_contract_require_from_contract() -> Result<()> { .require_from_contract(contract_id) .with_contracts(&[&lib_contract_instance]); - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); @@ -1412,7 +1396,7 @@ async fn can_configure_decoder_for_contract_log_decoding() -> Result<()> { } { // Multi call: decoding with too low max_tokens will fail - let response = MultiContractCallHandler::new(wallet.clone()) + let response = CallHandler::new_multi_call(wallet.clone()) .add_call(methods.i_log_a_1k_el_array()) .with_decoder_config(DecoderConfig { max_tokens: 100, @@ -1430,7 +1414,7 @@ async fn can_configure_decoder_for_contract_log_decoding() -> Result<()> { } { // Multi call: increasing limits makes the test pass - let response = MultiContractCallHandler::new(wallet.clone()) + let response = CallHandler::new_multi_call(wallet.clone()) .add_call(methods.i_log_a_1k_el_array()) .with_decoder_config(DecoderConfig { max_tokens: 1001, diff --git a/examples/contracts/src/lib.rs b/examples/contracts/src/lib.rs index 7740419ce6..ad2b8449c8 100644 --- a/examples/contracts/src/lib.rs +++ b/examples/contracts/src/lib.rs @@ -544,17 +544,17 @@ mod tests { // ANCHOR_END: multi_call_prepare // ANCHOR: multi_call_build - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); // ANCHOR_END: multi_call_build + let multi_call_handler_tmp = multi_call_handler.clone(); // ANCHOR: multi_call_values let (counter, array): (u64, [u64; 2]) = multi_call_handler.call().await?.value; // ANCHOR_END: multi_call_values + let multi_call_handler = multi_call_handler_tmp.clone(); // ANCHOR: multi_contract_call_response let response = multi_call_handler.call::<(u64, [u64; 2])>().await?; // ANCHOR_END: multi_contract_call_response @@ -562,6 +562,7 @@ mod tests { assert_eq!(counter, 42); assert_eq!(array, [42; 2]); + let multi_call_handler = multi_call_handler_tmp.clone(); // ANCHOR: submit_response_multicontract let submitted_tx = multi_call_handler.submit().await?; let (counter, array): (u64, [u64; 2]) = submitted_tx.response().await?.value; @@ -595,12 +596,10 @@ mod tests { let contract_methods = MyContract::new(contract_id, wallet.clone()).methods(); // ANCHOR: multi_call_cost_estimation - let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone()); - let call_handler_1 = contract_methods.initialize_counter(42); let call_handler_2 = contract_methods.get_array([42; 2]); - multi_call_handler + let multi_call_handler = CallHandler::new_multi_call(wallet.clone()) .add_call(call_handler_1) .add_call(call_handler_2); diff --git a/examples/rust_bindings/src/rust_bindings_formatted.rs b/examples/rust_bindings/src/rust_bindings_formatted.rs index a247fa5365..c0a33fc3fd 100644 --- a/examples/rust_bindings/src/rust_bindings_formatted.rs +++ b/examples/rust_bindings/src/rust_bindings_formatted.rs @@ -1,118 +1,139 @@ pub mod abigen_bindings { pub mod my_contract_mod { - use ::fuels::{ - accounts::{Account, ViewOnlyAccount}, - core::{ - codec, - traits::{Parameterize, Tokenizable}, - Configurables, - }, - programs::{ - contract::{self, ContractCallHandler}, - logs::{self, LogDecoder}, - }, - types::{bech32::Bech32ContractId, errors::Result, AssetId}, - }; - - pub struct MyContract { - contract_id: Bech32ContractId, - account: T, - log_decoder: LogDecoder, + #[derive(Debug, Clone)] + pub struct MyContract { + contract_id: ::fuels::types::bech32::Bech32ContractId, + account: A, + log_decoder: ::fuels::core::codec::LogDecoder, + encoder_config: ::fuels::core::codec::EncoderConfig, } - impl MyContract { + impl MyContract { pub fn new( - contract_id: impl ::core::convert::Into, - account: T, + contract_id: impl ::core::convert::Into<::fuels::types::bech32::Bech32ContractId>, + account: A, ) -> Self { - let contract_id: Bech32ContractId = contract_id.into(); - let log_decoder = LogDecoder::new(logs::log_formatters_lookup( - vec![], - contract_id.clone().into(), - )); + let contract_id: ::fuels::types::bech32::Bech32ContractId = contract_id.into(); + let log_decoder = ::fuels::core::codec::LogDecoder::new( + ::fuels::core::codec::log_formatters_lookup(vec![], contract_id.clone().into()), + ); + let encoder_config = ::fuels::core::codec::EncoderConfig::default(); Self { contract_id, account, log_decoder, + encoder_config, } } - pub fn contract_id(&self) -> &Bech32ContractId { + pub fn contract_id(&self) -> &::fuels::types::bech32::Bech32ContractId { &self.contract_id } - pub fn account(&self) -> T { + pub fn account(&self) -> A { self.account.clone() } - pub fn with_account(&self, account: U) -> MyContract { + pub fn with_account(self, account: U) -> MyContract { MyContract { - contract_id: self.contract_id.clone(), + contract_id: self.contract_id, account, - log_decoder: self.log_decoder.clone(), + log_decoder: self.log_decoder, + encoder_config: self.encoder_config, } } - pub async fn get_balances(&self) -> Result<::std::collections::HashMap> { - ViewOnlyAccount::try_provider(&self.account)? + pub fn with_encoder_config( + mut self, + encoder_config: ::fuels::core::codec::EncoderConfig, + ) -> MyContract { + self.encoder_config = encoder_config; + self + } + pub async fn get_balances( + &self, + ) -> ::fuels::types::errors::Result< + ::std::collections::HashMap<::fuels::types::AssetId, u64>, + > { + ::fuels::accounts::ViewOnlyAccount::try_provider(&self.account)? .get_contract_balances(&self.contract_id) .await .map_err(::std::convert::Into::into) } - pub fn methods(&self) -> MyContractMethods { + pub fn methods(&self) -> MyContractMethods { MyContractMethods { contract_id: self.contract_id.clone(), account: self.account.clone(), log_decoder: self.log_decoder.clone(), + encoder_config: self.encoder_config.clone(), } } } - pub struct MyContractMethods { - contract_id: Bech32ContractId, - account: T, - log_decoder: LogDecoder, + pub struct MyContractMethods { + contract_id: ::fuels::types::bech32::Bech32ContractId, + account: A, + log_decoder: ::fuels::core::codec::LogDecoder, + encoder_config: ::fuels::core::codec::EncoderConfig, } - impl MyContractMethods { - #[doc = "Calls the contract's `initialize_counter` function"] - pub fn initialize_counter(&self, value: u64) -> ContractCallHandler { - contract::method_hash( + impl MyContractMethods { + #[doc = " This method will read the counter from storage, increment it"] + #[doc = " and write the incremented value to storage"] + pub fn increment_counter( + &self, + value: ::core::primitive::u64, + ) -> ::fuels::programs::calls::CallHandler< + A, + ::fuels::programs::calls::ContractCall, + ::core::primitive::u64, + > { + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), - codec::encode_fn_selector("initialize_counter"), - &[Tokenizable::into_token(value)], + ::fuels::core::codec::encode_fn_selector("increment_counter"), + &[::fuels::core::traits::Tokenizable::into_token(value)], self.log_decoder.clone(), false, - ABIEncoder::new(EncoderConfig::default()), + self.encoder_config.clone(), ) } - #[doc = "Calls the contract's `increment_counter` function"] - pub fn increment_counter(&self, value: u64) -> ContractCallHandler { - contract::method_hash( + pub fn initialize_counter( + &self, + value: ::core::primitive::u64, + ) -> ::fuels::programs::calls::CallHandler< + A, + ::fuels::programs::calls::ContractCall, + ::core::primitive::u64, + > { + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), - codec::encode_fn_selector("increment_counter"), - &[value.into_token()], + ::fuels::core::codec::encode_fn_selector("initialize_counter"), + &[::fuels::core::traits::Tokenizable::into_token(value)], self.log_decoder.clone(), false, - ABIEncoder::new(EncoderConfig::default()), + self.encoder_config.clone(), ) } } - impl contract::SettableContract for MyContract { - fn id(&self) -> Bech32ContractId { + impl ::fuels::programs::calls::ContractDependency for MyContract { + fn id(&self) -> ::fuels::types::bech32::Bech32ContractId { self.contract_id.clone() } - fn log_decoder(&self) -> LogDecoder { + fn log_decoder(&self) -> ::fuels::core::codec::LogDecoder { self.log_decoder.clone() } } #[derive(Clone, Debug, Default)] pub struct MyContractConfigurables { offsets_with_data: ::std::vec::Vec<(u64, ::std::vec::Vec)>, + encoder: ::fuels::core::codec::ABIEncoder, } impl MyContractConfigurables { - pub fn new() -> Self { - ::std::default::Default::default() + pub fn new(encoder_config: ::fuels::core::codec::EncoderConfig) -> Self { + Self { + encoder: ::fuels::core::codec::ABIEncoder::new(encoder_config), + ..::std::default::Default::default() + } } } - impl From for Configurables { + impl From for ::fuels::core::Configurables { fn from(config: MyContractConfigurables) -> Self { - Configurables::new(config.offsets_with_data) + ::fuels::core::Configurables::new(config.offsets_with_data) } } } diff --git a/packages/fuels-accounts/src/provider/supported_versions.rs b/packages/fuels-accounts/src/provider/supported_versions.rs index 54d0a22301..0fafaa8ac8 100644 --- a/packages/fuels-accounts/src/provider/supported_versions.rs +++ b/packages/fuels-accounts/src/provider/supported_versions.rs @@ -1,6 +1,7 @@ -use crate::provider::supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION; use semver::Version; +use crate::provider::supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION; + #[derive(Debug, PartialEq, Eq)] pub(crate) struct VersionCompatibility { pub(crate) supported_version: Version, 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 5e689d291c..6758556011 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 @@ -38,18 +38,18 @@ pub(crate) fn contract_bindings( let code = quote! { #[derive(Debug, Clone)] - pub struct #name { + pub struct #name { contract_id: ::fuels::types::bech32::Bech32ContractId, - account: T, + account: A, log_decoder: ::fuels::core::codec::LogDecoder, encoder_config: ::fuels::core::codec::EncoderConfig, } - impl #name + impl #name { pub fn new( contract_id: impl ::core::convert::Into<::fuels::types::bech32::Bech32ContractId>, - account: T, + account: A, ) -> Self { let contract_id: ::fuels::types::bech32::Bech32ContractId = contract_id.into(); let log_decoder = ::fuels::core::codec::LogDecoder::new(#log_formatters); @@ -61,7 +61,7 @@ pub(crate) fn contract_bindings( &self.contract_id } - pub fn account(&self) -> T { + pub fn account(&self) -> A { self.account.clone() } @@ -76,7 +76,7 @@ pub(crate) fn contract_bindings( } pub fn with_encoder_config(mut self, encoder_config: ::fuels::core::codec::EncoderConfig) - -> #name:: { + -> #name:: { self.encoder_config = encoder_config; self @@ -89,7 +89,7 @@ pub(crate) fn contract_bindings( .map_err(::std::convert::Into::into) } - pub fn methods(&self) -> #methods_name { + pub fn methods(&self) -> #methods_name { #methods_name { contract_id: self.contract_id.clone(), account: self.account.clone(), @@ -100,19 +100,19 @@ pub(crate) fn contract_bindings( } // Implement struct that holds the contract methods - pub struct #methods_name { + pub struct #methods_name { contract_id: ::fuels::types::bech32::Bech32ContractId, - account: T, + account: A, log_decoder: ::fuels::core::codec::LogDecoder, encoder_config: ::fuels::core::codec::EncoderConfig, } - impl #methods_name { + impl #methods_name { #contract_functions } - impl - ::fuels::programs::contract::SettableContract for #name + impl + ::fuels::programs::calls::ContractDependency for #name { fn id(&self) -> ::fuels::types::bech32::Bech32ContractId { self.contract_id.clone() @@ -148,9 +148,6 @@ fn expand_functions(functions: &[FullABIFunction]) -> Result { /// Transforms a function defined in [`FullABIFunction`] into a [`TokenStream`] /// that represents that same function signature as a Rust-native function /// declaration. -/// -/// The generated function prepares the necessary data and proceeds to call -/// [::fuels_contract::contract::method_hash] for the actual call. pub(crate) fn expand_fn(abi_fun: &FullABIFunction) -> Result { let mut generator = FunctionGenerator::new(abi_fun)?; @@ -158,14 +155,14 @@ pub(crate) fn expand_fn(abi_fun: &FullABIFunction) -> Result { let original_output = generator.output_type(); generator.set_output_type( - quote! {::fuels::programs::contract::ContractCallHandler }, + quote! {::fuels::programs::calls::CallHandler }, ); let fn_selector = generator.fn_selector(); let arg_tokens = generator.tokenized_args(); let is_payable = abi_fun.is_payable(); let body = quote! { - ::fuels::programs::contract::method_hash( + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), #fn_selector, @@ -361,11 +358,11 @@ mod tests { &self, s_1: self::MyStruct1, s_2: self::MyStruct2 - ) -> ::fuels::programs::contract::ContractCallHandler { - ::fuels::programs::contract::method_hash( + ) -> ::fuels::programs::calls::CallHandler { + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), - ::fuels::core::codec::encode_fn_selector( "some_abi_funct"), + ::fuels::core::codec::encode_fn_selector("some_abi_funct"), &[ ::fuels::core::traits::Tokenizable::into_token(s_1), ::fuels::core::traits::Tokenizable::into_token(s_2) @@ -421,11 +418,11 @@ mod tests { let expected = quote! { #[doc = "This is a doc string"] - pub fn HelloWorld(&self, bimbam: ::core::primitive::bool) -> ::fuels::programs::contract::ContractCallHandler { - ::fuels::programs::contract::method_hash( + pub fn HelloWorld(&self, bimbam: ::core::primitive::bool) -> ::fuels::programs::calls::CallHandler { + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), - ::fuels::core::codec::encode_fn_selector( "HelloWorld"), + ::fuels::core::codec::encode_fn_selector("HelloWorld"), &[::fuels::core::traits::Tokenizable::into_token(bimbam)], self.log_decoder.clone(), false, @@ -538,8 +535,8 @@ mod tests { pub fn hello_world( &self, the_only_allowed_input: self::SomeWeirdFrenchCuisine - ) -> ::fuels::programs::contract::ContractCallHandler { - ::fuels::programs::contract::method_hash( + ) -> ::fuels::programs::calls::CallHandler { + ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), ::fuels::core::codec::encode_fn_selector( "hello_world"), diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs index 527d2ddf7a..c1624347df 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs @@ -40,16 +40,16 @@ pub(crate) fn script_bindings( let code = quote! { #[derive(Debug,Clone)] - pub struct #name{ - account: T, + pub struct #name{ + account: A, binary: ::std::vec::Vec, log_decoder: ::fuels::core::codec::LogDecoder, encoder_config: ::fuels::core::codec::EncoderConfig, } - impl #name + impl #name { - pub fn new(account: T, binary_filepath: &str) -> Self { + pub fn new(account: A, binary_filepath: &str) -> Self { let binary = ::std::fs::read(binary_filepath) .expect(&format!("could not read script binary {binary_filepath:?}")); Self { @@ -108,9 +108,10 @@ fn expand_fn(fn_abi: &FullABIFunction) -> Result { let mut generator = FunctionGenerator::new(fn_abi)?; let arg_tokens = generator.tokenized_args(); + let original_output_type = generator.output_type(); let body = quote! { let encoded_args = ::fuels::core::codec::ABIEncoder::new(self.encoder_config).encode(&#arg_tokens); - ::fuels::programs::script_calls::ScriptCallHandler::new( + ::fuels::programs::calls::CallHandler::new_script_call( self.binary.clone(), encoded_args, self.account.clone(), @@ -118,12 +119,8 @@ fn expand_fn(fn_abi: &FullABIFunction) -> Result { ) }; - let original_output_type = generator.output_type(); - generator - .set_output_type( - quote! {::fuels::programs::script_calls::ScriptCallHandler }, - ) + .set_output_type(quote! {::fuels::programs::calls::CallHandler }) .set_docs(fn_abi.doc_strings()?) .set_body(body); @@ -189,10 +186,10 @@ mod tests { let expected = quote! { #[doc = "This is a doc string"] #[doc = "This is another doc string"] - pub fn main(&self, bimbam: ::core::primitive::bool) -> ::fuels::programs::script_calls::ScriptCallHandler { + pub fn main(&self, bimbam: ::core::primitive::bool) -> ::fuels::programs::calls::CallHandler { let encoded_args=::fuels::core::codec::ABIEncoder::new(self.encoder_config) .encode(&[::fuels::core::traits::Tokenizable::into_token(bimbam)]); - ::fuels::programs::script_calls::ScriptCallHandler::new( + ::fuels::programs::calls::CallHandler::new_script_call( self.binary.clone(), encoded_args, self.account.clone(), diff --git a/packages/fuels-core/src/types/errors.rs b/packages/fuels-core/src/types/errors.rs index 34812a7d9b..1fc1378194 100644 --- a/packages/fuels-core/src/types/errors.rs +++ b/packages/fuels-core/src/types/errors.rs @@ -8,7 +8,7 @@ use thiserror::Error; pub mod transaction { use super::*; - #[derive(Error, Debug)] + #[derive(Error, Debug, Clone)] pub enum Reason { #[error("builder: {0}")] Builder(String), @@ -28,7 +28,7 @@ pub mod transaction { } use transaction::Reason; -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum Error { #[error("io: {0}")] IO(String), diff --git a/packages/fuels-core/src/utils/offsets.rs b/packages/fuels-core/src/utils/offsets.rs index 4d4c14e873..a3de72860a 100644 --- a/packages/fuels-core/src/utils/offsets.rs +++ b/packages/fuels-core/src/utils/offsets.rs @@ -1,8 +1,9 @@ -use crate::{error, types::errors::Result}; use fuel_asm::Instruction; use fuel_tx::{field::Script, ConsensusParameters}; use fuel_types::bytes::padded_len_usize; +use crate::{error, types::errors::Result}; + /// Gets the base offset for a script or a predicate. The offset depends on the `max_inputs` /// field of the `ConsensusParameters` and the static offset. pub fn base_offset_script(consensus_parameters: &ConsensusParameters) -> usize { diff --git a/packages/fuels-programs/src/calls.rs b/packages/fuels-programs/src/calls.rs new file mode 100644 index 0000000000..645d09a4a3 --- /dev/null +++ b/packages/fuels-programs/src/calls.rs @@ -0,0 +1,10 @@ +mod call_handler; +mod contract_call; +pub mod receipt_parser; +mod script_call; +pub mod traits; +pub mod utils; + +pub use call_handler::*; +pub use contract_call::*; +pub use script_call::*; diff --git a/packages/fuels-programs/src/calls/call_handler.rs b/packages/fuels-programs/src/calls/call_handler.rs new file mode 100644 index 0000000000..45971e3f43 --- /dev/null +++ b/packages/fuels-programs/src/calls/call_handler.rs @@ -0,0 +1,508 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use fuel_tx::{AssetId, Bytes32, Receipt}; +use fuels_accounts::{provider::TransactionCost, Account}; +use fuels_core::{ + codec::{ABIEncoder, DecoderConfig, EncoderConfig, LogDecoder}, + traits::{Parameterize, Tokenizable}, + types::{ + bech32::{Bech32Address, Bech32ContractId}, + errors::{error, transaction::Reason, Error, Result}, + input::Input, + output::Output, + transaction::{ScriptTransaction, Transaction, TxPolicies}, + transaction_builders::{ScriptTransactionBuilder, VariableOutputPolicy}, + tx_status::TxStatus, + Selector, Token, + }, +}; + +use crate::{ + calls::{ + receipt_parser::ReceiptParser, + traits::{ContractDependencyConfigurator, ResponseParser, TransactionTuner}, + utils::find_id_of_missing_contract, + CallParameters, ContractCall, ScriptCall, + }, + responses::{CallResponse, SubmitResponse}, +}; + +// Trait implemented by contract instances so that +// they can be passed to the `with_contracts` method +pub trait ContractDependency { + fn id(&self) -> Bech32ContractId; + fn log_decoder(&self) -> LogDecoder; +} + +#[derive(Debug, Clone)] +#[must_use = "contract calls do nothing unless you `call` them"] +/// Helper that handles submitting a call to a client and formatting the response +pub struct CallHandler { + pub account: A, + pub call: C, + pub tx_policies: TxPolicies, + pub log_decoder: LogDecoder, + pub datatype: PhantomData, + decoder_config: DecoderConfig, + // Initially `None`, gets set to the right tx id after the transaction is submitted + cached_tx_id: Option, + variable_output_policy: VariableOutputPolicy, +} + +impl CallHandler { + /// Sets the transaction policies for a given transaction. + /// Note that this is a builder method, i.e. use it as a chain: + /// ```ignore + /// let tx_policies = TxPolicies::default().with_gas_price(100); + /// my_contract_instance.my_method(...).with_tx_policies(tx_policies).call() + /// ``` + pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { + self.tx_policies = tx_policies; + self + } + + pub fn with_decoder_config(mut self, decoder_config: DecoderConfig) -> Self { + self.decoder_config = decoder_config; + self.log_decoder.set_decoder_config(decoder_config); + self + } + + /// If this method is not called, the default policy is to not add any variable outputs. + /// + /// # Parameters + /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the contract call. + /// + /// # Returns + /// - `Self`: The updated SDK configuration. + pub fn with_variable_output_policy(mut self, variable_outputs: VariableOutputPolicy) -> Self { + self.variable_output_policy = variable_outputs; + self + } +} + +impl CallHandler +where + A: Account, + C: TransactionTuner, + T: Tokenizable + Parameterize + Debug, +{ + pub async fn transaction_builder(&self) -> Result { + self.call + .transaction_builder(self.tx_policies, self.variable_output_policy, &self.account) + .await + } + + /// Returns the script that executes the contract call + pub async fn build_tx(&self) -> Result { + self.call + .build_tx(self.tx_policies, self.variable_output_policy, &self.account) + .await + } + + /// Get a call's estimated cost + pub async fn estimate_transaction_cost( + &self, + tolerance: Option, + block_horizon: Option, + ) -> Result { + let tx = self.build_tx().await?; + let provider = self.account.try_provider()?; + + let transaction_cost = provider + .estimate_transaction_cost(tx, tolerance, block_horizon) + .await?; + + Ok(transaction_cost) + } +} + +impl CallHandler +where + A: Account, + C: ContractDependencyConfigurator + TransactionTuner + ResponseParser, + T: Tokenizable + Parameterize + Debug, +{ + /// Sets external contracts as dependencies to this contract's call. + /// Effectively, this will be used to create [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] + /// pairs and set them into the transaction. Note that this is a builder + /// method, i.e. use it as a chain: + /// + /// ```ignore + /// my_contract_instance.my_method(...).with_contract_ids(&[another_contract_id]).call() + /// ``` + /// + /// [`Input::Contract`]: fuel_tx::Input::Contract + /// [`Output::Contract`]: fuel_tx::Output::Contract + pub fn with_contract_ids(mut self, contract_ids: &[Bech32ContractId]) -> Self { + self.call = self.call.with_external_contracts(contract_ids.to_vec()); + + self + } + + /// Sets external contract instances as dependencies to this contract's call. + /// Effectively, this will be used to: merge `LogDecoder`s and create + /// [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] pairs and set them into the transaction. + /// Note that this is a builder method, i.e. use it as a chain: + /// + /// ```ignore + /// my_contract_instance.my_method(...).with_contracts(&[another_contract_instance]).call() + /// ``` + pub fn with_contracts(mut self, contracts: &[&dyn ContractDependency]) -> Self { + self.call = self + .call + .with_external_contracts(contracts.iter().map(|c| c.id()).collect()); + for c in contracts { + self.log_decoder.merge(c.log_decoder()); + } + + self + } + + /// Call a contract's method on the node, in a state-modifying manner. + pub async fn call(mut self) -> Result> { + self.call_or_simulate(false).await + } + + pub async fn submit(mut self) -> Result> { + let tx = self.build_tx().await?; + let provider = self.account.try_provider()?; + + let tx_id = provider.send_transaction(tx.clone()).await?; + self.cached_tx_id = Some(tx_id); + + Ok(SubmitResponse::::new(tx_id, self)) + } + + /// Call a contract's method on the node, in a simulated manner, meaning the state of the + /// blockchain is *not* modified but simulated. + pub async fn simulate(&mut self) -> Result> { + self.call_or_simulate(true).await + } + + async fn call_or_simulate(&mut self, simulate: bool) -> Result> { + let tx = self.build_tx().await?; + let provider = self.account.try_provider()?; + + self.cached_tx_id = Some(tx.id(provider.chain_id())); + + let tx_status = if simulate { + provider.dry_run(tx).await? + } else { + provider.send_transaction_and_await_commit(tx).await? + }; + let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; + + self.get_response(receipts) + } + + /// Create a [`CallResponse`] from call receipts + pub fn get_response(&self, receipts: Vec) -> Result> { + let token = self + .call + .parse_call(&receipts, self.decoder_config, &T::param_type())?; + + Ok(CallResponse::new( + T::from_token(token)?, + receipts, + self.log_decoder.clone(), + self.cached_tx_id, + )) + } + + /// Create a [`CallResponse`] from `TxStatus` + pub fn get_response_from(&self, tx_status: TxStatus) -> Result> { + let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; + + self.get_response(receipts) + } + + pub async fn determine_missing_contracts(mut self, max_attempts: Option) -> Result { + let attempts = max_attempts.unwrap_or(10); + + for _ in 0..attempts { + match self.simulate().await { + Ok(_) => return Ok(self), + + Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => { + if let Some(contract_id) = find_id_of_missing_contract(receipts) { + self.call.append_external_contract(contract_id); + } + } + + Err(other_error) => return Err(other_error), + } + } + + self.simulate().await.map(|_| self) + } +} + +impl CallHandler +where + A: Account, + T: Tokenizable + Parameterize + Debug, +{ + pub fn new_contract_call( + contract_id: Bech32ContractId, + account: A, + encoded_selector: Selector, + args: &[Token], + log_decoder: LogDecoder, + is_payable: bool, + encoder_config: EncoderConfig, + ) -> Self { + let call = ContractCall { + contract_id, + encoded_selector, + encoded_args: ABIEncoder::new(encoder_config).encode(args), + call_parameters: CallParameters::default(), + external_contracts: vec![], + output_param: T::param_type(), + is_payable, + custom_assets: Default::default(), + }; + CallHandler { + account, + call, + tx_policies: TxPolicies::default(), + log_decoder, + datatype: PhantomData, + decoder_config: DecoderConfig::default(), + cached_tx_id: None, + variable_output_policy: VariableOutputPolicy::default(), + } + } + + /// Adds a custom `asset_id` with its `amount` and an optional `address` to be used for + /// generating outputs to this contract's call. + /// + /// # Parameters + /// - `asset_id`: The unique identifier of the asset being added. + /// - `amount`: The amount of the asset being added. + /// - `address`: The optional account address that the output amount will be sent to. + /// If not provided, the asset will be sent to the users account address. + /// Note that this is a builder method, i.e. use it as a chain: + /// + /// ```ignore + /// let asset_id = AssetId::from([3u8; 32]); + /// let amount = 5000; + /// my_contract_instance.my_method(...).add_custom_asset(asset_id, amount, None).call() + /// ``` + pub fn add_custom_asset( + mut self, + asset_id: AssetId, + amount: u64, + to: Option, + ) -> Self { + self.call.add_custom_asset(asset_id, amount, to); + self + } + + pub fn is_payable(&self) -> bool { + self.call.is_payable + } + + /// Sets the call parameters for a given contract call. + /// Note that this is a builder method, i.e. use it as a chain: + /// + /// ```ignore + /// let params = CallParameters { amount: 1, asset_id: AssetId::zeroed() }; + /// my_contract_instance.my_method(...).call_params(params).call() + /// ``` + pub fn call_params(mut self, params: CallParameters) -> Result { + if !self.is_payable() && params.amount() > 0 { + return Err(error!(Other, "assets forwarded to non-payable method")); + } + self.call.call_parameters = params; + + Ok(self) + } +} + +impl CallHandler +where + A: Account, + T: Parameterize + Tokenizable + Debug, +{ + pub fn new_script_call( + script_binary: Vec, + encoded_args: Result>, + account: A, + log_decoder: LogDecoder, + ) -> Self { + let call = ScriptCall { + script_binary, + encoded_args, + inputs: vec![], + outputs: vec![], + external_contracts: vec![], + }; + + Self { + account, + call, + tx_policies: TxPolicies::default(), + log_decoder, + datatype: PhantomData, + decoder_config: DecoderConfig::default(), + cached_tx_id: None, + variable_output_policy: VariableOutputPolicy::default(), + } + } + + pub fn with_outputs(mut self, outputs: Vec) -> Self { + self.call = self.call.with_outputs(outputs); + self + } + + pub fn with_inputs(mut self, inputs: Vec) -> Self { + self.call = self.call.with_inputs(inputs); + self + } +} + +impl CallHandler, ()> +where + A: Account, +{ + pub fn new_multi_call(account: A) -> Self { + Self { + account, + call: vec![], + tx_policies: TxPolicies::default(), + log_decoder: LogDecoder::new(Default::default()), + datatype: PhantomData, + decoder_config: DecoderConfig::default(), + cached_tx_id: None, + variable_output_policy: VariableOutputPolicy::default(), + } + } + + fn append_external_contract(mut self, contract_id: Bech32ContractId) -> Result { + if self.call.is_empty() { + return Err(error!( + Other, + "no calls added. Have you used '.add_calls()'?" + )); + } + + self.call + .iter_mut() + .take(1) + .for_each(|call| call.append_external_contract(contract_id.clone())); + + Ok(self) + } + + /// Adds a contract call to be bundled in the transaction + /// Note that this is a builder method + pub fn add_call( + mut self, + call_handler: CallHandler, + ) -> Self { + self.log_decoder.merge(call_handler.log_decoder); + self.call.push(call_handler.call); + + self + } + + /// Call contract methods on the node, in a state-modifying manner. + pub async fn call(mut self) -> Result> { + self.call_or_simulate(false).await + } + + pub async fn submit(mut self) -> Result, ()>> { + let tx = self.build_tx().await?; + let provider = self.account.try_provider()?; + + let tx_id = provider.send_transaction(tx).await?; + self.cached_tx_id = Some(tx_id); + + Ok(SubmitResponse::, ()>::new(tx_id, self)) + } + + /// Call contract methods on the node, in a simulated manner, meaning the state of the + /// blockchain is *not* modified but simulated. + /// It is the same as the [call] method because the API is more user-friendly this way. + /// + /// [call]: Self::call + pub async fn simulate(&mut self) -> Result> { + self.call_or_simulate(true).await + } + + async fn call_or_simulate( + &mut self, + simulate: bool, + ) -> Result> { + let tx = self.build_tx().await?; + let provider = self.account.try_provider()?; + + self.cached_tx_id = Some(tx.id(provider.chain_id())); + + let tx_status = if simulate { + provider.dry_run(tx).await? + } else { + provider.send_transaction_and_await_commit(tx).await? + }; + + let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; + + self.get_response(receipts) + } + + /// Simulates a call without needing to resolve the generic for the return type + async fn simulate_without_decode(&self) -> Result<()> { + let provider = self.account.try_provider()?; + let tx = self.build_tx().await?; + + provider.dry_run(tx).await?.check(None)?; + + Ok(()) + } + + /// Create a [`CallResponse`] from call receipts + pub fn get_response( + &self, + receipts: Vec, + ) -> Result> { + let mut receipt_parser = ReceiptParser::new(&receipts, self.decoder_config); + + let final_tokens = self + .call + .iter() + .map(|call| receipt_parser.parse_call(&call.contract_id, &call.output_param)) + .collect::>>()?; + + let tokens_as_tuple = Token::Tuple(final_tokens); + let response = CallResponse::::new( + T::from_token(tokens_as_tuple)?, + receipts, + self.log_decoder.clone(), + self.cached_tx_id, + ); + + Ok(response) + } + + /// Simulates the call and attempts to resolve missing contract outputs. + /// Forwards the received error if it cannot be fixed. + pub async fn determine_missing_contracts(mut self, max_attempts: Option) -> Result { + let attempts = max_attempts.unwrap_or(10); + + for _ in 0..attempts { + match self.simulate_without_decode().await { + Ok(_) => return Ok(self), + + Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => { + if let Some(contract_id) = find_id_of_missing_contract(receipts) { + self = self.append_external_contract(contract_id)?; + } + } + + Err(other_error) => return Err(other_error), + } + } + + self.simulate_without_decode().await.map(|_| self) + } +} diff --git a/packages/fuels-programs/src/calls/contract_call.rs b/packages/fuels-programs/src/calls/contract_call.rs new file mode 100644 index 0000000000..70f7ce38f8 --- /dev/null +++ b/packages/fuels-programs/src/calls/contract_call.rs @@ -0,0 +1,103 @@ +use std::{collections::HashMap, fmt::Debug}; + +use fuel_tx::AssetId; +use fuels_core::{ + constants::DEFAULT_CALL_PARAMS_AMOUNT, + types::{ + bech32::{Bech32Address, Bech32ContractId}, + errors::Result, + param_types::ParamType, + Selector, + }, +}; + +use crate::calls::utils::sealed; + +#[derive(Debug, Clone)] +/// Contains all data relevant to a single contract call +pub struct ContractCall { + pub contract_id: Bech32ContractId, + pub encoded_args: Result>, + pub encoded_selector: Selector, + pub call_parameters: CallParameters, + pub external_contracts: Vec, + pub output_param: ParamType, + pub is_payable: bool, + pub custom_assets: HashMap<(AssetId, Option), u64>, +} + +impl ContractCall { + pub fn with_contract_id(self, contract_id: Bech32ContractId) -> Self { + ContractCall { + contract_id, + ..self + } + } + + pub fn with_call_parameters(self, call_parameters: CallParameters) -> ContractCall { + ContractCall { + call_parameters, + ..self + } + } + + pub fn add_custom_asset(&mut self, asset_id: AssetId, amount: u64, to: Option) { + *self.custom_assets.entry((asset_id, to)).or_default() += amount; + } +} + +impl sealed::Sealed for ContractCall {} + +#[derive(Debug, Clone)] +pub struct CallParameters { + amount: u64, + asset_id: Option, + gas_forwarded: Option, +} + +impl CallParameters { + pub fn new(amount: u64, asset_id: AssetId, gas_forwarded: u64) -> Self { + Self { + amount, + asset_id: Some(asset_id), + gas_forwarded: Some(gas_forwarded), + } + } + + pub fn with_amount(mut self, amount: u64) -> Self { + self.amount = amount; + self + } + + pub fn amount(&self) -> u64 { + self.amount + } + + pub fn with_asset_id(mut self, asset_id: AssetId) -> Self { + self.asset_id = Some(asset_id); + self + } + + pub fn asset_id(&self) -> Option { + self.asset_id + } + + pub fn with_gas_forwarded(mut self, gas_forwarded: u64) -> Self { + self.gas_forwarded = Some(gas_forwarded); + self + } + + pub fn gas_forwarded(&self) -> Option { + self.gas_forwarded + } +} + +impl Default for CallParameters { + fn default() -> Self { + Self { + amount: DEFAULT_CALL_PARAMS_AMOUNT, + asset_id: None, + gas_forwarded: None, + } + } +} diff --git a/packages/fuels-programs/src/receipt_parser.rs b/packages/fuels-programs/src/calls/receipt_parser.rs similarity index 100% rename from packages/fuels-programs/src/receipt_parser.rs rename to packages/fuels-programs/src/calls/receipt_parser.rs diff --git a/packages/fuels-programs/src/calls/script_call.rs b/packages/fuels-programs/src/calls/script_call.rs new file mode 100644 index 0000000000..6a31a6b6a7 --- /dev/null +++ b/packages/fuels-programs/src/calls/script_call.rs @@ -0,0 +1,65 @@ +use std::{collections::HashSet, fmt::Debug}; + +use fuel_tx::{ContractId, Output}; +use fuels_core::types::{ + bech32::Bech32ContractId, + errors::{error, Result}, + input::Input, +}; +use itertools::chain; + +use crate::calls::utils::{generate_contract_inputs, generate_contract_outputs, sealed}; + +#[derive(Debug, Clone)] +/// Contains all data relevant to a single script call +pub struct ScriptCall { + pub script_binary: Vec, + pub encoded_args: Result>, + pub inputs: Vec, + pub outputs: Vec, + pub external_contracts: Vec, +} + +impl ScriptCall { + pub fn with_outputs(mut self, outputs: Vec) -> Self { + self.outputs = outputs; + self + } + + pub fn with_inputs(mut self, inputs: Vec) -> Self { + self.inputs = inputs; + self + } + + pub(crate) fn prepare_inputs_outputs(&self) -> Result<(Vec, Vec)> { + let contract_ids: HashSet = self + .external_contracts + .iter() + .map(|bech32| bech32.into()) + .collect(); + let num_of_contracts = contract_ids.len(); + + let inputs = chain!(generate_contract_inputs(contract_ids), self.inputs.clone(),).collect(); + + // Note the contract_outputs need to come first since the + // contract_inputs are referencing them via `output_index`. The node + // will, upon receiving our request, use `output_index` to index the + // `inputs` array we've sent over. + let outputs = chain!( + generate_contract_outputs(num_of_contracts), + self.outputs.clone(), + ) + .collect(); + + Ok((inputs, outputs)) + } + + pub(crate) fn compute_script_data(&self) -> Result> { + self.encoded_args + .as_ref() + .map(|b| b.to_owned()) + .map_err(|e| error!(Codec, "cannot encode script call arguments: {e}")) + } +} + +impl sealed::Sealed for ScriptCall {} diff --git a/packages/fuels-programs/src/calls/traits.rs b/packages/fuels-programs/src/calls/traits.rs new file mode 100644 index 0000000000..4a48c4db93 --- /dev/null +++ b/packages/fuels-programs/src/calls/traits.rs @@ -0,0 +1,7 @@ +mod contract_dep_configurator; +mod response_parser; +mod transaction_tuner; + +pub use contract_dep_configurator::*; +pub use response_parser::*; +pub use transaction_tuner::*; diff --git a/packages/fuels-programs/src/calls/traits/contract_dep_configurator.rs b/packages/fuels-programs/src/calls/traits/contract_dep_configurator.rs new file mode 100644 index 0000000000..e0a02acbad --- /dev/null +++ b/packages/fuels-programs/src/calls/traits/contract_dep_configurator.rs @@ -0,0 +1,34 @@ +use fuels_core::types::bech32::Bech32ContractId; + +use crate::calls::{utils::sealed, ContractCall, ScriptCall}; + +pub trait ContractDependencyConfigurator: sealed::Sealed { + fn append_external_contract(&mut self, contract_id: Bech32ContractId); + fn with_external_contracts(self, external_contracts: Vec) -> Self; +} + +impl ContractDependencyConfigurator for ContractCall { + fn append_external_contract(&mut self, contract_id: Bech32ContractId) { + self.external_contracts.push(contract_id) + } + + fn with_external_contracts(self, external_contracts: Vec) -> Self { + ContractCall { + external_contracts, + ..self + } + } +} + +impl ContractDependencyConfigurator for ScriptCall { + fn append_external_contract(&mut self, contract_id: Bech32ContractId) { + self.external_contracts.push(contract_id) + } + + fn with_external_contracts(self, external_contracts: Vec) -> Self { + ScriptCall { + external_contracts, + ..self + } + } +} diff --git a/packages/fuels-programs/src/calls/traits/response_parser.rs b/packages/fuels-programs/src/calls/traits/response_parser.rs new file mode 100644 index 0000000000..a2ccefbd8c --- /dev/null +++ b/packages/fuels-programs/src/calls/traits/response_parser.rs @@ -0,0 +1,38 @@ +use fuel_tx::Receipt; +use fuels_core::{ + codec::DecoderConfig, + types::{errors::Result, param_types::ParamType, Token}, +}; + +use crate::calls::{receipt_parser::ReceiptParser, utils::sealed, ContractCall, ScriptCall}; + +pub trait ResponseParser: sealed::Sealed { + fn parse_call( + &self, + receipts: &[Receipt], + decoder_config: DecoderConfig, + param_type: &ParamType, + ) -> Result; +} + +impl ResponseParser for ContractCall { + fn parse_call( + &self, + receipts: &[Receipt], + decoder_config: DecoderConfig, + param_type: &ParamType, + ) -> Result { + ReceiptParser::new(receipts, decoder_config).parse_call(&self.contract_id, param_type) + } +} + +impl ResponseParser for ScriptCall { + fn parse_call( + &self, + receipts: &[Receipt], + decoder_config: DecoderConfig, + param_type: &ParamType, + ) -> Result { + ReceiptParser::new(receipts, decoder_config).parse_script(param_type) + } +} diff --git a/packages/fuels-programs/src/calls/traits/transaction_tuner.rs b/packages/fuels-programs/src/calls/traits/transaction_tuner.rs new file mode 100644 index 0000000000..eaac4bd35b --- /dev/null +++ b/packages/fuels-programs/src/calls/traits/transaction_tuner.rs @@ -0,0 +1,140 @@ +use fuels_accounts::Account; +use fuels_core::types::{ + errors::{error, Result}, + transaction::{ScriptTransaction, TxPolicies}, + transaction_builders::{ + BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder, VariableOutputPolicy, + }, +}; + +use crate::calls::{ + utils::{build_tx_from_contract_calls, sealed, transaction_builder_from_contract_calls}, + ContractCall, ScriptCall, +}; + +#[async_trait::async_trait] +pub trait TransactionTuner: sealed::Sealed { + async fn transaction_builder( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result; + + async fn build_tx( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result; +} + +#[async_trait::async_trait] +impl TransactionTuner for ContractCall { + async fn transaction_builder( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result { + transaction_builder_from_contract_calls( + std::slice::from_ref(self), + tx_policies, + variable_output_policy, + account, + ) + .await + } + + async fn build_tx( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result { + build_tx_from_contract_calls( + std::slice::from_ref(self), + tx_policies, + variable_output_policy, + account, + ) + .await + } +} + +#[async_trait::async_trait] +impl TransactionTuner for ScriptCall { + async fn transaction_builder( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + _account: &T, + ) -> Result { + let (inputs, outputs) = self.prepare_inputs_outputs()?; + + Ok(ScriptTransactionBuilder::default() + .with_variable_output_policy(variable_output_policy) + .with_tx_policies(tx_policies) + .with_script(self.script_binary.clone()) + .with_script_data(self.compute_script_data()?) + .with_inputs(inputs) + .with_outputs(outputs) + .with_gas_estimation_tolerance(0.05)) + } + + async fn build_tx( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result { + let mut tb = self + .transaction_builder(tx_policies, variable_output_policy, account) + .await?; + + account.add_witnesses(&mut tb)?; + account.adjust_for_fee(&mut tb, 0).await?; + + tb.build(account.try_provider()?).await + } +} + +impl sealed::Sealed for Vec {} + +#[async_trait::async_trait] +impl TransactionTuner for Vec { + async fn transaction_builder( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result { + validate_contract_calls(self)?; + + transaction_builder_from_contract_calls(self, tx_policies, variable_output_policy, account) + .await + } + + /// Returns the script that executes the contract calls + async fn build_tx( + &self, + tx_policies: TxPolicies, + variable_output_policy: VariableOutputPolicy, + account: &T, + ) -> Result { + validate_contract_calls(self)?; + + build_tx_from_contract_calls(self, tx_policies, variable_output_policy, account).await + } +} + +fn validate_contract_calls(calls: &[ContractCall]) -> Result<()> { + if calls.is_empty() { + return Err(error!( + Other, + "no calls added. Have you used '.add_calls()'?" + )); + } + + Ok(()) +} diff --git a/packages/fuels-programs/src/call_utils.rs b/packages/fuels-programs/src/calls/utils.rs similarity index 88% rename from packages/fuels-programs/src/call_utils.rs rename to packages/fuels-programs/src/calls/utils.rs index 2a2c5124e6..7c8348ecc6 100644 --- a/packages/fuels-programs/src/call_utils.rs +++ b/packages/fuels-programs/src/calls/utils.rs @@ -11,7 +11,7 @@ use fuels_core::{ offsets::call_script_data_offset, types::{ bech32::{Bech32Address, Bech32ContractId}, - errors::{transaction::Reason, Error, Result}, + errors::Result, input::Input, param_types::ParamType, transaction::{ScriptTransaction, TxPolicies}, @@ -23,7 +23,7 @@ use fuels_core::{ }; use itertools::{chain, Itertools}; -use crate::contract::ContractCall; +use crate::calls::ContractCall; #[derive(Default)] /// Specifies offsets of [`Opcode::CALL`][`fuel_asm::Opcode::CALL`] parameters stored in the script @@ -39,52 +39,11 @@ pub(crate) mod sealed { pub trait Sealed {} } -#[async_trait::async_trait] -pub trait TxDependencyExtension: Sized + sealed::Sealed { - async fn simulate(&mut self) -> Result<()>; - - /// Appends additional external contracts as dependencies to this call. - /// Effectively, this will be used to create additional - /// [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] - /// pairs and set them into the transaction. Note that this is a builder - /// method, i.e. use it as a chain: - /// - /// ```ignore - /// my_contract_instance.my_method(...).append_contract(additional_contract_id).call() - /// my_script_instance.main(...).append_contract(additional_contract_id).call() - /// ``` - /// - /// [`Input::Contract`]: fuel_tx::Input::Contract - /// [`Output::Contract`]: fuel_tx::Output::Contract - fn append_contract(self, contract_id: Bech32ContractId) -> Self; - - /// Simulates the call and attempts to resolve missing contract outputs. - /// Forwards the received error if it cannot be fixed. - async fn determine_missing_contracts(mut self, max_attempts: Option) -> Result { - let attempts = max_attempts.unwrap_or(10); - - for _ in 0..attempts { - match self.simulate().await { - Ok(()) => return Ok(self), - - Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => { - if let Some(contract_id) = find_id_of_missing_contract(receipts) { - self = self.append_contract(contract_id); - } - } - - Err(other_error) => return Err(other_error), - } - } - - self.simulate().await.map(|()| self) - } -} - /// Creates a [`ScriptTransactionBuilder`] from contract calls. pub(crate) async fn transaction_builder_from_contract_calls( calls: &[ContractCall], tx_policies: TxPolicies, + variable_outputs: VariableOutputPolicy, account: &impl Account, ) -> Result { let calls_instructions_len = compute_calls_instructions_len(calls)?; @@ -115,6 +74,7 @@ pub(crate) async fn transaction_builder_from_contract_calls( ); Ok(ScriptTransactionBuilder::default() + .with_variable_output_policy(variable_outputs) .with_tx_policies(tx_policies) .with_script(script) .with_script_data(script_data.clone()) @@ -129,12 +89,12 @@ pub(crate) async fn transaction_builder_from_contract_calls( pub(crate) async fn build_tx_from_contract_calls( calls: &[ContractCall], tx_policies: TxPolicies, - account: &impl Account, variable_outputs: VariableOutputPolicy, + account: &impl Account, ) -> Result { - let mut tb = transaction_builder_from_contract_calls(calls, tx_policies, account) - .await? - .with_variable_output_policy(variable_outputs); + let mut tb = + transaction_builder_from_contract_calls(calls, tx_policies, variable_outputs, account) + .await?; let base_asset_id = *account.try_provider()?.base_asset_id(); let required_asset_amounts = calculate_required_asset_amounts(calls, base_asset_id); @@ -496,20 +456,18 @@ mod test { use rand::Rng; use super::*; - use crate::contract::CallParameters; - - impl ContractCall { - pub fn new_with_random_id() -> Self { - ContractCall { - contract_id: random_bech32_contract_id(), - encoded_args: Ok(Default::default()), - encoded_selector: [0; 8].to_vec(), - call_parameters: Default::default(), - external_contracts: Default::default(), - output_param: ParamType::Unit, - is_payable: false, - custom_assets: Default::default(), - } + use crate::calls::{traits::ContractDependencyConfigurator, CallParameters}; + + fn new_contract_call_with_random_id() -> ContractCall { + ContractCall { + contract_id: random_bech32_contract_id(), + encoded_args: Ok(Default::default()), + encoded_selector: [0; 8].to_vec(), + call_parameters: Default::default(), + external_contracts: Default::default(), + output_param: ParamType::Unit, + is_payable: false, + custom_assets: Default::default(), } } @@ -519,7 +477,7 @@ mod test { #[test] fn contract_input_present() { - let call = ContractCall::new_with_random_id(); + let call = new_contract_call_with_random_id(); let wallet = WalletUnlocked::new_random(None); @@ -544,9 +502,9 @@ mod test { #[test] fn contract_input_is_not_duplicated() { - let call = ContractCall::new_with_random_id(); + let call = new_contract_call_with_random_id(); let call_w_same_contract = - ContractCall::new_with_random_id().with_contract_id(call.contract_id.clone()); + new_contract_call_with_random_id().with_contract_id(call.contract_id.clone()); let wallet = WalletUnlocked::new_random(None); @@ -573,7 +531,7 @@ mod test { #[test] fn contract_output_present() { - let call = ContractCall::new_with_random_id(); + let call = new_contract_call_with_random_id(); let wallet = WalletUnlocked::new_random(None); @@ -594,7 +552,7 @@ mod test { fn external_contract_input_present() { // given let external_contract_id = random_bech32_contract_id(); - let call = ContractCall::new_with_random_id() + let call = new_contract_call_with_random_id() .with_external_contracts(vec![external_contract_id.clone()]); let wallet = WalletUnlocked::new_random(None); @@ -639,7 +597,7 @@ mod test { // given let external_contract_id = random_bech32_contract_id(); let call = - ContractCall::new_with_random_id().with_external_contracts(vec![external_contract_id]); + new_contract_call_with_random_id().with_external_contracts(vec![external_contract_id]); let wallet = WalletUnlocked::new_random(None); @@ -678,7 +636,7 @@ mod test { Input::resource_signed(coin) }) .collect(); - let call = ContractCall::new_with_random_id(); + let call = new_contract_call_with_random_id(); let wallet = WalletUnlocked::new_random(None); @@ -718,7 +676,7 @@ mod test { .with_asset_id(asset_id) }) .map(|call_parameters| { - ContractCall::new_with_random_id().with_call_parameters(call_parameters) + new_contract_call_with_random_id().with_call_parameters(call_parameters) }); let asset_id_amounts = calculate_required_asset_amounts(&calls, AssetId::zeroed()); @@ -735,7 +693,8 @@ mod test { use fuel_asm::Instruction; use fuels_core::types::param_types::{EnumVariants, ParamType}; - use crate::{call_utils::compute_calls_instructions_len, contract::ContractCall}; + use super::new_contract_call_with_random_id; + use crate::calls::utils::compute_calls_instructions_len; // movi, movi, lw, movi + call (for gas) const BASE_INSTRUCTION_COUNT: usize = 5; @@ -744,14 +703,14 @@ mod test { #[test] fn test_simple() { - let call = ContractCall::new_with_random_id(); + let call = new_contract_call_with_random_id(); let instructions_len = compute_calls_instructions_len(&[call]).unwrap(); assert_eq!(instructions_len, Instruction::SIZE * BASE_INSTRUCTION_COUNT); } #[test] fn test_with_gas_offset() { - let mut call = ContractCall::new_with_random_id(); + let mut call = new_contract_call_with_random_id(); call.call_parameters = call.call_parameters.with_gas_forwarded(0); let instructions_len = compute_calls_instructions_len(&[call]).unwrap(); assert_eq!( @@ -762,7 +721,7 @@ mod test { #[test] fn test_with_enum_with_only_non_heap_variants() { - let mut call = ContractCall::new_with_random_id(); + let mut call = new_contract_call_with_random_id(); call.output_param = ParamType::Enum { name: "".to_string(), enum_variants: EnumVariants::new(vec![ diff --git a/packages/fuels-programs/src/contract.rs b/packages/fuels-programs/src/contract.rs index 0030b5792b..fef7c95626 100644 --- a/packages/fuels-programs/src/contract.rs +++ b/packages/fuels-programs/src/contract.rs @@ -1,241 +1,22 @@ +mod load; +mod storage; + use std::{ - collections::HashMap, - default::Default, fmt::Debug, - fs, io, - marker::PhantomData, + fs, path::{Path, PathBuf}, }; -use fuel_tx::{AssetId, Bytes32, Contract as FuelContract, ContractId, Receipt, Salt, StorageSlot}; -use fuels_accounts::{provider::TransactionCost, Account}; -use fuels_core::{ - codec::{ABIEncoder, DecoderConfig, EncoderConfig, LogDecoder}, - constants::DEFAULT_CALL_PARAMS_AMOUNT, - traits::{Parameterize, Tokenizable}, - types::{ - bech32::{Bech32Address, Bech32ContractId}, - errors::{error, Result}, - param_types::ParamType, - transaction::{ScriptTransaction, Transaction, TxPolicies}, - transaction_builders::{ - CreateTransactionBuilder, ScriptTransactionBuilder, VariableOutputPolicy, - }, - tx_status::TxStatus, - Selector, Token, - }, - Configurables, -}; - -use crate::{ - call_response::FuelCallResponse, - call_utils::{ - build_tx_from_contract_calls, sealed, transaction_builder_from_contract_calls, - TxDependencyExtension, - }, - receipt_parser::ReceiptParser, - submit_response::{SubmitResponse, SubmitResponseMultiple}, +use fuel_tx::{Bytes32, Contract as FuelContract, ContractId, Salt, StorageSlot}; +use fuels_accounts::Account; +use fuels_core::types::{ + bech32::Bech32ContractId, + errors::{error, Result}, + transaction::TxPolicies, + transaction_builders::CreateTransactionBuilder, }; - -#[derive(Debug, Clone)] -pub struct CallParameters { - amount: u64, - asset_id: Option, - gas_forwarded: Option, -} - -impl CallParameters { - pub fn new(amount: u64, asset_id: AssetId, gas_forwarded: u64) -> Self { - Self { - amount, - asset_id: Some(asset_id), - gas_forwarded: Some(gas_forwarded), - } - } - - pub fn with_amount(mut self, amount: u64) -> Self { - self.amount = amount; - self - } - - pub fn amount(&self) -> u64 { - self.amount - } - - pub fn with_asset_id(mut self, asset_id: AssetId) -> Self { - self.asset_id = Some(asset_id); - self - } - - pub fn asset_id(&self) -> Option { - self.asset_id - } - - pub fn with_gas_forwarded(mut self, gas_forwarded: u64) -> Self { - self.gas_forwarded = Some(gas_forwarded); - self - } - - pub fn gas_forwarded(&self) -> Option { - self.gas_forwarded - } -} - -impl Default for CallParameters { - fn default() -> Self { - Self { - amount: DEFAULT_CALL_PARAMS_AMOUNT, - asset_id: None, - gas_forwarded: None, - } - } -} - -// Trait implemented by contract instances so that -// they can be passed to the `with_contracts` method -pub trait SettableContract { - fn id(&self) -> Bech32ContractId; - fn log_decoder(&self) -> LogDecoder; -} - -/// Configuration for contract storage -#[derive(Debug, Clone)] -pub struct StorageConfiguration { - autoload_storage: bool, - slot_overrides: StorageSlots, -} - -impl Default for StorageConfiguration { - fn default() -> Self { - Self { - autoload_storage: true, - slot_overrides: Default::default(), - } - } -} - -impl StorageConfiguration { - pub fn new(autoload_enabled: bool, slots: impl IntoIterator) -> Self { - let config = Self { - autoload_storage: autoload_enabled, - slot_overrides: Default::default(), - }; - - config.add_slot_overrides(slots) - } - - /// If enabled will try to automatically discover and load the storage configuration from the - /// storage config json file. - pub fn with_autoload(mut self, enabled: bool) -> Self { - self.autoload_storage = enabled; - self - } - - pub fn autoload_enabled(&self) -> bool { - self.autoload_storage - } - - /// Slots added via [`add_slot_overrides`] will override any - /// existing slots with matching keys. - pub fn add_slot_overrides( - mut self, - storage_slots: impl IntoIterator, - ) -> Self { - self.slot_overrides.add_overrides(storage_slots); - self - } - - /// Slots added via [`add_slot_overrides_from_file`] will override any - /// existing slots with matching keys. - /// - /// `path` - path to a JSON file containing the storage slots. - pub fn add_slot_overrides_from_file(mut self, path: impl AsRef) -> Result { - let slots = StorageSlots::load_from_file(path.as_ref())?; - self.slot_overrides.add_overrides(slots.into_iter()); - Ok(self) - } - - pub fn into_slots(self) -> impl Iterator { - self.slot_overrides.into_iter() - } -} - -#[derive(Debug, Clone, Default)] -struct StorageSlots { - storage_slots: HashMap, -} - -impl StorageSlots { - fn from(storage_slots: impl IntoIterator) -> Self { - let pairs = storage_slots.into_iter().map(|slot| (*slot.key(), slot)); - Self { - storage_slots: pairs.collect(), - } - } - - fn add_overrides(&mut self, storage_slots: impl IntoIterator) -> &mut Self { - let pairs = storage_slots.into_iter().map(|slot| (*slot.key(), slot)); - self.storage_slots.extend(pairs); - self - } - - fn load_from_file(storage_path: impl AsRef) -> Result { - let storage_path = storage_path.as_ref(); - validate_path_and_extension(storage_path, "json")?; - - let storage_json_string = std::fs::read_to_string(storage_path).map_err(|e| { - io::Error::new( - e.kind(), - format!("failed to read storage slots from: {storage_path:?}: {e}"), - ) - })?; - - let decoded_slots = serde_json::from_str::>(&storage_json_string)?; - - Ok(StorageSlots::from(decoded_slots)) - } - - fn into_iter(self) -> impl Iterator { - self.storage_slots.into_values() - } -} - -/// Configuration for contract deployment -#[derive(Debug, Clone, Default)] -pub struct LoadConfiguration { - storage: StorageConfiguration, - configurables: Configurables, - salt: Salt, -} - -impl LoadConfiguration { - pub fn new( - storage: StorageConfiguration, - configurables: impl Into, - salt: impl Into, - ) -> Self { - Self { - storage, - configurables: configurables.into(), - salt: salt.into(), - } - } - - pub fn with_storage_configuration(mut self, storage: StorageConfiguration) -> Self { - self.storage = storage; - self - } - - pub fn with_configurables(mut self, configurables: impl Into) -> Self { - self.configurables = configurables.into(); - self - } - - pub fn with_salt(mut self, salt: impl Into) -> Self { - self.salt = salt.into(); - self - } -} +pub use load::*; +pub use storage::*; /// [`Contract`] is a struct to interface with a contract. That includes things such as /// compiling, deploying, and running transactions against a contract. @@ -379,605 +160,13 @@ fn expected_storage_slots_filepath(contract_binary: &Path) -> Option { Some(dir.join(format!("{binary_filename}-storage_slots.json"))) } -fn validate_path_and_extension(file_path: &Path, extension: &str) -> Result<()> { - if !file_path.exists() { - return Err(error!(IO, "file {file_path:?} does not exist")); - } - - let path_extension = file_path - .extension() - .ok_or_else(|| error!(Other, "could not extract extension from: {file_path:?}"))?; - - if extension != path_extension { - return Err(error!( - Other, - "expected {file_path:?} to have '.{extension}' extension" - )); - } - - Ok(()) -} - -#[derive(Debug)] -/// Contains all data relevant to a single contract call -pub struct ContractCall { - pub contract_id: Bech32ContractId, - pub encoded_args: Result>, - pub encoded_selector: Selector, - pub call_parameters: CallParameters, - pub external_contracts: Vec, - pub output_param: ParamType, - pub is_payable: bool, - pub custom_assets: HashMap<(AssetId, Option), u64>, -} - -impl ContractCall { - pub fn with_contract_id(self, contract_id: Bech32ContractId) -> Self { - ContractCall { - contract_id, - ..self - } - } - - pub fn with_external_contracts( - self, - external_contracts: Vec, - ) -> ContractCall { - ContractCall { - external_contracts, - ..self - } - } - - pub fn with_call_parameters(self, call_parameters: CallParameters) -> ContractCall { - ContractCall { - call_parameters, - ..self - } - } - - pub fn append_external_contracts(&mut self, contract_id: Bech32ContractId) { - self.external_contracts.push(contract_id) - } - - pub fn add_custom_asset(&mut self, asset_id: AssetId, amount: u64, to: Option) { - *self.custom_assets.entry((asset_id, to)).or_default() += amount; - } -} - -#[derive(Debug)] -#[must_use = "contract calls do nothing unless you `call` them"] -/// Helper that handles submitting a call to a client and formatting the response -pub struct ContractCallHandler { - pub contract_call: ContractCall, - pub tx_policies: TxPolicies, - decoder_config: DecoderConfig, - // Initially `None`, gets set to the right tx id after the transaction is submitted - cached_tx_id: Option, - pub account: T, - pub datatype: PhantomData, - pub log_decoder: LogDecoder, - variable_output_policy: VariableOutputPolicy, -} - -impl ContractCallHandler -where - T: Account, - D: Tokenizable + Parameterize + Debug, -{ - /// Sets external contracts as dependencies to this contract's call. - /// Effectively, this will be used to create [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] - /// pairs and set them into the transaction. Note that this is a builder - /// method, i.e. use it as a chain: - /// - /// ```ignore - /// my_contract_instance.my_method(...).with_contract_ids(&[another_contract_id]).call() - /// ``` - /// - /// [`Input::Contract`]: fuel_tx::Input::Contract - /// [`Output::Contract`]: fuel_tx::Output::Contract - pub fn with_contract_ids(mut self, contract_ids: &[Bech32ContractId]) -> Self { - self.contract_call.external_contracts = contract_ids.to_vec(); - self - } - - /// If this method is not called, the default policy is to not add any variable outputs. - /// - /// # Parameters - /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the contract call. - /// - /// # Returns - /// - `Self`: The updated SDK configuration. - pub fn with_variable_output_policy(mut self, variable_outputs: VariableOutputPolicy) -> Self { - self.variable_output_policy = variable_outputs; - self - } - - /// Sets external contract instances as dependencies to this contract's call. - /// Effectively, this will be used to: merge `LogDecoder`s and create - /// [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] pairs and set them into the transaction. - /// Note that this is a builder method, i.e. use it as a chain: - /// - /// ```ignore - /// my_contract_instance.my_method(...).with_contracts(&[another_contract_instance]).call() - /// ``` - pub fn with_contracts(mut self, contracts: &[&dyn SettableContract]) -> Self { - self.contract_call.external_contracts = contracts.iter().map(|c| c.id()).collect(); - for c in contracts { - self.log_decoder.merge(c.log_decoder()); - } - self - } - - /// Adds a custom `asset_id` with its `amount` and an optional `address` to be used for - /// generating outputs to this contract's call. - /// - /// # Parameters - /// - `asset_id`: The unique identifier of the asset being added. - /// - `amount`: The amount of the asset being added. - /// - `address`: The optional account address that the output amount will be sent to. - /// If not provided, the asset will be sent to the users account address. - /// Note that this is a builder method, i.e. use it as a chain: - /// - /// ```ignore - /// let asset_id = AssetId::from([3u8; 32]); - /// let amount = 5000; - /// my_contract_instance.my_method(...).add_custom_asset(asset_id, amount, None).call() - /// ``` - pub fn add_custom_asset( - mut self, - asset_id: AssetId, - amount: u64, - to: Option, - ) -> Self { - self.contract_call.add_custom_asset(asset_id, amount, to); - self - } - - pub fn is_payable(&self) -> bool { - self.contract_call.is_payable - } - - /// Sets the transaction policies for a given transaction. - /// Note that this is a builder method, i.e. use it as a chain: - /// ```ignore - /// let tx_policies = TxPolicies::default().with_gas_price(100); - /// my_contract_instance.my_method(...).with_tx_policies(tx_policies).call() - /// ``` - pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { - self.tx_policies = tx_policies; - self - } - - pub fn with_decoder_config(mut self, decoder_config: DecoderConfig) -> Self { - self.decoder_config = decoder_config; - self.log_decoder.set_decoder_config(decoder_config); - self - } - - /// Sets the call parameters for a given contract call. - /// Note that this is a builder method, i.e. use it as a chain: - /// - /// ```ignore - /// let params = CallParameters { amount: 1, asset_id: AssetId::zeroed() }; - /// my_contract_instance.my_method(...).call_params(params).call() - /// ``` - pub fn call_params(mut self, params: CallParameters) -> Result { - if !self.is_payable() && params.amount > 0 { - return Err(error!(Other, "assets forwarded to non-payable method")); - } - self.contract_call.call_parameters = params; - Ok(self) - } - - pub async fn transaction_builder(&self) -> Result { - transaction_builder_from_contract_calls( - std::slice::from_ref(&self.contract_call), - self.tx_policies, - &self.account, - ) - .await - } - - /// Returns the script that executes the contract call - pub async fn build_tx(&self) -> Result { - build_tx_from_contract_calls( - std::slice::from_ref(&self.contract_call), - self.tx_policies, - &self.account, - self.variable_output_policy, - ) - .await - } - - /// Call a contract's method on the node, in a state-modifying manner. - pub async fn call(mut self) -> Result> { - self.call_or_simulate(false).await - } - - pub async fn submit(mut self) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - let tx_id = provider.send_transaction(tx.clone()).await?; - self.cached_tx_id = Some(tx_id); - - Ok(SubmitResponse::new(tx_id, self)) - } - - /// Call a contract's method on the node, in a simulated manner, meaning the state of the - /// blockchain is *not* modified but simulated. - pub async fn simulate(&mut self) -> Result> { - self.call_or_simulate(true).await - } - - async fn call_or_simulate(&mut self, simulate: bool) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - self.cached_tx_id = Some(tx.id(provider.chain_id())); - - let tx_status = if simulate { - provider.dry_run(tx).await? - } else { - provider.send_transaction_and_await_commit(tx).await? - }; - let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; - - self.get_response(receipts) - } - - /// Get a contract's estimated cost - pub async fn estimate_transaction_cost( - &self, - tolerance: Option, - block_horizon: Option, - ) -> Result { - let script = self.build_tx().await?; - let provider = self.account.try_provider()?; - - let transaction_cost = provider - .estimate_transaction_cost(script, tolerance, block_horizon) - .await?; - - Ok(transaction_cost) - } - - /// Create a [`FuelCallResponse`] from call receipts - pub fn get_response(&self, receipts: Vec) -> Result> { - let token = ReceiptParser::new(&receipts, self.decoder_config) - .parse_call(&self.contract_call.contract_id, &D::param_type())?; - - Ok(FuelCallResponse::new( - D::from_token(token)?, - receipts, - self.log_decoder.clone(), - self.cached_tx_id, - )) - } - - /// Create a [`FuelCallResponse`] from `TxStatus` - pub fn get_response_from(&self, tx_status: TxStatus) -> Result> { - let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; - - self.get_response(receipts) - } -} - -impl sealed::Sealed for ContractCallHandler {} - -#[async_trait::async_trait] -impl TxDependencyExtension for ContractCallHandler -where - T: Account, - D: Tokenizable + Parameterize + Debug + Send + Sync, -{ - async fn simulate(&mut self) -> Result<()> { - self.simulate().await?; - Ok(()) - } - - fn append_contract(mut self, contract_id: Bech32ContractId) -> Self { - self.contract_call.append_external_contracts(contract_id); - self - } -} - -/// Creates an ABI call based on a function [selector](Selector) and -/// the encoding of its call arguments, which is a slice of [`Token`]s. -/// It returns a prepared [`ContractCall`] that can further be used to -/// make the actual transaction. -/// This method is the underlying implementation of the functions -/// generated from an ABI JSON spec, i.e, this is what's generated: -/// -/// ```ignore -/// quote! { -/// #doc -/// pub fn #name(&self #input) -> #result { -/// contract::method_hash(#tokenized_signature, #arg) -/// } -/// } -/// ``` -/// -/// For more details see `code_gen` in `fuels-core`. -/// -/// Note that this needs an account because the contract instance needs an account for the calls -pub fn method_hash( - contract_id: Bech32ContractId, - account: T, - encoded_selector: Selector, - args: &[Token], - log_decoder: LogDecoder, - is_payable: bool, - encoder_config: EncoderConfig, -) -> ContractCallHandler { - let contract_call = ContractCall { - contract_id, - encoded_selector, - encoded_args: ABIEncoder::new(encoder_config).encode(args), - call_parameters: CallParameters::default(), - external_contracts: vec![], - output_param: D::param_type(), - is_payable, - custom_assets: Default::default(), - }; - - ContractCallHandler { - contract_call, - tx_policies: TxPolicies::default(), - cached_tx_id: None, - account, - datatype: PhantomData, - log_decoder, - decoder_config: DecoderConfig::default(), - variable_output_policy: VariableOutputPolicy::default(), - } -} - -#[derive(Debug)] -#[must_use = "contract calls do nothing unless you `call` them"] -/// Helper that handles bundling multiple calls into a single transaction -pub struct MultiContractCallHandler { - pub contract_calls: Vec, - pub log_decoder: LogDecoder, - pub tx_policies: TxPolicies, - // Initially `None`, gets set to the right tx id after the transaction is submitted - cached_tx_id: Option, - decoder_config: DecoderConfig, - pub account: T, - variable_output_policy: VariableOutputPolicy, -} - -impl MultiContractCallHandler { - pub fn new(account: T) -> Self { - Self { - contract_calls: vec![], - tx_policies: TxPolicies::default(), - cached_tx_id: None, - account, - log_decoder: LogDecoder::new(Default::default()), - decoder_config: DecoderConfig::default(), - variable_output_policy: VariableOutputPolicy::default(), - } - } - - pub fn with_decoder_config(&mut self, decoder_config: DecoderConfig) -> &mut Self { - self.decoder_config = decoder_config; - self.log_decoder.set_decoder_config(decoder_config); - self - } - - /// Adds a contract call to be bundled in the transaction - /// Note that this is a builder method - pub fn add_call( - &mut self, - call_handler: ContractCallHandler, - ) -> &mut Self { - self.log_decoder.merge(call_handler.log_decoder); - self.contract_calls.push(call_handler.contract_call); - self - } - - /// Sets the transaction policies for a given transaction. - /// Note that this is a builder method - pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { - self.tx_policies = tx_policies; - self - } - - /// If this method is not called, the default policy is to not add any variable outputs. - /// - /// # Parameters - /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the contract multi-call. - /// - /// # Returns - /// - `Self`: The updated SDK configuration. - pub fn with_variable_output_policy(mut self, variable_outputs: VariableOutputPolicy) -> Self { - self.variable_output_policy = variable_outputs; - self - } - - fn validate_contract_calls(&self) -> Result<()> { - if self.contract_calls.is_empty() { - return Err(error!( - Other, - "no calls added. Have you used '.add_calls()'?" - )); - } - - Ok(()) - } - - pub async fn transaction_builder(&self) -> Result { - self.validate_contract_calls()?; - - transaction_builder_from_contract_calls( - &self.contract_calls, - self.tx_policies, - &self.account, - ) - .await - } - - /// Returns the script that executes the contract calls - pub async fn build_tx(&self) -> Result { - self.validate_contract_calls()?; - - build_tx_from_contract_calls( - &self.contract_calls, - self.tx_policies, - &self.account, - self.variable_output_policy, - ) - .await - } - - /// Call contract methods on the node, in a state-modifying manner. - pub async fn call(&mut self) -> Result> { - self.call_or_simulate(false).await - } - - pub async fn submit(mut self) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - let tx_id = provider.send_transaction(tx).await?; - self.cached_tx_id = Some(tx_id); - - Ok(SubmitResponseMultiple::new(tx_id, self)) - } - - /// Call contract methods on the node, in a simulated manner, meaning the state of the - /// blockchain is *not* modified but simulated. - /// It is the same as the [call] method because the API is more user-friendly this way. - /// - /// [call]: Self::call - pub async fn simulate(&mut self) -> Result> { - self.call_or_simulate(true).await - } - - async fn call_or_simulate( - &mut self, - simulate: bool, - ) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - self.cached_tx_id = Some(tx.id(provider.chain_id())); - - let tx_status = if simulate { - provider.dry_run(tx).await? - } else { - provider.send_transaction_and_await_commit(tx).await? - }; - - let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; - - self.get_response(receipts) - } - - /// Simulates a call without needing to resolve the generic for the return type - async fn simulate_without_decode(&self) -> Result<()> { - let provider = self.account.try_provider()?; - let tx = self.build_tx().await?; - - provider.dry_run(tx).await?.check(None)?; - - Ok(()) - } - - /// Get a contract's estimated cost - pub async fn estimate_transaction_cost( - &self, - tolerance: Option, - block_horizon: Option, - ) -> Result { - let script = self.build_tx().await?; - - let transaction_cost = self - .account - .try_provider()? - .estimate_transaction_cost(script, tolerance, block_horizon) - .await?; - - Ok(transaction_cost) - } - - /// Create a [`FuelCallResponse`] from call receipts - pub fn get_response( - &self, - receipts: Vec, - ) -> Result> { - let mut receipt_parser = ReceiptParser::new(&receipts, self.decoder_config); - - let final_tokens = self - .contract_calls - .iter() - .map(|call| receipt_parser.parse_call(&call.contract_id, &call.output_param)) - .collect::>>()?; - - let tokens_as_tuple = Token::Tuple(final_tokens); - let response = FuelCallResponse::::new( - D::from_token(tokens_as_tuple)?, - receipts, - self.log_decoder.clone(), - self.cached_tx_id, - ); - - Ok(response) - } -} - -impl sealed::Sealed for MultiContractCallHandler {} - -#[async_trait::async_trait] -impl TxDependencyExtension for MultiContractCallHandler -where - T: Account, -{ - async fn simulate(&mut self) -> Result<()> { - self.simulate_without_decode().await?; - Ok(()) - } - - fn append_contract(mut self, contract_id: Bech32ContractId) -> Self { - self.contract_calls - .iter_mut() - .take(1) - .for_each(|call| call.append_external_contracts(contract_id.clone())); - self - } -} - #[cfg(test)] mod tests { - use std::collections::HashSet; - use fuels_core::types::errors::Error; use tempfile::tempdir; use super::*; - #[test] - fn merging_overrides_storage_slots() { - // given - let make_slot = |id, value| StorageSlot::new([id; 32].into(), [value; 32].into()); - - let slots = (1..3).map(|id| make_slot(id, 100)); - let original_config = StorageConfiguration::new(false, slots); - - let overlapping_slots = (2..4).map(|id| make_slot(id, 200)); - - // when - let original_config = original_config.add_slot_overrides(overlapping_slots); - - // then - assert_eq!( - HashSet::from_iter(original_config.slot_overrides.into_iter()), - HashSet::from([make_slot(1, 100), make_slot(2, 200), make_slot(3, 200)]) - ); - } - #[test] fn autoload_storage_slots() { // given diff --git a/packages/fuels-programs/src/contract/load.rs b/packages/fuels-programs/src/contract/load.rs new file mode 100644 index 0000000000..c7df990dc1 --- /dev/null +++ b/packages/fuels-programs/src/contract/load.rs @@ -0,0 +1,43 @@ +use std::{default::Default, fmt::Debug}; + +use fuel_tx::Salt; +use fuels_core::Configurables; + +use crate::contract::StorageConfiguration; + +/// Configuration for contract deployment +#[derive(Debug, Clone, Default)] +pub struct LoadConfiguration { + pub(crate) storage: StorageConfiguration, + pub(crate) configurables: Configurables, + pub(crate) salt: Salt, +} + +impl LoadConfiguration { + pub fn new( + storage: StorageConfiguration, + configurables: impl Into, + salt: impl Into, + ) -> Self { + Self { + storage, + configurables: configurables.into(), + salt: salt.into(), + } + } + + pub fn with_storage_configuration(mut self, storage: StorageConfiguration) -> Self { + self.storage = storage; + self + } + + pub fn with_configurables(mut self, configurables: impl Into) -> Self { + self.configurables = configurables.into(); + self + } + + pub fn with_salt(mut self, salt: impl Into) -> Self { + self.salt = salt.into(); + self + } +} diff --git a/packages/fuels-programs/src/contract/storage.rs b/packages/fuels-programs/src/contract/storage.rs new file mode 100644 index 0000000000..16859e1ec9 --- /dev/null +++ b/packages/fuels-programs/src/contract/storage.rs @@ -0,0 +1,155 @@ +use std::{collections::HashMap, default::Default, fmt::Debug, io, path::Path}; + +use fuel_tx::{Bytes32, StorageSlot}; +use fuels_core::types::errors::{error, Result}; + +/// Configuration for contract storage +#[derive(Debug, Clone)] +pub struct StorageConfiguration { + autoload_storage: bool, + slot_overrides: StorageSlots, +} + +impl Default for StorageConfiguration { + fn default() -> Self { + Self { + autoload_storage: true, + slot_overrides: Default::default(), + } + } +} + +impl StorageConfiguration { + pub fn new(autoload_enabled: bool, slots: impl IntoIterator) -> Self { + let config = Self { + autoload_storage: autoload_enabled, + slot_overrides: Default::default(), + }; + + config.add_slot_overrides(slots) + } + + /// If enabled will try to automatically discover and load the storage configuration from the + /// storage config json file. + pub fn with_autoload(mut self, enabled: bool) -> Self { + self.autoload_storage = enabled; + self + } + + pub fn autoload_enabled(&self) -> bool { + self.autoload_storage + } + + /// Slots added via [`add_slot_overrides`] will override any + /// existing slots with matching keys. + pub fn add_slot_overrides( + mut self, + storage_slots: impl IntoIterator, + ) -> Self { + self.slot_overrides.add_overrides(storage_slots); + self + } + + /// Slots added via [`add_slot_overrides_from_file`] will override any + /// existing slots with matching keys. + /// + /// `path` - path to a JSON file containing the storage slots. + pub fn add_slot_overrides_from_file(mut self, path: impl AsRef) -> Result { + let slots = StorageSlots::load_from_file(path.as_ref())?; + self.slot_overrides.add_overrides(slots.into_iter()); + Ok(self) + } + + pub fn into_slots(self) -> impl Iterator { + self.slot_overrides.into_iter() + } +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct StorageSlots { + storage_slots: HashMap, +} + +impl StorageSlots { + fn from(storage_slots: impl IntoIterator) -> Self { + let pairs = storage_slots.into_iter().map(|slot| (*slot.key(), slot)); + Self { + storage_slots: pairs.collect(), + } + } + + pub(crate) fn add_overrides( + &mut self, + storage_slots: impl IntoIterator, + ) -> &mut Self { + let pairs = storage_slots.into_iter().map(|slot| (*slot.key(), slot)); + self.storage_slots.extend(pairs); + self + } + + pub(crate) fn load_from_file(storage_path: impl AsRef) -> Result { + let storage_path = storage_path.as_ref(); + validate_path_and_extension(storage_path, "json")?; + + let storage_json_string = std::fs::read_to_string(storage_path).map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to read storage slots from: {storage_path:?}: {e}"), + ) + })?; + + let decoded_slots = serde_json::from_str::>(&storage_json_string)?; + + Ok(StorageSlots::from(decoded_slots)) + } + + pub(crate) fn into_iter(self) -> impl Iterator { + self.storage_slots.into_values() + } +} + +pub(crate) fn validate_path_and_extension(file_path: &Path, extension: &str) -> Result<()> { + if !file_path.exists() { + return Err(error!(IO, "file {file_path:?} does not exist")); + } + + let path_extension = file_path + .extension() + .ok_or_else(|| error!(Other, "could not extract extension from: {file_path:?}"))?; + + if extension != path_extension { + return Err(error!( + Other, + "expected {file_path:?} to have '.{extension}' extension" + )); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn merging_overrides_storage_slots() { + // given + let make_slot = |id, value| StorageSlot::new([id; 32].into(), [value; 32].into()); + + let slots = (1..3).map(|id| make_slot(id, 100)); + let original_config = StorageConfiguration::new(false, slots); + + let overlapping_slots = (2..4).map(|id| make_slot(id, 200)); + + // when + let original_config = original_config.add_slot_overrides(overlapping_slots); + + // then + assert_eq!( + HashSet::from_iter(original_config.slot_overrides.into_iter()), + HashSet::from([make_slot(1, 100), make_slot(2, 200), make_slot(3, 200)]) + ); + } +} diff --git a/packages/fuels-programs/src/lib.rs b/packages/fuels-programs/src/lib.rs index 5a4aa4a9cd..755e14c274 100644 --- a/packages/fuels-programs/src/lib.rs +++ b/packages/fuels-programs/src/lib.rs @@ -1,6 +1,3 @@ -pub mod call_response; -pub mod call_utils; +pub mod calls; pub mod contract; -pub mod receipt_parser; -pub mod script_calls; -mod submit_response; +pub mod responses; diff --git a/packages/fuels-programs/src/responses.rs b/packages/fuels-programs/src/responses.rs new file mode 100644 index 0000000000..47e3a43fef --- /dev/null +++ b/packages/fuels-programs/src/responses.rs @@ -0,0 +1,5 @@ +mod call; +mod submit; + +pub use call::*; +pub use submit::*; diff --git a/packages/fuels-programs/src/call_response.rs b/packages/fuels-programs/src/responses/call.rs similarity index 80% rename from packages/fuels-programs/src/call_response.rs rename to packages/fuels-programs/src/responses/call.rs index 75c31ea77e..91ebcf102c 100644 --- a/packages/fuels-programs/src/call_response.rs +++ b/packages/fuels-programs/src/responses/call.rs @@ -7,23 +7,21 @@ use fuels_core::{ types::errors::Result, }; -/// [`FuelCallResponse`] is a struct that is returned by a call to the contract or script. Its value +/// [`CallResponse`] is a struct that is returned by a call to the contract or script. Its value /// field holds the decoded typed value returned by the contract's method. The other field holds all /// the receipts returned by the call. -/// The name is `FuelCallResponse` instead of `CallResponse` because it would be ambiguous with the -/// `CALL` opcode. #[derive(Debug)] -// ANCHOR: fuel_call_response -pub struct FuelCallResponse { +// ANCHOR: call_response +pub struct CallResponse { pub value: D, pub receipts: Vec, pub gas_used: u64, pub log_decoder: LogDecoder, pub tx_id: Option, } -// ANCHOR_END: fuel_call_response +// ANCHOR_END: call_response -impl FuelCallResponse { +impl CallResponse { /// Get the gas used from ScriptResult receipt fn get_gas_used(receipts: &[Receipt]) -> u64 { receipts diff --git a/packages/fuels-programs/src/responses/submit.rs b/packages/fuels-programs/src/responses/submit.rs new file mode 100644 index 0000000000..0f23193290 --- /dev/null +++ b/packages/fuels-programs/src/responses/submit.rs @@ -0,0 +1,92 @@ +use std::fmt::Debug; + +use fuel_types::Bytes32; +use fuels_accounts::Account; +use fuels_core::{ + traits::{Parameterize, Tokenizable}, + types::errors::Result, +}; + +use crate::{ + calls::{ + traits::{ContractDependencyConfigurator, ResponseParser, TransactionTuner}, + CallHandler, ContractCall, + }, + responses::CallResponse, +}; + +/// Represents the response of a submitted transaction with customizable retry behavior. +/// +/// This struct holds information about the retry configuration, transaction ID (`tx_id`), +/// and the call handler that manages the type of call (contract or script). +/// +/// # Type Parameters +/// +/// - `T`: The account type associated with the transaction. +/// - `D`: The data type representing the response value. +/// - `C`: The call type. +/// +/// # Fields +/// +/// - `retry_config`: The retry configuration for the transaction. +/// - `tx_id`: The optional transaction ID of the submitted transaction. +/// - `call_handler`: The call handler that manages the type of call. +/// +/// ``` +#[derive(Debug)] +pub struct SubmitResponse { + tx_id: Bytes32, + call_handler: CallHandler, +} + +impl SubmitResponse +where + A: Account, + C: ContractDependencyConfigurator + TransactionTuner + ResponseParser, + T: Tokenizable + Parameterize + Debug, +{ + pub fn new(tx_id: Bytes32, call_handler: CallHandler) -> Self { + Self { + tx_id, + call_handler, + } + } + + pub async fn response(self) -> Result> { + let provider = self.call_handler.account.try_provider()?; + let receipts = provider + .tx_status(&self.tx_id) + .await? + .take_receipts_checked(Some(&self.call_handler.log_decoder))?; + + self.call_handler.get_response(receipts) + } + + pub fn tx_id(&self) -> Bytes32 { + self.tx_id + } +} + +/// Represents the response of a submitted transaction with multiple contract calls. +impl SubmitResponse, ()> { + pub fn new(tx_id: Bytes32, call_handler: CallHandler, ()>) -> Self { + Self { + tx_id, + call_handler, + } + } + + pub async fn response(self) -> Result> { + let provider = self.call_handler.account.try_provider()?; + let receipts = provider + .tx_status(&self.tx_id) + .await? + .take_receipts_checked(Some(&self.call_handler.log_decoder))?; + + self.call_handler.get_response(receipts) + } + + pub fn tx_id(&self) -> Bytes32 { + self.tx_id + } +} diff --git a/packages/fuels-programs/src/script_calls.rs b/packages/fuels-programs/src/script_calls.rs deleted file mode 100644 index ba84ae3c22..0000000000 --- a/packages/fuels-programs/src/script_calls.rs +++ /dev/null @@ -1,326 +0,0 @@ -use std::{collections::HashSet, fmt::Debug, marker::PhantomData}; - -use fuel_tx::{Bytes32, ContractId, Output, Receipt}; -use fuels_accounts::{provider::TransactionCost, Account}; -use fuels_core::{ - codec::{DecoderConfig, LogDecoder}, - error, - traits::{Parameterize, Tokenizable}, - types::{ - bech32::Bech32ContractId, - errors::Result, - input::Input, - transaction::{ScriptTransaction, Transaction, TxPolicies}, - transaction_builders::{ - BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder, - VariableOutputPolicy, - }, - tx_status::TxStatus, - }, -}; -use itertools::chain; - -use crate::{ - call_response::FuelCallResponse, - call_utils::{ - generate_contract_inputs, generate_contract_outputs, sealed, TxDependencyExtension, - }, - contract::SettableContract, - receipt_parser::ReceiptParser, - submit_response::SubmitResponse, -}; - -#[derive(Debug)] -/// Contains all data relevant to a single script call -pub struct ScriptCall { - pub script_binary: Vec, - pub encoded_args: Result>, - pub inputs: Vec, - pub outputs: Vec, - pub external_contracts: Vec, -} - -impl ScriptCall { - pub fn with_outputs(mut self, outputs: Vec) -> Self { - self.outputs = outputs; - self - } - - pub fn with_inputs(mut self, inputs: Vec) -> Self { - self.inputs = inputs; - self - } - - pub fn with_external_contracts(self, external_contracts: Vec) -> ScriptCall { - ScriptCall { - external_contracts, - ..self - } - } - - pub fn append_external_contracts(&mut self, contract_id: Bech32ContractId) { - self.external_contracts.push(contract_id) - } -} - -#[derive(Debug)] -#[must_use = "script calls do nothing unless you `call` them"] -/// Helper that handles submitting a script call to a client and formatting the response -pub struct ScriptCallHandler { - pub script_call: ScriptCall, - pub tx_policies: TxPolicies, - // Initially `None`, gets set to the right tx id after the transaction is submitted - cached_tx_id: Option, - decoder_config: DecoderConfig, - pub account: T, - pub datatype: PhantomData, - pub log_decoder: LogDecoder, - variable_output_policy: VariableOutputPolicy, -} - -impl ScriptCallHandler -where - D: Parameterize + Tokenizable + Debug, -{ - pub fn new( - script_binary: Vec, - encoded_args: Result>, - account: T, - log_decoder: LogDecoder, - ) -> Self { - let script_call = ScriptCall { - script_binary, - encoded_args, - inputs: vec![], - outputs: vec![], - external_contracts: vec![], - }; - Self { - script_call, - tx_policies: TxPolicies::default(), - cached_tx_id: None, - account, - datatype: PhantomData, - log_decoder, - decoder_config: DecoderConfig::default(), - variable_output_policy: VariableOutputPolicy::default(), - } - } - - /// Sets the transaction policies for a given transaction. - /// Note that this is a builder method, i.e. use it as a chain: - /// - /// ```ignore - /// let tx_policies = TxPolicies::default().with_gas_price(100); - /// instance.main(...).with_tx_policies(tx_policies).call() - /// ``` - pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self { - self.tx_policies = tx_policies; - self - } - - /// If this method is not called, the default policy is to not add any variable outputs. - /// - /// # Parameters - /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the script call. - /// - /// # Returns - /// - `Self`: The updated SDK configuration. - pub fn with_variable_output_policy( - mut self, - variable_output_policy: VariableOutputPolicy, - ) -> Self { - self.variable_output_policy = variable_output_policy; - self - } - - pub fn with_decoder_config(mut self, decoder_config: DecoderConfig) -> Self { - self.decoder_config = decoder_config; - self.log_decoder.set_decoder_config(decoder_config); - self - } - - pub fn with_outputs(mut self, outputs: Vec) -> Self { - self.script_call = self.script_call.with_outputs(outputs); - self - } - - pub fn with_inputs(mut self, inputs: Vec) -> Self { - self.script_call = self.script_call.with_inputs(inputs); - self - } - - pub fn with_contract_ids(mut self, contract_ids: &[Bech32ContractId]) -> Self { - self.script_call.external_contracts = contract_ids.to_vec(); - self - } - - pub fn with_contracts(mut self, contracts: &[&dyn SettableContract]) -> Self { - self.script_call.external_contracts = contracts.iter().map(|c| c.id()).collect(); - for c in contracts { - self.log_decoder.merge(c.log_decoder()); - } - self - } - - fn compute_script_data(&self) -> Result> { - self.script_call - .encoded_args - .as_ref() - .map(|b| b.to_owned()) - .map_err(|e| error!(Codec, "cannot encode script call arguments: {e}")) - } - - async fn prepare_inputs_outputs(&self) -> Result<(Vec, Vec)> { - let contract_ids: HashSet = self - .script_call - .external_contracts - .iter() - .map(|bech32| bech32.into()) - .collect(); - let num_of_contracts = contract_ids.len(); - - let inputs = chain!( - generate_contract_inputs(contract_ids), - self.script_call.inputs.clone(), - ) - .collect(); - - // Note the contract_outputs need to come first since the - // contract_inputs are referencing them via `output_index`. The node - // will, upon receiving our request, use `output_index` to index the - // `inputs` array we've sent over. - let outputs = chain!( - generate_contract_outputs(num_of_contracts), - self.script_call.outputs.clone(), - ) - .collect(); - - Ok((inputs, outputs)) - } - - pub async fn transaction_builder(&self) -> Result { - let (inputs, outputs) = self.prepare_inputs_outputs().await?; - - Ok(ScriptTransactionBuilder::default() - .with_variable_output_policy(self.variable_output_policy) - .with_tx_policies(self.tx_policies) - .with_script(self.script_call.script_binary.clone()) - .with_script_data(self.compute_script_data()?) - .with_inputs(inputs) - .with_outputs(outputs) - .with_gas_estimation_tolerance(0.05)) - } - - /// Returns the transaction that executes the script call - pub async fn build_tx(&self) -> Result { - let mut tb = self.transaction_builder().await?; - - self.account.add_witnesses(&mut tb)?; - self.account.adjust_for_fee(&mut tb, 0).await?; - - tb.build(self.account.try_provider()?).await - } - - /// Call a script on the node. If `simulate == true`, then the call is done in a - /// read-only manner, using a `dry-run`. The [`FuelCallResponse`] struct contains the `main`'s value - /// in its `value` field as an actual typed value `D` (if your method returns `bool`, - /// it will be a bool, works also for structs thanks to the `abigen!()`). - /// The other field of [`FuelCallResponse`], `receipts`, contains the receipts of the transaction. - async fn call_or_simulate(&mut self, simulate: bool) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - self.cached_tx_id = Some(tx.id(provider.chain_id())); - - let tx_status = if simulate { - provider.dry_run(tx).await? - } else { - provider.send_transaction_and_await_commit(tx).await? - }; - let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; - - self.get_response(receipts) - } - - /// Call a script on the node, in a state-modifying manner. - pub async fn call(mut self) -> Result> { - self.call_or_simulate(false).await - } - - pub async fn submit(mut self) -> Result> { - let tx = self.build_tx().await?; - let provider = self.account.try_provider()?; - - let tx_id = provider.send_transaction(tx).await?; - self.cached_tx_id = Some(tx_id); - - Ok(SubmitResponse::new(tx_id, self)) - } - - /// Call a script on the node, in a simulated manner, meaning the state of the - /// blockchain is *not* modified but simulated. - /// It is the same as the [`call`] method because the API is more user-friendly this way. - /// - /// [`call`]: Self::call - pub async fn simulate(&mut self) -> Result> { - self.call_or_simulate(true).await - } - - /// Get a scripts's estimated cost - pub async fn estimate_transaction_cost( - &self, - tolerance: Option, - block_horizon: Option, - ) -> Result { - let tx = self.build_tx().await?; - - let transaction_cost = self - .account - .try_provider()? - .estimate_transaction_cost(tx, tolerance, block_horizon) - .await?; - - Ok(transaction_cost) - } - - /// Create a [`FuelCallResponse`] from call receipts - pub fn get_response(&self, receipts: Vec) -> Result> { - let token = - ReceiptParser::new(&receipts, self.decoder_config).parse_script(&D::param_type())?; - - Ok(FuelCallResponse::new( - D::from_token(token)?, - receipts, - self.log_decoder.clone(), - self.cached_tx_id, - )) - } - - /// Create a [`FuelCallResponse`] from `TxStatus` - pub fn get_response_from(&self, tx_status: TxStatus) -> Result> { - let receipts = tx_status.take_receipts_checked(Some(&self.log_decoder))?; - - self.get_response(receipts) - } -} - -impl sealed::Sealed for ScriptCallHandler {} - -#[async_trait::async_trait] -impl TxDependencyExtension for ScriptCallHandler -where - T: Account, - D: Tokenizable + Parameterize + Debug + Send + Sync, -{ - async fn simulate(&mut self) -> Result<()> { - self.simulate().await?; - - Ok(()) - } - - fn append_contract(mut self, contract_id: Bech32ContractId) -> Self { - self.script_call.append_external_contracts(contract_id); - self - } -} diff --git a/packages/fuels-programs/src/submit_response.rs b/packages/fuels-programs/src/submit_response.rs deleted file mode 100644 index 666b6e7c7f..0000000000 --- a/packages/fuels-programs/src/submit_response.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fmt::Debug; - -use fuel_tx::Receipt; -use fuel_types::Bytes32; -use fuels_accounts::{provider::Provider, Account}; -use fuels_core::{ - codec::LogDecoder, - traits::{Parameterize, Tokenizable}, - types::errors::Result, -}; - -use crate::{ - call_response::FuelCallResponse, - contract::{ContractCallHandler, MultiContractCallHandler}, - script_calls::ScriptCallHandler, -}; - -/// Represents the response of a submitted transaction with customizable retry behavior. -/// -/// This struct holds information about the retry configuration, transaction ID (`tx_id`), -/// and the call handler that manages the type of call (contract or script). -/// -/// # Type Parameters -/// -/// - `T`: The account type associated with the transaction. -/// - `D`: The data type representing the response value. -/// -/// # Fields -/// -/// - `retry_config`: The retry configuration for the transaction. -/// - `tx_id`: The optional transaction ID of the submitted transaction. -/// - `call_handler`: The call handler that manages the type of call. -/// -/// ``` -#[derive(Debug)] -pub struct SubmitResponse { - tx_id: Bytes32, - call_handler: CallHandler, -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum CallHandler { - Contract(ContractCallHandler), - Script(ScriptCallHandler), -} - -impl From> for CallHandler { - fn from(value: ScriptCallHandler) -> Self { - Self::Script(value) - } -} - -impl From> for CallHandler { - fn from(value: ContractCallHandler) -> Self { - Self::Contract(value) - } -} - -impl CallHandler -where - T: Account, - D: Tokenizable + Parameterize + Debug, -{ - fn get_response(&self, receipts: Vec) -> Result> { - match self { - CallHandler::Contract(contract_handler) => contract_handler.get_response(receipts), - CallHandler::Script(script_handler) => script_handler.get_response(receipts), - } - } - - fn try_provider(&self) -> Result<&Provider> { - let account = match self { - CallHandler::Contract(contract_handler) => &contract_handler.account, - CallHandler::Script(script_handler) => &script_handler.account, - }; - - account.try_provider() - } - - fn log_decoder(&self) -> &LogDecoder { - match self { - CallHandler::Contract(handler) => &handler.log_decoder, - CallHandler::Script(handler) => &handler.log_decoder, - } - } -} - -impl SubmitResponse { - pub fn new(tx_id: Bytes32, call_handler: impl Into>) -> Self { - Self { - tx_id, - call_handler: call_handler.into(), - } - } - - pub async fn response(self) -> Result> { - let provider = self.call_handler.try_provider()?; - let receipts = provider - .tx_status(&self.tx_id) - .await? - .take_receipts_checked(Some(self.call_handler.log_decoder()))?; - - self.call_handler.get_response(receipts) - } - - pub fn tx_id(&self) -> Bytes32 { - self.tx_id - } -} - -/// Represents the response of a submitted transaction with multiple contract calls. -/// -/// This struct is similar to `SubmitResponse` but is designed to handle transactions -/// with multiple contract calls. -#[derive(Debug)] -pub struct SubmitResponseMultiple { - tx_id: Bytes32, - call_handler: MultiContractCallHandler, -} - -impl SubmitResponseMultiple { - pub fn new(tx_id: Bytes32, call_handler: MultiContractCallHandler) -> Self { - Self { - tx_id, - call_handler, - } - } - - pub async fn response(self) -> Result> { - let provider = self.call_handler.account.try_provider()?; - let receipts = provider - .tx_status(&self.tx_id) - .await? - .take_receipts_checked(Some(&self.call_handler.log_decoder))?; - - self.call_handler.get_response(receipts) - } - - pub fn tx_id(&self) -> Bytes32 { - self.tx_id - } -} diff --git a/packages/fuels/src/lib.rs b/packages/fuels/src/lib.rs index 88b344691a..5c671a8cbd 100644 --- a/packages/fuels/src/lib.rs +++ b/packages/fuels/src/lib.rs @@ -75,11 +75,8 @@ pub mod prelude { }, macros::setup_program_test, programs::{ - call_utils::TxDependencyExtension, - contract::{ - CallParameters, Contract, LoadConfiguration, MultiContractCallHandler, - SettableContract, StorageConfiguration, - }, + calls::{CallHandler, CallParameters, ContractDependency}, + contract::{Contract, LoadConfiguration, StorageConfiguration}, }, test_helpers::*, types::transaction_builders::*,