Skip to content

Commit

Permalink
feat: fee estimation for forc-deploy and point --testnet to devnet (#…
Browse files Browse the repository at this point in the history
…5990)

## Description

Estimation is hard to test as it is dependent on the network
configuration but this is nearly the same thing we have been doing for
scripts.

1. `--testnet` points to devnet.
2. For the new `max_fee` requirement, adds an estimation phase with dry
runing the transaction
3. Various consts are pointed to relevant devnet endpoints.

Most visible outcome is that `forc-deploy --testnet` can deploy to
devnet without any other flags etc.

---------

Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com>
  • Loading branch information
kayagokalp and sdankel committed May 13, 2024
1 parent 675135f commit b0af334
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 30 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions forc-plugins/forc-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ forc-util = { version = "0.57.0", path = "../../forc-util" }
forc-wallet = { workspace = true }
fuel-abi-types = { workspace = true }
fuel-core-client = { workspace = true, features = ["subscriptions"] }
fuel-core-types = { workspace = true }
fuel-crypto = { workspace = true }
fuel-tx = { workspace = true, features = ["test-helpers"] }
fuel-vm = { workspace = true }
Expand Down
7 changes: 6 additions & 1 deletion forc-plugins/forc-client/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ pub const BETA_2_ENDPOINT_URL: &str = "https://node-beta-2.fuel.network";
pub const BETA_3_ENDPOINT_URL: &str = "https://beta-3.fuel.network";
pub const BETA_4_ENDPOINT_URL: &str = "https://beta-4.fuel.network";
pub const BETA_5_ENDPOINT_URL: &str = "https://beta-5.fuel.network";
pub const BETA_FAUCET_URL: &str = "https://faucet-beta-5.fuel.network";
pub const BETA_2_FAUCET_URL: &str = "https://faucet-beta-2.fuel.network";
pub const BETA_3_FAUCET_URL: &str = "https://faucet-beta-3.fuel.network";
pub const BETA_4_FAUCET_URL: &str = "https://faucet-beta-4.fuel.network";
pub const BETA_5_FAUCET_URL: &str = "https://faucet-beta-5.fuel.network";
pub const DEVNET_FAUCET_URL: &str = "https://faucet-devnet.fuel.network";
pub const DEVNET_ENDPOINT_URL: &str = "https://devnet.fuel.network";
32 changes: 26 additions & 6 deletions forc-plugins/forc-client/src/op/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
cmd,
util::{
gas::get_estimated_max_fee,
node_url::get_node_url,
pkg::built_pkgs,
tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS},
Expand Down Expand Up @@ -237,21 +238,40 @@ pub async fn deploy_pkg(
WalletSelectionMode::ForcWallet
};

let max_fee = command.gas.max_fee.unwrap_or(0);
let provider = Provider::connect(node_url.clone()).await?;

let tx = TransactionBuilder::create(bytecode.as_slice().into(), salt, storage_slots.clone())
// We need a tx for estimation without the signature.
let mut tb =
TransactionBuilder::create(bytecode.as_slice().into(), salt, storage_slots.clone());
tb.maturity(command.maturity.maturity.into())
.add_output(Output::contract_created(contract_id, state_root));
let tx_for_estimation = tb.finalize_without_signature_inner();

// If user specified max_fee use that but if not, we will estimate with %10 safety margin.
let max_fee = if let Some(max_fee) = command.gas.max_fee {
max_fee
} else {
let estimation_margin = 10;
get_estimated_max_fee(
tx_for_estimation.clone(),
&provider,
&client,
estimation_margin,
)
.await?
};

let tx = tb
.max_fee_limit(max_fee)
.maturity(command.maturity.maturity.into())
.add_output(Output::contract_created(contract_id, state_root))
.finalize_signed(
Provider::connect(node_url.clone()).await?,
provider.clone(),
command.default_signer || command.unsigned,
command.signing_key,
wallet_mode,
)
.await?;

let tx = Transaction::from(tx);

let chain_id = client.chain_info().await?.consensus_parameters.chain_id();

