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