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