1use std::{fmt::Debug, path::Path, str::FromStr};
2
3use crate::{
4 log::extract_events,
5 xdr::{
6 Error as XdrError, ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData,
7 LedgerFootprint, Limits, Memo, Operation, OperationBody, Preconditions, RestoreFootprintOp,
8 SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
9 Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
10 TtlEntry, WriteXdr,
11 },
12};
13use clap::Parser;
14use stellar_strkey::DecodeError;
15
16use crate::commands::tx::fetch;
17use crate::{
18 assembled::simulate_and_assemble_transaction,
19 commands::{
20 contract::extend,
21 global,
22 txn_result::{TxnEnvelopeResult, TxnResult},
23 NetworkRunnable,
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 #[command(flatten)]
33 pub key: key::Args,
34
35 #[arg(long)]
37 pub ledgers_to_extend: Option<u32>,
38
39 #[arg(long)]
41 pub ttl_ledger_only: bool,
42
43 #[command(flatten)]
44 pub config: config::Args,
45
46 #[command(flatten)]
47 pub resources: crate::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("cannot parse contract ID {0}: {1}")]
81 CannotParseContractId(String, DecodeError),
82
83 #[error(transparent)]
84 Config(#[from] config::Error),
85
86 #[error("either `--key` or `--key-xdr` are required")]
87 KeyIsRequired,
88
89 #[error("xdr processing error: {0}")]
90 Xdr(#[from] XdrError),
91
92 #[error("Ledger entry not found")]
93 LedgerEntryNotFound,
94
95 #[error(transparent)]
96 Locator(#[from] locator::Error),
97
98 #[error("missing operation result")]
99 MissingOperationResult,
100
101 #[error(transparent)]
102 Rpc(#[from] rpc::Error),
103
104 #[error(transparent)]
105 Wasm(#[from] wasm::Error),
106
107 #[error(transparent)]
108 Key(#[from] key::Error),
109
110 #[error(transparent)]
111 Extend(#[from] extend::Error),
112
113 #[error(transparent)]
114 Data(#[from] data::Error),
115
116 #[error(transparent)]
117 Network(#[from] network::Error),
118
119 #[error(transparent)]
120 Fee(#[from] fetch::fee::Error),
121
122 #[error(transparent)]
123 Fetch(#[from] fetch::Error),
124}
125
126impl Cmd {
127 #[allow(clippy::too_many_lines)]
128 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
129 let res = self
130 .run_against_rpc_server(Some(global_args), None)
131 .await?
132 .to_envelope();
133 let expiration_ledger_seq = match res {
134 TxnEnvelopeResult::TxnEnvelope(tx) => {
135 println!("{}", tx.to_xdr_base64(Limits::none())?);
136 return Ok(());
137 }
138 TxnEnvelopeResult::Res(res) => res,
139 };
140 if let Some(ledgers_to_extend) = self.ledgers_to_extend {
141 extend::Cmd {
142 key: self.key.clone(),
143 ledgers_to_extend,
144 config: self.config.clone(),
145 resources: self.resources.clone(),
146 ttl_ledger_only: false,
147 build_only: self.build_only,
148 }
149 .run(global_args)
150 .await?;
151 } else {
152 println!("New ttl ledger: {expiration_ledger_seq}");
153 }
154
155 Ok(())
156 }
157}
158
159#[async_trait::async_trait]
160impl NetworkRunnable for Cmd {
161 type Error = Error;
162 type Result = TxnResult<u32>;
163
164 async fn run_against_rpc_server(
165 &self,
166 args: Option<&global::Args>,
167 config: Option<&config::Args>,
168 ) -> Result<TxnResult<u32>, Error> {
169 let config = config.unwrap_or(&self.config);
170 let quiet = args.is_some_and(|a| a.quiet);
171 let print = crate::print::Print::new(quiet);
172 let network = config.get_network()?;
173 tracing::trace!(?network);
174 let entry_keys = self.key.parse_keys(&config.locator, &network)?;
175 let client = network.rpc_client()?;
176 let source_account = config.source_account().await?;
177
178 let account_details = client
180 .get_account(&source_account.clone().to_string())
181 .await?;
182 let sequence: i64 = account_details.seq_num.into();
183
184 let tx = Box::new(Transaction {
185 source_account,
186 fee: config.get_inclusion_fee()?,
187 seq_num: SequenceNumber(sequence + 1),
188 cond: Preconditions::None,
189 memo: Memo::None,
190 operations: vec![Operation {
191 source_account: None,
192 body: OperationBody::RestoreFootprint(RestoreFootprintOp {
193 ext: ExtensionPoint::V0,
194 }),
195 }]
196 .try_into()?,
197 ext: TransactionExt::V1(SorobanTransactionData {
198 ext: SorobanTransactionDataExt::V0,
199 resources: SorobanResources {
200 footprint: LedgerFootprint {
201 read_only: vec![].try_into()?,
202 read_write: entry_keys.clone().try_into()?,
203 },
204 instructions: self.resources.instructions.unwrap_or_default(),
205 disk_read_bytes: 0,
206 write_bytes: 0,
207 },
208 resource_fee: 0,
209 }),
210 });
211 if self.build_only {
212 return Ok(TxnResult::Txn(tx));
213 }
214 let assembled = simulate_and_assemble_transaction(
215 &client,
216 &tx,
217 self.resources.resource_config(),
218 self.resources.resource_fee,
219 )
220 .await?;
221
222 let tx = assembled.transaction().clone();
223 let res = client
224 .send_transaction_polling(&config.sign(tx, quiet).await?)
225 .await?;
226 self.resources.print_cost_info(&res)?;
227 if args.is_none_or(|a| !a.no_cache) {
228 data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
229 }
230 let meta = res
231 .result_meta
232 .as_ref()
233 .ok_or(Error::MissingOperationResult)?;
234
235 tracing::trace!(?meta);
236
237 let events = extract_events(meta);
238
239 crate::log::event::all(&events);
240 crate::log::event::contract(&events, &print);
241
242 let changes = match meta {
245 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
246 if operations.is_empty() {
249 return Err(Error::LedgerEntryNotFound);
250 }
251
252 operations[0].changes.clone()
253 }
254 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
255 if operations.is_empty() {
258 return Err(Error::LedgerEntryNotFound);
259 }
260
261 operations[0].changes.clone()
262 }
263 _ => return Err(Error::LedgerEntryNotFound),
264 };
265 tracing::debug!("Changes:\nlen:{}\n{changes:#?}", changes.len());
266
267 if changes.is_empty() {
268 print.infoln("No changes detected, transaction was a no-op.");
269 let entry = client.get_full_ledger_entries(&entry_keys).await?;
270 let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
271
272 return Ok(TxnResult::Res(extension));
273 }
274
275 Ok(TxnResult::Res(
276 parse_changes(&changes.to_vec()).ok_or(Error::LedgerEntryNotFound)?,
277 ))
278 }
279}
280
281fn parse_changes(changes: &[LedgerEntryChange]) -> Option<u32> {
282 changes
283 .iter()
284 .filter_map(|change| match change {
285 LedgerEntryChange::Restored(LedgerEntry {
286 data:
287 LedgerEntryData::Ttl(TtlEntry {
288 live_until_ledger_seq,
289 ..
290 }),
291 ..
292 })
293 | LedgerEntryChange::Updated(LedgerEntry {
294 data:
295 LedgerEntryData::Ttl(TtlEntry {
296 live_until_ledger_seq,
297 ..
298 }),
299 ..
300 })
301 | LedgerEntryChange::Created(LedgerEntry {
302 data:
303 LedgerEntryData::Ttl(TtlEntry {
304 live_until_ledger_seq,
305 ..
306 }),
307 ..
308 }) => Some(*live_until_ledger_seq),
309 _ => None,
310 })
311 .max()
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::xdr::{
318 ContractDataDurability::Persistent, ContractDataEntry, ContractId, Hash, LedgerEntry,
319 LedgerEntryChange, LedgerEntryData, ScAddress, ScSymbol, ScVal, SequenceNumber, StringM,
320 TtlEntry,
321 };
322
323 #[test]
324 fn test_parse_changes_two_changes_restored() {
325 let ttl_entry = TtlEntry {
327 live_until_ledger_seq: 12345,
328 key_hash: Hash([0; 32]),
329 };
330
331 let changes = vec![
332 LedgerEntryChange::State(LedgerEntry {
333 data: LedgerEntryData::Ttl(ttl_entry.clone()),
334 last_modified_ledger_seq: 0,
335 ext: crate::xdr::LedgerEntryExt::V0,
336 }),
337 LedgerEntryChange::Restored(LedgerEntry {
338 data: LedgerEntryData::Ttl(ttl_entry),
339 last_modified_ledger_seq: 0,
340 ext: crate::xdr::LedgerEntryExt::V0,
341 }),
342 ];
343
344 let result = parse_changes(&changes);
345 assert_eq!(result, Some(12345));
346 }
347
348 #[test]
349 fn test_parse_two_changes_that_had_expired() {
350 let ttl_entry = TtlEntry {
351 live_until_ledger_seq: 55555,
352 key_hash: Hash([0; 32]),
353 };
354
355 let counter = "COUNTER".parse::<StringM<32>>().unwrap();
356 let contract_data_entry = ContractDataEntry {
357 ext: ExtensionPoint::default(),
358 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
359 key: ScVal::Symbol(ScSymbol(counter)),
360 durability: Persistent,
361 val: ScVal::U32(1),
362 };
363
364 let changes = vec![
365 LedgerEntryChange::Restored(LedgerEntry {
366 data: LedgerEntryData::Ttl(ttl_entry.clone()),
367 last_modified_ledger_seq: 37429,
368 ext: crate::xdr::LedgerEntryExt::V0,
369 }),
370 LedgerEntryChange::Restored(LedgerEntry {
371 data: LedgerEntryData::ContractData(contract_data_entry.clone()),
372 last_modified_ledger_seq: 37429,
373 ext: crate::xdr::LedgerEntryExt::V0,
374 }),
375 ];
376
377 let result = parse_changes(&changes);
378 assert_eq!(result, Some(55555));
379 }
380
381 #[test]
382 fn test_parse_changes_two_changes_updated() {
383 let ttl_entry = TtlEntry {
385 live_until_ledger_seq: 67890,
386 key_hash: Hash([0; 32]),
387 };
388
389 let changes = vec![
390 LedgerEntryChange::State(LedgerEntry {
391 data: LedgerEntryData::Ttl(ttl_entry.clone()),
392 last_modified_ledger_seq: 0,
393 ext: crate::xdr::LedgerEntryExt::V0,
394 }),
395 LedgerEntryChange::Updated(LedgerEntry {
396 data: LedgerEntryData::Ttl(ttl_entry),
397 last_modified_ledger_seq: 0,
398 ext: crate::xdr::LedgerEntryExt::V0,
399 }),
400 ];
401
402 let result = parse_changes(&changes);
403 assert_eq!(result, Some(67890));
404 }
405
406 #[test]
407 fn test_parse_changes_two_changes_created() {
408 let ttl_entry = TtlEntry {
410 live_until_ledger_seq: 11111,
411 key_hash: Hash([0; 32]),
412 };
413
414 let changes = vec![
415 LedgerEntryChange::State(LedgerEntry {
416 data: LedgerEntryData::Ttl(ttl_entry.clone()),
417 last_modified_ledger_seq: 0,
418 ext: crate::xdr::LedgerEntryExt::V0,
419 }),
420 LedgerEntryChange::Created(LedgerEntry {
421 data: LedgerEntryData::Ttl(ttl_entry),
422 last_modified_ledger_seq: 0,
423 ext: crate::xdr::LedgerEntryExt::V0,
424 }),
425 ];
426
427 let result = parse_changes(&changes);
428 assert_eq!(result, Some(11111));
429 }
430
431 #[test]
432 fn test_parse_changes_single_change_restored() {
433 let ttl_entry = TtlEntry {
435 live_until_ledger_seq: 22222,
436 key_hash: Hash([0; 32]),
437 };
438
439 let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
440 data: LedgerEntryData::Ttl(ttl_entry),
441 last_modified_ledger_seq: 0,
442 ext: crate::xdr::LedgerEntryExt::V0,
443 })];
444
445 let result = parse_changes(&changes);
446 assert_eq!(result, Some(22222));
447 }
448
449 #[test]
450 fn test_parse_changes_single_change_updated() {
451 let ttl_entry = TtlEntry {
453 live_until_ledger_seq: 33333,
454 key_hash: Hash([0; 32]),
455 };
456
457 let changes = vec![LedgerEntryChange::Updated(LedgerEntry {
458 data: LedgerEntryData::Ttl(ttl_entry),
459 last_modified_ledger_seq: 0,
460 ext: crate::xdr::LedgerEntryExt::V0,
461 })];
462
463 let result = parse_changes(&changes);
464 assert_eq!(result, Some(33333));
465 }
466
467 #[test]
468 fn test_parse_changes_single_change_created() {
469 let ttl_entry = TtlEntry {
471 live_until_ledger_seq: 44444,
472 key_hash: Hash([0; 32]),
473 };
474
475 let changes = vec![LedgerEntryChange::Created(LedgerEntry {
476 data: LedgerEntryData::Ttl(ttl_entry),
477 last_modified_ledger_seq: 0,
478 ext: crate::xdr::LedgerEntryExt::V0,
479 })];
480
481 let result = parse_changes(&changes);
482 assert_eq!(result, Some(44444));
483 }
484
485 #[test]
486 fn test_parse_changes_invalid_two_changes() {
487 let not_ttl_change = LedgerEntryChange::Restored(LedgerEntry {
489 data: LedgerEntryData::Account(crate::xdr::AccountEntry {
490 account_id: crate::xdr::AccountId(crate::xdr::PublicKey::PublicKeyTypeEd25519(
491 crate::xdr::Uint256([0; 32]),
492 )),
493 balance: 0,
494 seq_num: SequenceNumber(0),
495 num_sub_entries: 0,
496 inflation_dest: None,
497 flags: 0,
498 home_domain: crate::xdr::String32::default(),
499 thresholds: crate::xdr::Thresholds::default(),
500 signers: crate::xdr::VecM::default(),
501 ext: crate::xdr::AccountEntryExt::V0,
502 }),
503 last_modified_ledger_seq: 0,
504 ext: crate::xdr::LedgerEntryExt::V0,
505 });
506
507 let changes = vec![not_ttl_change.clone(), not_ttl_change];
508 let result = parse_changes(&changes);
509 assert_eq!(result, None);
510 }
511
512 #[test]
513 fn test_parse_changes_invalid_single_change() {
514 let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
516 data: LedgerEntryData::Account(crate::xdr::AccountEntry {
517 account_id: crate::xdr::AccountId(crate::xdr::PublicKey::PublicKeyTypeEd25519(
518 crate::xdr::Uint256([0; 32]),
519 )),
520 balance: 0,
521 seq_num: SequenceNumber(0),
522 num_sub_entries: 0,
523 inflation_dest: None,
524 flags: 0,
525 home_domain: crate::xdr::String32::default(),
526 thresholds: crate::xdr::Thresholds::default(),
527 signers: crate::xdr::VecM::default(),
528 ext: crate::xdr::AccountEntryExt::V0,
529 }),
530 last_modified_ledger_seq: 0,
531 ext: crate::xdr::LedgerEntryExt::V0,
532 })];
533
534 let result = parse_changes(&changes);
535 assert_eq!(result, None);
536 }
537
538 #[test]
539 fn test_parse_changes_empty_changes() {
540 let changes = vec![];
542
543 let result = parse_changes(&changes);
544 assert_eq!(result, None);
545 }
546}