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