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