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