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