Skip to content

feat!: add support for assemble tx #1634

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

Open
wants to merge 52 commits into
base: master
Choose a base branch
from
Open

feat!: add support for assemble tx #1634

wants to merge 52 commits into from

Conversation

hal3e
Copy link
Contributor

@hal3e hal3e commented Apr 2, 2025

closes: #1641

Release notes

In this release, we:

  • Added support for assemble transactions

Summary

We added support for assemble transactions and made it the default build strategy for all CallHandlers and transfers.

Breaking Changes

  • Contract dependencies → automatic & log decoding
    The old with_contracts/with_contract_ids APIs are gone—contract inputs are now set using assemble tx, and if you need to decode logs/reverts you pass in the external contract’s decoder:

    // old
    my_contract
      .methods()
      .foo(arg)
      .with_contracts(&[&external_instance])
      .call()
      .await?;
    
    // new
    my_contract
      .methods()
      .foo(arg)
      .add_log_decoder(external_instance.log_decoder())
      .call()
      .await?;
  • Dependency & output estimation → fully automatic
    You no longer need to chain on with_variable_output_policy(...) or call determine_missing_contracts(): the SDK will auto-estimate both missing contracts and variable outputs.

    // old
    call_handler
      .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
      .determine_missing_contracts()
      .call()
      .await?;
    
    // new
    call_handler
      .call()
      .await?;
  • Script gas limit & TxPolicies → dedicated handler method
    TxPolicies does not hold the script_gas_limit anymore. Use .with_script_gas_limit(n) directly on your call handler.

    // old
    let policies = TxPolicies::default().with_script_gas_limit(1_000_000);
    my_contract
      .methods()
      .bar()
      .with_tx_policies(policies)
      .call()
      .await?;
    
    // new
    my_contract
      .methods()
      .bar()
      .with_script_gas_limit(1_000_000)
      .call()
      .await?;
  • Transaction submission → submit
    send_transaction is renamed to submit

    // old
    let tx_id = provider.send_transaction(tx).await?;
    let status = provider.tx_status(&tx_id).await?;
    
    // new
    let tx_id = provider.submit(tx).await?;
    let status = provider.tx_status(&tx_id).await?;
  • Manual fee/inputs adjustment → “assemble” strategy
    You do not have to use wallet.adjust_for_fee(&mut tb, …).await? if you use the new assemble strategy.

    // old
    wallet.adjust_for_fee(&mut tb, used_base_amount).await?;
    let tx = tb.build(&provider).await?;
    
    // new
    let tx = tb.build(&provider).await?; // using ScriptBuildStrategy::AssembleTx under the hood
  • Removed the ContractDependency trait

