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::{
17    assembled::simulate_and_assemble_transaction,
18    commands::{
19        contract::extend,
20        global,
21        txn_result::{TxnEnvelopeResult, TxnResult},
22        NetworkRunnable,
23    },
24    config::{self, data, locator, network},
25    key, rpc, wasm, Pwd,
26};
27
28#[derive(Parser, Debug, Clone)]
29#[group(skip)]
30pub struct Cmd {
31    #[command(flatten)]
32    pub key: key::Args,
33    /// Number of ledgers to extend the entry
34    #[arg(long)]
35    pub ledgers_to_extend: Option<u32>,
36    /// Only print the new Time To Live ledger
37    #[arg(long)]
38    pub ttl_ledger_only: bool,
39    #[command(flatten)]
40    pub config: config::Args,
41    #[command(flatten)]
42    pub fee: crate::fee::Args,
43}
44
45impl FromStr for Cmd {
46    type Err = clap::error::Error;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        use clap::{CommandFactory, FromArgMatches};
50        Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
51    }
52}
53
54impl Pwd for Cmd {
55    fn set_pwd(&mut self, pwd: &Path) {
56        self.config.set_pwd(pwd);
57    }
58}
59
60#[derive(thiserror::Error, Debug)]
61pub enum Error {
62    #[error("parsing key {key}: {error}")]
63    CannotParseKey {
64        key: String,
65        error: soroban_spec_tools::Error,
66    },
67    #[error("parsing XDR key {key}: {error}")]
68    CannotParseXdrKey { key: String, error: XdrError },
69    #[error("cannot parse contract ID {0}: {1}")]
70    CannotParseContractId(String, DecodeError),
71    #[error(transparent)]
72    Config(#[from] config::Error),
73    #[error("either `--key` or `--key-xdr` are required")]
74    KeyIsRequired,
75    #[error("xdr processing error: {0}")]
76    Xdr(#[from] XdrError),
77    #[error("Ledger entry not found")]
78    LedgerEntryNotFound,
79    #[error(transparent)]
80    Locator(#[from] locator::Error),
81    #[error("missing operation result")]
82    MissingOperationResult,
83    #[error(transparent)]
84    Rpc(#[from] rpc::Error),
85    #[error(transparent)]
86    Wasm(#[from] wasm::Error),
87    #[error(transparent)]
88    Key(#[from] key::Error),
89    #[error(transparent)]
90    Extend(#[from] extend::Error),
91    #[error(transparent)]
92    Data(#[from] data::Error),
93    #[error(transparent)]
94    Network(#[from] network::Error),
95}
96
97impl Cmd {
98    #[allow(clippy::too_many_lines)]
99    pub async fn run(&self) -> Result<(), Error> {
100        let res = self.run_against_rpc_server(None, None).await?.to_envelope();
101        let expiration_ledger_seq = match res {
102            TxnEnvelopeResult::TxnEnvelope(tx) => {
103                println!("{}", tx.to_xdr_base64(Limits::none())?);
104                return Ok(());
105            }
106            TxnEnvelopeResult::Res(res) => res,
107        };
108        if let Some(ledgers_to_extend) = self.ledgers_to_extend {
109            extend::Cmd {
110                key: self.key.clone(),
111                ledgers_to_extend,
112                config: self.config.clone(),
113                fee: self.fee.clone(),
114                ttl_ledger_only: false,
115            }
116            .run()
117            .await?;
118        } else {
119            println!("New ttl ledger: {expiration_ledger_seq}");
120        }
121
122        Ok(())
123    }
124}
125
126#[async_trait::async_trait]
127impl NetworkRunnable for Cmd {
128    type Error = Error;
129    type Result = TxnResult<u32>;
130
131    async fn run_against_rpc_server(
132        &self,
133        args: Option<&global::Args>,
134        config: Option<&config::Args>,
135    ) -> Result<TxnResult<u32>, Error> {
136        let config = config.unwrap_or(&self.config);
137        let print = crate::print::Print::new(args.is_some_and(|a| a.quiet));
138        let network = config.get_network()?;
139        tracing::trace!(?network);
140        let entry_keys = self.key.parse_keys(&config.locator, &network)?;
141        let client = network.rpc_client()?;
142        let source_account = config.source_account().await?;
143
144        // Get the account sequence number
145        let account_details = client
146            .get_account(&source_account.clone().to_string())
147            .await?;
148        let sequence: i64 = account_details.seq_num.into();
149
150        let tx = Box::new(Transaction {
151            source_account,
152            fee: self.fee.fee,
153            seq_num: SequenceNumber(sequence + 1),
154            cond: Preconditions::None,
155            memo: Memo::None,
156            operations: vec![Operation {
157                source_account: None,
158                body: OperationBody::RestoreFootprint(RestoreFootprintOp {
159                    ext: ExtensionPoint::V0,
160                }),
161            }]
162            .try_into()?,
163            ext: TransactionExt::V1(SorobanTransactionData {
164                ext: SorobanTransactionDataExt::V0,
165                resources: SorobanResources {
166                    footprint: LedgerFootprint {
167                        read_only: vec![].try_into()?,
168                        read_write: entry_keys.clone().try_into()?,
169                    },
170                    instructions: self.fee.instructions.unwrap_or_default(),
171                    disk_read_bytes: 0,
172                    write_bytes: 0,
173                },
174                resource_fee: 0,
175            }),
176        });
177        if self.fee.build_only {
178            return Ok(TxnResult::Txn(tx));
179        }
180        let tx = simulate_and_assemble_transaction(&client, &tx)
181            .await?
182            .transaction()
183            .clone();
184        let res = client
185            .send_transaction_polling(&config.sign(tx).await?)
186            .await?;
187        if args.is_none_or(|a| !a.no_cache) {
188            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
189        }
190        let meta = res
191            .result_meta
192            .as_ref()
193            .ok_or(Error::MissingOperationResult)?;
194
195        tracing::trace!(?meta);
196
197        let events = extract_events(meta);
198
199        crate::log::event::all(&events);
200        crate::log::event::contract(&events, &print);
201
202        // The transaction from core will succeed regardless of whether it actually found &
203        // restored the entry, so we have to inspect the result meta to tell if it worked or not.
204        let changes = match meta {
205            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
206                // Simply check if there is exactly one entry here. We only support restoring a single
207                // entry via this command (which we should fix separately, but).
208                if operations.is_empty() {
209                    return Err(Error::LedgerEntryNotFound);
210                }
211
212                operations[0].changes.clone()
213            }
214            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
215                // Simply check if there is exactly one entry here. We only support restoring a single
216                // entry via this command (which we should fix separately, but).
217                if operations.is_empty() {
218                    return Err(Error::LedgerEntryNotFound);
219                }
220
221                operations[0].changes.clone()
222            }
223            _ => return Err(Error::LedgerEntryNotFound),
224        };
225        tracing::debug!("Changes:\nlen:{}\n{changes:#?}", changes.len());
226
227        if changes.is_empty() {
228            print.infoln("No changes detected, transaction was a no-op.");
229            let entry = client.get_full_ledger_entries(&entry_keys).await?;
230            let extension = entry.entries[0].live_until_ledger_seq;
231
232            return Ok(TxnResult::Res(extension));
233        }
234
235        Ok(TxnResult::Res(
236            parse_changes(&changes.to_vec()).ok_or(Error::LedgerEntryNotFound)?,
237        ))
238    }
239}
240
241fn parse_changes(changes: &[LedgerEntryChange]) -> Option<u32> {
242    match changes.len() {
243        // Handle case with 2 changes (original expected format)
244        2 => match (&changes[0], &changes[1]) {
245            (
246                LedgerEntryChange::State(_),
247                LedgerEntryChange::Restored(LedgerEntry {
248                    data:
249                        LedgerEntryData::Ttl(TtlEntry {
250                            live_until_ledger_seq,
251                            ..
252                        }),
253                    ..
254                })
255                | LedgerEntryChange::Updated(LedgerEntry {
256                    data:
257                        LedgerEntryData::Ttl(TtlEntry {
258                            live_until_ledger_seq,
259                            ..
260                        }),
261                    ..
262                })
263                | LedgerEntryChange::Created(LedgerEntry {
264                    data:
265                        LedgerEntryData::Ttl(TtlEntry {
266                            live_until_ledger_seq,
267                            ..
268                        }),
269                    ..
270                }),
271            ) => Some(*live_until_ledger_seq),
272            _ => None,
273        },
274        // Handle case with 1 change (single "Restored" type change)
275        1 => match &changes[0] {
276            LedgerEntryChange::Restored(LedgerEntry {
277                data:
278                    LedgerEntryData::Ttl(TtlEntry {
279                        live_until_ledger_seq,
280                        ..
281                    }),
282                ..
283            })
284            | LedgerEntryChange::Updated(LedgerEntry {
285                data:
286                    LedgerEntryData::Ttl(TtlEntry {
287                        live_until_ledger_seq,
288                        ..
289                    }),
290                ..
291            })
292            | LedgerEntryChange::Created(LedgerEntry {
293                data:
294                    LedgerEntryData::Ttl(TtlEntry {
295                        live_until_ledger_seq,
296                        ..
297                    }),
298                ..
299            }) => Some(*live_until_ledger_seq),
300            _ => None,
301        },
302        _ => None,
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::xdr::{Hash, LedgerEntry, LedgerEntryChange, LedgerEntryData, TtlEntry};
310
311    #[test]
312    fn test_parse_changes_two_changes_restored() {
313        // Test the original expected format with 2 changes
314        let ttl_entry = TtlEntry {
315            live_until_ledger_seq: 12345,
316            key_hash: Hash([0; 32]),
317        };
318
319        let changes = vec![
320            LedgerEntryChange::State(LedgerEntry {
321                data: LedgerEntryData::Ttl(ttl_entry.clone()),
322                last_modified_ledger_seq: 0,
323                ext: crate::xdr::LedgerEntryExt::V0,
324            }),
325            LedgerEntryChange::Restored(LedgerEntry {
326                data: LedgerEntryData::Ttl(ttl_entry),
327                last_modified_ledger_seq: 0,
328                ext: crate::xdr::LedgerEntryExt::V0,
329            }),
330        ];
331
332        let result = parse_changes(&changes);
333        assert_eq!(result, Some(12345));
334    }
335
336    #[test]
337    fn test_parse_changes_two_changes_updated() {
338        // Test the original expected format with 2 changes, but second change is Updated
339        let ttl_entry = TtlEntry {
340            live_until_ledger_seq: 67890,
341            key_hash: Hash([0; 32]),
342        };
343
344        let changes = vec![
345            LedgerEntryChange::State(LedgerEntry {
346                data: LedgerEntryData::Ttl(ttl_entry.clone()),
347                last_modified_ledger_seq: 0,
348                ext: crate::xdr::LedgerEntryExt::V0,
349            }),
350            LedgerEntryChange::Updated(LedgerEntry {
351                data: LedgerEntryData::Ttl(ttl_entry),
352                last_modified_ledger_seq: 0,
353                ext: crate::xdr::LedgerEntryExt::V0,
354            }),
355        ];
356
357        let result = parse_changes(&changes);
358        assert_eq!(result, Some(67890));
359    }
360
361    #[test]
362    fn test_parse_changes_two_changes_created() {
363        // Test the original expected format with 2 changes, but second change is Created
364        let ttl_entry = TtlEntry {
365            live_until_ledger_seq: 11111,
366            key_hash: Hash([0; 32]),
367        };
368
369        let changes = vec![
370            LedgerEntryChange::State(LedgerEntry {
371                data: LedgerEntryData::Ttl(ttl_entry.clone()),
372                last_modified_ledger_seq: 0,
373                ext: crate::xdr::LedgerEntryExt::V0,
374            }),
375            LedgerEntryChange::Created(LedgerEntry {
376                data: LedgerEntryData::Ttl(ttl_entry),
377                last_modified_ledger_seq: 0,
378                ext: crate::xdr::LedgerEntryExt::V0,
379            }),
380        ];
381
382        let result = parse_changes(&changes);
383        assert_eq!(result, Some(11111));
384    }
385
386    #[test]
387    fn test_parse_changes_single_change_restored() {
388        // Test the new single change format with Restored type
389        let ttl_entry = TtlEntry {
390            live_until_ledger_seq: 22222,
391            key_hash: Hash([0; 32]),
392        };
393
394        let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
395            data: LedgerEntryData::Ttl(ttl_entry),
396            last_modified_ledger_seq: 0,
397            ext: crate::xdr::LedgerEntryExt::V0,
398        })];
399
400        let result = parse_changes(&changes);
401        assert_eq!(result, Some(22222));
402    }
403
404    #[test]
405    fn test_parse_changes_single_change_updated() {
406        // Test the new single change format with Updated type
407        let ttl_entry = TtlEntry {
408            live_until_ledger_seq: 33333,
409            key_hash: Hash([0; 32]),
410        };
411
412        let changes = vec![LedgerEntryChange::Updated(LedgerEntry {
413            data: LedgerEntryData::Ttl(ttl_entry),
414            last_modified_ledger_seq: 0,
415            ext: crate::xdr::LedgerEntryExt::V0,
416        })];
417
418        let result = parse_changes(&changes);
419        assert_eq!(result, Some(33333));
420    }
421
422    #[test]
423    fn test_parse_changes_single_change_created() {
424        // Test the new single change format with Created type
425        let ttl_entry = TtlEntry {
426            live_until_ledger_seq: 44444,
427            key_hash: Hash([0; 32]),
428        };
429
430        let changes = vec![LedgerEntryChange::Created(LedgerEntry {
431            data: LedgerEntryData::Ttl(ttl_entry),
432            last_modified_ledger_seq: 0,
433            ext: crate::xdr::LedgerEntryExt::V0,
434        })];
435
436        let result = parse_changes(&changes);
437        assert_eq!(result, Some(44444));
438    }
439
440    #[test]
441    fn test_parse_changes_invalid_two_changes() {
442        // Test invalid 2-change format (first change is not State)
443        let ttl_entry = TtlEntry {
444            live_until_ledger_seq: 55555,
445            key_hash: Hash([0; 32]),
446        };
447
448        let changes = vec![
449            LedgerEntryChange::Restored(LedgerEntry {
450                data: LedgerEntryData::Ttl(ttl_entry.clone()),
451                last_modified_ledger_seq: 0,
452                ext: crate::xdr::LedgerEntryExt::V0,
453            }),
454            LedgerEntryChange::Restored(LedgerEntry {
455                data: LedgerEntryData::Ttl(ttl_entry),
456                last_modified_ledger_seq: 0,
457                ext: crate::xdr::LedgerEntryExt::V0,
458            }),
459        ];
460
461        let result = parse_changes(&changes);
462        assert_eq!(result, None);
463    }
464
465    #[test]
466    fn test_parse_changes_invalid_single_change() {
467        // Test invalid single change format (not TTL data)
468        let changes = vec![LedgerEntryChange::Restored(LedgerEntry {
469            data: LedgerEntryData::Account(crate::xdr::AccountEntry {
470                account_id: crate::xdr::AccountId(crate::xdr::PublicKey::PublicKeyTypeEd25519(
471                    crate::xdr::Uint256([0; 32]),
472                )),
473                balance: 0,
474                seq_num: SequenceNumber(0),
475                num_sub_entries: 0,
476                inflation_dest: None,
477                flags: 0,
478                home_domain: crate::xdr::String32::default(),
479                thresholds: crate::xdr::Thresholds::default(),
480                signers: crate::xdr::VecM::default(),
481                ext: crate::xdr::AccountEntryExt::V0,
482            }),
483            last_modified_ledger_seq: 0,
484            ext: crate::xdr::LedgerEntryExt::V0,
485        })];
486
487        let result = parse_changes(&changes);
488        assert_eq!(result, None);
489    }
490
491    #[test]
492    fn test_parse_changes_empty_changes() {
493        // Test empty changes array
494        let changes = vec![];
495
496        let result = parse_changes(&changes);
497        assert_eq!(result, None);
498    }
499
500    #[test]
501    fn test_parse_changes_three_changes() {
502        // Test with 3 changes (should return None)
503        let ttl_entry = TtlEntry {
504            live_until_ledger_seq: 66666,
505            key_hash: Hash([0; 32]),
506        };
507
508        let changes = vec![
509            LedgerEntryChange::State(LedgerEntry {
510                data: LedgerEntryData::Ttl(ttl_entry.clone()),
511                last_modified_ledger_seq: 0,
512                ext: crate::xdr::LedgerEntryExt::V0,
513            }),
514            LedgerEntryChange::Restored(LedgerEntry {
515                data: LedgerEntryData::Ttl(ttl_entry.clone()),
516                last_modified_ledger_seq: 0,
517                ext: crate::xdr::LedgerEntryExt::V0,
518            }),
519            LedgerEntryChange::Updated(LedgerEntry {
520                data: LedgerEntryData::Ttl(ttl_entry),
521                last_modified_ledger_seq: 0,
522                ext: crate::xdr::LedgerEntryExt::V0,
523            }),
524        ];
525
526        let result = parse_changes(&changes);
527        assert_eq!(result, None);
528    }
529
530    #[test]
531    fn test_parse_changes_mixed_invalid_types() {
532        // Test with mixed valid and invalid change types
533        let ttl_entry = TtlEntry {
534            live_until_ledger_seq: 77777,
535            key_hash: Hash([0; 32]),
536        };
537
538        let changes = vec![
539            LedgerEntryChange::State(LedgerEntry {
540                data: LedgerEntryData::Ttl(ttl_entry.clone()),
541                last_modified_ledger_seq: 0,
542                ext: crate::xdr::LedgerEntryExt::V0,
543            }),
544            LedgerEntryChange::State(LedgerEntry {
545                data: LedgerEntryData::Ttl(ttl_entry),
546                last_modified_ledger_seq: 0,
547                ext: crate::xdr::LedgerEntryExt::V0,
548            }),
549        ];
550
551        let result = parse_changes(&changes);
552        assert_eq!(result, None);
553    }
554}