soroban_cli/commands/contract/
restore.rs

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