Skip to content

Commit

Permalink
feat!: add output variable estimation for ScriptCallHandler (FuelLa…
Browse files Browse the repository at this point in the history
…bs#976)

Closes FuelLabs#1001 

Adds ts dependencies estimation to script calls. 

BREAKING CHANGE:  `TxDependencyExtension` needs to be in scope to use `append_variable_outputs` and `append_contract`

Co-authored-by: Halil Beglerović <[email protected]>
  • Loading branch information
MujkicA and hal3e authored Jun 21, 2023
1 parent 9890269 commit f6b4327
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 165 deletions.
2 changes: 1 addition & 1 deletion docs/src/calling-contracts/tx-dependency-estimation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ But this requires you to know the contract id of the external contract and the n

The minimal number of attempts corresponds to the number of external contracts and output variables needed and defaults to 10.

> **Note:** `estimate_tx_dependencies()` can also be used when working with multi calls.
> **Note:** `estimate_tx_dependencies()` can also be used when working with script calls or multi calls.
> **Note:** `estimate_tx_dependencies()` does not currently resolve the dependencies needed for logging from an external contract. For more information, see [here](./logs.md).
Expand Down
6 changes: 4 additions & 2 deletions examples/contracts/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#[cfg(test)]
mod tests {
use fuels::accounts::wallet::WalletUnlocked;
use fuels::types::errors::{error, Error, Result};
use fuels::{
accounts::wallet::WalletUnlocked,
types::errors::{error, Error, Result},
};

#[tokio::test]
async fn instantiate_client() -> Result<()> {
Expand Down
11 changes: 10 additions & 1 deletion packages/fuels-core/src/types/wrappers/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::hash::Hash;

use fuel_tx::{TxPointer, UtxoId};
use fuel_types::{Bytes32, ContractId};
use fuel_types::{AssetId, Bytes32, ContractId};

use crate::types::{coin_type::CoinType, unresolved_bytes::UnresolvedBytes};

Expand Down Expand Up @@ -56,6 +56,15 @@ impl Input {
}
}

pub fn asset_id(&self) -> Option<AssetId> {
match self {
Self::ResourceSigned { resource, .. } | Self::ResourcePredicate { resource, .. } => {
Some(resource.asset_id())
}
_ => None,
}
}

pub const fn contract(
utxo_id: UtxoId,
balance_root: Bytes32,
Expand Down
113 changes: 105 additions & 8 deletions packages/fuels-programs/src/call_utils.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use std::{collections::HashSet, iter, vec};

use fuel_tx::{AssetId, Bytes32, ContractId, Output, TxPointer, UtxoId};
use fuel_types::Word;
use fuel_abi_types::error_codes::FAILED_TRANSFER_TO_ADDRESS_SIGNAL;
use fuel_tx::{AssetId, Bytes32, ContractId, Output, PanicReason, Receipt, TxPointer, UtxoId};
use fuel_types::{Address, Word};
use fuel_vm::fuel_asm::{op, RegId};
use fuels_accounts::Account;
use fuels_core::{
constants::WORD_SIZE,
offsets::call_script_data_offset,
types::{
bech32::Bech32Address,
errors::Result,
bech32::{Bech32Address, Bech32ContractId},
errors::{Error as FuelsError, Result},
input::Input,
param_types::ParamType,
transaction::{ScriptTransaction, TxParameters},
Expand All @@ -30,6 +31,71 @@ pub(crate) struct CallOpcodeParamsOffset {
pub call_data_offset: usize,
}

/// How many times to attempt to resolve missing tx dependencies.
pub const DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS: u64 = 10;

#[async_trait::async_trait]
pub trait TxDependencyExtension: Sized {
async fn simulate(&mut self) -> Result<()>;

/// Appends `num` [`fuel_tx::Output::Variable`]s to the transaction.
/// Note that this is a builder method, i.e. use it as a chain:
///
/// ```ignore
/// my_contract_instance.my_method(...).append_variable_outputs(num).call()
/// my_script_instance.main(...).append_variable_outputs(num).call()
/// ```
///
/// [`Output::Variable`]: fuel_tx::Output::Variable
fn append_variable_outputs(self, num: u64) -> Self;

/// 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;

fn append_missing_dependencies(mut self, receipts: &[Receipt]) -> Self {
if is_missing_output_variables(receipts) {
self = self.append_variable_outputs(1);
}
if let Some(contract_id) = find_id_of_missing_contract(receipts) {
self = self.append_contract(contract_id);
}

self
}

/// Simulates the call and attempts to resolve missing tx dependencies.
/// Forwards the received error if it cannot be fixed.
async fn estimate_tx_dependencies(mut self, max_attempts: Option<u64>) -> Result<Self> {
let attempts = max_attempts.unwrap_or(DEFAULT_TX_DEP_ESTIMATION_ATTEMPTS);

for _ in 0..attempts {
match self.simulate().await {
Ok(_) => return Ok(self),

Err(FuelsError::RevertTransactionError { ref receipts, .. }) => {
self = self.append_missing_dependencies(receipts);
}

Err(other_error) => return Err(other_error),
}
}

self.simulate().await.map(|_| self)
}
}

/// Creates a [`ScriptTransaction`] from contract calls. The internal [Transaction] is
/// initialized with the actual script instructions, script data needed to perform the call and
/// transaction inputs/outputs consisting of assets and contracts.
Expand Down Expand Up @@ -356,8 +422,7 @@ fn extract_unique_asset_ids(asset_inputs: &[Input]) -> HashSet<AssetId> {
fn extract_variable_outputs(calls: &[ContractCall]) -> Vec<Output> {
calls
.iter()
.filter_map(|call| call.variable_outputs.clone())
.flatten()
.flat_map(|call| call.variable_outputs.clone())
.collect()
}

Expand Down Expand Up @@ -405,6 +470,38 @@ fn extract_unique_contract_ids(calls: &[ContractCall]) -> HashSet<ContractId> {
.collect()
}

pub fn is_missing_output_variables(receipts: &[Receipt]) -> bool {
receipts.iter().any(
|r| matches!(r, Receipt::Revert { ra, .. } if *ra == FAILED_TRANSFER_TO_ADDRESS_SIGNAL),
)
}

pub fn find_id_of_missing_contract(receipts: &[Receipt]) -> Option<Bech32ContractId> {
receipts.iter().find_map(|receipt| match receipt {
Receipt::Panic {
reason,
contract_id,
..
} if *reason.reason() == PanicReason::ContractNotInInputs => {
let contract_id = contract_id
.expect("panic caused by a contract not in inputs must have a contract id");
Some(Bech32ContractId::from(contract_id))
}
_ => None,
})
}

pub fn new_variable_outputs(num: usize) -> Vec<Output> {
vec![
Output::Variable {
amount: 0,
to: Address::zeroed(),
asset_id: AssetId::default(),
};
num
]
}

#[cfg(test)]
mod test {
use std::slice;
Expand Down Expand Up @@ -432,7 +529,7 @@ mod test {
encoded_selector: [0; 8],
call_parameters: Default::default(),
compute_custom_input_offset: false,
variable_outputs: None,
variable_outputs: vec![],
external_contracts: Default::default(),
output_param: ParamType::Unit,
is_payable: false,
Expand Down Expand Up @@ -482,7 +579,7 @@ mod test {
encoded_args: args[i].clone(),
call_parameters: CallParameters::new(i as u64, asset_ids[i], i as u64),
compute_custom_input_offset: i == 1,
variable_outputs: None,
variable_outputs: vec![],
external_contracts: vec![],
output_param: ParamType::Unit,
is_payable: false,
Expand Down
Loading

0 comments on commit f6b4327

Please sign in to comment.