Skip to content

Commit

Permalink
feat: get TransactionBuilder from script and contract calls (FuelLa…
Browse files Browse the repository at this point in the history
…bs#1220)

- `TransationBuilders` can be extracted from `contract` and
`script`calls.
- `FuelResponse` can be made from a `TxStatus`. This is useful to decode
return values and logs.
- New documentation regarding `TransactionBuilders` and custom
transactions

Co-authored-by: MujkicA <[email protected]>
  • Loading branch information
hal3e and MujkicA authored Nov 29, 2023
1 parent e9a7767 commit ce7c0b4
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 41 deletions.
3 changes: 3 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 43,9 @@
- [Running scripts](./running-scripts.md)
- [Predicates](./predicates/index.md)
- [Signatures example](./predicates/send-spend-predicate.md)
- [Custom transactions](./custom-transactions/index.md)
- [Transaction builders](./custom-transactions/transaction-builders.md)
- [Custom contract and script calls](./custom-transactions/custom-calls.md)
- [Types](./types/index.md)
- [Bytes32](./types/bytes32.md)
- [Address](./types/address.md)
Expand Down
15 changes: 15 additions & 0 deletions docs/src/custom-transactions/custom-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 1,15 @@
# 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.

## Custom contract call

```rust,ignore
{{#include ../../../examples/contracts/src/lib.rs:contract_call_tb}}
```

## Custom script call

```rust,ignore
{{#include ../../../packages/fuels/tests/scripts.rs:script_call_tb}}
```
3 changes: 3 additions & 0 deletions docs/src/custom-transactions/index.md
Original file line number Diff line number Diff line change
@@ -0,0 1,3 @@
# Custom transactions

Until now, we have used helpers to create transactions, send them with a provider, and parse the results. However, sometimes we must make custom transactions with specific inputs, outputs, witnesses, etc. In the next chapter, we will show how to use the Rust SDKs transaction builders to accomplish this.
85 changes: 85 additions & 0 deletions docs/src/custom-transactions/transaction-builders.md
Original file line number Diff line number Diff line change
@@ -0,0 1,85 @@
# Transaction Builders

The Rust SDK simplifies the creation of **Create** and **Script** transactions through two handy builder structs `CreateTransactionBuilder`, `ScriptTransactionBuilder`, and the `TransactionBuilder` trait.

Calling `build()` on a builder will result in the corresponding `CreateTransaction` or `ScriptTransaction` that can be submitted to the network.

## Role of the transaction builders

> **Note** This section contains additional information about the inner workings of the builders. If you are just interested in how to use them, you can skip to the next section.
The builders take on the heavy lifting behind the scenes, offering two standout advantages: handling predicate data offsets and managing witness indexing.

When your transaction involves predicates with dynamic data as inputs, like vectors, the dynamic data contains a pointer pointing to the beginning of the raw data. This pointer's validity hinges on the order of transaction inputs, and any shifting could render it invalid. However, the transaction builders conveniently postpone the resolution of these pointers until you finalize the build process.

Similarly, adding signatures for signed coins requires the signed coin input to hold an index corresponding to the signature in the witnesses array. These indexes can also become invalid if the witness order changes. The Rust SDK again defers the resolution of these indexes until the transaction is finalized. It handles the assignment of correct index witnesses behind the scenes, sparing you the hassle of dealing with indexing intricacies during input definition.

Another added benefit of the builder pattern is that it guards against changes once the tx is finalized. The transactions resultign from a builder don't permit any changes to the struct that could cause the transaction ID to be modified. This eliminates the headache of calculating and storing a transaction ID for future use, only to accidentally modify the transaction later, resulting in a different transaction ID.

## Creating a custom transaction

Here is an example outlining some of the features of the transaction builders.

In this scenario, we have a predicate that holds some bridged asset with ID **bridged_asset_id**. It releases it's locked assets if the transaction sends **ask_amount** of the base asset to the **receiver** address:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_receiver}}
```

Our goal is to create a transaction that will use our hot wallet to transfer the **ask_amount** to the **receiver** and then send the unlocked predicate assets to a second wallet that acts as our cold storage.

Let's start by instantiating a builder. Since we don't plan to deploy a contract, the `ScriptTransactionBuilder` is the appropriate choice:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx}}
```

Next, we need to define transaction inputs of the base asset that sum up to **ask_amount**. We also need transaction outputs that will assign those assets to the predicate address and thereby unlock it. The methods `get_asset_inputs_for_amount` and `get_asset_outputs_for_amount` can help with that. We need to specify the asset ID, the target amount, and the target address:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_io_base}}
```

Let's repeat the same process but this time for transferring the assets held by the predicate to our cold storage:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_io_other}}
```

We combine all of the inputs and outputs and set them on the builder:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_io}}
```

As we have used coins that require a signature, we sign the transaction builder with:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_sign}}
```

