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