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