> **Note** The signature is not created until the transaction is finalized with `build(&provider)`
We need to do one more thing before we stop thinking about transaction inputs. Executing the transaction also incurs a fee that is paid with the base asset. Our base asset inputs need to be large enough so that the total amount covers the transaction fee and any other operations we are doing. The Account trait lets us use `adjust_for_fee()` for adjusting the transaction inputs if needed to cover the fee. The second argument to `adjust_for_fee()` is the total amount of the base asset that we expect our tx to spend regardless of fees. In our case, this is the **ask_amount** we are transferring to the predicate.

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_adjust}}
```

We can also define transaction policies. For example, we can limit the gas price by doing the following:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_policies}}
```

Our builder needs a signature from the hot wallet to unlock its coins before we call `build()` and submit the resulting transaction through the provider:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_build}}
```

Finally, we verify the transaction succeeded and that the cold storage indeed holds the bridged asset now:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:custom_tx_verify}}
```
43 changes: 43 additions & 0 deletions examples/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,4 870,47 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn contract_custom_call() -> Result<()> {
use fuels::prelude::*;

setup_program_test!(
Wallets("wallet"),
Abigen(Contract(
name = "TestContract",
project = "packages/fuels/tests/contracts/contract_test"
)),
Deploy(
name = "contract_instance",
contract = "TestContract",
wallet = "wallet"
),
);
let provider = wallet.try_provider()?;

let counter = 42;

// ANCHOR: contract_call_tb
let call_handler = contract_instance.methods().initialize_counter(counter);

let mut tb = call_handler.transaction_builder().await?;

// customize the builder...

wallet.adjust_for_fee(&mut tb, 0).await?;
wallet.sign_transaction(&mut tb);

let tx = tb.build(provider).await?;

let tx_id = provider.send_transaction(tx).await?;
let tx_status = provider.tx_status(&tx_id).await?;

let response = call_handler.get_response_from(tx_status)?;

assert_eq!(counter, response.value);
// ANCHOR_END: contract_call_tb

Ok(())
}
}
117 changes: 115 additions & 2 deletions examples/cookbook/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 1,22 @@
#[cfg(test)]
mod tests {
use std::str::FromStr;

use fuels::{
accounts::{
predicate::Predicate, wallet::WalletUnlocked, Account, Signer, ViewOnlyAccount,
},
core::constants::BASE_ASSET_ID,
prelude::Result,
test_helpers::{setup_single_asset_coins, setup_test_provider},
types::{
transaction_builders::{BuildableTransaction, ScriptTransactionBuilder},
Bits256,
bech32::Bech32Address,
transaction::TxPolicies,
transaction_builders::{
BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
},
tx_status::TxStatus,
AssetId,
},
};

Expand All @@ -13,6 25,7 @@ mod tests {
use fuels::{
prelude::*,
test_helpers::{AssetConfig, WalletsConfig},
types::Bits256,
};

// ANCHOR: liquidity_abigen
Expand Down Expand Up @@ -217,4 230,104 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn custom_transaction() -> Result<()> {
let mut hot_wallet = WalletUnlocked::new_random(None);
let mut cold_wallet = WalletUnlocked::new_random(None);

let code_path = "../../packages/fuels/tests/predicates/swap/out/debug/swap.bin";
let mut predicate = Predicate::load_from(code_path)?;

let num_coins = 5;
let amount = 1000;
let bridged_asset_id = AssetId::from([1u8; 32]);
let base_coins =
setup_single_asset_coins(hot_wallet.address(), BASE_ASSET_ID, num_coins, amount);
let other_coins =
setup_single_asset_coins(predicate.address(), bridged_asset_id, num_coins, amount);

let provider = setup_test_provider(
base_coins.into_iter().chain(other_coins).collect(),
vec![],
None,
None,
)
.await?;

hot_wallet.set_provider(provider.clone());
cold_wallet.set_provider(provider.clone());
predicate.set_provider(provider.clone());

// ANCHOR: custom_tx_receiver
let ask_amount = 100;
let locked_amount = 500;
let bridged_asset_id = AssetId::from([1u8; 32]);
let receiver = Bech32Address::from_str(
"fuel1p8qt95dysmzrn2rmewntg6n6rg3l8ztueqafg5s6jmd9cgautrdslwdqdw",
)?;
// ANCHOR_END: custom_tx_receiver

// ANCHOR: custom_tx
let network_info = provider.network_info().await?;
let tb = ScriptTransactionBuilder::new(network_info);
// ANCHOR_END: custom_tx

// ANCHOR: custom_tx_io_base
let base_inputs = hot_wallet
.get_asset_inputs_for_amount(BASE_ASSET_ID, ask_amount)
.await?;
let base_outputs =
hot_wallet.get_asset_outputs_for_amount(&receiver, BASE_ASSET_ID, ask_amount);
// ANCHOR_END: custom_tx_io_base

// ANCHOR: custom_tx_io_other
let other_asset_inputs = predicate
.get_asset_inputs_for_amount(bridged_asset_id, locked_amount)
.await?;
let other_asset_outputs =
predicate.get_asset_outputs_for_amount(cold_wallet.address(), bridged_asset_id, 500);
// ANCHOR_END: custom_tx_io_other

// ANCHOR: custom_tx_io
let inputs = base_inputs
.into_iter()
.chain(other_asset_inputs.into_iter())
.collect();
let outputs = base_outputs
.into_iter()
.chain(other_asset_outputs.into_iter())
.collect();

let mut tb = tb.with_inputs(inputs).with_outputs(outputs);
// ANCHOR_END: custom_tx_io

// ANCHOR: custom_tx_sign
hot_wallet.sign_transaction(&mut tb);
// ANCHOR_END: custom_tx_sign

// ANCHOR: custom_tx_adjust
hot_wallet.adjust_for_fee(&mut tb, 100).await?;
// ANCHOR_END: custom_tx_adjust

// ANCHOR: custom_tx_policies
let tx_policies = TxPolicies::default().with_gas_price(1);
let tb = tb.with_tx_policies(tx_policies);
// ANCHOR_END: custom_tx_policies

// ANCHOR: custom_tx_build
let tx = tb.build(&provider).await?;
let tx_id = provider.send_transaction(tx).await?;
// ANCHOR_END: custom_tx_build

// ANCHOR: custom_tx_verify
let status = provider.tx_status(&tx_id).await?;
assert!(matches!(status, TxStatus::Success { .. }));

let balance = cold_wallet.get_asset_balance(&bridged_asset_id).await?;
assert_eq!(balance, locked_amount);
// ANCHOR_END: custom_tx_verify

Ok(())
}
}
47 changes: 29 additions & 18 deletions packages/fuels-programs/src/call_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 14,9 @@ use fuels_core::{
input::Input,
param_types::ParamType,
transaction::{ScriptTransaction, TxPolicies},
transaction_builders::{BuildableTransaction, ScriptTransactionBuilder},
transaction_builders::{
BuildableTransaction, ScriptTransactionBuilder, TransactionBuilder,
},
},
};
use itertools::{chain, Itertools};
Expand Down Expand Up @@ -96,30 98,24 @@ pub trait TxDependencyExtension: Sized {
}
}

