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