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) -> 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        // Get the account sequence number
170        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        // The transaction from core will succeed regardless of whether it actually found &
229        // restored the entry, so we have to inspect the result meta to tell if it worked or not.
230        let changes = match meta {
231            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
232                // Simply check if there is exactly one entry here. We only support restoring a single
233                // entry via this command (which we should fix separately, but).
234                if operations.is_empty() {
235                    return Err(Error::LedgerEntryNotFound);
236                }
237
238                operations[0].changes.clone()
239            }
240            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
241                // Simply check if there is exactly one entry here. We only support restoring a single
242                // entry via this command (which we should fix separately, but).
243                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        // Test the original expected format with 2 changes
312        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        // Test the original expected format with 2 changes, but second change is Updated
370        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        // Test the original expected format with 2 changes, but second change is Created
395        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        // Test the new single change format with Restored type
420        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        // Test the new single change format with Updated type
438        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        // Test the new single change format with Created type
456        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        // Test invalid 2-change format (not TTL data)
474        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        // Test invalid single change format (not TTL data)
501        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        // Test empty changes array
527        let changes = vec![];
528
529        let result = parse_changes(&changes);
530        assert_eq!(result, None);
531    }
532}