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 HEADING_TRANSACTION,
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, help_heading = HEADING_TRANSACTION)]
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 .execute(&self.config, global_args.quiet, global_args.no_cache)
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 pub async fn execute(
159 &self,
160 config: &config::Args,
161 quiet: bool,
162 no_cache: bool,
163 ) -> Result<TxnResult<u32>, Error> {
164 let print = crate::print::Print::new(quiet);
165 let network = config.get_network()?;
166 tracing::trace!(?network);
167 let entry_keys = self.key.parse_keys(&config.locator, &network)?;
168 let client = network.rpc_client()?;
169 client
170 .verify_network_passphrase(Some(&network.network_passphrase))
171 .await?;
172 let source_account = config.source_account()?;
173
174 let account_details = client
176 .get_account(&source_account.clone().to_string())
177 .await?;
178 let sequence: i64 = account_details.seq_num.into();
179
180 let tx = Box::new(Transaction {
181 source_account,
182 fee: config.get_inclusion_fee()?,
183 seq_num: SequenceNumber(sequence + 1),
184 cond: Preconditions::None,
185 memo: Memo::None,
186 operations: vec![Operation {
187 source_account: None,
188 body: OperationBody::RestoreFootprint(RestoreFootprintOp {
189 ext: ExtensionPoint::V0,
190 }),
191 }]
192 .try_into()?,
193 ext: TransactionExt::V1(SorobanTransactionData {
194 ext: SorobanTransactionDataExt::V0,
195 resources: SorobanResources {
196 footprint: LedgerFootprint {
197 read_only: vec![].try_into()?,
198 read_write: entry_keys.clone().try_into()?,
199 },
200 instructions: self.resources.instructions.unwrap_or_default(),
201 disk_read_bytes: 0,
202 write_bytes: 0,
203 },
204 resource_fee: 0,
205 }),
206 });
207 if self.build_only {
208 return Ok(TxnResult::Txn(tx));
209 }
210
211 let res = sim_sign_and_send_tx::<Error>(
212 &client,
213 &tx,
214 config,
215 &self.resources,
216 &[],
217 None,
220 quiet,
221 no_cache,
222 )
223 .await?;
224
225 let meta = res
226 .result_meta
227 .as_ref()
228 .ok_or(Error::MissingOperationResult)?;
229
230 tracing::trace!(?meta);
231
232 let events = extract_events(meta);
233
234 crate::log::event::all(&events);
235 crate::log::event::contract(&events, &print);
236
237 let changes = match meta {
240 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
241 if operations.is_empty() {
244 return Err(Error::LedgerEntryNotFound);
245 }
246
247 operations[0].changes.clone()
248 }
249 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
250 if operations.is_empty() {
253 return Err(Error::LedgerEntryNotFound);
254 }
255
256 operations[0].changes.clone()
257 }
258 _ => return Err(Error::LedgerEntryNotFound),
259 };
260 tracing::debug!("Changes:\nlen:{}\n{changes:#?}", changes.len());
261
262 if changes.is_empty() {
263 print.infoln("No changes detected, transaction was a no-op.");
264 let entry = client.get_full_ledger_entries(&entry_keys).await?;
265 let extension = entry
269 .entries
270 .first()
271 .ok_or(Error::LedgerEntryNotFound)?
272 .live_until_ledger_seq
273 .unwrap_or_default();
274
275 return Ok(TxnResult::Res(extension));
276 }
277
278 Ok(TxnResult::Res(
279 parse_changes(&changes.to_vec()).ok_or(Error::LedgerEntryNotFound)?,
280 ))
281 }
282}
283
284fn parse_changes(changes: &[LedgerEntryChange]) -> Option<u32> {
285 changes
286 .iter()
287 .filter_map(|change| match change {
288 LedgerEntryChange::Restored(LedgerEntry {
289 data:
290 LedgerEntryData::Ttl(TtlEntry {
291 live_until_ledger_seq,
292 ..
293 }),
294 ..
295 })
296 | LedgerEntryChange::Updated(LedgerEntry {
297 data:
298 LedgerEntryData::Ttl(TtlEntry {
299 live_until_ledger_seq,
300 ..
301 }),
302 ..
303 })
304 | LedgerEntryChange::Created(LedgerEntry {
305 data:
306 LedgerEntryData::Ttl(TtlEntry {
307 live_until_ledger_seq,
308 ..
309 }),
310 ..
311 }) => Some(*live_until_ledger_seq),
312 _ => None,
313 })
314 .max()
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::xdr::{
321 ContractDataDurability::Persistent, ContractDataEntry, ContractId, Hash, LedgerEntry,
322 LedgerEntryChange, LedgerEntryData, ScAddress, ScSymbol, ScVal, SequenceNumber, StringM,
323 TtlEntry,
324 };
325
326 #[test]
327 fn test_parse_changes_two_changes_restored() {
328 let ttl_entry = TtlEntry {
330 live_until_ledger_seq: 12345,
331 key_hash: Hash([0; 32]),
332 };
333
334 let changes = vec![
335 LedgerEntryChange::State(LedgerEntry {
336 data: LedgerEntryData::Ttl(ttl_entry.clone()),
337 last_modified_ledger_seq: 0,
338 ext: crate::xdr::LedgerEntryExt::V0,
339 }),
340 LedgerEntryChange::Restored(LedgerEntry {
341 data: LedgerEntryData::Ttl(ttl_entry),
342 last_modified_ledger_seq: 0,
343 ext: crate::xdr::LedgerEntryExt::V0,
344 }),
345 ];
346
347 let result = parse_changes(&changes);
348 assert_eq!(result, Some(12345));
349 }
350
351 #[test]
352 fn test_parse_two_changes_that_had_expired() {
353 let ttl_entry = TtlEntry {
354 live_until_ledger_seq: 55555,
355 key_hash: Hash([0; 32]),
356 };
357
358 let counter = "COUNTER".parse::<StringM<32>>().unwrap();
359 let contract_data_entry = ContractDataEntry {
360 ext: ExtensionPoint::default(),
361 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
362 key: ScVal::Symbol(ScSymbol(counter)),
363 durability: Persistent,
364 val: ScVal::U32(1),
365 };
366
367 let changes = vec![
368 LedgerEntryChange::Restored(LedgerEntry {
369 data: LedgerEntryData::Ttl(ttl_entry.clone()),
370 last_modified_ledger_seq: 37429,
371 ext: crate::xdr::LedgerEntryExt::V0,
372 }),
373 LedgerEntryChange::Restored(LedgerEntry {
374 data: LedgerEntryData::ContractData(contract_data_entry.clone()),
375 last_modified_ledger_seq: 37429,
376 ext: crate::xdr::LedgerEntryExt::V0,
377 }),
378 ];
379
380 let result = parse_changes(&changes);
381 assert_eq!(result, Some(55555));
382 }
383
384 #[test]
385 fn test_parse_changes_two_changes_updated() {
386 let ttl_entry = TtlEntry {
388 live_until_ledger_seq: 67890,
389 key_hash: Hash([0; 32]),
390 };
391
392 let changes = vec![
393 LedgerEntryChange::State(LedgerEntry {
394 data: LedgerEntryData::Ttl(ttl_entry.clone()),
395 last_modified_ledger_seq: 0,
396 ext: crate::xdr::LedgerEntryExt::V0,
397 }),
398 LedgerEntryChange::Updated(LedgerEntry {
399 data: LedgerEntryData::Ttl(ttl_entry),
400 last_modified_ledger_seq: 0,
401 ext: crate::xdr::LedgerEntryExt::V0,
402 }),
403 ];
404
405 let result = parse_changes(&changes);
406 assert_eq!(result, Some(67890));
407 }
408
409 #[test]
410 fn test_parse_changes_two_changes_created() {
411 let ttl_entry = TtlEntry {
413 live_until_ledger_seq: 11111,
414 key_hash: Hash([0; 32]),
415 };
416
417 let changes = vec![
418 LedgerEntryChange::State(LedgerEntry {
419 data: LedgerEntryData::Ttl(ttl_entry.clone()),
420 last_modified_ledger_seq: 0,
421 ext: crate::xdr::LedgerEntryExt::V0,
422 }),
423 LedgerEntryChange::Created(LedgerEntry {
424 data: LedgerEntryData::Ttl(ttl_entry),
425 last_modified_ledger_seq: 0,
426 ext: crate::xdr::LedgerEntryExt::V0,
427 }),
428 ];
429
430 let result = parse_changes(&changes);
431 assert_eq!(result, Some(11111));
432 }
433
434 #[test]
435 fn test_parse_changes_single_change_restored() {
436 let ttl_entry = TtlEntry {
438 live_until_ledger_seq: 22222,
439 key_hash: Hash([0; 32]),
440 };
441
442 let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
443 data: LedgerEntryData::Ttl(ttl_entry),
444 last_modified_ledger_seq: 0,
445 ext: crate::xdr::LedgerEntryExt::V0,
446 })];
447
448 let result = parse_changes(&changes);
449 assert_eq!(result, Some(22222));
450 }
451
452 #[test]
453 fn test_parse_changes_single_change_updated() {
454 let ttl_entry = TtlEntry {
456 live_until_ledger_seq: 33333,
457 key_hash: Hash([0; 32]),
458 };
459
460 let changes = vec![LedgerEntryChange::Updated(LedgerEntry {
461 data: LedgerEntryData::Ttl(ttl_entry),
462 last_modified_ledger_seq: 0,
463 ext: crate::xdr::LedgerEntryExt::V0,
464 })];
465
466 let result = parse_changes(&changes);
467 assert_eq!(result, Some(33333));
468 }
469
470 #[test]
471 fn test_parse_changes_single_change_created() {
472 let ttl_entry = TtlEntry {
474 live_until_ledger_seq: 44444,
475 key_hash: Hash([0; 32]),
476 };
477
478 let changes = vec![LedgerEntryChange::Created(LedgerEntry {
479 data: LedgerEntryData::Ttl(ttl_entry),
480 last_modified_ledger_seq: 0,
481 ext: crate::xdr::LedgerEntryExt::V0,
482 })];
483
484 let result = parse_changes(&changes);
485 assert_eq!(result, Some(44444));
486 }
487
488 #[test]
489 fn test_parse_changes_invalid_two_changes() {
490 let not_ttl_change = LedgerEntryChange::Restored(LedgerEntry {
492 data: LedgerEntryData::Account(crate::xdr::AccountEntry {
493 account_id: crate::xdr::AccountId(crate::xdr::PublicKey::PublicKeyTypeEd25519(
494 crate::xdr::Uint256([0; 32]),
495 )),
496 balance: 0,
497 seq_num: SequenceNumber(0),
498 num_sub_entries: 0,
499 inflation_dest: None,
500 flags: 0,
501 home_domain: crate::xdr::String32::default(),
502 thresholds: crate::xdr::Thresholds::default(),
503 signers: crate::xdr::VecM::default(),
504 ext: crate::xdr::AccountEntryExt::V0,
505 }),
506 last_modified_ledger_seq: 0,
507 ext: crate::xdr::LedgerEntryExt::V0,
508 });
509
510 let changes = vec![not_ttl_change.clone(), not_ttl_change];
511 let result = parse_changes(&changes);
512 assert_eq!(result, None);
513 }
514
515 #[test]
516 fn test_parse_changes_invalid_single_change() {
517 let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
519 data: LedgerEntryData::Account(crate::xdr::AccountEntry {
520 account_id: crate::xdr::AccountId(crate::xdr::PublicKey::PublicKeyTypeEd25519(
521 crate::xdr::Uint256([0; 32]),
522 )),
523 balance: 0,
524 seq_num: SequenceNumber(0),
525 num_sub_entries: 0,
526 inflation_dest: None,
527 flags: 0,
528 home_domain: crate::xdr::String32::default(),
529 thresholds: crate::xdr::Thresholds::default(),
530 signers: crate::xdr::VecM::default(),
531 ext: crate::xdr::AccountEntryExt::V0,
532 }),
533 last_modified_ledger_seq: 0,
534 ext: crate::xdr::LedgerEntryExt::V0,
535 })];
536
537 let result = parse_changes(&changes);
538 assert_eq!(result, None);
539 }
540
541 #[test]
542 fn test_parse_changes_empty_changes() {
543 let changes = vec![];
545
546 let result = parse_changes(&changes);
547 assert_eq!(result, None);
548 }
549}