Skip to content

Commit

Permalink
Use estimation to cover transaction fees (#556)
Browse files Browse the repository at this point in the history
Adds the method "add_fee_coins" which edits a transactions inputs and outputs such that the transaction fee can be covered based on an estimation. The method is called before signing and sending a transaction for deployment, asset transfers, predicates or contract calls.
  • Loading branch information
MujkicA authored Sep 8, 2022
1 parent 1cf6a8e commit 0c92fd4
Show file tree
Hide file tree
Showing 11 changed files with 545 additions and 237 deletions.
1 change: 1 addition & 0 deletions docs/src/calling-contracts/cost-estimation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Below are examples that show how to get the estimated transaction cost from sing

The transaction cost estimation can be used to set the gas limit for an actual call, or to show the user the estimated cost.

> **Note:** whenever you perform an action that starts a transaction (contract deployment, contract call, asset transfer), the SDK will automatically estimate the fee behind the scenes and prepare the transaction accordingly. A side-effect of this is that transactions require the wallet to own at least an amount of 1 of the base asset, even if the gas cost is set to 0 via `TxParameters`.
5 changes: 3 additions & 2 deletions docs/src/calling-contracts/tx-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ Transaction parameters are:

1. Gas price;
2. Gas limit;
3. Byte price;
4. Maturity.
3. Maturity.

You can configure these parameters by creating an instance of [`TxParameters`](https://github.com/FuelLabs/fuels-rs/blob/adf81bd451d7637ce0976363bd7784408430031a/packages/fuels-contract/src/parameters.rs#L7) and passing it to a chain method called `tx_params`:

Expand All @@ -24,3 +23,5 @@ This way:
```rust,ignore
{{#include ../../../examples/contracts/src/lib.rs:tx_parameters_default}}
```

As you might have noticed already, `TxParameters` can also be specified when deploying contracts or transfering assets by passing it to the respective methods.
6 changes: 4 additions & 2 deletions docs/src/cookbook/transfer-all-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Lets quickly go over the setup:

We prepare two wallets with randomized addresses. Next, we want one of our wallets to have some random assets, so we set them up with `setup_multiple_assets_coins()`. Having created the coins, we can start a provider and assign it to the previously created wallets.

Transactions require us to define input and output coins. Let's assume we do not know the assets owned by `wallet_1`. We retrieve its balances, i.e. tuples consisting of a string representing the asset id and the respective amount. This lets us use the helpers `get_asset_inputs_for_amount()`, `get_asset_outputs_for_amount()` to create the appropriate inputs and outputs:
Transactions require us to define input and output coins. Let's assume we do not know the assets owned by `wallet_1`. We retrieve its balances, i.e. tuples consisting of a string representing the asset id and the respective amount. This lets us use the helpers `get_asset_inputs_for_amount()`, `get_asset_outputs_for_amount()` to create the appropriate inputs and outputs.

For the sake of simplicity, we avoid transferring the base asset so we don't have to worry about transaction fees:

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

All that is left is to build the transaction with the provider's helper `build_transfer_transaction()`, have `wallet_1` sign it, and we can send it. Checking that the wallets balances are empty lets us confirm that we have indeed transferred all assets:
All that is left is to build the transaction with the helper `build_transfer_transaction()`, have `wallet_1` sign it, and we can send it. We confirm this by checking the number of balances present in the receiving wallet and their amount:

```rust,ignore
{{#include ../../../examples/cookbook/src/lib.rs:transfer_multiple_transaction}}
Expand Down
16 changes: 12 additions & 4 deletions examples/cookbook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ mod tests {

const NUM_ASSETS: u64 = 5;
const AMOUNT: u64 = 100_000;
const NUM_COINS: u64 = 10;
const NUM_COINS: u64 = 1;
let (coins, _) =
setup_multiple_assets_coins(wallet_1.address(), NUM_ASSETS, NUM_COINS, AMOUNT);

Expand All @@ -140,6 +140,10 @@ mod tests {
for (id_string, amount) in balances {
let id = AssetId::from_str(&id_string).unwrap();

// leave the base asset to cover transaction fees
if id == BASE_ASSET_ID {
continue;
}
let input = wallet_1.get_asset_inputs_for_amount(id, amount, 0).await?;
inputs.extend(input);

Expand All @@ -149,13 +153,17 @@ mod tests {
// ANCHOR_END: transfer_multiple_inout

// ANCHOR: transfer_multiple_transaction
let mut tx = provider.build_transfer_tx(&inputs, &outputs, TxParameters::default());
let mut tx = Wallet::build_transfer_tx(&inputs, &outputs, TxParameters::default());
wallet_1.sign_transaction(&mut tx).await?;

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

let balances = wallet_1.get_balances().await?;
assert!(balances.is_empty());
let balances = wallet_2.get_balances().await?;

assert_eq!(balances.len(), (NUM_ASSETS - 1) as usize);
for (_, balance) in balances {
assert_eq!(balance, AMOUNT);
}
// ANCHOR_END: transfer_multiple_transaction

// ANCHOR_END: transfer_multiple
Expand Down
10 changes: 6 additions & 4 deletions examples/predicates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mod tests {
let mut wallet = WalletUnlocked::new_from_private_key(secret_key1, None);
let mut wallet2 = WalletUnlocked::new_from_private_key(secret_key2, None);
let mut wallet3 = WalletUnlocked::new_from_private_key(secret_key3, None);
let mut receiver = WalletUnlocked::new_random(None);
let receiver = WalletUnlocked::new_random(None);
// ANCHOR_END: predicate_wallets

// ANCHOR: predicate_coins
Expand All @@ -48,7 +48,7 @@ mod tests {
)
.await;

[&mut wallet, &mut wallet2, &mut wallet3, &mut receiver]
[&mut wallet, &mut wallet2, &mut wallet3]
.iter_mut()
.for_each(|wallet| wallet.set_provider(provider.clone()));
// ANCHOR_END: predicate_coins
Expand Down Expand Up @@ -93,13 +93,15 @@ mod tests {

// ANCHOR: predicate_spend
let predicate_data = signatures.into_iter().flatten().collect();
receiver
.receive_from_predicate(
wallet
.spend_predicate(
predicate_address,
predicate_code,
amount_to_predicate,
asset_id,
receiver.address(),
Some(predicate_data),
TxParameters::default(),
)
.await?;

Expand Down
35 changes: 9 additions & 26 deletions packages/fuels-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use fuels_core::abi_encoder::ABIEncoder;
use fuels_core::parameters::StorageConfiguration;
use fuels_core::tx::{Bytes32, ContractId};
use fuels_core::{
constants::{BASE_ASSET_ID, DEFAULT_SPENDABLE_COIN_AMOUNT},
parameters::{CallParameters, TxParameters},
Selector, Token, Tokenizable,
};
Expand Down Expand Up @@ -237,13 +236,17 @@ impl Contract {
params: TxParameters,
) -> Result<Bech32ContractId, Error> {
let (mut tx, contract_id) =
Self::contract_deployment_transaction(compiled_contract, wallet, params).await?;
Self::contract_deployment_transaction(compiled_contract, params).await?;

let provider = wallet.get_provider()?;
// The first witness is the bytecode we're deploying.
// The signature will be appended at position 1 of
// the witness list
wallet.add_fee_coins(&mut tx, 0, 1).await?;
wallet.sign_transaction(&mut tx).await?;

let provider = wallet.get_provider()?;
let chain_info = provider.chain_info().await?;

wallet.sign_transaction(&mut tx).await?;
tx.validate_without_signature(
chain_info.latest_block.height.0,
&chain_info.consensus_parameters.into(),
Expand Down Expand Up @@ -312,7 +315,6 @@ impl Contract {
/// Crafts a transaction used to deploy a contract
pub async fn contract_deployment_transaction(
compiled_contract: &CompiledContract,
wallet: &WalletUnlocked,
params: TxParameters,
) -> Result<(Transaction, Bech32ContractId), Error> {
let bytecode_witness_index = 0;
Expand All @@ -321,26 +323,7 @@ impl Contract {

let (contract_id, state_root) = Self::compute_contract_id_and_state_root(compiled_contract);

let outputs: Vec<Output> = vec![
Output::contract_created(contract_id, state_root),
// Note that the change will be computed by the node.
// Here we only have to tell the node who will own the change and its asset ID.
// For now we use the BASE_ASSET_ID constant
Output::change(wallet.address().into(), 0, BASE_ASSET_ID),
];

// The first witness is the bytecode we're deploying.
// So, the signature will be appended at position 1 of
// the witness list.
let coin_witness_index = 1;

let inputs = wallet
.get_asset_inputs_for_amount(
AssetId::default(),
DEFAULT_SPENDABLE_COIN_AMOUNT,
coin_witness_index,
)
.await?;
let outputs = vec![Output::contract_created(contract_id, state_root)];

let tx = Transaction::create(
params.gas_price,
Expand All @@ -349,7 +332,7 @@ impl Contract {
bytecode_witness_index,
compiled_contract.salt,
storage_slots,
inputs,
vec![],
outputs,
witnesses,
);
Expand Down
39 changes: 14 additions & 25 deletions packages/fuels-contract/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use fuel_gql_client::fuel_vm::{consts::REG_ONE, prelude::Opcode};
use itertools::{chain, Itertools};

use fuel_gql_client::client::schema::coin::Coin;
use fuels_core::constants::DEFAULT_SPENDABLE_COIN_AMOUNT;
use fuels_core::parameters::TxParameters;
use fuels_signers::provider::Provider;
use fuels_signers::{Signer, WalletUnlocked};
Expand Down Expand Up @@ -61,7 +60,8 @@ impl Script {

let script = Self::get_instructions(calls, call_param_offsets);

let spendable_coins = Self::get_spendable_coins(wallet, calls).await?;
let required_asset_amounts = Self::calculate_required_asset_amounts(calls);
let spendable_coins = Self::get_spendable_coins(wallet, &required_asset_amounts).await?;

let (inputs, outputs) =
Self::get_transaction_inputs_outputs(calls, wallet.address(), spendable_coins);
Expand All @@ -76,6 +76,14 @@ impl Script {
outputs,
vec![],
);

let base_asset_amount = required_asset_amounts
.iter()
.find(|(asset_id, _)| *asset_id == AssetId::default());
match base_asset_amount {
Some((_, base_amount)) => wallet.add_fee_coins(&mut tx, *base_amount, 0).await?,
None => wallet.add_fee_coins(&mut tx, 0, 0).await?,
}
wallet.sign_transaction(&mut tx).await.unwrap();

Ok(Script::new(tx))
Expand All @@ -85,10 +93,10 @@ impl Script {
/// proceeds to request spendable coins from `wallet` to cover that cost.
async fn get_spendable_coins(
wallet: &WalletUnlocked,
calls: &[ContractCall],
required_asset_amounts: &[(AssetId, u64)],
) -> Result<Vec<Coin>, Error> {
stream::iter(Self::calculate_required_asset_amounts(calls))
.map(|(asset_id, amount)| wallet.get_spendable_coins(asset_id, amount))
stream::iter(required_asset_amounts)
.map(|(asset_id, amount)| wallet.get_spendable_coins(*asset_id, *amount))
.buffer_unordered(10)
.collect::<Vec<_>>()
.await
Expand All @@ -105,14 +113,9 @@ impl Script {
fn extract_required_amounts_per_asset_id(
calls: &[ContractCall],
) -> impl Iterator<Item = (AssetId, u64)> + '_ {
// TODO what to do about the default asset?
calls
.iter()
.map(|call| (call.call_parameters.asset_id, call.call_parameters.amount))
.chain(iter::once((
AssetId::default(),
DEFAULT_SPENDABLE_COIN_AMOUNT,
)))
}

fn sum_up_amounts_for_each_asset_id(
Expand Down Expand Up @@ -381,7 +384,6 @@ impl Script {
mod test {
use super::*;
use fuel_gql_client::client::schema::coin::CoinStatus;
use fuels_core::constants::BASE_ASSET_ID;
use fuels_core::parameters::CallParameters;
use fuels_types::bech32::Bech32ContractId;
use rand::Rng;
Expand Down Expand Up @@ -727,15 +729,6 @@ mod test {
assert_eq!(expected_outputs, actual_variable_outputs);
}

#[test]
fn will_require_base_asset_even_if_not_explicitly_asked_for() {
let asset_id_amounts = Script::calculate_required_asset_amounts(&[]);
assert_eq!(
asset_id_amounts,
[(BASE_ASSET_ID, DEFAULT_SPENDABLE_COIN_AMOUNT)]
)
}

#[test]
fn will_collate_same_asset_ids() {
let amounts = [100, 200];
Expand All @@ -751,11 +744,7 @@ mod test {

let asset_id_amounts = Script::calculate_required_asset_amounts(&calls);

let expected_asset_id_amounts = [
(BASE_ASSET_ID, DEFAULT_SPENDABLE_COIN_AMOUNT),
(asset_id, amounts.iter().sum()),
]
.into();
let expected_asset_id_amounts = [(asset_id, amounts.iter().sum())].into();

assert_eq!(
asset_id_amounts.into_iter().collect::<HashSet<_>>(),
Expand Down
2 changes: 0 additions & 2 deletions packages/fuels-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ pub const DEFAULT_MATURITY: u64 = 0;
// ANCHOR: default_call_parameters
// Limit for the actual contract call
pub const DEFAULT_FORWARDED_GAS: u64 = 1_000_000;
// Lower limit when querying spendable UTXOs
pub const DEFAULT_SPENDABLE_COIN_AMOUNT: u64 = 1_000_000;
// Bytes representation of the asset ID of the "base" asset used for gas fees.
pub const BASE_ASSET_ID: AssetId = AssetId::new([0u8; 32]);
// ANCHOR_END: default_call_parameters
Expand Down
Loading

0 comments on commit 0c92fd4

Please sign in to comment.