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 HEADING_TRANSACTION,
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, help_heading = HEADING_TRANSACTION)]
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 .execute(&self.config, global_args.quiet, global_args.no_cache)
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 #[allow(clippy::too_many_lines)]
189 pub async fn execute(
190 &self,
191 config: &config::Args,
192 quiet: bool,
193 no_cache: bool,
194 ) -> Result<TxnResult<u32>, Error> {
195 let print = Print::new(quiet);
196 let network = config.get_network()?;
197 tracing::trace!(?network);
198 let keys = self.key.parse_keys(&config.locator, &network)?;
199 let client = network.rpc_client()?;
200 client
201 .verify_network_passphrase(Some(&network.network_passphrase))
202 .await?;
203 let source_account = config.source_account()?;
204 let extend_to = self.ledgers_to_extend(&client).await?;
205
206 let account_details = client
208 .get_account(&source_account.clone().to_string())
209 .await?;
210 let sequence: i64 = account_details.seq_num.into();
211
212 let tx = Box::new(Transaction {
213 source_account,
214 fee: config.get_inclusion_fee()?,
215 seq_num: SequenceNumber(sequence + 1),
216 cond: Preconditions::None,
217 memo: Memo::None,
218 operations: vec![Operation {
219 source_account: None,
220 body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
221 ext: ExtensionPoint::V0,
222 extend_to,
223 }),
224 }]
225 .try_into()?,
226 ext: TransactionExt::V1(SorobanTransactionData {
227 ext: SorobanTransactionDataExt::V0,
228 resources: SorobanResources {
229 footprint: LedgerFootprint {
230 read_only: keys.clone().try_into()?,
231 read_write: vec![].try_into()?,
232 },
233 instructions: self.resources.instructions.unwrap_or_default(),
234 disk_read_bytes: 0,
235 write_bytes: 0,
236 },
237 resource_fee: 0,
238 }),
239 });
240 if self.build_only {
241 return Ok(TxnResult::Txn(tx));
242 }
243
244 let res = sim_sign_and_send_tx::<Error>(
245 &client,
246 &tx,
247 config,
248 &self.resources,
249 &[],
250 None,
253 quiet,
254 no_cache,
255 )
256 .await?;
257
258 let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
259 let events = extract_events(&meta);
260
261 crate::log::event::all(&events);
262 crate::log::event::contract(&events, &print);
263
264 let changes = match meta {
267 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
268 if operations.is_empty() {
271 return Err(Error::LedgerEntryNotFound);
272 }
273
274 operations[0].changes.clone()
275 }
276 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
277 if operations.is_empty() {
280 return Err(Error::LedgerEntryNotFound);
281 }
282
283 operations[0].changes.clone()
284 }
285 _ => return Err(Error::LedgerEntryNotFound),
286 };
287
288 if changes.is_empty() {
289 print.infoln("No changes detected, transaction was a no-op.");
290 let entry = client.get_full_ledger_entries(&keys).await?;
291 let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
292
293 return Ok(TxnResult::Res(extension));
294 }
295
296 match (&changes[0], &changes[1]) {
297 (
298 LedgerEntryChange::State(_),
299 LedgerEntryChange::Updated(LedgerEntry {
300 data:
301 LedgerEntryData::Ttl(TtlEntry {
302 live_until_ledger_seq,
303 ..
304 }),
305 ..
306 }),
307 ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
308 _ => Err(Error::LedgerEntryNotFound),
309 }
310 }
311}