/// 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.
pub(crate) async fn build_tx_from_contract_calls(
/// Creates a [`ScriptTransactionBuilder`] from contract calls.
pub(crate) async fn transaction_builder_from_contract_calls(
calls: &[ContractCall],
tx_policies: TxPolicies,
account: &impl Account,
) -> Result<ScriptTransaction> {
let provider = account.try_provider()?;
let consensus_parameters = provider.consensus_parameters();

) -> Result<ScriptTransactionBuilder> {
let calls_instructions_len = compute_calls_instructions_len(calls)?;
let consensus_parameters = account.try_provider()?.consensus_parameters();
let data_offset = call_script_data_offset(consensus_parameters, calls_instructions_len);

let (script_data, call_param_offsets) =
build_script_data_from_contract_calls(calls, data_offset);

let script = get_instructions(calls, call_param_offsets)?;

let required_asset_amounts = calculate_required_asset_amounts(calls);

let mut asset_inputs = vec![];

// Find the spendable resources required for those calls
let mut asset_inputs = vec![];
for (asset_id, amount) in &required_asset_amounts {
let resources = account
.get_asset_inputs_for_amount(*asset_id, *amount)
Expand All @@ -129,11 125,26 @@ pub(crate) async fn build_tx_from_contract_calls(

let (inputs, outputs) = get_transaction_inputs_outputs(calls, asset_inputs, account);

let network_info = provider.network_info().await?;
let mut tb =
ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_policies, network_info)
.with_script(script)
.with_script_data(script_data.clone());
let network_info = account.try_provider()?.network_info().await?;
Ok(ScriptTransactionBuilder::new(network_info)
.with_tx_policies(tx_policies)
.with_script(script)
.with_script_data(script_data.clone())
.with_inputs(inputs)
.with_outputs(outputs))
}

/// 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.
pub(crate) async fn build_tx_from_contract_calls(
calls: &[ContractCall],
tx_policies: TxPolicies,
account: &impl Account,
) -> Result<ScriptTransaction> {
let mut tb = transaction_builder_from_contract_calls(calls, tx_policies, account).await?;

let required_asset_amounts = calculate_required_asset_amounts(calls);

let used_base_amount = required_asset_amounts
.iter()
Expand All @@ -143,7 154,7 @@ pub(crate) async fn build_tx_from_contract_calls(
account.add_witnessses(&mut tb);
account.adjust_for_fee(&mut tb, used_base_amount).await?;

tb.build(provider).await
tb.build(account.try_provider()?).await
}

/// Compute the length of the calling scripts for the two types of contract calls: those that return
Expand Down
Loading

0 comments on commit ce7c0b4

Please sign in to comment.