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