Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-call #399

Merged
merged 18 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions packages/fuels-abigen-macro/tests/harness.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use fuel_gql_client::fuel_tx::{AssetId, ContractId, Receipt};
use sha2::{Digest, Sha256};
use std::str::FromStr;

use fuels::contract::contract::MultiContractCallHandler;
use fuels::prelude::{
abigen, launch_provider_and_get_wallet, setup_multiple_assets_coins, setup_single_asset_coins,
setup_test_provider, CallParameters, Contract, Error, LocalWallet, Provider, Signer,
Expand All @@ -10,6 +8,8 @@ use fuels::prelude::{
use fuels_core::tx::Address;
use fuels_core::Tokenizable;
use fuels_core::{constants::BASE_ASSET_ID, Token};
use sha2::{Digest, Sha256};
use std::str::FromStr;

/// Note: all the tests and examples below require pre-compiled Sway projects.
/// To compile these projects, run `cargo run --bin build-test-projects`.
Expand Down Expand Up @@ -1910,3 +1910,75 @@ async fn nested_enums_are_correctly_encoded_decoded() {

assert_eq!(response.value, expected_none);
}

#[tokio::test]
async fn test_multi_call() {
abigen!(
MyContract,
"packages/fuels-abigen-macro/tests/test_projects/contract_test/out/debug/contract_test-abi.json"
);

let wallet = launch_provider_and_get_wallet().await;

let contract_id = Contract::deploy(
"tests/test_projects/contract_test/out/debug/contract_test.bin",
&wallet,
TxParameters::default(),
)
.await
.unwrap();

let contract_instance = MyContract::new(contract_id.to_string(), wallet.clone());

let call_handler_1 = contract_instance.initialize_counter(42);
let call_handler_2 = contract_instance.get_array([42; 2].to_vec());

let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone());

multi_call_handler
.add_call(call_handler_1)
.add_call(call_handler_2);

let (counter, array): (u64, Vec<u64>) = multi_call_handler.call().await.unwrap().values;

assert_eq!(counter, 42);
assert_eq!(array, [42; 2]);
}

#[tokio::test]
async fn test_multi_call_script_workflow() {
abigen!(
MyContract,
"packages/fuels-abigen-macro/tests/test_projects/contract_test/out/debug/contract_test-abi.json"
);

let wallet = launch_provider_and_get_wallet().await;
let client = &wallet.get_provider().unwrap().client;

let contract_id = Contract::deploy(
"tests/test_projects/contract_test/out/debug/contract_test.bin",
&wallet,
TxParameters::default(),
)
.await
.unwrap();

let contract_instance = MyContract::new(contract_id.to_string(), wallet.clone());

let call_handler_1 = contract_instance.initialize_counter(42);
let call_handler_2 = contract_instance.get_array([42; 2].to_vec());

let mut multi_call_handler = MultiContractCallHandler::new(wallet.clone());

multi_call_handler
.add_call(call_handler_1)
.add_call(call_handler_2);

let script = multi_call_handler.get_script().await;
let receipts = script.call(client).await.unwrap();
let (counter, array): (u64, Vec<u64>) =
multi_call_handler.get_response(receipts).unwrap().values;

assert_eq!(counter, 42);
assert_eq!(array, [42; 2]);
}
162 changes: 145 additions & 17 deletions packages/fuels-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use fuels_core::{
ParamType, ReturnLocation, Selector, Token, Tokenizable,
};
use fuels_signers::{provider::Provider, LocalWallet, Signer};
use std::collections::HashSet;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::path::Path;
Expand Down Expand Up @@ -44,17 +45,37 @@ pub struct CallResponse<D> {
// ANCHOR_END: call_response

impl<D> CallResponse<D> {
pub fn new(value: D, receipts: Vec<Receipt>) -> Self {
// Get all the logs from LogData receipts and put them in the `logs` property
let logs_vec = receipts
/// Get all the logs from LogData receipts
pub fn get_logs(receipts: &[Receipt]) -> Vec<String> {
receipts
.iter()
.filter(|r| matches!(r, Receipt::LogData { .. }))
.map(|r| hex::encode(r.data().unwrap()))
.collect::<Vec<String>>();
.collect::<Vec<String>>()
}

pub fn new(value: D, receipts: Vec<Receipt>) -> Self {
Self {
value,
logs: Self::get_logs(&receipts),
receipts,
}
}
}

#[derive(Debug)]
pub struct MultiCallResponse<D> {
pub values: D,
pub receipts: Vec<Receipt>,
pub logs: Vec<String>,
}

impl<D> MultiCallResponse<D> {
pub fn new(values: D, receipts: Vec<Receipt>) -> Self {
Self {
values,
logs: CallResponse::<D>::get_logs(&receipts),
receipts,
logs: logs_vec,
}
}
}
Expand Down Expand Up @@ -107,14 +128,11 @@ impl Contract {

let compute_custom_input_offset = Contract::should_compute_custom_input_offset(args);

let maturity = 0;

let contract_call = ContractCall {
contract_id,
encoded_selector,
encoded_args,
call_parameters,
maturity,
compute_custom_input_offset,
variable_outputs: None,
external_contracts: None,
Expand Down Expand Up @@ -266,10 +284,9 @@ pub struct ContractCall {
pub encoded_args: Vec<u8>,
pub encoded_selector: Selector,
pub call_parameters: CallParameters,
pub maturity: u64,
pub compute_custom_input_offset: bool,
pub variable_outputs: Option<Vec<Output>>,
pub external_contracts: Option<Vec<ContractId>>,
pub external_contracts: Option<HashSet<ContractId>>,
pub output_param: Option<ParamType>,
}

Expand All @@ -278,8 +295,8 @@ impl ContractCall {
/// decode the values and return them.
pub fn get_decoded_output(
param_type: &ParamType,
mut receipts: Vec<Receipt>,
) -> Result<(Token, Vec<Receipt>), Error> {
receipts: &mut Vec<Receipt>,
) -> Result<Token, Error> {
// Multiple returns are handled as one `Tuple` (which has its own `ParamType`)

let (encoded_value, index) = match param_type.get_return_location() {
Expand All @@ -305,8 +322,9 @@ impl ContractCall {
if let Some(i) = index {
receipts.remove(i);
}

let decoded_value = ABIDecoder::decode_single(param_type, &encoded_value)?;
Ok((decoded_value, receipts))
Ok(decoded_value)
}
}

Expand All @@ -331,7 +349,7 @@ where
/// Note that this is a builder method, i.e. use it as a chain:
/// `my_contract_instance.my_method(...).set_contracts(&[another_contract_id]).call()`.
pub fn set_contracts(mut self, contract_ids: &[ContractId]) -> Self {
self.contract_call.external_contracts = Some(contract_ids.to_vec());
self.contract_call.external_contracts = Some(HashSet::from_iter(contract_ids.to_owned()));
self
}

Expand Down Expand Up @@ -393,8 +411,10 @@ where
self.get_response(receipts)
}

/// Returns the script that executes the contract call
pub async fn get_script(&self) -> Script {
Script::from_contract_call(&self.contract_call, &self.tx_parameters, &self.wallet).await
Script::from_contract_calls(vec![&self.contract_call], &self.tx_parameters, &self.wallet)
.await
}

/// Call a contract's method on the node, in a state-modifying manner.
Expand All @@ -410,17 +430,125 @@ where
}

/// Create a CallResponse from call receipts
pub fn get_response(&self, receipts: Vec<Receipt>) -> Result<CallResponse<D>, Error> {
pub fn get_response(&self, mut receipts: Vec<Receipt>) -> Result<CallResponse<D>, Error> {
match self.contract_call.output_param.as_ref() {
None => Ok(CallResponse::new(D::from_token(Token::Unit)?, receipts)),
Some(param_type) => {
let (token, receipts) = ContractCall::get_decoded_output(param_type, receipts)?;
let token = ContractCall::get_decoded_output(param_type, &mut receipts)?;
Ok(CallResponse::new(D::from_token(token)?, receipts))
}
}
}
}

#[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: Option<Vec<ContractCall>>,
pub tx_parameters: TxParameters,
pub wallet: LocalWallet,
pub fuel_client: FuelClient,
}

impl MultiContractCallHandler {
pub fn new(wallet: LocalWallet) -> Self {
Self {
contract_calls: None,
tx_parameters: TxParameters::default(),
fuel_client: wallet.get_provider().unwrap().client.clone(),
wallet,
}
}

/// Adds a contract call to be bundled in the transaction
/// Note that this is a builder method
pub fn add_call<D: Tokenizable>(&mut self, call_handler: ContractCallHandler<D>) -> &mut Self {
match self.contract_calls.as_mut() {
Some(c) => c.push(call_handler.contract_call),
None => self.contract_calls = Some(vec![call_handler.contract_call]),
}
self
}

/// Clears all added contract calls
/// Note that this is a builder method
pub fn clear_calls<D: Tokenizable>(&mut self) -> &mut Self {
if let Some(calls) = self.contract_calls.as_mut() {
calls.clear()
}
self
}

/// Sets the transaction parameters for a given transaction.
/// Note that this is a builder method
pub fn tx_params(&mut self, params: TxParameters) -> &mut Self {
self.tx_parameters = params;
self
}

/// Returns the script that executes the contract calls
pub async fn get_script(&self) -> Script {
Script::from_contract_calls(
self.contract_calls.as_ref().unwrap().iter().collect(),
MujkicA marked this conversation as resolved.
Show resolved Hide resolved
&self.tx_parameters,
&self.wallet,
)
.await
}

/// Call contract methods on the node, in a state-modifying manner.
pub async fn call<D: Tokenizable + Debug>(&self) -> Result<MultiCallResponse<D>, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may look simple, but this line took us days of debugging and redesign to get right 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the trick with bundling the output tokens of all the separate calls into a Token::Tupple so that the generic D effectively covers multiple types to avoid the need for dynamic dispatch

Self::call_or_simulate(self, false).await
MujkicA marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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.
pub async fn simulate<D: Tokenizable + Debug>(&self) -> Result<MultiCallResponse<D>, Error> {
Self::call_or_simulate(self, true).await
}

#[tracing::instrument]
async fn call_or_simulate<D: Tokenizable + Debug>(
&self,
simulate: bool,
) -> Result<MultiCallResponse<D>, Error> {
let script = self.get_script().await;

let receipts = if simulate {
script.simulate(&self.fuel_client).await.unwrap()
} else {
script.call(&self.fuel_client).await.unwrap()
};
tracing::debug!(target: "receipts", "{:?}", receipts);

self.get_response(receipts)
}

/// Create a MultiCallResponse from call receipts
pub fn get_response<D: Tokenizable + Debug>(
&self,
mut receipts: Vec<Receipt>,
) -> Result<MultiCallResponse<D>, Error> {
let mut final_tokens = vec![];

for call in self.contract_calls.as_ref().unwrap().iter() {
// We only aggregate the tokens if the contract call has an output parameter
if let Some(param_type) = call.output_param.as_ref() {
let decoded = ContractCall::get_decoded_output(param_type, &mut receipts)?;

final_tokens.push(decoded.clone());
}
}

let tokens_as_tuple = Token::Tuple(final_tokens);
let response = MultiCallResponse::<D>::new(D::from_token(tokens_as_tuple)?, receipts);

Ok(response)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading