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

[cherry-pick][cli] Add gas estimate feature (#17322) #17457

Merged
Show file tree
Hide file tree
Changes from all 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
218 changes: 211 additions & 7 deletions crates/sui/src/client_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ macro_rules! serialize_or_execute {
}};
}

/// Only to be used within CLI
pub const GAS_SAFE_OVERHEAD: u64 = 1000;

#[derive(Parser)]
#[clap(rename_all = "kebab-case")]
pub enum SuiClientCommands {
Expand Down Expand Up @@ -613,9 +616,12 @@ pub enum SuiClientCommands {
/// Global options for most transaction execution related commands
#[derive(Args, Debug)]
pub struct Opts {
/// Gas budget for this transaction (in MIST)
/// An optional gas budget for this transaction (in MIST). If gas budget is not provided, the
/// tool will first perform a dry run to estimate the gas cost, and then it will execute the
/// transaction. Please note that this incurs a small cost in performance due to the additional
/// dry run call.
#[arg(long)]
pub gas_budget: u64,
pub gas_budget: Option<u64>,
/// Perform a dry run of the transaction, without executing it.
#[arg(long)]
pub dry_run: bool,
Expand Down Expand Up @@ -647,7 +653,7 @@ impl Opts {
/// Uses the passed gas_budget for the gas budget variable and sets all other flags to false.
pub fn for_testing(gas_budget: u64) -> Self {
Self {
gas_budget,
gas_budget: Some(gas_budget),
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
Expand All @@ -657,7 +663,7 @@ impl Opts {
/// and sets all other flags to false.
pub fn for_testing_dry_run(gas_budget: u64) -> Self {
Self {
gas_budget,
gas_budget: Some(gas_budget),
dry_run: true,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
Expand Down Expand Up @@ -935,11 +941,27 @@ impl SuiClientCommands {
)
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
sender,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
sender,
tx_kind,
tx_kind.clone(),
gas_budget,
gas_price,
gas.into_iter().collect(),
Expand Down Expand Up @@ -1041,6 +1063,21 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
sender,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1266,6 +1303,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1321,6 +1372,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1372,6 +1437,20 @@ impl SuiClientCommands {
)
.await;
}
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
tx_kind.clone(),
gas_price,
Some(vec![object_id]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1456,6 +1535,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
from,
kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1530,6 +1623,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
kind.clone(),
gas_price,
Some(input_coins.clone()),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1583,6 +1690,20 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
Some(input_coins.clone()),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(signer, tx_kind, gas_budget, gas_price, input_coins, None)
Expand Down Expand Up @@ -1735,6 +1856,21 @@ impl SuiClientCommands {
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};

let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -1788,6 +1924,21 @@ impl SuiClientCommands {
)
.await;
}

let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => {
estimate_gas_budget(
context,
signer,
tx_kind.clone(),
gas_price,
gas.map(|x| vec![x]),
None,
)
.await?
}
};
let data = client
.transaction_builder()
.tx_data(
Expand Down Expand Up @@ -2998,15 +3149,19 @@ fn format_balance(
}

/// Helper function to reduce code duplication for executing dry run
async fn execute_dry_run(
pub async fn execute_dry_run(
context: &mut WalletContext,
signer: SuiAddress,
kind: TransactionKind,
gas_budget: u64,
gas_budget: Option<u64>,
gas_price: u64,
gas_payment: Option<Vec<ObjectID>>,
sponsor: Option<SuiAddress>,
) -> Result<SuiClientCommandResult, anyhow::Error> {
let gas_budget = match gas_budget {
Some(gas_budget) => gas_budget,
None => max_gas_budget(context).await?,
};
let dry_run_tx_data = context
.get_client()
.await?
Expand All @@ -3022,3 +3177,52 @@ async fn execute_dry_run(
.map_err(|e| anyhow!("Dry run failed: {e}"))?;
Ok(SuiClientCommandResult::DryRun(response))
}

/// Call a dry run with the transaction data to estimate the gas budget.
/// The estimated gas budget is computed as following:
/// * the maximum between A and B, where:
/// A = computation cost + GAS_SAFE_OVERHEAD * reference gas price
/// B = computation cost + storage cost - storage rebate + GAS_SAFE_OVERHEAD * reference gas price
/// overhead
///
/// This gas estimate is computed exactly as in the TypeScript SDK
/// <https://github.com/MystenLabs/sui/blob/3c4369270605f78a243842098b7029daf8d883d9/sdk/typescript/src/transactions/TransactionBlock.ts#L845-L858>
pub async fn estimate_gas_budget(
context: &mut WalletContext,
signer: SuiAddress,
kind: TransactionKind,
gas_price: u64,
gas_payment: Option<Vec<ObjectID>>,
sponsor: Option<SuiAddress>,
) -> Result<u64, anyhow::Error> {
let client = context.get_client().await?;
let Ok(SuiClientCommandResult::DryRun(dry_run)) =
execute_dry_run(context, signer, kind, None, gas_price, gas_payment, sponsor).await
else {
bail!("Could not automatically determine the gas budget. Please supply one using the --gas-budget flag.")
};

let safe_overhead = GAS_SAFE_OVERHEAD * client.read_api().get_reference_gas_price().await?;
let computation_cost_with_overhead =
dry_run.effects.gas_cost_summary().computation_cost + safe_overhead;

let gas_usage = dry_run.effects.gas_cost_summary().net_gas_usage() + safe_overhead as i64;
Ok(computation_cost_with_overhead.max(if gas_usage < 0 { 0 } else { gas_usage as u64 }))
}

/// Queries the protocol config for the maximum gas allowed in a transaction.
pub async fn max_gas_budget(context: &mut WalletContext) -> Result<u64, anyhow::Error> {
let cfg = context
.get_client()
.await?
.read_api()
.get_protocol_config(None)
.await?;
Ok(match cfg.attributes.get("max_tx_gas") {
Some(Some(sui_json_rpc_types::SuiProtocolConfigValue::U64(y))) => *y,
_ => bail!(
"Could not automatically find the maximum gas allowed in a transaction from the \
protocol config. Please provide a gas budget with the --gas-budget flag."
),
})
}
2 changes: 1 addition & 1 deletion crates/sui/src/client_ptb/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub struct ProgramMetadata {
pub gas_object_id: Option<Spanned<ObjectID>>,
pub json_set: bool,
pub dry_run_set: bool,
pub gas_budget: Spanned<u64>,
pub gas_budget: Option<Spanned<u64>>,
}

/// A parsed module access consisting of the address, module name, and function name.
Expand Down
11 changes: 3 additions & 8 deletions crates/sui/src/client_ptb/displays/ptb_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@ impl<'a> Display for PTBPreview<'a> {
builder.push_record([command, vals]);
}
}
// index of horizontal line to draw after commands
let line_index = builder.count_rows();
builder.push_record([
GAS_BUDGET,
self.program_metadata.gas_budget.value.to_string().as_str(),
]);
if let Some(gas_budget) = self.program_metadata.gas_budget {
builder.push_record([GAS_BUDGET, gas_budget.value.to_string().as_str()]);
}
if let Some(gas_coin_id) = self.program_metadata.gas_object_id {
builder.push_record([GAS_COIN, gas_coin_id.value.to_string().as_str()]);
}
Expand All @@ -53,8 +50,6 @@ impl<'a> Display for PTBPreview<'a> {
table.with(TableStyle::rounded().horizontals([
HorizontalLine::new(1, TableStyle::modern().get_horizontal()),
HorizontalLine::new(2, TableStyle::modern().get_horizontal()),
HorizontalLine::new(2, TableStyle::modern().get_horizontal()),
HorizontalLine::new(line_index + 2, TableStyle::modern().get_horizontal()),
]));
table.with(tabled::settings::style::BorderSpanCorrection);

Expand Down
10 changes: 1 addition & 9 deletions crates/sui/src/client_ptb/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,6 @@ impl<'a, I: Iterator<Item = &'a str>> ProgramParser<'a, I> {
.push(err!(sp, "Trailing {tok} found after the last command",));
}

let Some(gas_budget) = self.state.gas_budget else {
self.state.errors.push(err!(
sp => help: { "Use --gas-budget <u64> to set a gas budget" },
"Gas budget not set."
));
return Err(self.state.errors);
};

if self.state.errors.is_empty() {
Ok((
A::Program {
Expand All @@ -215,7 +207,7 @@ impl<'a, I: Iterator<Item = &'a str>> ProgramParser<'a, I> {
gas_object_id: self.state.gas_object_id,
json_set: self.state.json_set,
dry_run_set: self.state.dry_run_set,
gas_budget,
gas_budget: self.state.gas_budget,
},
))
} else {
Expand Down
Loading
Loading