Skip to main content

soroban_cli/commands/contract/
restore.rs

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