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::{
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
240 let res = sim_sign_and_send_tx::<Error>(
241 &client,
242 &tx,
243 config,
244 &self.resources,
245 &[],
246 quiet,
247 no_cache,
248 )
249 .await?;
250
251 let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
252 let events = extract_events(&meta);
253
254 crate::log::event::all(&events);
255 crate::log::event::contract(&events, &print);
256
257 let changes = match meta {
260 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
261 if operations.is_empty() {
264 return Err(Error::LedgerEntryNotFound);
265 }
266
267 operations[0].changes.clone()
268 }
269 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
270 if operations.is_empty() {
273 return Err(Error::LedgerEntryNotFound);
274 }
275
276 operations[0].changes.clone()
277 }
278 _ => return Err(Error::LedgerEntryNotFound),
279 };
280
281 if changes.is_empty() {
282 print.infoln("No changes detected, transaction was a no-op.");
283 let entry = client.get_full_ledger_entries(&keys).await?;
284 let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
285
286 return Ok(TxnResult::Res(extension));
287 }
288
289 match (&changes[0], &changes[1]) {
290 (
291 LedgerEntryChange::State(_),
292 LedgerEntryChange::Updated(LedgerEntry {
293 data:
294 LedgerEntryData::Ttl(TtlEntry {
295 live_until_ledger_seq,
296 ..
297 }),
298 ..
299 }),
300 ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
301 _ => Err(Error::LedgerEntryNotFound),
302 }
303 }
304}