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::{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    /// 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 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        // Get the account sequence number
174        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        // The transaction from core will succeed regardless of whether it actually found &
233        // restored the entry, so we have to inspect the result meta to tell if it worked or not.
234        let changes = match meta {
235            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
236                // Simply check if there is exactly one entry here. We only support restoring a single
237                // entry via this command (which we should fix separately, but).
238                if operations.is_empty() {
239                    return Err(Error::LedgerEntryNotFound);
240                }
241
242                operations[0].changes.clone()
243            }
244            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
245                // Simply check if there is exactly one entry here. We only support restoring a single
246                // entry via this command (which we should fix separately, but).
247                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        // Test the original expected format with 2 changes
316        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        // Test the original expected format with 2 changes, but second change is Updated
374        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        // Test the original expected format with 2 changes, but second change is Created
399        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        // Test the new single change format with Restored type
424        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        // Test the new single change format with Updated type
442        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        // Test the new single change format with Created type
460        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        // Test invalid 2-change format (not TTL data)
478        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        // Test invalid single change format (not TTL data)
505        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        // Test empty changes array
531        let changes = vec![];
532
533        let result = parse_changes(&changes);
534        assert_eq!(result, None);
535    }
536}