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