soroban_cli/commands/contract/
extend.rs1use std::{fmt::Debug, num::TryFromIntError, path::Path, str::FromStr};
2
3use crate::{
4 log::extract_events,
5 print::Print,
6 xdr::{
7 ConfigSettingEntry, ConfigSettingId, Error as XdrError, ExtendFootprintTtlOp,
8 ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint,
9 LedgerKey, LedgerKeyConfigSetting, Limits, Memo, Operation, OperationBody, Preconditions,
10 SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
11 Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
12 TtlEntry, WriteXdr,
13 },
14};
15use clap::{command, Parser};
16
17use crate::commands::tx::fetch;
18use crate::{
19 assembled::simulate_and_assemble_transaction,
20 commands::{
21 global,
22 txn_result::{TxnEnvelopeResult, TxnResult},
23 NetworkRunnable,
24 },
25 config::{self, data, locator, network},
26 key, rpc, wasm, Pwd,
27};
28
29#[derive(Parser, Debug, Clone)]
30#[group(skip)]
31pub struct Cmd {
32 #[arg(long, required = true)]
34 pub ledgers_to_extend: u32,
35
36 #[arg(long)]
38 pub ttl_ledger_only: bool,
39
40 #[command(flatten)]
41 pub key: key::Args,
42
43 #[command(flatten)]
44 pub config: config::Args,
45
46 #[command(flatten)]
47 pub fee: crate::fee::Args,
48}
49
50impl FromStr for Cmd {
51 type Err = clap::error::Error;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 use clap::{CommandFactory, FromArgMatches};
55 Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
56 }
57}
58
59impl Pwd for Cmd {
60 fn set_pwd(&mut self, pwd: &Path) {
61 self.config.set_pwd(pwd);
62 }
63}
64
65#[derive(thiserror::Error, Debug)]
66pub enum Error {
67 #[error("parsing key {key}: {error}")]
68 CannotParseKey {
69 key: String,
70 error: soroban_spec_tools::Error,
71 },
72
73 #[error("parsing XDR key {key}: {error}")]
74 CannotParseXdrKey { key: String, error: XdrError },
75
76 #[error(transparent)]
77 Config(#[from] config::Error),
78
79 #[error("either `--key` or `--key-xdr` are required")]
80 KeyIsRequired,
81
82 #[error("xdr processing error: {0}")]
83 Xdr(#[from] XdrError),
84
85 #[error("Ledger entry not found")]
86 LedgerEntryNotFound,
87
88 #[error("missing operation result")]
89 MissingOperationResult,
90
91 #[error(transparent)]
92 Rpc(#[from] rpc::Error),
93
94 #[error(transparent)]
95 Wasm(#[from] wasm::Error),
96
97 #[error(transparent)]
98 Key(#[from] key::Error),
99
100 #[error(transparent)]
101 Data(#[from] data::Error),
102
103 #[error(transparent)]
104 Network(#[from] network::Error),
105
106 #[error(transparent)]
107 Locator(#[from] locator::Error),
108
109 #[error(transparent)]
110 IntError(#[from] TryFromIntError),
111
112 #[error("Failed to fetch state archival settings from network")]
113 StateArchivalSettingsNotFound,
114
115 #[error("Ledgers to extend ({requested}) exceeds network maximum ({max})")]
116 LedgersToExtendTooLarge { requested: u32, max: u32 },
117
118 #[error(transparent)]
119 Fee(#[from] fetch::fee::Error),
120
121 #[error(transparent)]
122 Fetch(#[from] fetch::Error),
123}
124
125impl Cmd {
126 #[allow(clippy::too_many_lines)]
127 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
128 let res = self
129 .run_against_rpc_server(Some(global_args), None)
130 .await?
131 .to_envelope();
132 match res {
133 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
134 TxnEnvelopeResult::Res(ttl_ledger) => {
135 if self.ttl_ledger_only {
136 println!("{ttl_ledger}");
137 } else {
138 println!("New ttl ledger: {ttl_ledger}");
139 }
140 }
141 }
142
143 Ok(())
144 }
145
146 async fn get_max_entry_ttl(client: &rpc::Client) -> Result<u32, Error> {
147 let key = LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
148 config_setting_id: ConfigSettingId::StateArchival,
149 });
150
151 let entries = client.get_full_ledger_entries(&[key]).await?;
152
153 if let Some(entry) = entries.entries.first() {
154 if let LedgerEntryData::ConfigSetting(ConfigSettingEntry::StateArchival(settings)) =
155 &entry.val
156 {
157 return Ok(settings.max_entry_ttl);
158 }
159 }
160
161 Err(Error::StateArchivalSettingsNotFound)
162 }
163
164 async fn ledgers_to_extend(&self, client: &rpc::Client) -> Result<u32, Error> {
165 let max_entry_ttl = Self::get_max_entry_ttl(client).await?;
166
167 tracing::trace!(
168 "Checking ledgers_to_extend: requested={}, max_entry_ttl={}",
169 self.ledgers_to_extend,
170 max_entry_ttl
171 );
172
173 if self.ledgers_to_extend > max_entry_ttl {
174 return Err(Error::LedgersToExtendTooLarge {
175 requested: self.ledgers_to_extend,
176 max: max_entry_ttl,
177 });
178 }
179
180 Ok(self.ledgers_to_extend)
181 }
182}
183
184#[async_trait::async_trait]
185impl NetworkRunnable for Cmd {
186 type Error = Error;
187 type Result = TxnResult<u32>;
188
189 #[allow(clippy::too_many_lines)]
190 async fn run_against_rpc_server(
191 &self,
192 args: Option<&global::Args>,
193 config: Option<&config::Args>,
194 ) -> Result<TxnResult<u32>, Self::Error> {
195 let config = config.unwrap_or(&self.config);
196 let quiet = args.is_some_and(|a| a.quiet);
197 let print = Print::new(quiet);
198 let network = config.get_network()?;
199 tracing::trace!(?network);
200 let keys = self.key.parse_keys(&config.locator, &network)?;
201 let client = network.rpc_client()?;
202 let source_account = config.source_account().await?;
203 let extend_to = self.ledgers_to_extend(&client).await?;
204
205 let account_details = client
207 .get_account(&source_account.clone().to_string())
208 .await?;
209 let sequence: i64 = account_details.seq_num.into();
210
211 let tx = Box::new(Transaction {
212 source_account,
213 fee: self.fee.fee,
214 seq_num: SequenceNumber(sequence + 1),
215 cond: Preconditions::None,
216 memo: Memo::None,
217 operations: vec![Operation {
218 source_account: None,
219 body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
220 ext: ExtensionPoint::V0,
221 extend_to,
222 }),
223 }]
224 .try_into()?,
225 ext: TransactionExt::V1(SorobanTransactionData {
226 ext: SorobanTransactionDataExt::V0,
227 resources: SorobanResources {
228 footprint: LedgerFootprint {
229 read_only: keys.clone().try_into()?,
230 read_write: vec![].try_into()?,
231 },
232 instructions: self.fee.instructions.unwrap_or_default(),
233 disk_read_bytes: 0,
234 write_bytes: 0,
235 },
236 resource_fee: 0,
237 }),
238 });
239 if self.fee.build_only {
240 return Ok(TxnResult::Txn(tx));
241 }
242 let assembled =
243 simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?;
244
245 let tx = assembled.transaction().clone();
246 let res = client
247 .send_transaction_polling(&config.sign(tx, quiet).await?)
248 .await?;
249 self.fee.print_cost_info(&res)?;
250
251 if args.is_none_or(|a| !a.no_cache) {
252 data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
253 }
254
255 let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
256 let events = extract_events(&meta);
257
258 crate::log::event::all(&events);
259 crate::log::event::contract(&events, &print);
260
261 let changes = match meta {
264 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
265 if operations.is_empty() {
268 return Err(Error::LedgerEntryNotFound);
269 }
270
271 operations[0].changes.clone()
272 }
273 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
274 if operations.is_empty() {
277 return Err(Error::LedgerEntryNotFound);
278 }
279
280 operations[0].changes.clone()
281 }
282 _ => return Err(Error::LedgerEntryNotFound),
283 };
284
285 if changes.is_empty() {
286 print.infoln("No changes detected, transaction was a no-op.");
287 let entry = client.get_full_ledger_entries(&keys).await?;
288 let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
289
290 return Ok(TxnResult::Res(extension));
291 }
292
293 match (&changes[0], &changes[1]) {
294 (
295 LedgerEntryChange::State(_),
296 LedgerEntryChange::Updated(LedgerEntry {
297 data:
298 LedgerEntryData::Ttl(TtlEntry {
299 live_until_ledger_seq,
300 ..
301 }),
302 ..
303 }),
304 ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
305 _ => Err(Error::LedgerEntryNotFound),
306 }
307 }
308}