Checklist

  • All changes are covered by tests (or not applicable)
  • All changes are documented (or not applicable)
  • I reviewed the entire PR myself (preferably, on GH UI)
  • I described all Breaking Changes (or there's none)

@hal3e hal3e self-assigned this Apr 2, 2025
@hal3e hal3e changed the title feat: support for assemble tx feat!: add support for assemble tx Apr 23, 2025
@segfault-magnet
Copy link
Contributor

It seems that we're not passing the sdk cached coins to the exclude filter of the assemble tx, so the node doesn't know that we already plan to use a UTXO and that it shouldn't be a candidate when filling required balances.

@hal3e
Copy link
Contributor Author

hal3e commented May 6, 2025

It seems that we're not passing the sdk cached coins to the exclude filter of the assemble tx, so the node doesn't know that we already plan to use a UTXO and that it shouldn't be a candidate when filling required balances.

d1a83ec

AurelienFT
AurelienFT previously approved these changes May 7, 2025
Copy link
Contributor

@AurelienFT AurelienFT left a comment

Choose a reason for hiding this comment

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

LGTM

Comment on lines +417 to +425
let exclude = match exclude {
Some((mut utxos, mut nonces)) => {
utxos.extend(cache_utxos);
nonces.extend(cache_nonces);

Some((utxos, nonces))
}
None => Some((cache_utxos, cache_nonces)),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

If the provider is used concurrently then the cache can have entries unrelated to the owners mentioned here. Technically the minimum we can send here is just the cached utxos and nonces of the owners mentioned in require balances.

If the user clones the provider a lot (like they should do if they want to reuse the underlying connection pool) and if they use it to send concurrent transactions then we could end up having this request be bigger than it needs to be.

Not sure that's necessarily a huge deal, but just pointing it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hal3e hal3e requested a review from segfault-magnet May 12, 2025 13:51
If your contract method is calling other contracts you will have to add the appropriate `Inputs` and `Outputs` to your transaction. For your convenience, the `CallHandler` provides methods that prepare those inputs and outputs for you. You have two methods that you can use: `with_contracts(&[&contract_instance, ...])` and `with_contract_ids(&[&contract_id, ...])`.

`with_contracts(&[&contract_instance, ...])` requires contract instances that were created using the `abigen` macro. When setting the external contracts with this method, logs and require revert errors originating from the external contract can be propagated and decoded by the calling contract.
If your contract method is calling other contracts you will have to add the appropriate `Inputs` and `Outputs` to your transaction. For your convenience, the `CallHandler` will fill in all missing `Inputs`/`Outputs` before sending the transaction.
Copy link
Member

Choose a reason for hiding this comment

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

I'm not super familiar with the bounds of this "fill in all missing Inputs/Outputs". How much of this is communicated back to the sender before they click "send"?

@@ -0,0 +1,35 @@
# Assemble Transactions

Assemble transactions makes it possible to create a minimal `TransactionBuilder` and let the fuel node fill in the missing details. The node will add missing inputs, outputs, set transactions limits etc. Below is an example how the assemble strategy can be used to create a transfer.
Copy link
Member

Choose a reason for hiding this comment

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

Along the lines of previous question, when is the tx signed?

Copy link
Member

Choose a reason for hiding this comment

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

I see in the snippet code:

tb.add_signer(wallet.signer().clone())?;

I assume this happens on the client side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The transaction is signed at the end after receiving the assembled tx. This happens in the TransactionBuilder's build method.

@@ -291,21 +288,20 @@ async fn test_contract_call_fee_estimation() -> Result<()> {
),
);

let gas_limit = 800;
let gas_limit = 3800;
Copy link
Member

Choose a reason for hiding this comment

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

Why did this need to increase so much? I assume it's somewhat arbitrary, but I wouldn't expect assemble-tx to significantly increase costs.

Copy link
Contributor Author

@hal3e hal3e May 28, 2025

Choose a reason for hiding this comment

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

I had to increase the scipt gas limit here as the old value would have no effect. As it is lower then the script gas estimated by assemble tx it would have been overwritten by the higher value.

@@ -390,13 +390,13 @@ async fn multi_call_contract_with_contract_logs() -> Result<()> {

let call_handler_1 = contract_caller_instance
.methods()
.logs_from_external_contract(contract_instance.id())
.with_contracts(&[&contract_instance]);
.logs_from_external_contract(contract_instance.contract_id())
Copy link
Member

Choose a reason for hiding this comment

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

(With my limited context) I think I kinda prefer the id() syntax more. What was the thought process of changing that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The id() method was part of a removed ContractDependency trait. I wanted the method to be clearer on what id it returns :). we can, of course, change it to id if it is clear enough.

@@ -835,6 +810,43 @@ async fn transactions_with_the_same_utxo() -> Result<()> {
Ok(())
}

#[cfg(feature = "coin-cache")]
#[tokio::test]
async fn transfers_at_same_time_with_cache() -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we could name it something like

Suggested change
async fn transfers_at_same_time_with_cache() -> Result<()> {
async fn transfer__assemble_tx_can_remove_used_coins() -> Result<()> {

Also can we add given/when/then?

@@ -1453,3 +1465,51 @@ async fn is_account_query_test() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn assemble_tx_transfer() -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

I guess this test exists for the purpose of documentation. I think we should mark it as such, maybe in the name? What do you think?

@@ -123,6 +123,27 @@ async fn adjust_fee_empty_transaction() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn adjust_for_fee_error() -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add more clarity here. G/W/T and a more informative name?

Ok(script_transaction)
}

async fn assemble_tx(
Copy link
Member

Choose a reason for hiding this comment

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

Do we have tests for each implementation of assemble_tx, for all the tx types?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for assemble tx
6 participants