let deployment_request = client.submit_and_await_commit(&tx).map(|res| match res {
Expand Down
4 changes: 2 additions & 2 deletions forc-plugins/forc-client/src/op/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod encode;
use crate::{
cmd,
util::{
gas::get_gas_used,
gas::get_script_gas_used,
node_url::get_node_url,
pkg::built_pkgs,
tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS},
Expand Down Expand Up @@ -120,7 +120,7 @@ pub async fn run_pkg(
script_gas_limit
// Dry run tx and get `gas_used`
} else {
get_gas_used(tb.clone().finalize_without_signature_inner(), &provider).await?
get_script_gas_used(tb.clone().finalize_without_signature_inner(), &provider).await?
};
tb.script_gas_limit(script_gas_limit);

Expand Down
81 changes: 71 additions & 10 deletions forc-plugins/forc-client/src/util/gas.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use anyhow::Result;

use fuel_core_client::client::FuelClient;
use fuel_core_types::services::executor::TransactionExecutionResult;
use fuel_tx::{
field::{Inputs, Witnesses},
Buildable, Chargeable, Input, Script, TxPointer,
field::{Inputs, MaxFeeLimit, Witnesses},
Buildable, Chargeable, Create, Input, Script, Transaction, TxPointer,
};
use fuels_accounts::provider::Provider;
use fuels_core::types::transaction_builders::DryRunner;
use fuels_core::{
constants::DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON, types::transaction_builders::DryRunner,
};

fn no_spendable_input<'a, I: IntoIterator<Item = &'a Input>>(inputs: I) -> bool {
!inputs.into_iter().any(|i| {
Expand All @@ -18,14 +23,15 @@ fn no_spendable_input<'a, I: IntoIterator<Item = &'a Input>>(inputs: I) -> bool
})
}

pub(crate) async fn get_gas_used(mut tx: Script, provider: &Provider) -> Result<u64> {
pub(crate) async fn get_script_gas_used(mut tx: Script, provider: &Provider) -> Result<u64> {
let no_spendable_input = no_spendable_input(tx.inputs());
let base_asset_id = provider.base_asset_id();
if no_spendable_input {
tx.inputs_mut().push(Input::coin_signed(
Default::default(),
Default::default(),
1_000_000_000,
Default::default(),
*base_asset_id,
TxPointer::default(),
0,
));
Expand All @@ -34,18 +40,73 @@ pub(crate) async fn get_gas_used(mut tx: Script, provider: &Provider) -> Result<
// and increase the witness limit
tx.witnesses_mut().push(Default::default());
}
let consensus_params = provider.consensus_parameters();

// Get `max_gas` used by everything except the script execution. Add `1` because of rounding.
let consensus_params = provider.consensus_parameters();
let max_gas_per_tx = consensus_params.tx_params().max_gas_per_tx();
let max_gas = tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
// Increase `script_gas_limit` to the maximum allowed value.
tx.set_script_gas_limit(max_gas_per_tx - max_gas);

let tolerance = 0.1;
let gas_used = provider
.dry_run_and_get_used_gas(tx.clone().into(), tolerance)
.await?;
get_gas_used(Transaction::Script(tx), provider).await
}

/// Returns gas_used for an arbitrary tx, by doing dry run with the provided `Provider`.
pub(crate) async fn get_gas_used(tx: Transaction, provider: &Provider) -> Result<u64> {
let tolerance = 0.1;
let gas_used = provider.dry_run_and_get_used_gas(tx, tolerance).await?;
Ok(gas_used)
}

/// Returns an estimation for the max fee of `Create` transactions.
/// Accepts a `tolerance` which is used to add some safety margin to the estimation.
/// Resulting estimation is calculated as `(dry_run_estimation * tolerance)/100 + dry_run_estimation)`.
pub(crate) async fn get_estimated_max_fee(
tx: Create,
provider: &Provider,
client: &FuelClient,
tolerance: u64,
) -> Result<u64> {
let mut tx = tx.clone();
// Add dummy input to get past validation for dry run.
let no_spendable_input = no_spendable_input(tx.inputs());
let base_asset_id = provider.base_asset_id();
if no_spendable_input {
tx.inputs_mut().push(Input::coin_signed(
Default::default(),
Default::default(),
1_000_000_000,
*base_asset_id,
TxPointer::default(),
0,
));

// Add an empty `Witness` for the `coin_signed` we just added
// and increase the witness limit
tx.witnesses_mut().push(Default::default());
}
let consensus_params = provider.consensus_parameters();
let gas_price = provider
.estimate_gas_price(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON)
.await?
.gas_price;
let max_fee = tx.max_fee(
consensus_params.gas_costs(),
consensus_params.fee_params(),
gas_price,
);
tx.set_max_fee_limit(max_fee as u64);
let tx = Transaction::from(tx);

let tx_status = client
.dry_run_opt(&[tx], Some(false))
.await
.map(|mut status_vec| status_vec.remove(0))?;
let total_fee = match tx_status.result {
TransactionExecutionResult::Success { total_fee, .. } => total_fee,
TransactionExecutionResult::Failed { total_fee, .. } => total_fee,
};

let total_fee_with_tolerance = ((total_fee * tolerance) / 100) + total_fee;
Ok(total_fee_with_tolerance)
}
15 changes: 13 additions & 2 deletions forc-plugins/forc-client/src/util/node_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub fn get_node_url(
node_target.target.clone(),
node_target.node_url.clone(),
) {
(true, None, None) => Target::Beta5.target_url(),
(true, None, None) => Target::testnet().target_url(),
(false, Some(target), None) => target.target_url(),
(false, None, Some(node_url)) => node_url,
(false, None, None) => manifest_network
Expand All @@ -38,7 +38,7 @@ fn test_get_node_url_testnet() {
};

let actual = get_node_url(&input, &None).unwrap();
assert_eq!("https://beta-5.fuel.network", actual);
assert_eq!("https://devnet.fuel.network", actual);
}

#[test]
Expand Down Expand Up @@ -101,6 +101,17 @@ fn test_get_node_url_beta3() {
assert_eq!("https://beta-3.fuel.network", actual);
}

#[test]
fn test_get_node_url_devnet() {
let input = NodeTarget {
target: Some(Target::Devnet),
node_url: None,
testnet: false,
};
let actual = get_node_url(&input, &None).unwrap();
assert_eq!("https://devnet.fuel.network", actual);
}

#[test]
fn test_get_node_url_local() {
let input = NodeTarget {
Expand Down
26 changes: 21 additions & 5 deletions forc-plugins/forc-client/src/util/target.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::constants::{
BETA_2_ENDPOINT_URL, BETA_3_ENDPOINT_URL, BETA_4_ENDPOINT_URL, BETA_5_ENDPOINT_URL, NODE_URL,
BETA_2_ENDPOINT_URL, BETA_2_FAUCET_URL, BETA_3_ENDPOINT_URL, BETA_3_FAUCET_URL,
BETA_4_ENDPOINT_URL, BETA_4_FAUCET_URL, BETA_5_ENDPOINT_URL, BETA_5_FAUCET_URL,
DEVNET_ENDPOINT_URL, DEVNET_FAUCET_URL, NODE_URL,
};
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
Expand All @@ -12,6 +14,7 @@ pub enum Target {
Beta3,
Beta4,
Beta5,
Devnet,
Local,
}

Expand All @@ -28,6 +31,7 @@ impl Target {
Target::Beta3 => BETA_3_ENDPOINT_URL,
Target::Beta4 => BETA_4_ENDPOINT_URL,
Target::Beta5 => BETA_5_ENDPOINT_URL,
Target::Devnet => DEVNET_ENDPOINT_URL,
Target::Local => NODE_URL,
};
url.to_string()
Expand All @@ -43,10 +47,18 @@ impl Target {
}
}

pub fn is_testnet(&self) -> bool {
pub fn testnet() -> Self {
Target::Devnet
}

pub fn faucet_url(&self) -> String {
match self {
Target::Beta2 | Target::Beta3 | Target::Beta4 | Target::Beta5 => true,
Target::Local => false,
Target::Beta2 => BETA_2_FAUCET_URL.to_string(),
Target::Beta3 => BETA_3_FAUCET_URL.to_string(),
Target::Beta4 => BETA_4_FAUCET_URL.to_string(),
Target::Beta5 => BETA_5_FAUCET_URL.to_string(),
Target::Devnet => DEVNET_FAUCET_URL.to_string(),
Target::Local => "http://localhost:3000".to_string(),
}
}
}
Expand All @@ -60,12 +72,15 @@ impl FromStr for Target {
"beta-3" => Ok(Target::Beta3),
"beta-4" => Ok(Target::Beta4),
"beta-5" => Ok(Target::Beta5),
"devnet" => Ok(Target::Devnet),
"local" => Ok(Target::Local),
_ => bail!(
"'{s}' is not a valid target name. Possible values: '{}', '{}', '{}', '{}'",
"'{s}' is not a valid target name. Possible values: '{}', '{}', '{}', '{}', '{}', '{}'",
Target::Beta2,
Target::Beta3,
Target::Beta4,
Target::Beta5,
Target::Devnet,
Target::Local
),
}
Expand All @@ -79,6 +94,7 @@ impl std::fmt::Display for Target {
Target::Beta3 => "beta-3",
Target::Beta4 => "beta-4",
Target::Beta5 => "beta-5",
Target::Devnet => "devnet",
Target::Local => "local",
};
write!(f, "{}", s)
Expand Down
11 changes: 7 additions & 4 deletions forc-plugins/forc-client/src/util/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{io::Write, str::FromStr};
use anyhow::{Error, Result};
use async_trait::async_trait;
use forc_tracing::println_warning;

use fuel_crypto::{Message, PublicKey, SecretKey, Signature};
use fuel_tx::{field, Address, Buildable, ContractId, Input, Output, TransactionBuilder, Witness};
use fuels_accounts::{provider::Provider, wallet::Wallet, ViewOnlyAccount};
Expand All @@ -22,7 +23,7 @@ use forc_wallet::{
utils::default_wallet_path,
};

use crate::constants::BETA_FAUCET_URL;
use crate::util::target::Target;

/// The maximum time to wait for a transaction to be included in a block by the node
pub const TX_SUBMIT_TIMEOUT_MS: u64 = 30_000u64;
Expand Down Expand Up @@ -169,7 +170,8 @@ impl<Tx: Buildable + field::Witnesses + Send> TransactionBuilderExt<Tx> for Tran
signing_key: Option<SecretKey>,
wallet_mode: WalletSelectionMode,
) -> Result<Tx> {
let params = provider.chain_info().await?.consensus_parameters;
let chain_info = provider.chain_info().await?;
let params = chain_info.consensus_parameters;
let signing_key = match (wallet_mode, signing_key, default_sign) {
(WalletSelectionMode::ForcWallet, None, false) => {
// TODO: This is a very simple TUI, we should consider adding a nice TUI
Expand Down Expand Up @@ -217,10 +219,11 @@ impl<Tx: Buildable + field::Witnesses + Send> TransactionBuilderExt<Tx> for Tran
let first_account = accounts
.get(&0)
.ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))?;
let faucet_link = format!("{}/?address={first_account}", BETA_FAUCET_URL);
let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet());
let faucet_link = format!("{}/?address={first_account}", target.faucet_url());
anyhow::bail!("Your wallet does not have any funds to pay for the transaction.\
\n\nIf you are interacting with a testnet consider using the faucet.\
\n-> beta-5 network faucet: {faucet_link}\
\n-> {target} network faucet: {faucet_link}\
\nIf you are interacting with a local node, consider providing a chainConfig which funds your account.")
}
print_account_balances(&accounts, &account_balances);
Expand Down

0 comments on commit b0af334

Please sign in to comment.